Refactored config API.

Removed form_field attributes.
Added extra fields for HTML rendering like label and help text.
Added fields for sorting and grouping. Removed old collection system.
Added config groups to config view via OPTIONS requests.
Regrouped all variables.
Added validation. Changed internal handling.
This commit is contained in:
Norman Jäckel 2015-06-17 18:32:05 +02:00
parent 4506d787ee
commit c5fbe2e9ee
11 changed files with 663 additions and 705 deletions

View File

@ -1,12 +1,12 @@
from datetime import datetime from datetime import datetime
from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.validators import MaxLengthValidator, MinValueValidator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy
from openslides.config.api import ConfigCollection, ConfigVariable from openslides.config.api import ConfigVariable
from .models import Item from .models import Item
@ -15,87 +15,72 @@ def validate_start_time(value):
try: try:
datetime.strptime(value, '%d.%m.%Y %H:%M') datetime.strptime(value, '%d.%m.%Y %H:%M')
except ValueError: except ValueError:
raise ValidationError(_('Invalid input.')) raise DjangoValidationError(_('Invalid input.'))
# TODO: Reinsert the datepicker scripts in the template
def setup_agenda_config(sender, **kwargs): def setup_agenda_config(sender, **kwargs):
""" """
Receiver function to setup all agenda config variables. It is connected to Receiver function to setup all agenda config variables. They are not
the signal openslides.config.signals.config_signal during app loading. grouped. This function connected to the signal
openslides.config.signals.config_signal during app loading.
""" """
# TODO: Insert validator for the format or use other field carefully. # TODO: Use an input type with generic datetime support.
agenda_start_event_date_time = ConfigVariable( yield ConfigVariable(
name='agenda_start_event_date_time', name='agenda_start_event_date_time',
default_value='', default_value='',
form_field=forms.CharField( label=ugettext_lazy('Begin of event'),
validators=[validate_start_time, ], help_text=ugettext_lazy('Input format: DD.MM.YYYY HH:MM'),
widget=forms.DateTimeInput(format='%d.%m.%Y %H:%M'), weight=210,
required=False, group=ugettext_lazy('Agenda'),
label=ugettext_lazy('Begin of event'), validators=(validate_start_time,))
help_text=ugettext_lazy('Input format: DD.MM.YYYY HH:MM')))
agenda_show_last_speakers = ConfigVariable( yield ConfigVariable(
name='agenda_show_last_speakers', name='agenda_show_last_speakers',
default_value=1, default_value=1,
form_field=forms.IntegerField( input_type='integer',
min_value=0, label=ugettext_lazy('Number of last speakers to be shown on the projector'),
label=ugettext_lazy('Number of last speakers to be shown on the projector'))) weight=220,
group=ugettext_lazy('Agenda'),
validators=(MinValueValidator(0),))
agenda_couple_countdown_and_speakers = ConfigVariable( yield ConfigVariable(
name='agenda_couple_countdown_and_speakers', name='agenda_couple_countdown_and_speakers',
default_value=False, default_value=False,
form_field=forms.BooleanField( input_type='boolean',
label=ugettext_lazy('Couple countdown with the list of speakers'), label=ugettext_lazy('Couple countdown with the list of speakers'),
help_text=ugettext_lazy('[Begin speach] starts the countdown, [End speach] stops the countdown.'), help_text=ugettext_lazy('[Begin speach] starts the countdown, [End speach] stops the countdown.'),
required=False)) weight=230,
group=ugettext_lazy('Agenda'))
agenda_number_prefix = ConfigVariable( yield ConfigVariable(
name='agenda_number_prefix', name='agenda_number_prefix',
default_value='', default_value='',
form_field=forms.CharField( label=ugettext_lazy('Numbering prefix for agenda items'),
label=ugettext_lazy('Numbering prefix for agenda items'), weight=240,
max_length=20, group=ugettext_lazy('Agenda'),
required=False)) validators=(MaxLengthValidator(20),))
agenda_numeral_system = ConfigVariable( yield ConfigVariable(
name='agenda_numeral_system', name='agenda_numeral_system',
default_value='arabic', default_value='arabic',
form_field=forms.ChoiceField( input_type='choice',
label=ugettext_lazy('Numeral system for agenda items'), label=ugettext_lazy('Numeral system for agenda items'),
widget=forms.Select(), choices=(
choices=( {'value': 'arabic', 'display_name': ugettext_lazy('Arabic')},
('arabic', ugettext_lazy('Arabic')), {'value': 'roman', 'display_name': ugettext_lazy('Roman')}),
('roman', ugettext_lazy('Roman'))), weight=250,
required=False)) group=ugettext_lazy('Agenda'))
extra_stylefiles = ['css/jquery-ui-timepicker.css']
extra_javascript = ['js/jquery/jquery-ui-timepicker-addon.min.js',
'js/jquery/jquery-ui-sliderAccess.min.js',
'js/jquery/datepicker-config.js']
return ConfigCollection(title=ugettext_noop('Agenda'),
url='agenda',
weight=20,
variables=(agenda_start_event_date_time,
agenda_show_last_speakers,
agenda_couple_countdown_and_speakers,
agenda_number_prefix,
agenda_numeral_system),
extra_context={'extra_stylefiles': extra_stylefiles,
'extra_javascript': extra_javascript})
def listen_to_related_object_delete_signal(sender, instance, **kwargs): def listen_to_related_object_delete_signal(sender, instance, **kwargs):
""" """
Receiver function to changed agenda items of a related items that is to Receiver function to change agenda items of a related item that is to
be deleted. It is connected to the signal be deleted. It is connected to the signal
django.db.models.signals.pre_delete during app loading. django.db.models.signals.pre_delete during app loading.
""" """
if hasattr(instance, 'get_agenda_title'): if hasattr(instance, 'get_agenda_title'):
for item in Item.objects.filter(content_type=ContentType.objects.get_for_model(sender), object_id=instance.pk): for item in Item.objects.filter(content_type=ContentType.objects.get_for_model(sender), object_id=instance.pk):
item.title = '< Item for deleted slide (%s) >' % instance.get_agenda_title() item.title = '< Item for deleted (%s) >' % instance.get_agenda_title()
item.content_type = None item.content_type = None
item.object_id = None item.object_id = None
item.save() item.save()

View File

