From e01d456e7b924aba7d6a81363174d324a95905c8 Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Sat, 7 Jul 2012 14:48:21 +0200 Subject: [PATCH] clean up the config app. Use jsonfield to save the config values. Syncdb necessary. --- openslides/config/forms.py | 60 +++++++++++--- openslides/config/models.py | 85 +++++++++---------- openslides/config/signals.py | 2 +- openslides/config/urls.py | 9 +-- openslides/config/views.py | 46 ++++++----- openslides/main.py | 11 +-- openslides/utils/jsonfield/README | 13 +++ openslides/utils/jsonfield/__init__.py | 1 + openslides/utils/jsonfield/fields.py | 75 +++++++++++++++++ openslides/utils/jsonfield/models.py | 1 + openslides/utils/jsonfield/tests.py | 108 +++++++++++++++++++++++++ 11 files changed, 321 insertions(+), 90 deletions(-) create mode 100644 openslides/utils/jsonfield/README create mode 100644 openslides/utils/jsonfield/__init__.py create mode 100644 openslides/utils/jsonfield/fields.py create mode 100644 openslides/utils/jsonfield/models.py create mode 100644 openslides/utils/jsonfield/tests.py diff --git a/openslides/config/forms.py b/openslides/config/forms.py index bc46e6412..54e8c7187 100644 --- a/openslides/config/forms.py +++ b/openslides/config/forms.py @@ -10,19 +10,59 @@ :license: GNU GPL, see LICENSE for more details. """ -from django.forms import Form, CharField, TextInput, BooleanField, IntegerField, ChoiceField, Textarea, Select +from django import forms from utils.forms import CssClassMixin from models import config from utils.translation_ext import ugettext as _ -class GeneralConfigForm(Form, CssClassMixin): - event_name = CharField(widget=TextInput(),label=_("Event name"), max_length=30) - event_description = CharField(widget=TextInput(),label=_("Short description of event"), max_length=100, required=False) - event_date = CharField(widget=TextInput(), required=False, label=_("Event date")) - event_location = CharField(widget=TextInput(), required=False, label=_("Event location")) - event_organizer = CharField(widget=TextInput(), required=False, label=_("Event organizer")) - system_enable_anonymous = BooleanField(required=False, label=_("Allow access for anonymous guest users") ) - frontpage_title = CharField(widget=TextInput(), required=False, label=_("Title") ) - frontpage_welcometext = CharField(widget=Textarea(), required=False, label=_("Welcome text") ) +class GeneralConfigForm(forms.Form, CssClassMixin): + event_name = forms.CharField( + widget=forms.TextInput(), + label=_("Event name"), + max_length=30, + ) + + event_description = forms.CharField( + widget=forms.TextInput(), + label=_("Short description of event"), + required=False, + max_length=100, + + ) + + event_date = forms.CharField( + widget=forms.TextInput(), + label=_("Event date"), + required=False, + ) + + event_location = forms.CharField( + widget=forms.TextInput(), + label=_("Event location"), + required=False, + ) + + event_organizer = forms.CharField( + widget=forms.TextInput(), + label=_("Event organizer"), + required=False, + ) + + system_enable_anonymous = forms.BooleanField( + label=_("Allow access for anonymous guest users"), + required=False, + ) + + frontpage_title = forms.CharField( + widget=forms.TextInput(), + label=_("Title"), + required=False, + ) + + frontpage_welcometext = forms.CharField( + widget=forms.Textarea(), + label=_("Welcome text"), + required=False, + ) diff --git a/openslides/config/models.py b/openslides/config/models.py index 85f620f70..726bc1191 100644 --- a/openslides/config/models.py +++ b/openslides/config/models.py @@ -9,21 +9,26 @@ :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ -from pickle import dumps, loads -import base64 +from django.conf import settings +from django.core.urlresolvers import reverse from django.db import models from django.dispatch import receiver +from django.utils.importlib import import_module +from django.utils.translation import ugettext as _, ugettext_noop -from utils.translation_ext import ugettext as _ +from openslides.utils.jsonfield import JSONField +from openslides.utils.signals import template_manipulation from openslides.config.signals import default_config_value -import settings class ConfigStore(models.Model): + """ + Stores the config values. + """ key = models.CharField(max_length=100, primary_key=True) - value = models.TextField() + value = JSONField() def __unicode__(self): return self.key @@ -31,41 +36,23 @@ class ConfigStore(models.Model): class Meta: verbose_name = 'config' permissions = ( - ('can_manage_config', _("Can manage configuration", fixstr=True)), + ('can_manage_config', ugettext_noop("Can manage configuration")), ) -# TODO: -# I used base64 to save pickled Data, there has to be another way see: -# http://stackoverflow.com/questions/2524970/djangounicodedecodeerror-while-storing-pickled-data - class Config(object): - def load_config(self): - self.config = {} - for key, value in ConfigStore.objects.all().values_list(): - self.config[key] = loads(base64.decodestring(str(value))) - + """ + Access the config values via config[...] + """ def __getitem__(self, key): - # Had to be deactivated, because in more than one thread the values have - # to be loaded on each request. - ## try: - ## self.config - ## except AttributeError: - ## self.load_config() - ## try: - ## return self.config[key] - ## except KeyError: - ## pass - try: - return loads(base64.decodestring(str(ConfigStore.objects.get(key=key).value))) + return ConfigStore.objects.get(key=key).value except ConfigStore.DoesNotExist: pass - for receiver, value in default_config_value.send(sender='config', key=key): + for receiver, value in default_config_value.send(sender='config', + key=key): if value is not None: -# if settings.DEBUG: -# print 'Using default for %s' % key return value if settings.DEBUG: print "No default value for: %s" % key @@ -76,22 +63,25 @@ class Config(object): c = ConfigStore.objects.get(pk=key) except ConfigStore.DoesNotExist: c = ConfigStore(pk=key) - c.value = base64.encodestring(dumps(value)) + c.value = value c.save() - try: - self.config[key] = value - except AttributeError: - self.load_config() - self.config[key] = value + + def __contains__(self, item): + return ConfigStore.objects.filter(key=item).exists() + config = Config() @receiver(default_config_value, dispatch_uid="config_default_config") def default_config(sender, key, **kwargs): + """ + Global default values. + """ return { 'event_name': 'OpenSlides', - 'event_description': _('Presentation system for agenda, applications and elections'), + 'event_description': + _('Presentation system for agenda, applications and elections'), 'event_date': '', 'event_location': '', 'event_organizer': '', @@ -99,25 +89,23 @@ def default_config(sender, key, **kwargs): 'frontpage_title': _('Welcome'), 'frontpage_welcometext': _('Welcome to OpenSlides!'), 'show_help_text': True, - 'help_text': _("Get professional support for OpenSlides on %s.") % "www.openslides.org", + 'help_text': _("Get professional support for OpenSlides on %s.") % + " \ + www.openslides.org", 'system_enable_anonymous': False, }.get(key) -from django.dispatch import receiver -from django.core.urlresolvers import reverse -from django.utils.importlib import import_module -import settings - -from openslides.utils.signals import template_manipulation - - @receiver(template_manipulation, dispatch_uid="config_submenu") def set_submenu(sender, request, context, **kwargs): + """ + Submenu for the config tab. + """ if not request.path.startswith('/config/'): return None menu_links = [ - (reverse('config_general'), _('General'), request.path == reverse('config_general') ), + (reverse('config_general'), _('General'), + request.path == reverse('config_general')), ] for app in settings.INSTALLED_APPS: @@ -139,7 +127,8 @@ def set_submenu(sender, request, context, **kwargs): ) menu_links.append ( - (reverse('config_version'), _('Version'), request.path == reverse('config_version') ) + (reverse('config_version'), _('Version'), + request.path == reverse('config_version')) ) context.update({ diff --git a/openslides/config/signals.py b/openslides/config/signals.py index c0418c290..7cdc7c7e9 100644 --- a/openslides/config/signals.py +++ b/openslides/config/signals.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ openslides.config.signals - ~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~~~~~~~~~~~~~~~~~~~~~~~~~ Defines Signals for the config. diff --git a/openslides/config/urls.py b/openslides/config/urls.py index e873bc9f6..727fafadb 100644 --- a/openslides/config/urls.py +++ b/openslides/config/urls.py @@ -10,14 +10,13 @@ :license: GNU GPL, see LICENSE for more details. """ -from django.conf.urls.defaults import * +from django.conf import settings +from django.conf.urls.defaults import patterns, url from django.utils.importlib import import_module -import settings +from openslides.config.views import GeneralConfig, VersionConfig -from views import GeneralConfig, VersionConfig - -urlpatterns = patterns('config.views', +urlpatterns = patterns('', url(r'^general/$', GeneralConfig.as_view(), name='config_general', diff --git a/openslides/config/views.py b/openslides/config/views.py index 3daf008cc..749d7dae0 100644 --- a/openslides/config/views.py +++ b/openslides/config/views.py @@ -10,27 +10,26 @@ :license: GNU GPL, see LICENSE for more details. """ -from django.shortcuts import redirect -from django.core.urlresolvers import reverse +from django.conf import settings from django.contrib import messages from django.contrib.auth.models import Group, Permission -from django.utils.translation import ugettext as _ -from django.template.loader import render_to_string +from django.core.urlresolvers import reverse from django.utils.importlib import import_module -import settings +from django.utils.translation import ugettext as _ from openslides import get_version -from utils.utils import template, permission_required -from utils.views import FormView, TemplateView -from utils.template import Tab +from openslides.utils.template import Tab +from openslides.utils.views import FormView, TemplateView -from forms import GeneralConfigForm - -from models import config +from openslides.config.forms import GeneralConfigForm +from openslides.config.models import config class GeneralConfig(FormView): + """ + Gereral config values. + """ permission_required = 'config.can_manage_config' form_class = GeneralConfigForm template_name = 'config/general.html' @@ -57,7 +56,8 @@ class GeneralConfig(FormView): # frontpage config['frontpage_title'] = form.cleaned_data['frontpage_title'] - config['frontpage_welcometext'] = form.cleaned_data['frontpage_welcometext'] + config['frontpage_welcometext'] = \ + form.cleaned_data['frontpage_welcometext'] # system if form.cleaned_data['system_enable_anonymous']: @@ -66,21 +66,29 @@ class GeneralConfig(FormView): try: anonymous = Group.objects.get(name='Anonymous') except Group.DoesNotExist: - default_perms = [u'can_see_agenda', u'can_see_projector', u'can_see_application'] + default_perms = [u'can_see_agenda', u'can_see_projector', + u'can_see_application'] anonymous = Group() anonymous.name = 'Anonymous' anonymous.save() - anonymous.permissions = Permission.objects.filter(codename__in=default_perms) + anonymous.permissions = Permission.objects.filter( + codename__in=default_perms) anonymous.save() - messages.success(self.request, _('Anonymous access enabled. Please modify the "Anonymous" group to fit your required permissions.')) + messages.success(self.request, + _('Anonymous access enabled. Please modify the "Anonymous" \ + group to fit your required permissions.')) else: config['system_enable_anonymous'] = False - messages.success(self.request, _('General settings successfully saved.')) + messages.success(self.request, + _('General settings successfully saved.')) return super(GeneralConfig, self).form_valid(form) class VersionConfig(TemplateView): + """ + Show version infos. + """ permission_required = 'config.can_manage_config' template_name = 'config/version.html' @@ -99,12 +107,14 @@ class VersionConfig(TemplateView): plugin_name = mod.__name__.split('.')[0] context['versions'].append((plugin_name, plugin_version)) - return context def register_tab(request): - selected = True if request.path.startswith('/config/') else False + """ + Register the config tab. + """ + selected = request.path.startswith('/config/') return Tab( title=_('Configuration'), url=reverse('config_general'), diff --git a/openslides/main.py b/openslides/main.py index aafe02c7e..192d826fd 100644 --- a/openslides/main.py +++ b/openslides/main.py @@ -61,7 +61,7 @@ def main(argv = None): if opts.reset_admin: create_or_reset_admin_user() - # NOTE: --insecure is needed so static files will be served if + # NOTE: --insecure is needed so static files will be served if # DEBUG is set to False argv = ["", "runserver", "--noreload", "--insecure"] if opts.nothread: @@ -186,13 +186,8 @@ def set_system_url(url): from openslides.config.models import config key = "participant_pdf_system_url" - try: - if key in config.config: - return - except AttributeError: - config.load_config() - if key in config.config: - return + if key in config: + return config[key] = url diff --git a/openslides/utils/jsonfield/README b/openslides/utils/jsonfield/README new file mode 100644 index 000000000..74113188c --- /dev/null +++ b/openslides/utils/jsonfield/README @@ -0,0 +1,13 @@ +https://github.com/bradjasper/django-jsonfield + +django-jsonfield is a reusable django field that allows you to store validated JSON in your model. + +It silently takes care of serialization. To use, simple add the field to one of your models. + +=== + +from django.db import models +from jsonfield import JSONField + +class MyModel(models.Model): +json = JSONField() diff --git a/openslides/utils/jsonfield/__init__.py b/openslides/utils/jsonfield/__init__.py new file mode 100644 index 000000000..b8d87c831 --- /dev/null +++ b/openslides/utils/jsonfield/__init__.py @@ -0,0 +1 @@ +from fields import JSONField \ No newline at end of file diff --git a/openslides/utils/jsonfield/fields.py b/openslides/utils/jsonfield/fields.py new file mode 100644 index 000000000..029ca74db --- /dev/null +++ b/openslides/utils/jsonfield/fields.py @@ -0,0 +1,75 @@ +from django.db import models +from django.core.serializers.json import DjangoJSONEncoder +from django.utils import simplejson as json +from django.utils.translation import ugettext_lazy as _ + +from django.forms.fields import Field +from django.forms.util import ValidationError as FormValidationError + +class JSONFormField(Field): + def clean(self, value): + + if not value and not self.required: + return None + + value = super(JSONFormField, self).clean(value) + + if isinstance(value, basestring): + try: + json.loads(value) + except ValueError: + raise FormValidationError(_("Enter valid JSON")) + return value + +class JSONField(models.TextField): + """JSONField is a generic textfield that serializes/unserializes JSON objects""" + + # Used so to_python() is called + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + self.dump_kwargs = kwargs.pop('dump_kwargs', {'cls': DjangoJSONEncoder}) + self.load_kwargs = kwargs.pop('load_kwargs', {}) + + super(JSONField, self).__init__(*args, **kwargs) + + def to_python(self, value): + """Convert string value to JSON""" + if isinstance(value, basestring): + try: + return json.loads(value, **self.load_kwargs) + except ValueError: + pass + return value + + def get_db_prep_value(self, value, connection, prepared=False): + """Convert JSON object to a string""" + + if isinstance(value, basestring): + return value + return json.dumps(value, **self.dump_kwargs) + + def value_to_string(self, obj): + value = self._get_val_from_obj(obj) + return self.get_prep_value(value) + + def value_from_object(self, obj): + return json.dumps(super(JSONField, self).value_from_object(obj)) + + def formfield(self, **kwargs): + + if "form_class" not in kwargs: + kwargs["form_class"] = JSONFormField + + field = super(JSONField, self).formfield(**kwargs) + + if not field.help_text: + field.help_text = "Enter valid JSON" + + return field + +try: + from south.modelsinspector import add_introspection_rules + add_introspection_rules([], ["^jsonfield\.fields\.JSONField"]) +except ImportError: + pass diff --git a/openslides/utils/jsonfield/models.py b/openslides/utils/jsonfield/models.py new file mode 100644 index 000000000..e5faf1b16 --- /dev/null +++ b/openslides/utils/jsonfield/models.py @@ -0,0 +1 @@ +# Django needs this to see it as a project diff --git a/openslides/utils/jsonfield/tests.py b/openslides/utils/jsonfield/tests.py new file mode 100644 index 000000000..2eb87ec37 --- /dev/null +++ b/openslides/utils/jsonfield/tests.py @@ -0,0 +1,108 @@ +from django.db import models +from django.test import TestCase +from django.utils import simplejson as json + +from fields import JSONField + + +class JsonModel(models.Model): + json = JSONField() + + +class ComplexEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, complex): + return { + '__complex__': True, + 'real': obj.real, + 'imag': obj.imag, + } + + return json.JSONEncoder.default(self, obj) + + +def as_complex(dct): + if '__complex__' in dct: + return complex(dct['real'], dct['imag']) + return dct + + +class JSONModelCustomEncoders(models.Model): + # A JSON field that can store complex numbers + json = JSONField( + dump_kwargs={'cls': ComplexEncoder}, + load_kwargs={'object_hook': as_complex}, + ) + + +class JSONFieldTest(TestCase): + """JSONField Wrapper Tests""" + + def test_json_field_create(self): + """Test saving a JSON object in our JSONField""" + + json_obj = { + "item_1": "this is a json blah", + "blergh": "hey, hey, hey"} + + obj = JsonModel.objects.create(json=json_obj) + new_obj = JsonModel.objects.get(id=obj.id) + + self.failUnlessEqual(new_obj.json, json_obj) + + def test_json_field_modify(self): + """Test modifying a JSON object in our JSONField""" + + json_obj_1 = {'a': 1, 'b': 2} + json_obj_2 = {'a': 3, 'b': 4} + + obj = JsonModel.objects.create(json=json_obj_1) + + self.failUnlessEqual(obj.json, json_obj_1) + + obj.json = json_obj_2 + + self.failUnlessEqual(obj.json, json_obj_2) + + obj.save() + + self.failUnlessEqual(obj.json, json_obj_2) + + self.assert_(obj) + + def test_json_field_load(self): + """Test loading a JSON object from the DB""" + + json_obj_1 = {'a': 1, 'b': 2} + + obj = JsonModel.objects.create(json=json_obj_1) + + new_obj = JsonModel.objects.get(id=obj.id) + + self.failUnlessEqual(new_obj.json, json_obj_1) + + def test_json_list(self): + """Test storing a JSON list""" + + json_obj = ["my", "list", "of", 1, "objs", {"hello": "there"}] + + obj = JsonModel.objects.create(json=json_obj) + new_obj = JsonModel.objects.get(id=obj.id) + self.failUnlessEqual(new_obj.json, json_obj) + + def test_empty_objects(self): + """Test storing empty objects""" + + for json_obj in [{}, [], 0, '', False]: + obj = JsonModel.objects.create(json=json_obj) + new_obj = JsonModel.objects.get(id=obj.id) + self.failUnlessEqual(json_obj, obj.json) + self.failUnlessEqual(json_obj, new_obj.json) + + def test_custom_encoder(self): + """Test encoder_cls and object_hook""" + value = 1 + 3j # A complex number + + obj = JSONModelCustomEncoders.objects.create(json=value) + new_obj = JSONModelCustomEncoders.objects.get(pk=obj.pk) + self.failUnlessEqual(value, new_obj.json)