From 60db5886259a0dff7d56bcf53be1bacc25461c20 Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 2 Mar 2014 02:18:13 +0100 Subject: [PATCH 1/6] added basic complex config support; still pretty simple approach; not ideal --- html/templates/modalFrames/configFrame.html | 44 ++++++----- html/templates/settings.html | 46 +++++------ xdm/classes.py | 85 ++++++++++++++++++--- 3 files changed, 121 insertions(+), 54 deletions(-) diff --git a/html/templates/modalFrames/configFrame.html b/html/templates/modalFrames/configFrame.html index 6fa4563..f511975 100644 --- a/html/templates/modalFrames/configFrame.html +++ b/html/templates/modalFrames/configFrame.html @@ -23,28 +23,30 @@
{{plugin.screenName}}
{% if 'select' in config.name %} - + {% else %} - {% if config.curType() == 'int' and 'enable' not in config.name -%} - {% set inputType = 'number" step="any'%} - {% elif config.curType() == 'bool' or 'enable' in config.name%} - {% if config.value %} - - {# this is a little dodgy. we append the checked attr with manipulating the string that is set for the input type#} - {% set inputType = 'checkbox" checked="checked'%} - {% else %} - {% set inputType = 'checkbox'%} - {% endif %} - - {% elif 'password' in config.name %} - {% set inputType = 'password'%} - {% else %} - {% set inputType = 'text'%} - {%- endif %} + {% if config.curType() == 'complex' -%} + gommblex + {% elif config.curType() == 'int' and 'enable' not in config.name %} + {% set inputType = 'number" step="any'%} + {% elif config.curType() == 'bool' or 'enable' in config.name%} + {% if config.value %} + + {# this is a little dodgy. we append the checked attr with manipulating the string that is set for the input type#} + {% set inputType = 'checkbox" checked="checked'%} + {% else %} + {% set inputType = 'checkbox'%} + {% endif %} + + {% elif 'password' in config.name %} + {% set inputType = 'password'%} + {% else %} + {% set inputType = 'text'%} + {%- endif %} {% endif %}
diff --git a/html/templates/settings.html b/html/templates/settings.html index 59d176d..8d7f2ba 100644 --- a/html/templates/settings.html +++ b/html/templates/settings.html @@ -63,29 +63,31 @@

{{ plugin.screenName }}{% if plugin._type != 'MediaTypeManager'%} ({{ plugin
{% if 'select' in config.name %} - + {% else %} - {% if config.curType() == 'int' and 'enable' not in config.name -%} - {% set inputType = 'number" step="any'%} - {% elif config.curType() == 'bool' or 'enable' in config.name%} - {% if config.value %} - - {# this is a little dodgy. we append the checked attr with manipulating the string that is set for the input type#} - {% set inputType = 'checkbox" checked="checked'%} - {% else %} - {% set inputType = 'checkbox'%} - {% endif %} - - {% elif 'password' in config.name %} - {% set inputType = 'password'%} - {% else %} - {% set inputType = 'text'%} - {%- endif %} - + {% if config.curType() == 'complex' -%} + gommblex + {% elif config.curType() == 'int' and 'enable' not in config.name %} + {% set inputType = 'number" step="any'%} + {% elif config.curType() == 'bool' or 'enable' in config.name%} + {% if config.value %} + + {# this is a little dodgy. we append the checked attr with manipulating the string that is set for the input type#} + {% set inputType = 'checkbox" checked="checked'%} + {% else %} + {% set inputType = 'checkbox'%} + {% endif %} + + {% elif 'password' in config.name %} + {% set inputType = 'password'%} + {% else %} + {% set inputType = 'text'%} + {%- endif %} + {% endif %}
diff --git a/xdm/classes.py b/xdm/classes.py index f025d71..589e226 100644 --- a/xdm/classes.py +++ b/xdm/classes.py @@ -787,6 +787,8 @@ def _get_value(self): return datetime.replace(tzinfo=None) else: return self._value_datetime + #elif self._value_data != None: + # return self._value_data else: return self._value_char @@ -838,6 +840,37 @@ def __cmp__(self, other): return -1 return 1 +class ComplexDataType(object): + value = None + def __init__(self, **kwargs): + for arg, value in kwargs.iteritems(): + if not arg.startswith("__"): + setattr(self, arg, value) + + def dump(self): + base = dict(self.__class__.__dict__) + base.update({"_className_": self.__class__.__name__}) + base.update(dict(self.__dict__)) + return json.dumps(base) + + @staticmethod + def load(*data, **po): + if "_className_" in data: + cls = data.pop("_className_") + return locals()[cls](**data) + return data + +class ListType(ComplexDataType): + multiple = False + selected = None + +class Select(ListType): + pass + +class MultiSelect(ListType): + multiple = True + selected = [] + class Config(BaseModel): module = CharField(default='system') # system, plugin ... you know this kind of thing @@ -850,11 +883,16 @@ class Config(BaseModel): _value_int = FloatField(True) _value_char = CharField(True) _value_bool = BooleanField(True) + _value_data = TextField(True) class Meta: database = xdm.CONFIG_DATABASE order_by = ('name',) + @classmethod + def _migrate(cls): + return cls._migrateNewField(cls._value_data) + def copy(self): new = Config() new.module = self.module @@ -874,25 +912,48 @@ def _get_value(self): if float(self._value_int).is_integer(): return int(self._value_int) return self._value_int - else: - return unicode(self._value_char) + elif self._value_data != None: + # try json decoding, based on a quick stupid check + try: + return json.loads(self._value_data, object_hook=ComplexDataType.load) + except ValueError, e: + raise Exception("couldn't decode value for config %s, value: %s; error: %s" % (self.name, self._value_data, e)) + return unicode(self._value_char) def _set_value(self, value): - if type(value).__name__ in ('float', 'int'): - self._value_char = None - self._value_bool = None + _type = type(value).__name__ + self._value_char = None + self._value_bool = None + self._value_data = None + self._value_int = None + + if _type in ('float', 'int'): self._value_int = value return - if type(value).__name__ in ('str', 'unicode'): - self._value_bool = None - self._value_int = None + if _type in ('str', 'unicode'): self._value_char = value return - if type(value).__name__ in ('bool', 'NoneType'): - self._value_char = None - self._value_int = None + if _type in ('bool', 'NoneType'): self._value_bool = value return + + if _type == "dict": + if not "selected" in value or not "options" in value: + raise Exception("need to supply 'options' and 'selected' to a ComplexType in config") + + obj = None + selType = type(value["selected"]) + + # single selection + if selType in types.StringTypes: + cls = Select + # multi selection + elif selType in (types.ListType, types.TupleType): + cls = MultiSelect + + self._value_data = cls(**value).dump() + return + raise Exception('unknown config save type %s for config %s' % (type(value), self.name)) value = property(_get_value, _set_value) @@ -902,6 +963,8 @@ def curType(self): return 'bool' elif self._value_int: return 'int' + elif self._value_data: + return "complex" else: return 'str' From 435e1cd749c8154d98d24800cf149e7f87551c74 Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 2 Mar 2014 06:32:33 +0100 Subject: [PATCH 2/6] some stupidity fixes; make selected a property; remove locals() which was stupid; don't save internals as json --- xdm/classes.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/xdm/classes.py b/xdm/classes.py index 589e226..261148f 100644 --- a/xdm/classes.py +++ b/xdm/classes.py @@ -844,33 +844,41 @@ class ComplexDataType(object): value = None def __init__(self, **kwargs): for arg, value in kwargs.iteritems(): - if not arg.startswith("__"): - setattr(self, arg, value) + setattr(self, arg, value) def dump(self): base = dict(self.__class__.__dict__) base.update({"_className_": self.__class__.__name__}) base.update(dict(self.__dict__)) - return json.dumps(base) + + # drop internals, we currently don't need them saved + dumped = dict((key, base[key]) for key in [key for key in base.keys() if not key.startswith("__") and not key.endswith("__")]) + return json.dumps(dumped) @staticmethod - def load(*data, **po): + def load(data): if "_className_" in data: cls = data.pop("_className_") - return locals()[cls](**data) + return complexDataTypes[cls](**data) return data class ListType(ComplexDataType): multiple = False - selected = None + + def _get_selected(self): + return self.value + def _set_selected(self, value): + self.value = value + + selected = property(_get_selected, _set_selected) class Select(ListType): pass class MultiSelect(ListType): multiple = True - selected = [] +complexDataTypes = dict([(cls.__name__, cls) for cls in (Select, MultiSelect)]) class Config(BaseModel): module = CharField(default='system') # system, plugin ... you know this kind of thing @@ -913,7 +921,7 @@ def _get_value(self): return int(self._value_int) return self._value_int elif self._value_data != None: - # try json decoding, based on a quick stupid check + # try json decoding, based on complex data types try: return json.loads(self._value_data, object_hook=ComplexDataType.load) except ValueError, e: From 7dff7f189a32ce583614cbbbd7b651dadf944f0a Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 2 Mar 2014 07:08:43 +0100 Subject: [PATCH 3/6] fix template to use new complex data types if given --- html/templates/modalFrames/configFrame.html | 10 +++++++--- html/templates/settings.html | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/html/templates/modalFrames/configFrame.html b/html/templates/modalFrames/configFrame.html index f511975..f35d5ee 100644 --- a/html/templates/modalFrames/configFrame.html +++ b/html/templates/modalFrames/configFrame.html @@ -28,10 +28,14 @@
{{plugin.screenName}}
{% endfor %} + {% elif config.curType() == 'complex' %} + {% else %} - {% if config.curType() == 'complex' -%} - gommblex - {% elif config.curType() == 'int' and 'enable' not in config.name %} + {% if config.curType() == 'int' and 'enable' not in config.name %} {% set inputType = 'number" step="any'%} {% elif config.curType() == 'bool' or 'enable' in config.name%} {% if config.value %} diff --git a/html/templates/settings.html b/html/templates/settings.html index 8d7f2ba..d79c65b 100644 --- a/html/templates/settings.html +++ b/html/templates/settings.html @@ -68,10 +68,14 @@

{{ plugin.screenName }}{% if plugin._type != 'MediaTypeManager'%} ({{ plugin {% endfor %} + {% elif config.curType() == 'complex' %} + {% else %} - {% if config.curType() == 'complex' -%} - gommblex - {% elif config.curType() == 'int' and 'enable' not in config.name %} + {% if config.curType() == 'int' and 'enable' not in config.name -%} {% set inputType = 'number" step="any'%} {% elif config.curType() == 'bool' or 'enable' in config.name%} {% if config.value %} From 31e49bba64dca03112425b7a2ad8c17e688bddbd Mon Sep 17 00:00:00 2001 From: panni Date: Mon, 3 Mar 2014 05:27:20 +0100 Subject: [PATCH 4/6] support complex data types; uses select multiple or multiple checkboxes for the moment; implementation is hacky as is the whole form handling --- html/templates/modalFrames/configFrame.html | 31 +++++++++- html/templates/settings.html | 31 +++++++++- xdm/classes.py | 63 +++++++++++++++++++-- xdm/helper.py | 14 ++++- xdm/web/ajax.py | 9 ++- 5 files changed, 131 insertions(+), 17 deletions(-) diff --git a/html/templates/modalFrames/configFrame.html b/html/templates/modalFrames/configFrame.html index f35d5ee..7dd2689 100644 --- a/html/templates/modalFrames/configFrame.html +++ b/html/templates/modalFrames/configFrame.html @@ -29,11 +29,36 @@
{{plugin.screenName}}
{% endfor %} {% elif config.curType() == 'complex' %} - + -- + + {% endif %} {% for v in config.value.options %} - + {% endfor %} - + {% else %} + + {% endif %} {% else %} {% if config.curType() == 'int' and 'enable' not in config.name %} {% set inputType = 'number" step="any'%} diff --git a/html/templates/settings.html b/html/templates/settings.html index d79c65b..adb8fd3 100644 --- a/html/templates/settings.html +++ b/html/templates/settings.html @@ -69,11 +69,36 @@

{{ plugin.screenName }}{% if plugin._type != 'MediaTypeManager'%} ({{ plugin {% endfor %} {% elif config.curType() == 'complex' %} - + -- + + {% endif %} {% for v in config.value.options %} - + {% endfor %} - + {% else %} + + {% endif %} {% else %} {% if config.curType() == 'int' and 'enable' not in config.name -%} {% set inputType = 'number" step="any'%} diff --git a/xdm/classes.py b/xdm/classes.py index 261148f..c5df3bc 100644 --- a/xdm/classes.py +++ b/xdm/classes.py @@ -842,17 +842,33 @@ def __cmp__(self, other): class ComplexDataType(object): value = None + _skipStorage = ("_skipStorage",) # don't store those attributes in the database (most likely class attributes, not settings) + def __init__(self, **kwargs): for arg, value in kwargs.iteritems(): setattr(self, arg, value) + # recursively merge self._skipStorage + #fixme:; may be used for self._config inheritance later on + ss = [] + for parentClass in self.__class__.__mro__[:-1]: + ss += list(parentClass._skipStorage) + self._skipStorage = ss + + def __eq__(self, other): + """ + the data of this instance equals other? implement! + """ + raise NotImplementedError + def dump(self): base = dict(self.__class__.__dict__) base.update({"_className_": self.__class__.__name__}) base.update(dict(self.__dict__)) # drop internals, we currently don't need them saved - dumped = dict((key, base[key]) for key in [key for key in base.keys() if not key.startswith("__") and not key.endswith("__")]) + dumped = dict((key, base[key]) for key in [key for key in base.keys() if not key.startswith("__") and not key.endswith("__")\ + and key not in self._skipStorage]) return json.dumps(dumped) @staticmethod @@ -864,7 +880,18 @@ def load(data): class ListType(ComplexDataType): multiple = False - + required = False #fixme: handle this if true + _options = None + + def __eq__(self, other): + """ + compares self.selected to other["selected"], expects other to be a dict + """ + if not type(other) == types.DictType or ("selected" in other and not type(other["selected"] == types.ListType)): + raise ValueError + return self.selected == other["selected"] + + # self.value holds the selected value list def _get_selected(self): return self.value def _set_selected(self, value): @@ -872,11 +899,27 @@ def _set_selected(self, value): selected = property(_get_selected, _set_selected) + # self._options holds the available options + def _get_options(self): + return self._options + + def _set_options(self, value): + self._options = value + + options = property(_get_options, _set_options) + class Select(ListType): pass class MultiSelect(ListType): multiple = True + use_checkboxes = False # use multiple checkboxes instead of select[type=multiple]; can be True or "inline" + + #def __init__(self, **kwargs): + # super(MultiSelect, self).__init__(**kwargs) + # + # # inherit skipStorage; may be done automatically i guess + # self._skipStorage = list(super(MultiSelect, self)._skipStorage) + list(self._skipStorage) complexDataTypes = dict([(cls.__name__, cls) for cls in (Select, MultiSelect)]) @@ -930,24 +973,28 @@ def _get_value(self): def _set_value(self, value): _type = type(value).__name__ + + oldValueData = self.value if self._value_data else None self._value_char = None self._value_bool = None - self._value_data = None self._value_int = None + self._value_data = None if _type in ('float', 'int'): self._value_int = value return + if _type in ('str', 'unicode'): self._value_char = value return + if _type in ('bool', 'NoneType'): self._value_bool = value return if _type == "dict": - if not "selected" in value or not "options" in value: - raise Exception("need to supply 'options' and 'selected' to a ComplexType in config") + if not "selected" in value: + raise Exception("need to supply 'selected' to a ComplexType in config") obj = None selType = type(value["selected"]) @@ -959,6 +1006,12 @@ def _set_value(self, value): elif selType in (types.ListType, types.TupleType): cls = MultiSelect + if not "options" in value: + # "options" is missing, we're coming from a form which only supplies the selected values. reuse old config instance and update it + oldValueData.selected = value["selected"] + self._value_data = oldValueData.dump() + return + self._value_data = cls(**value).dump() return diff --git a/xdm/helper.py b/xdm/helper.py index a8c86fe..b1545b7 100644 --- a/xdm/helper.py +++ b/xdm/helper.py @@ -30,6 +30,7 @@ from xdm import common import xdm import shutil +import types import base64 import random @@ -305,8 +306,19 @@ def sameElements(a, b): return False return True -def convertV(cur_v): +def convertV(cur_v, _type): """helper function to convert kwargs to python typed values""" + + # this is a pretty nasty hack around the weird handling below to support lists which do not consist of ["on", "off"] (enabled checkbox). + # it basically is a multiselect scenario + #fixme: rewrite the whole area + if _type == "complex": + if cur_v != "__none__": + cur_v = [cur_v] if type(cur_v) != types.ListType else cur_v + else: + cur_v = [] + + return {"selected": cur_v} try: f = float(cur_v) if f.is_integer(): diff --git a/xdm/web/ajax.py b/xdm/web/ajax.py index a0b2583..2f316ff 100755 --- a/xdm/web/ajax.py +++ b/xdm/web/ajax.py @@ -383,8 +383,6 @@ def _save(self, **kwargs): except UnicodeDecodeError: k = k.decode('latin-1') # for some obscure reason cherrypy (i think) encodes param key into latin-1 some times k = k.encode('utf-8') # but i like utf-8 - # print k, repr(k) - # print v, repr(v) log(u"config K:%s V:%s" % (k, v)) parts = k.split('-') @@ -396,6 +394,7 @@ def _save(self, **kwargs): class_name = parts[0] instance_name = parts[1] config_name = parts[2] + config_type = parts[3] if len(parts) > 3 else None _cacheName = "%s %s" % (class_name, instance_name) if _cacheName in _plugin_cache: plugin = _plugin_cache[_cacheName] @@ -404,10 +403,10 @@ def _save(self, **kwargs): _plugin_cache[_cacheName] = plugin if plugin: log(u"We have a plugin: %s (%s)" % (class_name, instance_name)) - new_value = helper.convertV(v) + new_value = helper.convertV(v, config_type) if element is None: # normal settings page old_value = getattr(plugin.c, config_name) - new_value = helper.convertV(v) + new_value = helper.convertV(v, config_type) if old_value == new_value: continue if element is not None: # we got an element id so its an element config @@ -419,7 +418,7 @@ def _save(self, **kwargs): cur_c.value = new_value cur_c.save() else: # normal settings page - setattr(plugin.c, config_name, convertV(v)) # saving value + setattr(plugin.c, config_name, convertV(v, config_type)) # saving value if plugin.config_meta[config_name] and element is None: # this returns none if 'on_change_actions' in plugin.config_meta[config_name] and old_value != new_value: From 78fa4e9185cded0609bf5b2a126acb368f06ed4a Mon Sep 17 00:00:00 2001 From: panni Date: Mon, 3 Mar 2014 05:31:05 +0100 Subject: [PATCH 5/6] remove obsolete code --- xdm/classes.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/xdm/classes.py b/xdm/classes.py index c5df3bc..a7b3524 100644 --- a/xdm/classes.py +++ b/xdm/classes.py @@ -915,12 +915,6 @@ class MultiSelect(ListType): multiple = True use_checkboxes = False # use multiple checkboxes instead of select[type=multiple]; can be True or "inline" - #def __init__(self, **kwargs): - # super(MultiSelect, self).__init__(**kwargs) - # - # # inherit skipStorage; may be done automatically i guess - # self._skipStorage = list(super(MultiSelect, self)._skipStorage) + list(self._skipStorage) - complexDataTypes = dict([(cls.__name__, cls) for cls in (Select, MultiSelect)]) class Config(BaseModel): From 13ae20b7415c798a3c602f3ea6ff2faede8b28be Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 9 Mar 2014 05:13:27 +0100 Subject: [PATCH 6/6] append NoneType to convertV call; fixes Getcategories on Newznab; fixme? --- xdm/api/__init__.py | 2 +- xdm/web/ajax.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/xdm/api/__init__.py b/xdm/api/__init__.py index e6127b9..70c4dd0 100644 --- a/xdm/api/__init__.py +++ b/xdm/api/__init__.py @@ -94,7 +94,7 @@ def rest(self, *args, **kwargs): for name in p_function.args: log('function %s needs %s' % (method, name)) if name in kwargs: - fn_args.append(convertV(kwargs[name])) + fn_args.append(convertV(kwargs[name], None)) #fixme: None correct? try: log("calling %s with %s" % (p_function, fn_args)) diff --git a/xdm/web/ajax.py b/xdm/web/ajax.py index 2f316ff..3fb8d8b 100755 --- a/xdm/web/ajax.py +++ b/xdm/web/ajax.py @@ -46,7 +46,7 @@ def _globals(self): @cherrypy.expose def pluginCall(self, **kwargs): - log("Plugin ajay call with: %s" % kwargs) + log("Plugin ajax call with: %s" % kwargs) p_type = kwargs['p_type'] p_instance = kwargs['p_instance'] action = kwargs['action'] @@ -58,13 +58,14 @@ def pluginCall(self, **kwargs): log('function %s needs %s' % (action, name)) field_name = 'field_%s' % name if field_name in kwargs: - fn_args.append(convertV(kwargs[field_name])) + fn_args.append(convertV(kwargs[field_name], None)) #fixme: None correct? continue else: log("Field '%s' not found in kwargs. tring array" % field_name) field_name = 'field_%s[]' % name if field_name in kwargs: - fn_args.append(convertV(kwargs[field_name])) + fn_args.append(convertV(kwargs[field_name], None)) #fixme: None correct? + else: log.warning("Field %s not found in kwargs. this will probably not work out" % field_name)