diff options
| author | 2013-01-06 15:54:52 +0100 | |
|---|---|---|
| committer | 2013-01-06 15:54:52 +0100 | |
| commit | 6f8b5347dfa119a3df21f4ca8ba8c2b1537a726a (patch) | |
| tree | 627c4d99f0aaa4c8022b70b3ebe72f201d924dd6 | |
| parent | removed unneeded stuff (diff) | |
| download | pyload-6f8b5347dfa119a3df21f4ca8ba8c2b1537a726a.tar.xz | |
first working parts of config api
| -rw-r--r-- | module/Api.py | 91 | ||||
| -rw-r--r-- | module/PluginManager.py | 14 | ||||
| -rw-r--r-- | module/api/ApiComponent.py | 7 | ||||
| -rw-r--r-- | module/api/ConfigApi.py | 77 | ||||
| -rw-r--r-- | module/config/ConfigManager.py | 26 | ||||
| -rw-r--r-- | module/config/ConfigParser.py | 5 | ||||
| -rw-r--r-- | module/database/ConfigDatabase.py | 10 | ||||
| -rw-r--r-- | module/remote/json_converter.py | 4 | ||||
| -rw-r--r-- | module/remote/pyload.thrift | 21 | ||||
| -rw-r--r-- | module/remote/ttypes.py | 39 | ||||
| -rw-r--r-- | module/remote/ttypes_debug.py | 11 | ||||
| -rw-r--r-- | module/utils/__init__.py | 4 | ||||
| -rw-r--r-- | module/web/api_app.py | 6 | ||||
| -rw-r--r-- | module/web/static/js/default.js | 11 | ||||
| -rw-r--r-- | module/web/static/js/views/settingsView.js | 25 | ||||
| -rw-r--r-- | module/web/templates/default/settings.html | 4 | ||||
| -rw-r--r-- | tests/manager/test_configManager.py | 26 | ||||
| -rw-r--r-- | tests/other/test_configparser.py | 5 | 
18 files changed, 251 insertions, 135 deletions
| diff --git a/module/Api.py b/module/Api.py index dfc7b608f..80bfd1673 100644 --- a/module/Api.py +++ b/module/Api.py @@ -18,12 +18,11 @@  import re  from functools import partial -from types import MethodType, CodeType -from dis import opmap +from types import MethodType  from remote.ttypes import * -from utils import bits_set, get_index +from utils import bits_set  # contains function names mapped to their permissions  # unlisted functions are for admins only @@ -41,88 +40,38 @@ def RequirePerm(bits):      return _Dec -# we will byte-hacking the method to add user as keyword argument +# decorator to annotate user methods, these methods must have user=None kwarg.  class UserContext(object): -    """Decorator to mark methods that require a specific user""" -      def __new__(cls, f, *args, **kwargs): -        fc = f.func_code - -        try: -            i = get_index(fc.co_names, "user") -        except ValueError: # functions does not uses user, so no need to modify -            return f -          user_context[f.__name__] = True -        new_names = tuple([x for x in fc.co_names if f != "user"]) -        new_varnames = tuple([x for x in fc.co_varnames] + ["user"]) -        new_code = fc.co_code - -        # subtract 1 from higher LOAD_GLOBAL -        for x in range(i + 1, len(fc.co_names)): -            new_code = new_code.replace(chr(opmap['LOAD_GLOBAL']) + chr(x), chr(opmap['LOAD_GLOBAL']) + chr(x - 1)) - -        # load argument instead of global -        new_code = new_code.replace(chr(opmap['LOAD_GLOBAL']) + chr(i), chr(opmap['LOAD_FAST']) + chr(fc.co_argcount)) - -        new_fc = CodeType(fc.co_argcount + 1, fc.co_nlocals + 1, fc.co_stacksize, fc.co_flags, new_code, fc.co_consts, -            new_names, new_varnames, fc.co_filename, fc.co_name, fc.co_firstlineno, fc.co_lnotab, fc.co_freevars, -            fc.co_cellvars) - -        f.func_code = new_fc - -        # None as default argument for user -        if f.func_defaults: -            f.func_defaults = tuple([x for x in f.func_defaults] + [None]) -        else: -            f.func_defaults = (None,) -          return f  urlmatcher = re.compile(r"((https?|ftps?|xdcc|sftp):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+\-=\\\.&]*)", re.IGNORECASE) -  stateMap = {      DownloadState.All: frozenset(getattr(DownloadStatus, x) for x in dir(DownloadStatus) if not x.startswith("_")), -    DownloadState.Finished : frozenset((DownloadStatus.Finished, DownloadStatus.Skipped)), -    DownloadState.Unfinished : None, # set below -    DownloadState.Failed : frozenset((DownloadStatus.Failed, DownloadStatus.TempOffline, DownloadStatus.Aborted)), +    DownloadState.Finished: frozenset((DownloadStatus.Finished, DownloadStatus.Skipped)), +    DownloadState.Unfinished: None, # set below +    DownloadState.Failed: frozenset((DownloadStatus.Failed, DownloadStatus.TempOffline, DownloadStatus.Aborted)),      DownloadState.Unmanaged: None, #TODO  } -stateMap[DownloadState.Unfinished] =  frozenset(stateMap[DownloadState.All].difference(stateMap[DownloadState.Finished])) +stateMap[DownloadState.Unfinished] = frozenset(stateMap[DownloadState.All].difference(stateMap[DownloadState.Finished]))  def state_string(state):      return ",".join(str(x) for x in stateMap[state]) +  def has_permission(userPermission, Permission):      return bits_set(Permission, userPermission)  from datatypes.User import User -class UserApi(object): -    """  Proxy object for api that provides all methods in user context """ - -    def __init__(self, api, user): -        self.api = api -        self._user = user - -    def __getattr__(self, item): -        f = self.api.__getattribute__(item) -        if f.func_name in user_context: -            return partial(f, user=self._user) - -        return f - -    @property -    def user(self): -        return self._user -  class Api(Iface):      """      **pyLoads API** -    This is accessible either internal via core.api, thrift backend or json api. +    This is accessible either internal via core.api, websocket backend or json api.      see Thrift specification file remote/thriftbackend/pyload.thrift\      for information about data structures and what methods are usable with rpc. @@ -139,6 +88,10 @@ class Api(Iface):          self.core = core          self.user_apis = {} +    @property +    def user(self): +        return None #TODO return default user? +      @classmethod      def initComponents(cls):          # Allow extending the api @@ -164,7 +117,6 @@ class Api(Iface):          return cls.EXTEND -      def withUserContext(self, uid):          """ Returns a proxy version of the api, to call method in user context @@ -179,7 +131,7 @@ class Api(Iface):              if not user: #TODO: anonymous user?                  return None -            self.user_apis[uid] = UserApi(self, User.fromUserData(self, user)) +            self.user_apis[uid] = UserApi(self.core, User.fromUserData(self, user))          return self.user_apis[uid] @@ -262,3 +214,18 @@ class Api(Iface):          self.core.db.setPermission(user, permission)          self.core.db.setRole(user, role) + +class UserApi(Api): +    """  Proxy object for api that provides all methods in user context """ + +    def __init__(self, core, user): +        # No need to init super class +        self.core = core +        self._user = user + +    def withUserContext(self, uid): +        raise Exception("Not allowed") + +    @property +    def user(self): +        return self._user
\ No newline at end of file diff --git a/module/PluginManager.py b/module/PluginManager.py index 6f2268388..d74123040 100644 --- a/module/PluginManager.py +++ b/module/PluginManager.py @@ -60,10 +60,9 @@ class PluginManager:          self.plugins = {}          self.modules = {} # cached modules          self.history = [] # match history to speedup parsing (type, name) +        self.user_context = {} # plugins working with user context          self.createIndex() -        # TODO, replacement for this? -        #self.core.config.parseValues(self.core.config.PLUGIN)          #register for import addon          sys.meta_path.append(self) @@ -213,6 +212,10 @@ class PluginManager:                  else: # activated flag missing                      config.insert(0, ("activated", "bool", "Activated", False)) +            # Everything that is no addon and user_context=True, is added to dict +            if folder != "addons" or attrs.get("user_context", False): +                self.user_context[name] = True +              try:                  self.core.config.addConfigSection(name, name, desc, long_desc, config)              except: @@ -386,6 +389,13 @@ class PluginManager:          return True +    def isUserPlugin(self, plugin): +        """ A plugin suitable for multiple user """ +        return plugin in self.user_context + +    def isPluginType(self, plugin, type): +        return plugin in self.plugins[type] +      def loadIcons(self):          """Loads all icons from plugins, plugin type is not in result, because its not important here. diff --git a/module/api/ApiComponent.py b/module/api/ApiComponent.py index ba96b3be9..a89e3e0a5 100644 --- a/module/api/ApiComponent.py +++ b/module/api/ApiComponent.py @@ -7,11 +7,16 @@ from module.remote.ttypes import Iface  Iface = object  class ApiComponent(Iface): -    def __init__(self, core): +    __slots__ = [] + +    def __init__(self, core, user):          # Only for auto completion, this class can not be instantiated          from pyload import Core +        from module.datatypes.User import User          assert isinstance(core, Core)          assert issubclass(ApiComponent, Iface)          self.core = core +        assert isinstance(user, User) +        self.user = user          # No instantiating!          raise Exception()
\ No newline at end of file diff --git a/module/api/ConfigApi.py b/module/api/ConfigApi.py index f3f2e6950..ffcdbabec 100644 --- a/module/api/ConfigApi.py +++ b/module/api/ConfigApi.py @@ -1,7 +1,7 @@  #!/usr/bin/env python  # -*- coding: utf-8 -*- -from module.Api import Api, UserContext, RequirePerm, Permission, ConfigHolder, ConfigItem +from module.Api import Api, UserContext, RequirePerm, Permission, ConfigHolder, ConfigItem, PluginInfo  from module.utils import to_string  from ApiComponent import ApiComponent @@ -9,6 +9,7 @@ from ApiComponent import ApiComponent  class ConfigApi(ApiComponent):      """ Everything related to configuration """ +    @UserContext      def getConfigValue(self, section, option):          """Retrieve config value. @@ -17,9 +18,10 @@ class ConfigApi(ApiComponent):          :rtype: str          :return: config value as string          """ -        value = self.core.config.get(section, option) +        value = self.core.config.get(section, option, self.user)          return to_string(value) +    @UserContext      def setConfigValue(self, section, option, value):          """Set new config value. @@ -30,49 +32,69 @@ class ConfigApi(ApiComponent):          if option in ("limit_speed", "max_speed"): #not so nice to update the limit              self.core.requestFactory.updateBucket() -        self.core.config.set(section, option, value) +        self.core.config.set(section, option, value, self.user)      def getConfig(self):          """Retrieves complete config of core. -        :rtype: ConfigHolder -        :return: dict with section mapped to config +        :rtype: dict of section -> ConfigHolder          """ -        # TODO -        return dict([(section, ConfigHolder(section, data.name, data.description, data.long_desc, [ -        ConfigItem(option, d.name, d.description, d.type, to_string(d.default), -            to_string(self.core.config.get(section, option))) for -        option, d in data.config.iteritems()])) for -                     section, data in self.core.config.getBaseSections()]) +        data = {} +        for section, config, values in self.core.config.iterCoreSections(): +            holder = ConfigHolder(section, config.name, config.description, config.long_desc) +            holder.items = [ConfigItem(option, x.name, x.description, x.type, to_string(x.default), +                to_string(values.get(option, x.default))) for option, x in config.config.iteritems()] +            data[section] = holder +        return data -    def getConfigRef(self): -        """Config instance, not for RPC""" -        return self.core.config +    def getCoreConfig(self): +        """ Retrieves core config sections -    def getGlobalPlugins(self): -        """All global plugins/addons, only admin can use this +        :rtype: list of PluginInfo +        """ +        return [PluginInfo(section, config.name, config.description, False, False) +                for section, config, values in self.core.config.iterCoreSections()] -        :return: list of `ConfigInfo` +    @UserContext +    @RequirePerm(Permission.Plugins) +    def getPluginConfig(self): +        """All plugins and addons the current user has configured + +        :rtype: list of PluginInfo          """ -        pass +        # TODO: include addons that are activated by default +        data = [] +        for name, config, values in self.core.config.iterSections(self.user): +            if not values: continue +            item = PluginInfo(name, config.name, config.description, +                self.core.pluginManager.isPluginType(name, "addons"), +                self.core.pluginManager.isUserPlugin(name), +                values.get("activated", False)) +            data.append(item) + +        return data      @UserContext      @RequirePerm(Permission.Plugins) -    def getUserPlugins(self): -        """List of plugins every user can configure for himself +    def getAvailablePlugins(self): +        """List of all available plugins, that are configurable -        :return: list of `ConfigInfo` +        :rtype: list of PluginInfo          """ -        pass +        # TODO: filter user_context / addons when not allowed +        return [PluginInfo(name, config.name, config.description, +            self.core.pluginManager.isPluginType(name, "addons"), +            self.core.pluginManager.isUserPlugin(name)) +                for name, config, values in self.core.config.iterSections(self.user)]      @UserContext      @RequirePerm(Permission.Plugins)      def configurePlugin(self, plugin): -        """Get complete config options for an plugin +        """Get complete config options for desired section -        :param plugin: Name of the plugin to configure -        :return: :class:`ConfigHolder` +        :param plugin: Name of plugin or config section +        :rtype: ConfigHolder          """          pass @@ -82,7 +104,7 @@ class ConfigApi(ApiComponent):      def saveConfig(self, config):          """Used to save a configuration, core config can only be saved by admins -        :param config: :class:`ConfigHolder +        :param config: :class:`ConfigHolder`          """          pass @@ -92,9 +114,8 @@ class ConfigApi(ApiComponent):          """Deletes modified config          :param plugin: plugin name -        :return:          """ -        pass +        self.core.config.delete(plugin, self.user)      @RequirePerm(Permission.Plugins)      def setConfigHandler(self, plugin, iid, value): diff --git a/module/config/ConfigManager.py b/module/config/ConfigManager.py index 4898b0c66..eb96a49f6 100644 --- a/module/config/ConfigManager.py +++ b/module/config/ConfigManager.py @@ -5,7 +5,7 @@ from new_collections import OrderedDict  from module.Api import InvalidConfigSection  from module.common.json_layer import json -from module.utils import from_string +from module.utils import from_string, primary_uid  from ConfigParser import ConfigParser @@ -57,7 +57,7 @@ class ConfigManager(ConfigParser):          else:              # We need the id and not the instance              # Will be None for admin user and so the same as internal access -            user = user.primary if user else None +            user = primary_uid(user)              try:                  # Check if this config exists                  # Configs without meta data can not be loaded! @@ -87,7 +87,7 @@ class ConfigManager(ConfigParser):              changed = self.parser.set(section, option, value, sync)          else:              # associated id -            user = user.primary if user else None +            user = primary_uid(user)              data = self.config[section].config[option]              value = from_string(value, data.type)              old_value = self.get(section, option) @@ -113,6 +113,22 @@ class ConfigManager(ConfigParser):          self.db.deleteConfig(section, user) +    def iterCoreSections(self): +        return self.parser.iterSections() -    def iterSections(self): -        pass
\ No newline at end of file +    def iterSections(self, user=None): +        """ Yields: section, metadata, values """ + +        user = primary_uid(user) +        values = self.db.loadConfigsForUser(user) + +        # Every section needs to be json decoded +        for section, data in values.items(): +            try: +                values[section] = json.loads(data) if data else {} +            except ValueError: +                values[section] = {} +                self.core.print_exc() + +        for name, config in self.config.iteritems(): +            yield name, config, values[name] if name in values else {} diff --git a/module/config/ConfigParser.py b/module/config/ConfigParser.py index 135e84bbc..2f974b75e 100644 --- a/module/config/ConfigParser.py +++ b/module/config/ConfigParser.py @@ -163,7 +163,10 @@ class ConfigParser:          return self.config[section].config[option]      def iterSections(self): -        return self.config.iteritems() +        """ Yields section, config info, values, for all sections """ + +        for name, config in self.config.iteritems(): +            yield name, config, self.values[name] if name in self.values else {}      def addConfigSection(self, section, name, desc, long_desc, config):          """Adds a section to the config. `config` is a list of config tuples as used in plugin api defined as: diff --git a/module/database/ConfigDatabase.py b/module/database/ConfigDatabase.py index 7dd5909b8..2e9fdd9a0 100644 --- a/module/database/ConfigDatabase.py +++ b/module/database/ConfigDatabase.py @@ -42,6 +42,16 @@ class ConfigMethods(DatabaseMethods):          return configs +    @queue +    def loadConfigsForUser(self, user=None): +        if user is None: user = -1 +        self.c.execute('SELECT plugin, config FROM settings WHERE user=?', (user,)) +        configs = {} +        for r in self.c: +            configs[r[0]] = r[1] + +        return configs +      @async      def clearAllConfigs(self):          self.c.execute('DELETE FROM settings') diff --git a/module/remote/json_converter.py b/module/remote/json_converter.py index ad7645ccc..5b85f30cd 100644 --- a/module/remote/json_converter.py +++ b/module/remote/json_converter.py @@ -11,6 +11,9 @@ import ttypes  from ttypes import BaseObject  from ttypes import ExceptionObject +# compact json separator +separators = (',', ':') +  # json encoder that accepts TBase objects  class BaseEncoder(json.JSONEncoder): @@ -34,6 +37,7 @@ def convert_obj(dct):  def dumps(*args, **kwargs):      kwargs['cls'] = BaseEncoder +    kwargs['separators'] = separators      return json.dumps(*args, **kwargs) diff --git a/module/remote/pyload.thrift b/module/remote/pyload.thrift index 514c3215c..077810ecb 100644 --- a/module/remote/pyload.thrift +++ b/module/remote/pyload.thrift @@ -246,11 +246,12 @@ struct ConfigHolder {  }  struct ConfigInfo { -  1: string name, +  1: string name    2: string label,    3: string description, -  4: bool saved, -  5: bool activated, +  4: bool addon, +  5: bool user_context, +  6: optional bool activated,  }  struct EventInfo { @@ -353,10 +354,16 @@ service Pyload {    ///////////////////////    map<string, ConfigHolder> getConfig(), -  list<ConfigInfo> getGlobalPlugins(), -  list<ConfigInfo> getUserPlugins(), +  string getConfigValue(1: string section, 2: string option), + +  // two methods with ambigous classification, could be configuration or addon related +  list<ConfigInfo> getCoreConfig(), +  list<ConfigInfo> getPluginConfig(), +  list<ConfigInfo> getAvailablePlugins(),    ConfigHolder configurePlugin(1: PluginName plugin), + +  void setConfigValue(1: string section, 2: string option, 3: string value),    void saveConfig(1: ConfigHolder config),    void deleteConfig(1: PluginName plugin),    void setConfigHandler(1: PluginName plugin, 2: InteractionID iid, 3: JSONString value), @@ -511,8 +518,8 @@ service Pyload {    // Addon Methods    /////////////////////// -  map<PluginName, list<AddonInfo>> getAllInfo(), -  list<AddonInfo> getInfoByPlugin(1: PluginName plugin), +  //map<PluginName, list<AddonInfo>> getAllInfo(), +  //list<AddonInfo> getInfoByPlugin(1: PluginName plugin),    map<PluginName, list<AddonService>> getAddonHandler(),    bool hasAddonHandler(1: PluginName plugin, 2: string func), diff --git a/module/remote/ttypes.py b/module/remote/ttypes.py index 0b9faea98..e8ef26dd2 100644 --- a/module/remote/ttypes.py +++ b/module/remote/ttypes.py @@ -138,16 +138,6 @@ class ConfigHolder(BaseObject):  		self.info = info  		self.handler = handler -class ConfigInfo(BaseObject): -	__slots__ = ['name', 'label', 'description', 'saved', 'activated'] - -	def __init__(self, name=None, label=None, description=None, saved=None, activated=None): -		self.name = name -		self.label = label -		self.description = description -		self.saved = saved -		self.activated = activated -  class ConfigItem(BaseObject):  	__slots__ = ['name', 'label', 'description', 'type', 'default_value', 'value'] @@ -282,6 +272,17 @@ class PackageStats(BaseObject):  		self.sizetotal = sizetotal  		self.sizedone = sizedone +class PluginInfo(BaseObject): +	__slots__ = ['name', 'label', 'description', 'addon', 'user_context', 'activated'] + +	def __init__(self, name=None, label=None, description=None, addon=None, user_context=None, activated=None): +		self.name = name +		self.label = label +		self.description = description +		self.addon = addon +		self.user_context = user_context +		self.activated = activated +  class ProgressInfo(BaseObject):  	__slots__ = ['plugin', 'name', 'statusmsg', 'eta', 'done', 'total', 'download'] @@ -413,14 +414,18 @@ class Iface(object):  		pass  	def getAllFiles(self):  		pass -	def getAllInfo(self): -		pass  	def getAllUserData(self):  		pass +	def getAvailablePlugins(self): +		pass  	def getCollector(self):  		pass  	def getConfig(self):  		pass +	def getConfigValue(self, section, option): +		pass +	def getCoreConfig(self): +		pass  	def getEvents(self, uuid):  		pass  	def getFileInfo(self, fid): @@ -431,10 +436,6 @@ class Iface(object):  		pass  	def getFilteredFiles(self, state):  		pass -	def getGlobalPlugins(self): -		pass -	def getInfoByPlugin(self, plugin): -		pass  	def getInteractionTask(self, mode):  		pass  	def getLog(self, offset): @@ -445,6 +446,8 @@ class Iface(object):  		pass  	def getPackageInfo(self, pid):  		pass +	def getPluginConfig(self): +		pass  	def getProgressInfo(self):  		pass  	def getServerStatus(self): @@ -453,8 +456,6 @@ class Iface(object):  		pass  	def getUserData(self):  		pass -	def getUserPlugins(self): -		pass  	def getWSAddress(self):  		pass  	def hasAddonHandler(self, plugin, func): @@ -499,6 +500,8 @@ class Iface(object):  		pass  	def setConfigHandler(self, plugin, iid, value):  		pass +	def setConfigValue(self, section, option, value): +		pass  	def setInteractionResult(self, iid, result):  		pass  	def setPackageFolder(self, pid, path): diff --git a/module/remote/ttypes_debug.py b/module/remote/ttypes_debug.py index 03d619dd4..17d602f97 100644 --- a/module/remote/ttypes_debug.py +++ b/module/remote/ttypes_debug.py @@ -10,7 +10,6 @@ classes = {  	'AddonInfo' : [basestring, basestring, basestring],  	'AddonService' : [basestring, basestring, (list, basestring), (None, int)],  	'ConfigHolder' : [basestring, basestring, basestring, basestring, (list, ConfigItem), (None, (list, AddonInfo)), (None, (list, InteractionTask))], -	'ConfigInfo' : [basestring, basestring, basestring, bool, bool],  	'ConfigItem' : [basestring, basestring, basestring, basestring, (None, basestring), basestring],  	'DownloadInfo' : [basestring, basestring, basestring, int, basestring, basestring],  	'DownloadProgress' : [int, int, int, int], @@ -24,6 +23,7 @@ classes = {  	'PackageDoesNotExists' : [int],  	'PackageInfo' : [int, basestring, basestring, int, int, basestring, basestring, basestring, int, (list, basestring), int, int, PackageStats, (list, int), (list, int)],  	'PackageStats' : [int, int, int, int], +	'PluginInfo' : [basestring, basestring, basestring, bool, bool, (None, bool)],  	'ProgressInfo' : [basestring, basestring, basestring, int, int, int, (None, DownloadProgress)],  	'ServerStatus' : [int, int, int, bool, bool, bool],  	'ServiceDoesNotExists' : [basestring, basestring], @@ -64,27 +64,27 @@ methods = {  	'getAccounts': (list, AccountInfo),  	'getAddonHandler': (dict, basestring, list),  	'getAllFiles': TreeCollection, -	'getAllInfo': (dict, basestring, list),  	'getAllUserData': (dict, int, UserData), +	'getAvailablePlugins': (list, PluginInfo),  	'getCollector': (list, LinkStatus),  	'getConfig': (dict, basestring, ConfigHolder), +	'getConfigValue': basestring, +	'getCoreConfig': (list, PluginInfo),  	'getEvents': (list, EventInfo),  	'getFileInfo': FileInfo,  	'getFileTree': TreeCollection,  	'getFilteredFileTree': TreeCollection,  	'getFilteredFiles': TreeCollection, -	'getGlobalPlugins': (list, ConfigInfo), -	'getInfoByPlugin': (list, AddonInfo),  	'getInteractionTask': InteractionTask,  	'getLog': (list, basestring),  	'getNotifications': (list, InteractionTask),  	'getPackageContent': TreeCollection,  	'getPackageInfo': PackageInfo, +	'getPluginConfig': (list, PluginInfo),  	'getProgressInfo': (list, ProgressInfo),  	'getServerStatus': ServerStatus,  	'getServerVersion': basestring,  	'getUserData': UserData, -	'getUserPlugins': (list, ConfigInfo),  	'getWSAddress': basestring,  	'hasAddonHandler': bool,  	'isInteractionWaiting': bool, @@ -107,6 +107,7 @@ methods = {  	'restartPackage': None,  	'saveConfig': None,  	'setConfigHandler': None, +	'setConfigValue': None,  	'setInteractionResult': None,  	'setPackageFolder': bool,  	'setPassword': bool, diff --git a/module/utils/__init__.py b/module/utils/__init__.py index 3adb695ee..4692c59cb 100644 --- a/module/utils/__init__.py +++ b/module/utils/__init__.py @@ -221,6 +221,10 @@ def get_index(l, value):      # Matches behavior of list.index      raise ValueError("list.index(x): x not in list") +def primary_uid(user): +    """ Gets primary user id for user instances or ints """ +    if type(user) == int: return user +    return user.primary if user else None  def html_unescape(text):      """Removes HTML or XML character references and entities from a text string""" diff --git a/module/web/api_app.py b/module/web/api_app.py index ff8d0134c..c0a7df528 100644 --- a/module/web/api_app.py +++ b/module/web/api_app.py @@ -46,7 +46,7 @@ def call_api(func, args=""):          kwargs[x] = unquote(y)      try: -        return callApi(func, *args, **kwargs) +        return callApi(api, func, *args, **kwargs)      except ExceptionObject, e:          return HTTPError(400, dumps(e))      except Exception, e: @@ -55,13 +55,13 @@ def call_api(func, args=""):  # TODO Better error codes on invalid input -def callApi(func, *args, **kwargs): +def callApi(api, func, *args, **kwargs):      if not hasattr(PYLOAD.EXTERNAL, func) or func.startswith("_"):          print "Invalid API call", func          return HTTPError(404, dumps("Not Found"))      # TODO: encoding -    result = getattr(PYLOAD, func)(*[loads(x) for x in args], +    result = getattr(api, func)(*[loads(x) for x in args],                                     **dict([(x, loads(y)) for x, y in kwargs.iteritems()]))      # null is invalid json response diff --git a/module/web/static/js/default.js b/module/web/static/js/default.js index 4436a85c1..bb9701935 100644 --- a/module/web/static/js/default.js +++ b/module/web/static/js/default.js @@ -1,5 +1,5 @@ -define('default', ['jquery', 'app', 'views/headerView', 'views/packageTreeView'], -    function($, App, HeaderView, TreeView) { +define('default', ['require', 'jquery', 'app', 'views/headerView', 'views/packageTreeView'], +    function(require, $, App, HeaderView, TreeView) {          App.init = function() {              App.header = new HeaderView(); @@ -13,5 +13,12 @@ define('default', ['jquery', 'app', 'views/headerView', 'views/packageTreeView']              });          }; +        App.initSettingsView = function() { +            require(['views/settingsView'], function(SettingsView) { +                App.settingsView = new SettingsView(); +                App.settingsView.render(); +            }); +        }; +          return App;      });
\ No newline at end of file diff --git a/module/web/static/js/views/settingsView.js b/module/web/static/js/views/settingsView.js new file mode 100644 index 000000000..f4e118233 --- /dev/null +++ b/module/web/static/js/views/settingsView.js @@ -0,0 +1,25 @@ +define(['jquery', 'underscore', 'backbone'], +    function($, _, Backbone) { + +        // Renders a single package item +        return Backbone.View.extend({ + +            el: "#content", +//            template: _.compile($("#template-package").html()), + +            events: { + +            }, + +            initialize: function() { +                $.ajax("/api/getCoreConfig"); +                $.ajax("/api/getPluginConfig"); +                $.ajax("/api/getAvailablePlugins"); +                console.log("Settings initialized"); +            }, + +            render: function() { +            } + +        }); +    });
\ No newline at end of file diff --git a/module/web/templates/default/settings.html b/module/web/templates/default/settings.html index 60676187b..591dbc30c 100644 --- a/module/web/templates/default/settings.html +++ b/module/web/templates/default/settings.html @@ -8,6 +8,10 @@      <link href="static/css/default/settings.less" rel="stylesheet/less" type="text/css" media="screen"/>
  {% endblock %}
 +{% block require %}
 +    App.initSettingsView();
 +{% endblock %}
 +
  {% block content %}
      <div class="container-fluid">
          <div class="row-fluid">
 diff --git a/tests/manager/test_configManager.py b/tests/manager/test_configManager.py index 1cfb6a5de..0fed702c1 100644 --- a/tests/manager/test_configManager.py +++ b/tests/manager/test_configManager.py @@ -27,6 +27,9 @@ class TestConfigManager(TestCase):      def setUp(self):          self.db.clearAllConfigs() +        # used by some tests, needs to be deleted +        self.config.delete("plugin", adminUser) +      def addConfig(self):          self.config.addConfigSection("plugin", "Name", "desc", "something", @@ -42,10 +45,12 @@ class TestConfigManager(TestCase):          d = self.db.loadAllConfigs()          assert d[0]["plugin"] == "some value" +        assert self.db.loadConfigsForUser(0)["plugin"] == "some value"          self.db.deleteConfig("plugin", 0)          assert 0 not in self.db.loadAllConfigs() +        assert "plugin" not in self.db.loadConfigsForUser(0)          self.db.deleteConfig("plugin") @@ -86,7 +91,26 @@ class TestConfigManager(TestCase):      def test_sections(self): -        pass +        self.addConfig() + +        i = 0 +        # there should be only one section, with no values +        for name, config, values in self.config.iterSections(adminUser): +            assert name == "plugin" +            assert values == {} +            i +=1 +        assert i == 1 + +        assert self.config.set("plugin", "value", True, user=adminUser) + +        i = 0 +        # now we assert the correct values +        for name, config, values in self.config.iterSections(adminUser): +            assert name == "plugin" +            assert values == {"value":True} +            i +=1 +        assert i == 1 +      @raises(InvalidConfigSection)      def test_restricted_access(self): diff --git a/tests/other/test_configparser.py b/tests/other/test_configparser.py index 51ffc5a68..09b686738 100644 --- a/tests/other/test_configparser.py +++ b/tests/other/test_configparser.py @@ -25,6 +25,11 @@ class TestConfigParser():          assert "general" in self.config          assert "foobaar" not in self.config +    def test_iter(self): +        for section, config, values in self.config.iterSections(): +            assert isinstance(section, basestring) +            assert isinstance(config.config, dict) +            assert isinstance(values, dict)      @raises(KeyError)      def test_invalid_config(self): | 