@ -1,97 +1,91 @@
from django import forms from django.core.validators import MinValueValidator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy
from openslides.config.api import ( from openslides.config.api import ConfigVariable
ConfigGroup,
ConfigGroupedCollection,
ConfigVariable,
)
from openslides.poll.models import PERCENT_BASE_CHOICES from openslides.poll.models import PERCENT_BASE_CHOICES
def setup_assignment_config(sender, **kwargs): def setup_assignment_config(sender, **kwargs):
""" """
Receiver function to setup all assignment config variables. It is Receiver function to setup all assignment config variables. They are
grouped in 'Ballot and ballot papers' and 'PDF'. This function is
connected to the signal openslides.config.signals.config_signal during connected to the signal openslides.config.signals.config_signal during
app loading. app loading.
""" """
# Ballot and ballot papers # Ballot and ballot papers
assignments_poll_vote_values = ConfigVariable(
yield ConfigVariable(
name='assignments_poll_vote_values', name='assignments_poll_vote_values',
default_value='auto', default_value='auto',
form_field=forms.ChoiceField( input_type='choice',
widget=forms.Select(), label=ugettext_lazy('Election method'),
required=False, choices=(
label=ugettext_lazy('Election method'), {'value': 'auto', 'display_name': ugettext_lazy('Automatic assign of method')},
choices=( {'value': 'votes', 'display_name': ugettext_lazy('Always one option per candidate')},
('auto', ugettext_lazy('Automatic assign of method')), {'value': 'yesnoabstain', 'display_name': ugettext_lazy('Always Yes-No-Abstain per candidate')}),
('votes', ugettext_lazy('Always one option per candidate')), weight=410,
('yesnoabstain', ugettext_lazy('Always Yes-No-Abstain per candidate'))))) group=ugettext_lazy('Elections'),
assignments_poll_100_percent_base = ConfigVariable( subgroup=ugettext_lazy('Ballot and ballot papers'))
yield ConfigVariable(
name='assignments_poll_100_percent_base', name='assignments_poll_100_percent_base',
default_value='WITHOUT_INVALID', default_value='WITHOUT_INVALID',
form_field=forms.ChoiceField( input_type='choice',
widget=forms.Select(), label=ugettext_lazy('The 100 % base of an election result consists of'),
required=False, choices=PERCENT_BASE_CHOICES,
label=ugettext_lazy('The 100 % base of an election result consists of'), weight=420,
choices=PERCENT_BASE_CHOICES)) group=ugettext_lazy('Elections'),
assignments_pdf_ballot_papers_selection = ConfigVariable( subgroup=ugettext_lazy('Ballot and ballot papers'))
yield ConfigVariable(
name='assignments_pdf_ballot_papers_selection', name='assignments_pdf_ballot_papers_selection',
default_value='CUSTOM_NUMBER', default_value='CUSTOM_NUMBER',
form_field=forms.ChoiceField( input_type='choice',
widget=forms.Select(), label=ugettext_lazy('Number of ballot papers (selection)'),
required=False, choices=(
label=ugettext_lazy('Number of ballot papers (selection)'), {'value': 'NUMBER_OF_DELEGATES', 'display_name': ugettext_lazy('Number of all delegates')},
choices=( {'value': 'NUMBER_OF_ALL_PARTICIPANTS', 'display_name': ugettext_lazy('Number of all participants')},
('NUMBER_OF_DELEGATES', ugettext_lazy('Number of all delegates')), {'value': 'CUSTOM_NUMBER', 'display_name': ugettext_lazy('Use the following custom number')}),
('NUMBER_OF_ALL_PARTICIPANTS', ugettext_lazy('Number of all participants')), weight=430,
('CUSTOM_NUMBER', ugettext_lazy('Use the following custom number'))))) group=ugettext_lazy('Elections'),
assignments_pdf_ballot_papers_number = ConfigVariable( subgroup=ugettext_lazy('Ballot and ballot papers'))
yield ConfigVariable(
name='assignments_pdf_ballot_papers_number', name='assignments_pdf_ballot_papers_number',
default_value=8, default_value=8,
form_field=forms.IntegerField( input_type='integer',
widget=forms.TextInput(attrs={'class': 'small-input'}), label=ugettext_lazy('Custom number of ballot papers'),
required=False, weight=440,
min_value=1, group=ugettext_lazy('Elections'),
label=ugettext_lazy('Custom number of ballot papers'))) subgroup=ugettext_lazy('Ballot and ballot papers'),
assignments_publish_winner_results_only = ConfigVariable( validators=(MinValueValidator(1),))
yield ConfigVariable(
name='assignments_publish_winner_results_only', name='assignments_publish_winner_results_only',
default_value=False, default_value=False,
form_field=forms.BooleanField( input_type='boolean',
required=False, label=ugettext_lazy('Publish election result for elected candidates only '
label=ugettext_lazy('Publish election result for elected candidates only ' '(projector view)'),
'(projector view)'))) weight=450,
group_ballot = ConfigGroup( group=ugettext_lazy('Elections'),
title=ugettext_lazy('Ballot and ballot papers'), subgroup=ugettext_lazy('Ballot and ballot papers'))
variables=(assignments_poll_vote_values,
assignments_poll_100_percent_base,
assignments_pdf_ballot_papers_selection,
assignments_pdf_ballot_papers_number,
assignments_publish_winner_results_only))
# PDF # PDF
assignments_pdf_title = ConfigVariable(
yield ConfigVariable(
name='assignments_pdf_title', name='assignments_pdf_title',
default_value=_('Elections'), default_value=_('Elections'),
translatable=True, label=ugettext_lazy('Title for PDF document (all elections)'),
form_field=forms.CharField( weight=460,
widget=forms.TextInput(), group=ugettext_lazy('Elections'),
required=False, subgroup=ugettext_lazy('PDF'),
label=ugettext_lazy('Title for PDF document (all elections)'))) translatable=True)
assignments_pdf_preamble = ConfigVariable(
yield ConfigVariable(
name='assignments_pdf_preamble', name='assignments_pdf_preamble',
default_value='', default_value='',
form_field=forms.CharField( label=ugettext_lazy('Preamble text for PDF document (all elections)'),
widget=forms.Textarea(), weight=470,
required=False, group=ugettext_lazy('Elections'),
label=ugettext_lazy('Preamble text for PDF document (all elections)'))) subgroup=ugettext_lazy('PDF'))
group_pdf = ConfigGroup(
title=ugettext_lazy('PDF'),
variables=(assignments_pdf_title, assignments_pdf_preamble))
return ConfigGroupedCollection(
title=ugettext_noop('Elections'),
url='assignment',
weight=40,
groups=(group_ballot, group_pdf))

View File

