diff options
-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): |