• Facebook
  • Twitter
  • Reddit
  • StumbleUpon
  • Digg
  • email

"""Taken and modified from the dbsettings project.
 
http://code.google.com/p/django-values/
"""
from decimal import Decimal
from django import forms
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
try:
    from django.utils import simplejson
except ImportError:
    import simplejson
from django.utils.datastructures import SortedDict
from django.utils.encoding import smart_str
from django.utils.translation import gettext, ugettext_lazy as _
from keyedcache import cache_set
from livesettings.models import find_setting, LongSetting, Setting, SettingNotSet
from livesettings.overrides import get_overrides
from satchmo_utils import load_module, is_string_like, is_list_or_tuple
import datetime
import logging
import signals
 
__all__ = ['BASE_GROUP', 'ConfigurationGroup', 'Value', 'BooleanValue', 'DecimalValue', 'DurationValue',
      'FloatValue', 'IntegerValue', 'ModuleValue', 'PercentValue', 'PositiveIntegerValue', 'SortedDotDict',
      'StringValue', 'LongStringValue', 'MultipleStringValue']
 
_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 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)
        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 values(self):
        vals = super(ConfigurationGroup, self).values()
        return [v for v in vals if v.enabled()]
 
BASE_GROUP = ConfigurationGroup('BASE', _('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`.
        """
        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.requires = kwargs.pop('requires', None)
        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(smart_str(x[1]))
                note = gettext('Default value: ') + ", ".join(work)
 
            else:
                note = _("Default value: %s") % 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):            
        try:
            val = self.setting.value
 
        except SettingNotSet, sns:
            if self.use_default:
                val = self.default
            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:        
                db_value = self.get_db_prep_save(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)
 
                return True
        else:
            log.debug('not updating setting %s.%s - livesettings db is disabled',self.group.key, self.key)
 
        return False
 
    def value(self):
        val = self._value()
        return self.to_python(val)
 
    value = property(fget = value)
 
    def editor_value(self):
        val = self._value()
        return self.to_editor(val)
 
    editor_value = property(fget = editor_value)
 
    # 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 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 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