@ -1,74 +1,39 @@
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import ugettext as _
from .exceptions import ConfigError, ConfigNotFound from .exceptions import ConfigError, ConfigNotFound
from .models import ConfigStore from .models import ConfigStore
from .signals import config_signal from .signals import config_signal
INPUT_TYPE_MAPPING = {
'string': str,
'integer': int,
'boolean': bool,
'choice': str}
class ConfigHandler(object):
class ConfigHandler:
""" """
An simple object class to wrap the config variables. It is a container A simple object class to wrap the config variables. It is a container
object. To get a config variable use x = config[...], to set it use object. To get a config variable use x = config[...], to set it use
config[...] = x. config[...] = x.
""" """
def __getitem__(self, key): def __getitem__(self, key):
"""
Returns the value of the config variable. Builds the cache if it
does not exist.
"""
try: try:
return self._cache[key] return self._cache[key]
except KeyError: except KeyError:
raise ConfigNotFound('The config variable %s was not found.' % key) raise ConfigNotFound(_('The config variable %s was not found.') % key)
except AttributeError: except AttributeError:
self.setup_cache() self.setup_cache()
return self[key] return self[key]
def __setitem__(self, key, value):
# Check if the variable is defined.
if key not in self:
raise ConfigNotFound('The config variable %s was not found.' % key)
# Save the new value to the database.
updated_rows = ConfigStore.objects.filter(key=key).update(value=value)
if not updated_rows:
ConfigStore.objects.create(key=key, value=value)
# Update cache.
self._cache[key] = value
# Call on_change callback.
if self.get_config_variables()[key].on_change:
self.get_config_variables()[key].on_change()
def items(self):
"""
Returns key-value pairs of all config variables.
"""
if not hasattr(self, '_cache'):
self.setup_cache()
return self._cache.items()
def get_config_variables(self):
"""
Returns a dictionary with all ConfigVariable instances of all
collections. The key is the name of the config variables.
"""
result = {}
for receiver, config_collection in config_signal.send(sender='get_config_variables'):
for config_variable in config_collection.variables:
if config_variable.name in result:
raise ConfigError('Too many values for config variable %s found.' % config_variable.name)
result[config_variable.name] = config_variable
return result
def get_default(self, key):
"""
Returns the default value for 'key'.
"""
try:
return self.get_config_variables()[key].default_value
except KeyError:
raise ConfigNotFound('The config variable %s was not found.' % key)
def setup_cache(self): def setup_cache(self):
""" """
Loads all config variables from the database by sending a signal to Creates a cache of all config variables with their current value.
save the default to the cache.
""" """
self._cache = {} self._cache = {}
for key, config_variable in self.get_config_variables().items(): for key, config_variable in self.get_config_variables().items():
@ -84,6 +49,62 @@ class ConfigHandler(object):
else: else:
return True return True
def __setitem__(self, key, value):
"""
Sets the new value. First it validates the input.
"""
# Check if the variable is defined.
try:
config_variable = config.get_config_variables()[key]
except KeyError:
raise ConfigNotFound(_('The config variable %s was not found.') % key)
# Validate datatype and run validators.
expected_type = INPUT_TYPE_MAPPING[config_variable.input_type]
if not isinstance(value, expected_type):
raise ConfigError(_('Wrong datatype. Expected %s, got %s.') % (expected_type, type(value)))
if config_variable.input_type == 'choice' and value not in map(lambda choice: choice['value'], config_variable.choices):
raise ConfigError(_('Invalid input. Choice does not match.'))
for validator in config_variable.validators:
try:
validator(value)
except DjangoValidationError as e:
raise ConfigError(e.messages[0])
# Save the new value to the database.
updated_rows = ConfigStore.objects.filter(key=key).update(value=value)
if not updated_rows:
ConfigStore.objects.create(key=key, value=value)
# Update cache.
if hasattr(self, '_cache'):
self._cache[key] = value
# Call on_change callback.
if config_variable.on_change:
config_variable.on_change()
def items(self):
"""
Returns key-value pairs of all config variables.
"""
if not hasattr(self, '_cache'):
self.setup_cache()
return self._cache.items()
def get_config_variables(self):
"""
Returns a dictionary with all ConfigVariable instances of all
signal receivers. The key is the name of the config variable.
"""
result = {}
for receiver, config_collection in config_signal.send(sender='get_config_variables'):
for config_variable in config_collection:
if config_variable.name in result:
raise ConfigError(_('Too many values for config variable %s found.') % config_variable.name)
result[config_variable.name] = config_variable
return result
def get_all_translatable(self): def get_all_translatable(self):
""" """
Generator to get all config variables as strings when their values are Generator to get all config variables as strings when their values are
@ -100,92 +121,67 @@ use x = config[...], to set it use config[...] = x.
""" """
class ConfigBaseCollection(object): class ConfigVariable:
""" """
An abstract base class for simple and grouped config collections. The A simple object class to wrap new config variables.
attributes title and url are required for collections that should be
shown as a view. The attribute weight is used for the order of the The keyword arguments 'name' and 'default_value' are required.
links in the submenu of the views. The attribute extra_context can be
used to insert extra css and js files into the template. The keyword arguments 'input_type', 'label' and 'help_text' are for
rendering a HTML form element. If you set 'input_type' to 'choice' you
have to provide 'choices', which is a list of dictionaries containing a
value and a display_name of every possible choice.
The keyword arguments 'weight', 'group' and 'subgroup' are for sorting
and grouping.
The keyword argument validators expects an interable of validator
functions. Such a function gets the value and raises Django's
ValidationError if the value is invalid.
The keyword argument 'on_change' can be a callback which is called
every time, the variable is changed.
If the argument 'translatable' is set, OpenSlides is able to translate
the value during setup of the database if the admin uses the respective
command line option.
""" """
def __init__(self, title=None, url=None, weight=0, extra_context={}): def __init__(self, name, default_value, input_type='string', label=None,
self.title = title help_text=None, choices=None, weight=0, group=None,
self.url = url subgroup=None, validators=None, on_change=None, translatable=False):
self.weight = weight if input_type not in INPUT_TYPE_MAPPING:
self.extra_context = extra_context raise ValueError(_('Invalid value for config attribute input_type.'))
if input_type == 'choice' and choices is None:
def is_shown(self): raise ConfigError(_("Either config attribute 'choices' must not be None or "
""" "'input_type' must not be 'choice'."))
Returns True if at least one variable of the collection has a form field. elif input_type != 'choice' and choices is not None:
""" raise ConfigError(_("Either config attribute 'choices' must be None or "
for variable in self.variables: "'input_type' must be 'choice'."))
if variable.form_field is not None:
is_shown = True
break
else:
is_shown = False
if is_shown and (self.title is None or self.url is None):
raise ConfigError('The config collection %s must have a title and an url attribute.' % self)
return is_shown
class ConfigGroupedCollection(ConfigBaseCollection):
"""
A simple object class for a grouped config collection. Developers have to
set the groups attribute (tuple). The config variables are available
via the variables attribute. The collection is shown as a view via the config
main menu entry if there is at least one variable with a form field.
"""
def __init__(self, groups, **kwargs):
self.groups = groups
super(ConfigGroupedCollection, self).__init__(**kwargs)
@property
def variables(self):
for group in self.groups:
for variable in group.variables:
yield variable
class ConfigCollection(ConfigBaseCollection):
"""
A simple object class for a ungrouped config collection. Developers have
to set the variables (tuple) directly. The collection is shown as a view via
the config main menu entry if there is at least one variable with a
form field.
"""
def __init__(self, variables, **kwargs):
self.variables = variables
super(ConfigCollection, self).__init__(**kwargs)
class ConfigGroup(object):
"""
A simple object class representing a group of variables (tuple) with
a special title.
"""
def __init__(self, title, variables):
self.title = title
self.variables = variables
def get_field_names(self):
return [variable.name for variable in self.variables if variable.form_field is not None]
class ConfigVariable(object):
"""
A simple object class to wrap new config variables. The keyword
arguments 'name' and 'default_value' are required. The keyword
argument 'form_field' has to be set if the variable should appear
on the ConfigView. The argument 'on_change' can get a callback
which is called every time, the variable is changed. If the argument
'translatable' is set, OpenSlides is able to translate the value during
setup of the database if the admin uses the respective command line option.
"""
def __init__(self, name, default_value, form_field=None, on_change=None, translatable=False):
self.name = name self.name = name
self.default_value = default_value self.default_value = default_value
self.form_field = form_field self.input_type = input_type
self.label = label or name
self.help_text = help_text or ''
self.choices = choices
self.weight = weight
self.group = group or _('General')
self.subgroup = subgroup
self.validators = validators or ()
self.on_change = on_change self.on_change = on_change
self.translatable = translatable self.translatable = translatable
@property
def data(self):
"""
Property with all data for OPTIONS requests.
"""
data = {
'key': self.name,
'value': config[self.name],
'input_type': self.input_type,
'label': self.label,
'help_text': self.help_text
}
if self.input_type == 'choice':
data['choices'] = self.choices
return data

View File

@ -1,39 +1,72 @@
from django.core.exceptions import ValidationError as DjangoValidationError from collections import OrderedDict
from operator import attrgetter
from django.http import Http404 from django.http import Http404
from openslides.utils.rest_api import Response, ValidationError, ViewSet from openslides.utils.rest_api import (
Response,
SimpleMetadata,
ValidationError,
ViewSet,
)
from .api import config from .api import config
from .exceptions import ConfigNotFound from .exceptions import ConfigError, ConfigNotFound
class ConfigMetadata(SimpleMetadata):
"""
Custom metadata class to add config info to responses on OPTIONS requests.
"""
def determine_metadata(self, request, view):
# Sort config variables by weight.
config_variables = sorted(config.get_config_variables().values(), key=attrgetter('weight'))
# Build tree.
config_groups = []
for config_variable in config_variables:
if not config_groups or config_groups[-1]['name'] != config_variable.group:
config_groups.append(OrderedDict(
name=config_variable.group,
subgroups=[]))
if not config_groups[-1]['subgroups'] or config_groups[-1]['subgroups'][-1]['name'] != config_variable.subgroup:
config_groups[-1]['subgroups'].append(OrderedDict(
name=config_variable.subgroup,
items=[]))
config_groups[-1]['subgroups'][-1]['items'].append(config_variable.data)
# Add tree to metadata.
metadata = super().determine_metadata(request, view)
metadata['config_groups'] = config_groups
return metadata
class ConfigViewSet(ViewSet): class ConfigViewSet(ViewSet):
""" """
API endpoint to list, retrieve and update the config. API endpoint to list, retrieve and update the config.
""" """
metadata_class = ConfigMetadata
def list(self, request): def list(self, request):
""" """
Lists all config variables. Everybody can see them. Lists all config variables. Everybody can see them.
""" """
# TODO: Check if we need permission check here. return Response([{'key': key, 'value': value} for key, value in config.items()])
data = ({'key': key, 'value': value} for key, value in config.items())
return Response(data)
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
""" """
Retrieves one config variable. Everybody can see it. Retrieves a config variable. Everybody can see it.
""" """
# TODO: Check if we need permission check here.
key = kwargs['pk'] key = kwargs['pk']
try: try:
data = {'key': key, 'value': config[key]} value = config[key]
except ConfigNotFound: except ConfigNotFound:
raise Http404 raise Http404
return Response(data) return Response({'key': key, 'value': value})
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
""" """
Updates one config variable. Only managers can do this. Updates a config variable. Only managers can do this.
Example: {"value": 42} Example: {"value": 42}
""" """
@ -41,22 +74,16 @@ class ConfigViewSet(ViewSet):
if not request.user.has_perm('config.can_manage'): if not request.user.has_perm('config.can_manage'):
self.permission_denied(request) self.permission_denied(request)
# Check if pk is a valid config variable key.
key = kwargs['pk'] key = kwargs['pk']
if key not in config:
raise Http404
# Validate value.
form_field = config.get_config_variables()[key].form_field
value = request.data['value'] value = request.data['value']
if form_field:
try:
form_field.clean(value)
except DjangoValidationError as e:
raise ValidationError({'detail': e.messages[0]})
# Change value. # Validate and change value.
config[key] = value try:
config[key] = value
except ConfigNotFound:
raise Http404
except ConfigError as e:
raise ValidationError({'detail': e})
# Return response. # Return response.
return Response({'key': key, 'value': value}) return Response({'key': key, 'value': value})

View File

@ -1,13 +1,9 @@
from django import forms from django.core.validators import MaxLengthValidator
from django.dispatch import Signal from django.dispatch import Signal
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy
from openslides.config.api import ( from openslides.config.api import ConfigVariable
ConfigGroup,
ConfigGroupedCollection,
ConfigVariable,
)
# This signal is sent when the migrate command is done. That means it is sent # This signal is sent when the migrate command is done. That means it is sent
# after post_migrate sending and creating all Permission objects. Don't use it # after post_migrate sending and creating all Permission objects. Don't use it
@ -18,147 +14,123 @@ post_permission_creation = Signal()
def setup_general_config(sender, **kwargs): def setup_general_config(sender, **kwargs):
""" """
Receiver function to setup general config variables for OpenSlides. Receiver function to setup general config variables for OpenSlides.
They are grouped in 'Event', 'Projector' and 'System'. This function is There are two main groups: 'General' and 'Projector'. The group
connected to the signal openslides.config.signals.config_signal during 'General' has subgroups. This function is connected to the signal
app loading. openslides.config.signals.config_signal during app loading.
""" """
general_event_name = ConfigVariable( # General Event
yield ConfigVariable(
name='general_event_name', name='general_event_name',
default_value='OpenSlides', default_value='OpenSlides',
form_field=forms.CharField( label=ugettext_lazy('Event name'),
widget=forms.TextInput(), weight=110,
label=ugettext_lazy('Event name'), group=ugettext_lazy('General'),
max_length=50)) subgroup=ugettext_lazy('Event'),
validators=(MaxLengthValidator(50),))
general_event_description = ConfigVariable( yield ConfigVariable(
name='general_event_description', name='general_event_description',
default_value=_('Presentation and assembly system'), default_value=_('Presentation and assembly system'),
translatable=True, label=ugettext_lazy('Short description of event'),
form_field=forms.CharField( weight=115,
widget=forms.TextInput(), group=ugettext_lazy('General'),
label=ugettext_lazy('Short description of event'), subgroup=ugettext_lazy('Event'),
required=False, validators=(MaxLengthValidator(100),),
max_length=100)) translatable=True)
general_event_date = ConfigVariable( yield ConfigVariable(
name='general_event_date', name='general_event_date',
default_value='', default_value='',
form_field=forms.CharField( label=ugettext_lazy('Event date'),
widget=forms.TextInput(), weight=120,
label=ugettext_lazy('Event date'), group=ugettext_lazy('General'),
required=False)) subgroup=ugettext_lazy('Event'))
general_event_location = ConfigVariable( yield ConfigVariable(
name='general_event_location', name='general_event_location',
default_value='', default_value='',
form_field=forms.CharField( label=ugettext_lazy('Event location'),
widget=forms.TextInput(), weight=125,
label=ugettext_lazy('Event location'), group=ugettext_lazy('General'),
required=False)) subgroup=ugettext_lazy('Event'))
# TODO: Check whether this variable is ever used. # TODO: Check whether this variable is ever used.
general_event_organizer = ConfigVariable( yield ConfigVariable(
name='general_event_organizer', name='general_event_organizer',
default_value='', default_value='',
form_field=forms.CharField( label=ugettext_lazy('Event organizer'),
widget=forms.TextInput(), weight=130,
label=ugettext_lazy('Event organizer'), group=ugettext_lazy('General'),
required=False)) subgroup=ugettext_lazy('Event'))
general_system_enable_anonymous = ConfigVariable( # General System
yield ConfigVariable(
name='general_system_enable_anonymous', name='general_system_enable_anonymous',
default_value=False, default_value=False,
form_field=forms.BooleanField( input_type='boolean',
label=ugettext_lazy('Allow access for anonymous guest users'), label=ugettext_lazy('Allow access for anonymous guest users'),
required=False)) weight=135,
group=ugettext_lazy('General'),
subgroup=ugettext_lazy('System'))
projector_enable_logo = ConfigVariable( # Projector
yield ConfigVariable(
name='projector_enable_logo', name='projector_enable_logo',
default_value=True, default_value=True,
form_field=forms.BooleanField( input_type='boolean',
label=ugettext_lazy('Show logo on projector'), label=ugettext_lazy('Show logo on projector'),
help_text=ugettext_lazy('You can find and replace the logo under "openslides/projector/static/img/logo-projector.png".'), help_text=ugettext_lazy('You can find and replace the logo under "openslides/core/static/...".'), # TODO: Update path.
required=False)) weight=150,
group=ugettext_lazy('Projector'))
projector_enable_title = ConfigVariable( yield ConfigVariable(
name='projector_enable_title', name='projector_enable_title',
default_value=True, default_value=True,
form_field=forms.BooleanField( input_type='boolean',
label=ugettext_lazy('Show title and description of event on projector'), label=ugettext_lazy('Show title and description of event on projector'),
required=False)) weight=155,
group=ugettext_lazy('Projector'))
projector_backgroundcolor1 = ConfigVariable( yield ConfigVariable(
name='projector_backgroundcolor1', name='projector_backgroundcolor1',
default_value='#444444', default_value='#444444',
form_field=forms.CharField( label=ugettext_lazy('Background color of projector header'),
widget=forms.TextInput(), help_text=ugettext_lazy('Use web color names like "red" or hex numbers like "#ff0000".'),
label=ugettext_lazy('Background color of projector header'), weight=160,
help_text=ugettext_lazy('Use web color names like "red" or hex numbers like "#ff0000".'), group=ugettext_lazy('Projector'))
required=True))
projector_backgroundcolor2 = ConfigVariable( yield ConfigVariable(
name='projector_backgroundcolor2', name='projector_backgroundcolor2',
default_value='#222222', default_value='#222222',
form_field=forms.CharField( label=ugettext_lazy('Second (optional) background color for linear color gradient'),
widget=forms.TextInput(), help_text=ugettext_lazy('Use web color names like "red" or hex numbers like "#ff0000".'),
label=ugettext_lazy('Second (optional) background color for linear color gradient'), weight=165,
help_text=ugettext_lazy('Use web color names like "red" or hex numbers like "#ff0000".'), group=ugettext_lazy('Projector'))
required=False))
projector_fontcolor = ConfigVariable( yield ConfigVariable(
name='projector_fontcolor', name='projector_fontcolor',
default_value='#F5F5F5', default_value='#F5F5F5',
form_field=forms.CharField( label=ugettext_lazy('Font color of projector header'),
widget=forms.TextInput(), help_text=ugettext_lazy('Use web color names like "red" or hex numbers like "#ff0000".'),
label=ugettext_lazy('Font color of projector header'), weight=170,
help_text=ugettext_lazy('Use web color names like "red" or hex numbers like "#ff0000".'), group=ugettext_lazy('Projector'))
required=True))
projector_welcome_title = ConfigVariable( yield ConfigVariable(
name='projector_welcome_title', name='projector_welcome_title',
default_value=_('Welcome to OpenSlides'), default_value=_('Welcome to OpenSlides'),
translatable=True, label=ugettext_lazy('Title'),
form_field=forms.CharField( help_text=ugettext_lazy('Also used for the default welcome slide.'),
widget=forms.TextInput(), weight=175,
label=ugettext_lazy('Title'), group=ugettext_lazy('Projector'),
help_text=ugettext_lazy('Also used for the default welcome slide.'), translatable=True)
required=False))
projector_welcome_text = ConfigVariable( yield ConfigVariable(
name='projector_welcome_text', name='projector_welcome_text',
default_value=_('[Place for your welcome text.]'), default_value=_('[Space for your welcome text.]'),
translatable=True, label=ugettext_lazy('Welcome text'),
form_field=forms.CharField( weight=180,
widget=forms.Textarea(), group=ugettext_lazy('Projector'),
label=ugettext_lazy('Welcome text'), translatable=True)
required=False))
group_event = ConfigGroup(
title=ugettext_lazy('Event'),
variables=(
general_event_name,
general_event_description,
general_event_date,
general_event_location,
general_event_organizer))
group_system = ConfigGroup(
title=ugettext_lazy('System'),
variables=(general_system_enable_anonymous,))
group_projector = ConfigGroup(
title=ugettext_lazy('Projector'),
variables=(
projector_enable_logo,
projector_enable_title,
projector_backgroundcolor1,
projector_backgroundcolor2,
projector_fontcolor,
projector_welcome_title,
projector_welcome_text))
return ConfigGroupedCollection(
title=ugettext_noop('General'),
url='general',
weight=10,
groups=(group_event, group_system, group_projector))

View File

@ -1,12 +1,8 @@
from django import forms from django.core.validators import MinValueValidator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import pgettext, ugettext_lazy, ugettext_noop from django.utils.translation import pgettext, ugettext_lazy, ugettext_noop
from openslides.config.api import ( from openslides.config.api import ConfigVariable
ConfigGroup,
ConfigGroupedCollection,
ConfigVariable,
)
from openslides.poll.models import PERCENT_BASE_CHOICES from openslides.poll.models import PERCENT_BASE_CHOICES
from .models import State, Workflow from .models import State, Workflow
@ -14,166 +10,165 @@ from .models import State, Workflow
def setup_motion_config(sender, **kwargs): def setup_motion_config(sender, **kwargs):
""" """
Receiver function to setup all motion config variables. It is connected to Receiver function to setup all motion config variables. They are
the signal openslides.config.signals.config_signal during app loading. grouped in 'General', 'Amendments', 'Supporters', 'Voting and ballot
papers' and 'PDF'. This function connected to the signal
openslides.config.signals.config_signal during app loading.
""" """
# General # General
motions_workflow = ConfigVariable(
yield ConfigVariable(
name='motions_workflow', name='motions_workflow',
default_value='1', default_value='1',
form_field=forms.ChoiceField( input_type='choice',
widget=forms.Select(), label=ugettext_lazy('Workflow of new motions'),
label=ugettext_lazy('Workflow of new motions'), choices=({'value': str(workflow.pk), 'display_name': ugettext_lazy(workflow.name)} for workflow in Workflow.objects.all()),
required=True, weight=310,
choices=[(str(workflow.pk), ugettext_lazy(workflow.name)) for workflow in Workflow.objects.all()])) group=ugettext_lazy('Motion'),
motions_identifier = ConfigVariable( subgroup=ugettext_lazy('General'))
yield ConfigVariable(
name='motions_identifier', name='motions_identifier',
default_value='per_category', default_value='per_category',
form_field=forms.ChoiceField( input_type='choice',
widget=forms.Select(), label=ugettext_lazy('Identifier'),
required=True, choices=(
label=ugettext_lazy('Identifier'), {'value': 'per_category', 'display_name': ugettext_lazy('Numbered per category')},
choices=[ {'value': 'serially_numbered', 'display_name': ugettext_lazy('Serially numbered')},
('per_category', ugettext_lazy('Numbered per category')), {'value': 'manually', 'display_name': ugettext_lazy('Set it manually')}),
('serially_numbered', ugettext_lazy('Serially numbered')), weight=315,
('manually', ugettext_lazy('Set it manually'))])) group=ugettext_lazy('Motion'),
motions_preamble = ConfigVariable( subgroup=ugettext_lazy('General'))
yield ConfigVariable(
name='motions_preamble', name='motions_preamble',
default_value=_('The assembly may decide,'), default_value=_('The assembly may decide,'),
translatable=True, label=ugettext_lazy('Motion preamble'),
form_field=forms.CharField( weight=320,
widget=forms.TextInput(), group=ugettext_lazy('Motion'),
required=False, subgroup=ugettext_lazy('General'),
label=ugettext_lazy('Motion preamble'))) translatable=True)
motions_stop_submitting = ConfigVariable(
yield ConfigVariable(
name='motions_stop_submitting', name='motions_stop_submitting',
default_value=False, default_value=False,
form_field=forms.BooleanField( input_type='boolean',
label=ugettext_lazy('Stop submitting new motions by non-staff users'), label=ugettext_lazy('Stop submitting new motions by non-staff users'),
required=False)) weight=325,
motions_allow_disable_versioning = ConfigVariable( group=ugettext_lazy('Motion'),
subgroup=ugettext_lazy('General'))
yield ConfigVariable(
name='motions_allow_disable_versioning', name='motions_allow_disable_versioning',
default_value=False, default_value=False,
form_field=forms.BooleanField( input_type='boolean',
label=ugettext_lazy('Allow to disable versioning'), label=ugettext_lazy('Allow to disable versioning'),
required=False)) weight=330,
group_general = ConfigGroup( group=ugettext_lazy('Motion'),
title=ugettext_lazy('General'), subgroup=ugettext_lazy('General'))
variables=(
motions_workflow,
motions_identifier,
motions_preamble,
motions_stop_submitting,
motions_allow_disable_versioning))
# Amendments # Amendments
motions_amendments_enabled = ConfigVariable(
yield ConfigVariable(
name='motions_amendments_enabled', name='motions_amendments_enabled',
default_value=False, default_value=False,
form_field=forms.BooleanField( input_type='boolean',
label=ugettext_lazy('Activate amendments'), label=ugettext_lazy('Activate amendments'),
required=False)) weight=335,
group=ugettext_lazy('Motion'),
subgroup=ugettext_lazy('Amendments'))
motions_amendments_prefix = ConfigVariable( yield ConfigVariable(
name='motions_amendments_prefix', name='motions_amendments_prefix',
default_value=pgettext('Prefix for the identifier for amendments', 'A'), default_value=pgettext('Prefix for the identifier for amendments', 'A'),
form_field=forms.CharField( label=ugettext_lazy('Prefix for the identifier for amendments'),
required=False, weight=340,
label=ugettext_lazy('Prefix for the identifier for amendments'))) group=ugettext_lazy('Motion'),
subgroup=ugettext_lazy('Amendments'))
group_amendments = ConfigGroup(
title=ugettext_lazy('Amendments'),
variables=(motions_amendments_enabled, motions_amendments_prefix))
# Supporters # Supporters
motions_min_supporters = ConfigVariable(
yield ConfigVariable(
name='motions_min_supporters', name='motions_min_supporters',
default_value=0, default_value=0,
form_field=forms.IntegerField( input_type='integer',
widget=forms.TextInput(attrs={'class': 'small-input'}), label=ugettext_lazy('Number of (minimum) required supporters for a motion'),
label=ugettext_lazy('Number of (minimum) required supporters for a motion'), help_text=ugettext_lazy('Choose 0 to disable the supporting system.'),
min_value=0, weight=345,
help_text=ugettext_lazy('Choose 0 to disable the supporting system.'))) group=ugettext_lazy('Motion'),
motions_remove_supporters = ConfigVariable( subgroup=ugettext_lazy('Supporters'),
validators=(MinValueValidator(0),))
yield ConfigVariable(
name='motions_remove_supporters', name='motions_remove_supporters',
default_value=False, default_value=False,
form_field=forms.BooleanField( input_type='boolean',
label=ugettext_lazy('Remove all supporters of a motion if a submitter edits his motion in early state'), label=ugettext_lazy('Remove all supporters of a motion if a submitter edits his motion in early state'),
required=False)) weight=350,
group_supporters = ConfigGroup( group=ugettext_lazy('Motion'),
title=ugettext_lazy('Supporters'), subgroup=ugettext_lazy('Supporters'))
variables=(motions_min_supporters, motions_remove_supporters))
# Voting and ballot papers # Voting and ballot papers
motions_poll_100_percent_base = ConfigVariable(
yield ConfigVariable(
name='motions_poll_100_percent_base', name='motions_poll_100_percent_base',
default_value='WITHOUT_INVALID', default_value='WITHOUT_INVALID',
form_field=forms.ChoiceField( input_type='choice',
widget=forms.Select(), label=ugettext_lazy('The 100 % base of a voting result consists of'),
required=False, choices=PERCENT_BASE_CHOICES,
label=ugettext_lazy('The 100 % base of a voting result consists of'), weight=355,
choices=PERCENT_BASE_CHOICES)) group=ugettext_lazy('Motion'),
motions_pdf_ballot_papers_selection = ConfigVariable( subgroup=ugettext_lazy('Voting and ballot papers'))
yield ConfigVariable(
name='motions_pdf_ballot_papers_selection', name='motions_pdf_ballot_papers_selection',
default_value='CUSTOM_NUMBER', default_value='CUSTOM_NUMBER',
form_field=forms.ChoiceField( input_type='choice',
widget=forms.Select(), label=ugettext_lazy('Number of ballot papers (selection)'),
required=False, choices=(
label=ugettext_lazy('Number of ballot papers (selection)'), {'value': 'NUMBER_OF_DELEGATES', 'display_name': ugettext_lazy('Number of all delegates')},
choices=[ {'value': 'NUMBER_OF_ALL_PARTICIPANTS', 'display_name': ugettext_lazy('Number of all participants')},
('NUMBER_OF_DELEGATES', ugettext_lazy('Number of all delegates')), {'value': 'CUSTOM_NUMBER', 'display_name': ugettext_lazy('Use the following custom number')}),
('NUMBER_OF_ALL_PARTICIPANTS', ugettext_lazy('Number of all participants')), weight=360,
('CUSTOM_NUMBER', ugettext_lazy("Use the following custom number"))])) group=ugettext_lazy('Motion'),
motions_pdf_ballot_papers_number = ConfigVariable( subgroup=ugettext_lazy('Voting and ballot papers'))
yield ConfigVariable(
name='motions_pdf_ballot_papers_number', name='motions_pdf_ballot_papers_number',
default_value=8, default_value=8,
form_field=forms.IntegerField( input_type='integer',
widget=forms.TextInput(attrs={'class': 'small-input'}), label=ugettext_lazy('Custom number of ballot papers'),
required=False, weight=365,
min_value=1, group=ugettext_lazy('Motion'),
label=ugettext_lazy('Custom number of ballot papers'))) subgroup=ugettext_lazy('Voting and ballot papers'),
group_ballot_papers = ConfigGroup( validators=(MinValueValidator(1),))
title=ugettext_lazy('Voting and ballot papers'),
variables=(
motions_poll_100_percent_base,
motions_pdf_ballot_papers_selection,
motions_pdf_ballot_papers_number))
# PDF # PDF
motions_pdf_title = ConfigVariable(
yield ConfigVariable(
name='motions_pdf_title', name='motions_pdf_title',
default_value=_('Motions'), default_value=_('Motions'),
translatable=True, label=ugettext_lazy('Title for PDF document (all motions)'),
form_field=forms.CharField( weight=370,
widget=forms.TextInput(), group=ugettext_lazy('Motion'),
required=False, subgroup=ugettext_lazy('PDF'),
label=ugettext_lazy('Title for PDF document (all motions)'))) translatable=True)
motions_pdf_preamble = ConfigVariable(
yield ConfigVariable(
name='motions_pdf_preamble', name='motions_pdf_preamble',
default_value='', default_value='',
form_field=forms.CharField( label=ugettext_lazy('Preamble text for PDF document (all motions)'),
widget=forms.Textarea(), weight=375,
required=False, group=ugettext_lazy('Motion'),
label=ugettext_lazy('Preamble text for PDF document (all motions)'))) subgroup=ugettext_lazy('PDF'))
motions_pdf_paragraph_numbering = ConfigVariable(
yield ConfigVariable(
name='motions_pdf_paragraph_numbering', name='motions_pdf_paragraph_numbering',
default_value=False, default_value=False,
form_field=forms.BooleanField( label=ugettext_lazy('Show paragraph numbering (only in PDF)'),
label=ugettext_lazy('Show paragraph numbering (only in PDF)'), weight=380,
required=False)) group=ugettext_lazy('Motion'),
group_pdf = ConfigGroup( subgroup=ugettext_lazy('PDF'))
title=ugettext_lazy('PDF'),
variables=(
motions_pdf_title,
motions_pdf_preamble,
motions_pdf_paragraph_numbering))
return ConfigGroupedCollection(
title=ugettext_noop('Motion'),
url='motion',
weight=30,
groups=(group_general, group_amendments, group_supporters,
group_ballot_papers, group_pdf))
def create_builtin_workflows(sender, **kwargs): def create_builtin_workflows(sender, **kwargs):

View File

@ -68,9 +68,9 @@ class BaseVote(models.Model):
PERCENT_BASE_CHOICES = ( PERCENT_BASE_CHOICES = (
('WITHOUT_INVALID', ugettext_lazy('Only all valid votes')), {'value': 'WITHOUT_INVALID', 'display_name': ugettext_lazy('Only all valid votes')},
('WITH_INVALID', ugettext_lazy('All votes cast (including invalid votes)')), {'value': 'WITH_INVALID', 'display_name': ugettext_lazy('All votes cast (including invalid votes)')},
('DISABLED', ugettext_lazy('Disabled (no percents)'))) {'value': 'DISABLED', 'display_name': ugettext_lazy('Disabled (no percents)')})
class CollectDefaultVotesMixin(models.Model): class CollectDefaultVotesMixin(models.Model):

View File

@ -1,109 +1,93 @@
from django import forms
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.config.api import ( from openslides.config.api import ConfigVariable
ConfigGroup,
ConfigGroupedCollection,
ConfigVariable,
)
from .models import Group, Permission, User from .models import Group, Permission, User
def setup_users_config(sender, **kwargs): def setup_users_config(sender, **kwargs):
""" """
Receiver function to setup all users config variables. It is connected Receiver function to setup all users config variables. They are grouped
to the signal openslides.config.signals.config_signal during app loading. in 'Sorting' and 'PDF'. This function is connected to the signal
openslides.config.signals.config_signal during app loading.
""" """
# General
users_sort_users_by_first_name = ConfigVariable( # Sorting
yield ConfigVariable(
name='users_sort_users_by_first_name', name='users_sort_users_by_first_name',
default_value=False, default_value=False,
form_field=forms.BooleanField( input_type='boolean',
required=False, label=ugettext_lazy('Sort users by first name'),
label=ugettext_lazy('Sort users by first name'), help_text=ugettext_lazy('Disable for sorting by last name'),
help_text=ugettext_lazy('Disable for sorting by last name'))) weight=510,
group=ugettext_lazy('Users'),
group_general = ConfigGroup( subgroup=ugettext_lazy('Sorting'))
title=ugettext_lazy('Sorting'),
variables=(users_sort_users_by_first_name,))
# PDF # PDF
users_pdf_welcometitle = ConfigVariable(
yield ConfigVariable(
name='users_pdf_welcometitle', name='users_pdf_welcometitle',
default_value=_('Welcome to OpenSlides!'), default_value=_('Welcome to OpenSlides!'),
translatable=True, label=ugettext_lazy('Title for access data and welcome PDF'),
form_field=forms.CharField( weight=520,
widget=forms.Textarea(), group=ugettext_lazy('Users'),
required=False, subgroup=ugettext_lazy('PDF'),
label=ugettext_lazy('Title for access data and welcome PDF'))) translatable=True)
users_pdf_welcometext = ConfigVariable( yield ConfigVariable(
name='users_pdf_welcometext', name='users_pdf_welcometext',
default_value=_('[Place for your welcome and help text.]'), default_value=_('[Place for your welcome and help text.]'),
translatable=True, label=ugettext_lazy('Help text for access data and welcome PDF'),
form_field=forms.CharField( weight=530,
widget=forms.Textarea(), group=ugettext_lazy('Users'),
required=False, subgroup=ugettext_lazy('PDF'),
label=ugettext_lazy('Help text for access data and welcome PDF'))) translatable=True)
users_pdf_url = ConfigVariable( # TODO: Use Django's URLValidator here.
yield ConfigVariable(
name='users_pdf_url', name='users_pdf_url',
default_value='http://example.com:8000', default_value='http://example.com:8000',
form_field=forms.CharField( label=ugettext_lazy('System URL'),
widget=forms.TextInput(), help_text=ugettext_lazy('Used for QRCode in PDF of access data.'),
required=False, weight=540,
label=ugettext_lazy('System URL'), group=ugettext_lazy('Users'),
help_text=ugettext_lazy('Used for QRCode in PDF of access data.'))) subgroup=ugettext_lazy('PDF'))
users_pdf_wlan_ssid = ConfigVariable( yield ConfigVariable(
name='users_pdf_wlan_ssid', name='users_pdf_wlan_ssid',
default_value='', default_value='',
form_field=forms.CharField( label=ugettext_lazy('WLAN name (SSID)'),
widget=forms.TextInput(), help_text=ugettext_lazy('Used for WLAN QRCode in PDF of access data.'),
required=False, weight=550,
label=ugettext_lazy('WLAN name (SSID)'), group=ugettext_lazy('Users'),
help_text=ugettext_lazy('Used for WLAN QRCode in PDF of access data.'))) subgroup=ugettext_lazy('PDF'))
users_pdf_wlan_password = ConfigVariable( yield ConfigVariable(
name='users_pdf_wlan_password', name='users_pdf_wlan_password',
default_value='', default_value='',
form_field=forms.CharField( label=ugettext_lazy('WLAN password'),
widget=forms.TextInput(), help_text=ugettext_lazy('Used for WLAN QRCode in PDF of access data.'),
required=False, weight=560,
label=ugettext_lazy('WLAN password'), group=ugettext_lazy('Users'),
help_text=ugettext_lazy('Used for WLAN QRCode in PDF of access data.'))) subgroup=ugettext_lazy('PDF'))
users_pdf_wlan_encryption = ConfigVariable( yield ConfigVariable(
name='users_pdf_wlan_encryption', name='users_pdf_wlan_encryption',
default_value='', default_value='',
form_field=forms.ChoiceField( input_type='choice',
widget=forms.Select(), label=ugettext_lazy('WLAN encryption'),
required=False, help_text=ugettext_lazy('Used for WLAN QRCode in PDF of access data.'),
label=ugettext_lazy('WLAN encryption'), choices=(
help_text=ugettext_lazy('Used for WLAN QRCode in PDF of access data.'), {'value': '', 'display_name': '---------'},
choices=( {'value': 'WEP', 'display_name': ugettext_lazy('WEP')},
('', '---------'), {'value': 'WPA', 'display_name': ugettext_lazy('WPA/WPA2')},
('WEP', 'WEP'), {'value': 'nopass', 'display_name': ugettext_lazy('No encryption')}),
('WPA', 'WPA/WPA2'), weight=570,
('nopass', ugettext_lazy('No encryption'))))) group=ugettext_lazy('Users'),
subgroup=ugettext_lazy('PDF'))
group_pdf = ConfigGroup(
title=ugettext_lazy('PDF'),
variables=(users_pdf_welcometitle,
users_pdf_welcometext,
users_pdf_url,
users_pdf_wlan_ssid,
users_pdf_wlan_password,
users_pdf_wlan_encryption))
return ConfigGroupedCollection(
title=ugettext_noop('Users'),
url='users',
weight=50,
groups=(group_general, group_pdf))
def create_builtin_groups_and_admin(**kwargs): def create_builtin_groups_and_admin(**kwargs):

View File

@ -4,6 +4,7 @@ from urllib.parse import urlparse
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework.decorators import detail_route # noqa from rest_framework.decorators import detail_route # noqa
from rest_framework.decorators import list_route # noqa from rest_framework.decorators import list_route # noqa
from rest_framework.metadata import SimpleMetadata # noqa
from rest_framework.mixins import DestroyModelMixin, UpdateModelMixin # noqa from rest_framework.mixins import DestroyModelMixin, UpdateModelMixin # noqa
from rest_framework.response import Response # noqa from rest_framework.response import Response # noqa
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter

View File

@ -1,11 +1,11 @@
from django import forms
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.dispatch import receiver from django.dispatch import receiver
from rest_framework import status from rest_framework import status
from rest_framework.test import APIClient from rest_framework.test import APIClient
from openslides.config.api import ConfigCollection, ConfigVariable, config from openslides.config.api import ConfigVariable, config
from openslides.config.signals import config_signal from openslides.config.signals import config_signal
from openslides.utils.rest_api import ValidationError
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -38,7 +38,51 @@ class ConfigViewSet(TestCase):
reverse('config-detail', args=['test_var_ohhii4iavoh5Phoh5ahg']), reverse('config-detail', args=['test_var_ohhii4iavoh5Phoh5ahg']),
{'value': 'test_value_string'}) {'value': 'test_value_string'})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'detail': 'Enter a whole number.'}) self.assertEqual(response.data, {'detail': "Wrong datatype. Expected <class 'int'>, got <class 'str'>."})
def test_update_good_choice(self):
self.client = APIClient()
self.client.login(username='admin', password='admin')
response = self.client.put(
reverse('config-detail', args=['test_var_wei0Rei9ahzooSohK1ph']),
{'value': 'key_2_yahb2ain1aeZ1lea1Pei'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(config['test_var_wei0Rei9ahzooSohK1ph'], 'key_2_yahb2ain1aeZ1lea1Pei')
def test_update_bad_choice(self):
self.client = APIClient()
self.client.login(username='admin', password='admin')
response = self.client.put(
reverse('config-detail', args=['test_var_wei0Rei9ahzooSohK1ph']),
{'value': 'test_value_bad_string'})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'detail': 'Invalid input. Choice does not match.'})
def test_update_validator_ok(self):
self.client = APIClient()
self.client.login(username='admin', password='admin')
response = self.client.put(
reverse('config-detail', args=['test_var_Hi7Oje8Oith7goopeeng']),
{'value': 'valid_string'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(config['test_var_Hi7Oje8Oith7goopeeng'], 'valid_string')
def test_update_validator_invalid(self):
self.client = APIClient()
self.client.login(username='admin', password='admin')
response = self.client.put(
reverse('config-detail', args=['test_var_Hi7Oje8Oith7goopeeng']),
{'value': 'invalid_string'})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'detail': 'Invalid input.'})
def validator_for_testing(value):
"""
Validator for testing.
"""
if value == 'invalid_string':
raise ValidationError({'detail': 'Invalid input.'})
@receiver(config_signal, dispatch_uid='set_simple_config_view_integration_config_test') @receiver(config_signal, dispatch_uid='set_simple_config_view_integration_config_test')
@ -47,14 +91,29 @@ def set_simple_config_view_integration_config_test(sender, **kwargs):
Sets a simple config view with some config variables but without Sets a simple config view with some config variables but without
grouping. grouping.
""" """
return ConfigCollection( yield ConfigVariable(
title='Config vars for testing', name='test_var_aeW3Quahkah1phahCheo',
url='test_url_ieXao5Wae5Duoy6Wohtu', default_value=None,
variables=(ConfigVariable(name='test_var_aeW3Quahkah1phahCheo', label='test_label_aeNahsheu8phahk8taYo')
default_value=None),
ConfigVariable(name='test_var_Xeiizi7ooH8Thuk5aida', yield ConfigVariable(
default_value='', name='test_var_Xeiizi7ooH8Thuk5aida',
form_field=forms.CharField()), default_value='')
ConfigVariable(name='test_var_ohhii4iavoh5Phoh5ahg',
default_value=0, yield ConfigVariable(
form_field=forms.IntegerField()))) name='test_var_ohhii4iavoh5Phoh5ahg',
default_value=0,
input_type='integer')
yield ConfigVariable(
name='test_var_wei0Rei9ahzooSohK1ph',
default_value='key_1_Queit2juchoocos2Vugh',
input_type='choice',
choices=(
{'value': 'key_1_Queit2juchoocos2Vugh', 'display_name': 'label_1_Queit2juchoocos2Vugh'},
{'value': 'key_2_yahb2ain1aeZ1lea1Pei', 'display_name': 'label_2_yahb2ain1aeZ1lea1Pei'}))
yield ConfigVariable(
name='test_var_Hi7Oje8Oith7goopeeng',
default_value='',
validators=(validator_for_testing,))

