OpenSlides/openslides/config/api.py
Norman Jäckel c5fbe2e9ee 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.
2015-06-18 21:13:30 +02:00

188 lines
6.8 KiB
Python

from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import ugettext as _
from .exceptions import ConfigError, ConfigNotFound
from .models import ConfigStore
from .signals import config_signal
INPUT_TYPE_MAPPING = {
'string': str,
'integer': int,
'boolean': bool,
'choice': str}
class ConfigHandler:
"""
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
config[...] = x.
"""
def __getitem__(self, key):
"""
Returns the value of the config variable. Builds the cache if it
does not exist.
"""
try:
return self._cache[key]
except KeyError:
raise ConfigNotFound(_('The config variable %s was not found.') % key)
except AttributeError:
self.setup_cache()
return self[key]
def setup_cache(self):
"""
Creates a cache of all config variables with their current value.
"""
self._cache = {}
for key, config_variable in self.get_config_variables().items():
self._cache[key] = config_variable.default_value
for config_object in ConfigStore.objects.all():
self._cache[config_object.key] = config_object.value
def __contains__(self, key):
try:
config[key]
except ConfigNotFound:
return False
else:
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):
"""
Generator to get all config variables as strings when their values are
intended to be translated.
"""
for config_variable in self.get_config_variables().values():
if config_variable.translatable:
yield config_variable.name
config = ConfigHandler()
"""
Final entry point to get an set config variables. To get a config variable
use x = config[...], to set it use config[...] = x.
"""
class ConfigVariable:
"""
A simple object class to wrap new config variables.
The keyword arguments 'name' and 'default_value' are required.
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, name, default_value, input_type='string', label=None,
help_text=None, choices=None, weight=0, group=None,
subgroup=None, validators=None, on_change=None, translatable=False):
if input_type not in INPUT_TYPE_MAPPING:
raise ValueError(_('Invalid value for config attribute input_type.'))
if input_type == 'choice' and choices is None:
raise ConfigError(_("Either config attribute 'choices' must not be None or "
"'input_type' must not be 'choice'."))
elif input_type != 'choice' and choices is not None:
raise ConfigError(_("Either config attribute 'choices' must be None or "
"'input_type' must be 'choice'."))
self.name = name
self.default_value = default_value
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.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