Merge branch 'master' of git://github.com/melinath/philo
[philo.git] / philo / models / fields / __init__.py
1 from django import forms
2 from django.core.exceptions import ValidationError
3 from django.core.validators import validate_slug
4 from django.db import models
5 from django.utils import simplejson as json
6 from django.utils.text import capfirst
7 from django.utils.translation import ugettext_lazy as _
8
9 from philo.forms.fields import JSONFormField
10 from philo.validators import TemplateValidator, json_validator
11 #from philo.models.fields.entities import *
12
13
14 class TemplateField(models.TextField):
15         """A :class:`TextField` which is validated with a :class:`.TemplateValidator`. ``allow``, ``disallow``, and ``secure`` will be passed into the validator's construction."""
16         def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs):
17                 super(TemplateField, self).__init__(*args, **kwargs)
18                 self.validators.append(TemplateValidator(allow, disallow, secure))
19
20
21 class JSONDescriptor(object):
22         def __init__(self, field):
23                 self.field = field
24         
25         def __get__(self, instance, owner):
26                 if instance is None:
27                         raise AttributeError # ?
28                 
29                 if self.field.name not in instance.__dict__:
30                         json_string = getattr(instance, self.field.attname)
31                         instance.__dict__[self.field.name] = json.loads(json_string)
32                 
33                 return instance.__dict__[self.field.name]
34         
35         def __set__(self, instance, value):
36                 instance.__dict__[self.field.name] = value
37                 setattr(instance, self.field.attname, json.dumps(value))
38         
39         def __delete__(self, instance):
40                 del(instance.__dict__[self.field.name])
41                 setattr(instance, self.field.attname, json.dumps(None))
42
43
44 class JSONField(models.TextField):
45         """A :class:`TextField` which stores its value on the model instance as a python object and stores its value in the database as JSON. Validated with :func:`.json_validator`."""
46         default_validators = [json_validator]
47         
48         def get_attname(self):
49                 return "%s_json" % self.name
50         
51         def contribute_to_class(self, cls, name):
52                 super(JSONField, self).contribute_to_class(cls, name)
53                 setattr(cls, name, JSONDescriptor(self))
54                 models.signals.pre_init.connect(self.fix_init_kwarg, sender=cls)
55         
56         def fix_init_kwarg(self, sender, args, kwargs, **signal_kwargs):
57                 # Anything passed in as self.name is assumed to come from a serializer and
58                 # will be treated as a json string.
59                 if self.name in kwargs:
60                         value = kwargs.pop(self.name)
61                         
62                         # Hack to handle the xml serializer's handling of "null"
63                         if value is None:
64                                 value = 'null'
65                         
66                         kwargs[self.attname] = value
67         
68         def formfield(self, *args, **kwargs):
69                 kwargs["form_class"] = JSONFormField
70                 return super(JSONField, self).formfield(*args, **kwargs)
71
72
73 class SlugMultipleChoiceField(models.Field):
74         """Stores a selection of multiple items with unique slugs in the form of a comma-separated list."""
75         __metaclass__ = models.SubfieldBase
76         description = _("Comma-separated slug field")
77         
78         def get_internal_type(self):
79                 return "TextField"
80         
81         def to_python(self, value):
82                 if not value:
83                         return []
84                 
85                 if isinstance(value, list):
86                         return value
87                 
88                 return value.split(',')
89         
90         def get_prep_value(self, value):
91                 return ','.join(value)
92         
93         def formfield(self, **kwargs):
94                 # This is necessary because django hard-codes TypedChoiceField for things with choices.
95                 defaults = {
96                         'widget': forms.CheckboxSelectMultiple,
97                         'choices': self.get_choices(include_blank=False),
98                         'label': capfirst(self.verbose_name),
99                         'required': not self.blank,
100                         'help_text': self.help_text
101                 }
102                 if self.has_default():
103                         if callable(self.default):
104                                 defaults['initial'] = self.default
105                                 defaults['show_hidden_initial'] = True
106                         else:
107                                 defaults['initial'] = self.get_default()
108                 
109                 for k in kwargs.keys():
110                         if k not in ('coerce', 'empty_value', 'choices', 'required',
111                                                  'widget', 'label', 'initial', 'help_text',
112                                                  'error_messages', 'show_hidden_initial'):
113                                 del kwargs[k]
114                 
115                 defaults.update(kwargs)
116                 form_class = forms.TypedMultipleChoiceField
117                 return form_class(**defaults)
118         
119         def validate(self, value, model_instance):
120                 invalid_values = []
121                 for val in value:
122                         try:
123                                 validate_slug(val)
124                         except ValidationError:
125                                 invalid_values.append(val)
126                 
127                 if invalid_values:
128                         # should really make a custom message.
129                         raise ValidationError(self.error_messages['invalid_choice'] % invalid_values)
130
131
132 try:
133         from south.modelsinspector import add_introspection_rules
134 except ImportError:
135         pass
136 else:
137         add_introspection_rules([], ["^philo\.models\.fields\.SlugMultipleChoiceField"])
138         add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"])
139         add_introspection_rules([], ["^philo\.models\.fields\.JSONField"])