View File

@ -1,19 +1,8 @@
from django import forms
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.dispatch import receiver from django.dispatch import receiver
from django.test.client import Client
from openslides.config.api import ( from openslides.config.api import ConfigVariable, config
ConfigCollection,
ConfigGroup,
ConfigGroupedCollection,
ConfigVariable,
config,
)
from openslides.config.exceptions import ConfigError, ConfigNotFound from openslides.config.exceptions import ConfigError, ConfigNotFound
from openslides.config.signals import config_signal from openslides.config.signals import config_signal
from openslides.users.models import User
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -86,79 +75,47 @@ class HandleConfigTest(TestCase):
value='new_string_kbmbnfhdgibkdjshg452bc') value='new_string_kbmbnfhdgibkdjshg452bc')
self.assertEqual(config['var_with_callback_ghvnfjd5768gdfkwg0hm2'], 'new_string_kbmbnfhdgibkdjshg452bc') self.assertEqual(config['var_with_callback_ghvnfjd5768gdfkwg0hm2'], 'new_string_kbmbnfhdgibkdjshg452bc')
def test_get_default(self):
"""
Tests the methode 'default'.
"""
self.assertEqual(config.get_default('string_var'), 'default_string_rien4ooCZieng6ah')
self.assertRaisesMessage(
ConfigNotFound,
'The config variable unknown_var was not found.',
config.get_default,
'unknown_var')
class ConfigWeightTest(TestCase):
def setUp(self):
# Setup the permission
ct = ContentType.objects.get(app_label='config', model='configstore')
perm = Permission.objects.get(content_type=ct, codename='can_manage')
# Setup two users
self.manager = User.objects.create_user('config_test_manager', 'default')
self.manager.user_permissions.add(perm)
# Login
self.client_manager = Client()
self.client_manager.login(username='config_test_manager', password='default')
def test_order_of_config_views_abstract(self):
config_collection_dict = {}
for signal_receiver, config_collection in config_signal.send(sender=self):
config_collection_dict[signal_receiver.__name__] = config_collection
self.assertGreater(config_collection_dict['set_grouped_config_view'].weight, config_collection_dict['set_simple_config_view'].weight)
@receiver(config_signal, dispatch_uid='set_grouped_config_view_for_testing') @receiver(config_signal, dispatch_uid='set_grouped_config_view_for_testing')
def set_grouped_config_view(sender, **kwargs): def set_grouped_config_view(sender, **kwargs):
""" """
Sets a grouped config collection view which can be reached under the url Sets a grouped config collection. There are some variables, one variable
'/config/testgroupedpage1/'. There are some variables, one variable
with a string as default value, one with a boolean as default value, with a string as default value, one with a boolean as default value,
one with an integer as default value, one with choices and one one with an integer as default value, one with choices and one hidden
hidden variable. These variables are grouped in two groups. variable. These variables are grouped in two subgroups.
""" """
string_var = ConfigVariable( yield ConfigVariable(
name='string_var', name='string_var',
default_value='default_string_rien4ooCZieng6ah', default_value='default_string_rien4ooCZieng6ah',
form_field=forms.CharField()) group='Config vars for testing 1',
bool_var = ConfigVariable( subgroup='Group 1 aiYeix2mCieQuae3')
yield ConfigVariable(
name='bool_var', name='bool_var',
default_value=True, default_value=True,
form_field=forms.BooleanField(required=False)) input_type='boolean',
integer_var = ConfigVariable( group='Config vars for testing 1',
subgroup='Group 1 aiYeix2mCieQuae3')
yield ConfigVariable(
name='integer_var', name='integer_var',
default_value=3, default_value=3,
form_field=forms.IntegerField()) input_type='integer',
group_1 = ConfigGroup(title='Group 1 aiYeix2mCieQuae3', variables=(string_var, bool_var, integer_var)) group='Config vars for testing 1',
subgroup='Group 1 aiYeix2mCieQuae3')
hidden_var = ConfigVariable( yield ConfigVariable(
name='hidden_var', name='hidden_var',
default_value='hidden_value') default_value='hidden_value',
choices_var = ConfigVariable( group='Config vars for testing 1',
subgroup='Group 2 Toongai7ahyahy7B')
yield ConfigVariable(
name='choices_var', name='choices_var',
default_value='1', default_value='1',
form_field=forms.ChoiceField(choices=(('1', 'Choice One Ughoch4ocoche6Ee'), ('2', 'Choice Two Vahnoh5yalohv5Eb')))) input_type='choice',
group_2 = ConfigGroup(title='Group 2 Toongai7ahyahy7B', variables=(hidden_var, choices_var)) choices=(
{'value': '1', 'display_name': 'Choice One Ughoch4ocoche6Ee'},
return ConfigGroupedCollection( {'value': '2', 'display_name': 'Choice Two Vahnoh5yalohv5Eb'}),
title='Config vars for testing 1', group='Config vars for testing 1',
url='testgroupedpage1', subgroup='Group 2 Toongai7ahyahy7B')
weight=10000,
groups=(group_1, group_2),
extra_context={'extra_stylefiles': ['styles/test-config-sjNN56dFGDrg2.css'],
'extra_javascript': ['javascript/test-config-djg4dFGVslk4209f.js']})
@receiver(config_signal, dispatch_uid='set_simple_config_view_for_testing') @receiver(config_signal, dispatch_uid='set_simple_config_view_for_testing')
@ -167,12 +124,9 @@ def set_simple_config_view(sender, **kwargs):
Sets a simple config view with some config variables but without Sets a simple config view with some config variables but without
grouping. grouping.
""" """
return ConfigCollection( yield ConfigVariable(name='additional_config_var', default_value='BaeB0ahcMae3feem')
title='Config vars for testing 2', yield ConfigVariable(name='additional_config_var_2', default_value='')
url='testsimplepage1', yield ConfigVariable(name='none_config_var', default_value=None)
variables=(ConfigVariable(name='additional_config_var', default_value='BaeB0ahcMae3feem'),
ConfigVariable(name='additional_config_var_2', default_value='', form_field=forms.CharField()),
ConfigVariable(name='none_config_var', default_value=None)))
# Do not connect to the signal now but later inside the test. # Do not connect to the signal now but later inside the test.
@ -180,29 +134,20 @@ def set_simple_config_view_multiple_vars(sender, **kwargs):
""" """
Sets a bad config view with some multiple config vars. Sets a bad config view with some multiple config vars.
""" """
return ConfigCollection( yield ConfigVariable(name='multiple_config_var', default_value='foobar1')
title='Config vars for testing 3', yield ConfigVariable(name='multiple_config_var', default_value='foobar2')
url='testsimplepage2',
variables=(ConfigVariable(name='multiple_config_var', default_value='foobar1'),
ConfigVariable(name='multiple_config_var', default_value='foobar2')))
@receiver(config_signal, dispatch_uid='set_simple_config_collection_disabled_view_for_testing') @receiver(config_signal, dispatch_uid='set_simple_config_collection_disabled_view_for_testing')
def set_simple_config_collection_disabled_view(sender, **kwargs): def set_simple_config_collection_disabled_view(sender, **kwargs):
return ConfigCollection( yield ConfigVariable(name='hidden_config_var_2', default_value='')
title='Ho5iengaoon5Hoht',
url='testsimplepage3',
variables=(ConfigVariable(name='hidden_config_var_2', default_value=''),))
@receiver(config_signal, dispatch_uid='set_simple_config_collection_with_callback_for_testing') @receiver(config_signal, dispatch_uid='set_simple_config_collection_with_callback_for_testing')
def set_simple_config_collection_with_callback(sender, **kwargs): def set_simple_config_collection_with_callback(sender, **kwargs):
def callback(): def callback():
raise Exception('Change callback dhcnfg34dlg06kdg successfully called.') raise Exception('Change callback dhcnfg34dlg06kdg successfully called.')
return ConfigCollection( yield ConfigVariable(
title='Hvndfhsbgkridfgdfg', name='var_with_callback_ghvnfjd5768gdfkwg0hm2',
url='testsimplepage4', default_value='',
variables=(ConfigVariable( on_change=callback)
name='var_with_callback_ghvnfjd5768gdfkwg0hm2',
default_value='',
on_change=callback),))