Corrections to Blog.entry_tags to use taggit APIs. Tweaks to penfield migration 0005...
[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.utils.registry import RegistryIterator
11 from philo.validators import TemplateValidator, json_validator
12 from philo.forms.widgets import EmbedWidget
13 #from philo.models.fields.entities import *
14
15
16 class TemplateField(models.Field):
17         """A :class:`TextField` which is validated with a :class:`.TemplateValidator`. ``allow``, ``disallow``, and ``secure`` will be passed into the validator's construction."""
18         def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs):
19                 super(TemplateField, self).__init__(*args, **kwargs)
20                 self.validators.append(TemplateValidator(allow, disallow, secure))
21         
22         def formfield(self, **kwargs):
23                 defaults = {'widget': EmbedWidget}
24                 defaults.update(kwargs)
25                 return super(TemplateField, self).formfield(**defaults)
26
27
28 class JSONDescriptor(object):
29         def __init__(self, field):
30                 self.field = field
31         
32         def __get__(self, instance, owner):
33                 if instance is None:
34                         raise AttributeError # ?
35                 
36                 if self.field.name not in instance.__dict__:
37                         json_string = getattr(instance, self.field.attname)
38                         instance.__dict__[self.field.name] = json.loads(json_string)
39                 
40                 return instance.__dict__[self.field.name]
41         
42         def __set__(self, instance, value):
43                 instance.__dict__[self.field.name] = value
44                 setattr(instance, self.field.attname, json.dumps(value))
45         
46         def __delete__(self, instance):
47                 del(instance.__dict__[self.field.name])
48                 setattr(instance, self.field.attname, json.dumps(None))
49
50
51 class JSONField(models.TextField):
52         """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`."""
53         default_validators = [json_validator]
54         
55         def get_attname(self):
56                 return "%s_json" % self.name
57         
58         def contribute_to_class(self, cls, name):
59                 super(JSONField, self).contribute_to_class(cls, name)
60                 setattr(cls, name, JSONDescriptor(self))
61                 models.signals.pre_init.connect(self.fix_init_kwarg, sender=cls)
62         
63         def fix_init_kwarg(self, sender, args, kwargs, **signal_kwargs):
64                 # Anything passed in as self.name is assumed to come from a serializer and
65                 # will be treated as a json string.
66                 if self.name in kwargs:
67                         value = kwargs.pop(self.name)
68                         
69                         # Hack to handle the xml serializer's handling of "null"
70                         if value is None:
71                                 value = 'null'
72                         
73                         kwargs[self.attname] = value
74         
75         def formfield(self, *args, **kwargs):
76                 kwargs["form_class"] = JSONFormField
77                 return super(JSONField, self).formfield(*args, **kwargs)
78
79
80 class SlugMultipleChoiceField(models.Field):
81         """Stores a selection of multiple items with unique slugs in the form of a comma-separated list. Also knows how to correctly handle :class:`RegistryIterator`\ s passed in as choices."""
82         __metaclass__ = models.SubfieldBase
83         description = _("Comma-separated slug field")
84         
85         def get_internal_type(self):
86                 return "TextField"
87         
88         def to_python(self, value):
89                 if not value:
90                         return []
91                 
92                 if isinstance(value, list):
93                         return value
94                 
95                 return value.split(',')
96         
97         def get_prep_value(self, value):
98                 return ','.join(value)
99         
100         def formfield(self, **kwargs):
101                 # This is necessary because django hard-codes TypedChoiceField for things with choices.
102                 defaults = {
103                         'widget': forms.CheckboxSelectMultiple,
104                         'choices': self.get_choices(include_blank=False),
105                         'label': capfirst(self.verbose_name),
106                         'required': not self.blank,
107                         'help_text': self.help_text
108                 }
109                 if self.has_default():
110                         if callable(self.default):
111                                 defaults['initial'] = self.default
112                                 defaults['show_hidden_initial'] = True
113                         else:
114                                 defaults['initial'] = self.get_default()
115                 
116                 for k in kwargs.keys():
117                         if k not in ('coerce', 'empty_value', 'choices', 'required',
118                                                  'widget', 'label', 'initial', 'help_text',
119                                                  'error_messages', 'show_hidden_initial'):
120                                 del kwargs[k]
121                 
122                 defaults.update(kwargs)
123                 form_class = forms.TypedMultipleChoiceField
124                 return form_class(**defaults)
125         
126         def validate(self, value, model_instance):
127                 invalid_values = []
128                 for val in value:
129                         try:
130                                 validate_slug(val)
131                         except ValidationError:
132                                 invalid_values.append(val)
133                 
134                 if invalid_values:
135                         # should really make a custom message.
136                         raise ValidationError(self.error_messages['invalid_choice'] % invalid_values)
137         
138         def _get_choices(self):
139                 if isinstance(self._choices, RegistryIterator):
140                         return self._choices.copy()
141                 elif hasattr(self._choices, 'next'):
142                         choices, self._choices = itertools.tee(self._choices)
143                         return choices
144                 else:
145                         return self._choices
146         choices = property(_get_choices)
147
148
149 try:
150         from south.modelsinspector import add_introspection_rules
151 except ImportError:
152         pass
153 else:
154         add_introspection_rules([], ["^philo\.models\.fields\.SlugMultipleChoiceField"])
155         add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"])
156         add_introspection_rules([], ["^philo\.models\.fields\.JSONField"])