"""Taken and modified from the dbsettings project. http://code.google.com/p/django-values/ """ from decimal import Decimal from django import forms from django.conf import settings as django_settings from django.core.exceptions import ImproperlyConfigured from django.core.cache import cache from django.utils import simplejson from django.utils.datastructures import SortedDict from django.utils.encoding import force_unicode from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy from django.core.files import storage from askbot.deps.livesettings.models import find_setting, LongSetting, Setting, SettingNotSet from askbot.deps.livesettings.overrides import get_overrides from askbot.deps.livesettings.utils import load_module, is_string_like, is_list_or_tuple from askbot.deps.livesettings.widgets import ImageInput import datetime import logging import signals import os __all__ = ['BASE_GROUP', 'BASE_SUPER_GROUP', 'ConfigurationGroup', 'Value', 'BooleanValue', 'DecimalValue', 'DurationValue', 'FloatValue', 'IntegerValue', 'ModuleValue', 'PercentValue', 'PositiveIntegerValue', 'SortedDotDict', 'StringValue', 'SuperGroup', 'ImageValue', 'LongStringValue', 'MultipleStringValue', 'URLValue'] _WARN = {} log = logging.getLogger('configuration') NOTSET = object() class SortedDotDict(SortedDict): def __getattr__(self, key): try: return self[key] except: raise AttributeError, key def __iter__(self): vals = self.values() for k in vals: yield k def values(self): vals = super(SortedDotDict, self).values() vals = [v for v in vals if isinstance(v, (ConfigurationGroup, Value))] vals.sort() return vals class SuperGroup(object): """Aggregates ConfigurationGroup's into super-groups that are used only for the presentation in the UI""" def __init__(self, name, ordering = 0): self.name = name self.ordering = ordering self.groups = list() def append(self, group): """adds instance of :class:`ConfigurationGroup` to the super group """ if group not in self.groups: self.groups.append(group) BASE_SUPER_GROUP = SuperGroup(ugettext_lazy('Main')) class ConfigurationGroup(SortedDotDict): """A simple wrapper for a group of configuration values""" def __init__(self, key, name, *args, **kwargs): """Create a new ConfigurationGroup. Arguments: - key - group name - for display to user Named Arguments: - ordering: integer, optional, defaults to 1. - requires: See `Value` requires. The default `requires` all member values will have if not overridden. - requiresvalue: See `Values` requires_value. The default `requires_value` if not overridden on the `Value` objects. """ self.key = key self.name = name self.ordering = kwargs.pop('ordering', 1) self.requires = kwargs.pop('requires', None) self.super_group = kwargs.pop('super_group', BASE_SUPER_GROUP) self.super_group.append(self) if self.requires: reqval = kwargs.pop('requiresvalue', key) if not is_list_or_tuple(reqval): reqval = (reqval, reqval) self.requires_value = reqval[0] self.requires.add_choice(reqval) super(ConfigurationGroup, self).__init__(*args, **kwargs) def __cmp__(self, other): return cmp((self.ordering, self.name), (other.ordering, other.name)) def __eq__(self, other): return (type(self) == type(other) and self.ordering == other.ordering and self.name == other.name) def __ne__(self, other): return not self == other def dict_values(self, load_modules=True): vals = {} keys = super(ConfigurationGroup, self).keys() for key in keys: v = self[key] if isinstance(v, Value): value = v.value else: value = v vals[key] = value return vals def values(self): vals = super(ConfigurationGroup, self).values() return [v for v in vals if v.enabled()] BASE_GROUP = ConfigurationGroup( 'BASE', ugettext_lazy('Base Settings'), ordering=0 ) class Value(object): creation_counter = 0 def __init__(self, group, key, **kwargs): """ Create a new Value object for configuration. Args: - `ConfigurationGroup` - key - a string key Named arguments: - `description` - Will be passed to the field for form usage. Should be a translation proxy. Ex: _('example') - `help_text` - Will be passed to the field for form usage. - `choices` - If given, then the form field will use a select box - `ordering` - Defaults to alphabetical by key if not given. - `requires` - If given as a `Value`, then this field will only be rendered if that Value evaluates true (for Boolean requires) or the proper key is in the associated value. - `requiresvalue` - If set, then this field will only be rendered if that value is in the list returned by self.value. Defaults to self.key. - `hidden` - If true, then render a hidden field. - `default` - If given, then this Value will return that default whenever it has no assocated `Setting`. - `update_callback` - if given, then this value will call the callback whenever updated - `clear_cache` - if `True` - clear all the caches on updates """ self.group = group self.key = key self.description = kwargs.get('description', None) self.help_text = kwargs.get('help_text') self.choices = kwargs.get('choices',[]) self.ordering = kwargs.pop('ordering', 0) self.hidden = kwargs.pop('hidden', False) self.update_callback = kwargs.pop('update_callback', None) self.requires = kwargs.pop('requires', None) self.clear_cache = kwargs.pop('clear_cache', False) if self.requires: reqval = kwargs.pop('requiresvalue', key) if not is_list_or_tuple(reqval): reqval = (reqval, reqval) self.requires_value = reqval[0] self.requires.add_choice(reqval) elif group.requires: self.requires = group.requires self.requires_value = group.requires_value if kwargs.has_key('default'): self.default = kwargs.pop('default') self.use_default = True else: self.use_default = False self.creation_counter = Value.creation_counter Value.creation_counter += 1 def __cmp__(self, other): return cmp((self.ordering, self.description, self.creation_counter), (other.ordering, other.description, other.creation_counter)) def __eq__(self, other): if type(self) == type(other): return self.value == other.value else: return self.value == other def __iter__(self): return iter(self.value) def __unicode__(self): return unicode(self.value) def __str__(self): return str(self.value) def add_choice(self, choice): """Add a choice if it doesn't already exist.""" if not is_list_or_tuple(choice): choice = (choice, choice) skip = False for k, v in self.choices: if k == choice[0]: skip = True break if not skip: self.choices += (choice, ) def choice_field(self, **kwargs): if self.hidden: kwargs['widget'] = forms.MultipleHiddenInput() return forms.ChoiceField(choices=self.choices, **kwargs) def _choice_values(self): choices = self.choices vals = self.value return [x for x in choices if x[0] in vals] choice_values = property(fget=_choice_values) def copy(self): new_value = self.__class__(self.key) new_value.__dict__ = self.__dict__.copy() return new_value def _default_text(self): if not self.use_default: note = "" else: if self.default == "": note = _('Default value: ""') elif self.choices: work = [] for x in self.choices: if x[0] in self.default: work.append(force_unicode(x[1])) note = _('Default value: ') + unicode(u", ".join(work)) else: note = _("Default value: %s") % force_unicode(self.default) return note default_text = property(fget=_default_text) def enabled(self): enabled = False try: if not self.requires: enabled = True else: v = self.requires.value if self.requires.choices: enabled = self.requires_value == v or self.requires_value in v elif v: enabled = True except SettingNotSet: pass return enabled def make_field(self, **kwargs): if self.choices: if self.hidden: kwargs['widget'] = forms.MultipleHiddenInput() field = self.choice_field(**kwargs) else: if self.hidden: kwargs['widget'] = forms.HiddenInput() field = self.field(**kwargs) field.group = self.group field.default_text = self.default_text return field def make_setting(self, db_value): log.debug('new setting %s.%s', self.group.key, self.key) return Setting(group=self.group.key, key=self.key, value=db_value) def _setting(self): return find_setting(self.group.key, self.key) setting = property(fget = _setting) def _value(self): use_db, overrides = get_overrides() if not use_db: try: val = overrides[self.group.key][self.key] except KeyError: if self.use_default: val = self.default else: raise SettingNotSet('%s.%s is not in your LIVESETTINGS_OPTIONS' % (self.group.key, self.key)) else: try: val = self.setting.value except SettingNotSet, sns: if self.use_default: val = self.default if overrides: # maybe override the default grp = overrides.get(self.group.key, {}) if grp.has_key(self.key): val = grp[self.key] else: val = NOTSET except AttributeError, ae: log.error("Attribute error: %s", ae) log.error("%s: Could not get _value of %s", self.key, self.setting) raise(ae) except Exception, e: global _WARN log.error(e) if str(e).find("configuration_setting") > -1: if not _WARN.has_key('configuration_setting'): log.warn('Error loading setting %s.%s from table, OK if you are in syncdb', self.group.key, self.key) _WARN['configuration_setting'] = True if self.use_default: val = self.default else: raise ImproperlyConfigured("All settings used in startup must have defaults, %s.%s does not", self.group.key, self.key) else: import traceback traceback.print_exc() log.warn("Problem finding settings %s.%s, %s", self.group.key, self.key, e) raise SettingNotSet("Startup error, couldn't load %s.%s" %(self.group.key, self.key)) return val def update(self, value): use_db, overrides = get_overrides() if use_db: current_value = self.value new_value = self.to_python(value) if current_value != new_value: if self.update_callback: new_value = apply(self.update_callback, (current_value, new_value)) db_value = self.get_db_prep_save(new_value) try: s = self.setting s.value = db_value except SettingNotSet: s = self.make_setting(db_value) if self.use_default and self.default == new_value: if s.id: log.info("Deleted setting %s.%s", self.group.key, self.key) s.delete() else: log.info("Updated setting %s.%s = %s", self.group.key, self.key, value) s.save() signals.configuration_value_changed.send(self, old_value=current_value, new_value=new_value, setting=self) if self.clear_cache: cache.clear() return True else: log.debug('not updating setting %s.%s - askbot.deps.livesettings db is disabled',self.group.key, self.key) return False @property def value(self): val = self._value() return self.to_python(val) @property def editor_value(self): val = self._value() return self.to_editor(val) # Subclasses should override the following methods where applicable def to_python(self, value): "Returns a native Python object suitable for immediate use" if value == NOTSET: value = None return value def get_db_prep_save(self, value): "Returns a value suitable for storage into a CharField" if value == NOTSET: value = "" return unicode(value) def to_editor(self, value): "Returns a value suitable for display in a form widget" if value == NOTSET: return NOTSET return unicode(value) ############### # VALUE TYPES # ############### class BooleanValue(Value): class field(forms.BooleanField): def __init__(self, *args, **kwargs): kwargs['required'] = False forms.BooleanField.__init__(self, *args, **kwargs) def add_choice(self, choice): # ignore choice adding for boolean types pass def to_python(self, value): if value in (True, 't', 'True', 1, '1'): return True return False to_editor = to_python class DecimalValue(Value): class field(forms.DecimalField): def __init__(self, *args, **kwargs): kwargs['required'] = False forms.DecimalField.__init__(self, *args, **kwargs) def to_python(self, value): if value==NOTSET: return Decimal("0") try: return Decimal(value) except TypeError, te: log.warning("Can't convert %s to Decimal for settings %s.%s", value, self.group.key, self.key) raise TypeError(te) def to_editor(self, value): if value == NOTSET: return "0" else: return unicode(value) # DurationValue has a lot of duplication and ugliness because of issue #2443 # Until DurationField is sorted out, this has to do some extra work class DurationValue(Value): class field(forms.CharField): def clean(self, value): try: return datetime.timedelta(seconds=float(value)) except (ValueError, TypeError): raise forms.ValidationError('This value must be a real number.') except OverflowError: raise forms.ValidationError('The maximum allowed value is %s' % datetime.timedelta.max) def to_python(self, value): if value == NOTSET: value = 0 if isinstance(value, datetime.timedelta): return value try: return datetime.timedelta(seconds=float(value)) except (ValueError, TypeError): raise forms.ValidationError('This value must be a real number.') except OverflowError: raise forms.ValidationError('The maximum allowed value is %s' % datetime.timedelta.max) def get_db_prep_save(self, value): if value == NOTSET: return NOTSET else: return unicode(value.days * 24 * 3600 + value.seconds + float(value.microseconds) / 1000000) class FloatValue(Value): class field(forms.FloatField): def __init__(self, *args, **kwargs): kwargs['required'] = False forms.FloatField.__init__(self, *args, **kwargs) def to_python(self, value): if value == NOTSET: value = 0 return float(value) def to_editor(self, value): if value == NOTSET: return "0" else: return unicode(value) class IntegerValue(Value): class field(forms.IntegerField): def __init__(self, *args, **kwargs): kwargs['required'] = False forms.IntegerField.__init__(self, *args, **kwargs) def to_python(self, value): if value == NOTSET: value = 0 return int(value) def to_editor(self, value): if value == NOTSET: return "0" else: return unicode(value) class PercentValue(Value): class field(forms.DecimalField): def __init__(self, *args, **kwargs): kwargs['required'] = False forms.DecimalField.__init__(self, 100, 0, 5, 2, *args, **kwargs) class widget(forms.TextInput): def render(self, *args, **kwargs): # Place a percent sign after a smaller text field attrs = kwargs.pop('attrs', {}) attrs['size'] = attrs['max_length'] = 6 return forms.TextInput.render(self, attrs=attrs, *args, **kwargs) + '%' def to_python(self, value): if value == NOTSET: value = 0 return Decimal(value) / 100 def to_editor(self, value): if value == NOTSET: return "0" else: return unicode(value) class PositiveIntegerValue(IntegerValue): class field(forms.IntegerField): def __init__(self, *args, **kwargs): kwargs['min_value'] = 0 forms.IntegerField.__init__(self, *args, **kwargs) class StringValue(Value): class field(forms.CharField): def __init__(self, *args, **kwargs): kwargs['required'] = False forms.CharField.__init__(self, *args, **kwargs) def to_python(self, value): if value == NOTSET: value = "" return unicode(value) to_editor = to_python class URLValue(Value): class field(forms.URLField): def __init__(self, *args, **kwargs): kwargs['required'] = False forms.URLField.__init__(self, *args, **kwargs) class LongStringValue(Value): class field(forms.CharField): def __init__(self, *args, **kwargs): kwargs['required'] = False kwargs['widget'] = forms.Textarea() forms.CharField.__init__(self, *args, **kwargs) def make_setting(self, db_value): log.debug('new long setting %s.%s', self.group.key, self.key) return LongSetting(group=self.group.key, key=self.key, value=db_value) def to_python(self, value): if value == NOTSET: value = "" return unicode(value) to_editor = to_python class ImageValue(StringValue): def __init__(self, *args, **kwargs): self.allowed_file_extensions = kwargs.pop( 'allowed_file_extensions', ('jpg', 'gif', 'png') ) self.upload_directory = kwargs.pop( 'upload_directory', django_settings.MEDIA_ROOT ) self.upload_url = kwargs.pop( 'upload_url', django_settings.MEDIA_URL ) self.url_resolver = kwargs.pop('url_resolver', None) super(ImageValue, self).__init__(*args, **kwargs) class field(forms.FileField): def __init__(self, *args, **kwargs): kwargs['required'] = False self.allowed_file_extensions = kwargs.pop('allowed_file_extensions') url_resolver = kwargs.pop('url_resolver') kwargs['widget'] = ImageInput(url_resolver = url_resolver) forms.FileField.__init__(self, *args, **kwargs) def clean(self, file_data, initial=None): if not file_data and initial: return initial (base_name, ext) = os.path.splitext(file_data.name) #first character in ext is . if ext[1:].lower() not in self.allowed_file_extensions: error_message = _('Allowed image file types are %(types)s') \ % {'types': ', '.join(self.allowed_file_extensions)} raise forms.ValidationError(error_message) def make_field(self, **kwargs): kwargs['url_resolver'] = self.url_resolver kwargs['allowed_file_extensions'] = self.allowed_file_extensions return super(StringValue, self).make_field(**kwargs) def update(self, uploaded_file): """uploaded_file is an instance of django UploadedFile object """ #0) initialize file storage file_storage_class = storage.get_storage_class() storage_settings = {} if django_settings.DEFAULT_FILE_STORAGE == \ 'django.core.files.storage.FileSystemStorage': storage_settings = { 'location': self.upload_directory, 'base_url': self.upload_url } file_storage = file_storage_class(**storage_settings) #1) come up with a file name #todo: need better function here to calc name file_name = file_storage.get_available_name(uploaded_file.name) file_storage.save(file_name, uploaded_file) url = file_storage.url(file_name) old_file = self.value old_file = old_file.replace(self.upload_url, '', 1) old_file_path = os.path.join(self.upload_directory, old_file) if os.path.isfile(old_file_path): os.unlink(old_file_path) #saved file path is relative to the upload_directory #so that things could be easily relocated super(ImageValue, self).update(url) class MultipleStringValue(Value): class field(forms.CharField): def __init__(self, *args, **kwargs): kwargs['required'] = False forms.CharField.__init__(self, *args, **kwargs) def choice_field(self, **kwargs): kwargs['required'] = False return forms.MultipleChoiceField(choices=self.choices, **kwargs) def get_db_prep_save(self, value): if is_string_like(value): value = [value] return simplejson.dumps(value) def to_python(self, value): if not value or value == NOTSET: return [] if is_list_or_tuple(value): return value else: try: return simplejson.loads(value) except: if is_string_like(value): return [value] else: log.warning('Could not decode returning empty list: %s', value) return [] to_editor = to_python class ModuleValue(Value): """Handles setting modules, storing them as strings in the db.""" class field(forms.CharField): def __init__(self, *args, **kwargs): kwargs['required'] = False forms.CharField.__init__(self, *args, **kwargs) def load_module(self, module): """Load a child module""" value = self._value() if value == NOTSET: raise SettingNotSet("%s.%s", self.group.key, self.key) else: return load_module("%s.%s" % (value, module)) def to_python(self, value): if value == NOTSET: v = {} else: v = load_module(value) return v def to_editor(self, value): if value == NOTSET: value = "" return value