Merge branch 'release/0.9.2'
[philo.git] / philo / models / fields / entities.py
1 import datetime
2 from itertools import tee
3
4 from django import forms
5 from django.core.exceptions import FieldError, ValidationError
6 from django.db import models
7 from django.db.models.fields import NOT_PROVIDED
8 from django.utils.text import capfirst
9
10 from philo.models import ManyToManyValue, JSONValue, ForeignKeyValue, Attribute, Entity
11 from philo.signals import entity_class_prepared
12
13
14 __all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
15
16
17 ATTRIBUTE_REGISTRY = '_attribute_registry'
18
19
20 class AttributeProxyField(object):
21         """
22         :class:`AttributeProxyField`\ s can be assigned as fields on a subclass of :class:`philo.models.base.Entity`. They act like any other model fields, but instead of saving their data to the model's table, they save it to :class:`.Attribute`\ s related to a model instance. Additionally, a new :class:`.Attribute` will be created for an instance if and only if the field's value has been set. This is relevant i.e. for :class:`.PassthroughAttributeMapper`\ s and :class:`.TreeAttributeMapper`\ s, where even an :class:`.Attribute` with a value of ``None`` will prevent a passthrough.
23         
24         Example::
25         
26                 class Thing(Entity):
27                         numbers = models.PositiveIntegerField()
28                         improvised = JSONAttribute(models.BooleanField)
29         
30         :param attribute_key: The key of the attribute that will be used to store this field's value, if it is different than the field's name.
31         
32         The remaining parameters have the same meaning as for ordinary model fields.
33         
34         """
35         def __init__(self, attribute_key=None, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs):
36                 self.attribute_key = attribute_key
37                 self.verbose_name = verbose_name
38                 self.help_text = help_text
39                 self.default = default
40                 self.editable = editable
41                 self._choices = choices or []
42         
43         def actually_contribute_to_class(self, sender, **kwargs):
44                 sender._entity_meta.add_proxy_field(self)
45                 setattr(sender, self.name, AttributeFieldDescriptor(self))
46                 opts = sender._entity_meta
47                 if not hasattr(opts, '_has_attribute_fields'):
48                         opts._has_attribute_fields = True
49                         models.signals.post_save.connect(process_attribute_fields, sender=sender)
50         
51         def contribute_to_class(self, cls, name):
52                 if self.attribute_key is None:
53                         self.attribute_key = name
54                 if issubclass(cls, Entity):
55                         self.name = self.attname = name
56                         self.model = cls
57                         if self.verbose_name is None and name:
58                                 self.verbose_name = name.replace('_', ' ')
59                         entity_class_prepared.connect(self.actually_contribute_to_class, sender=cls)
60                 else:
61                         raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__)
62         
63         def formfield(self, form_class=forms.CharField, **kwargs):
64                 """
65                 Returns a form field capable of accepting values for the :class:`AttributeProxyField`.
66                 
67                 """
68                 defaults = {
69                         'required': False,
70                         'label': capfirst(self.verbose_name),
71                         'help_text': self.help_text
72                 }
73                 if self.has_default():
74                         defaults['initial'] = self.default
75                 defaults.update(kwargs)
76                 return form_class(**defaults)
77         
78         def value_from_object(self, obj):
79                 """Returns the value of this field in the given model instance."""
80                 return getattr(obj, self.name)
81         
82         def get_storage_value(self, value):
83                 """Final conversion of ``value`` before it gets stored on an :class:`.Entity` instance. This will be called during :meth:`.EntityForm.save`."""
84                 return value
85         
86         def validate_value(self, value):
87                 "Raise an appropriate exception if ``value`` is not valid for this :class:`AttributeProxyField`."
88                 pass
89         
90         def has_default(self):
91                 """Returns ``True`` if a default value was provided and ``False`` otherwise."""
92                 return self.default is not NOT_PROVIDED
93         
94         def _get_choices(self):
95                 """Returns the choices passed into the constructor."""
96                 if hasattr(self._choices, 'next'):
97                         choices, self._choices = tee(self._choices)
98                         return choices
99                 else:
100                         return self._choices
101         choices = property(_get_choices)
102         
103         @property
104         def value_class(self):
105                 """Each :class:`AttributeProxyField` subclass can define a value_class to use for creation of new :class:`.AttributeValue`\ s"""
106                 raise AttributeError("value_class must be defined on %s subclasses." % self.__class__.__name__)
107
108
109 class AttributeFieldDescriptor(object):
110         def __init__(self, field):
111                 self.field = field
112         
113         def get_registry(self, instance):
114                 if ATTRIBUTE_REGISTRY not in instance.__dict__:
115                         instance.__dict__[ATTRIBUTE_REGISTRY] = {'added': set(), 'removed': set()}
116                 return instance.__dict__[ATTRIBUTE_REGISTRY]
117         
118         def __get__(self, instance, owner):
119                 if instance is None:
120                         return self
121                 
122                 if self.field.name not in instance.__dict__:
123                         instance.__dict__[self.field.name] = instance.attributes.get(self.field.attribute_key, None)
124                 
125                 return instance.__dict__[self.field.name]
126         
127         def __set__(self, instance, value):
128                 if instance is None:
129                         raise AttributeError("%s must be accessed via instance" % self.field.name)
130                 
131                 self.field.validate_value(value)
132                 instance.__dict__[self.field.name] = value
133                 
134                 registry = self.get_registry(instance)
135                 registry['added'].add(self.field)
136                 registry['removed'].discard(self.field)
137         
138         def __delete__(self, instance):
139                 del instance.__dict__[self.field.name]
140                 
141                 registry = self.get_registry(instance)
142                 registry['added'].discard(self.field)
143                 registry['removed'].add(self.field)
144
145
146 def process_attribute_fields(sender, instance, created, **kwargs):
147         """This function is attached to each :class:`Entity` subclass's post_save signal. Any :class:`Attribute`\ s managed by :class:`AttributeProxyField`\ s which have been removed will be deleted, and any new attributes will be created."""
148         if ATTRIBUTE_REGISTRY in instance.__dict__:
149                 registry = instance.__dict__[ATTRIBUTE_REGISTRY]
150                 instance.attribute_set.filter(key__in=[field.attribute_key for field in registry['removed']]).delete()
151                 
152                 for field in registry['added']:
153                         # TODO: Should this perhaps just use instance.attributes[field.attribute_key] = getattr(instance, field.name, None)?
154                         # (Would eliminate the need for field.value_class.)
155                         try:
156                                 attribute = instance.attribute_set.get(key=field.attribute_key)
157                         except Attribute.DoesNotExist:
158                                 attribute = Attribute()
159                                 attribute.entity = instance
160                                 attribute.key = field.attribute_key
161                         attribute.set_value(value=getattr(instance, field.name, None), value_class=field.value_class)
162                 del instance.__dict__[ATTRIBUTE_REGISTRY]
163
164
165 class JSONAttribute(AttributeProxyField):
166         """
167         Handles an :class:`.Attribute` with a :class:`.JSONValue`.
168         
169         :param field_template: A django form field instance that will be used to guide rendering and interpret values. For example, using :class:`django.forms.BooleanField` will make this field render as a checkbox.
170         
171         """
172         
173         value_class = JSONValue
174         
175         def __init__(self, field_template=None, **kwargs):
176                 super(JSONAttribute, self).__init__(**kwargs)
177                 if field_template is None:
178                         field_template = models.CharField(max_length=255)
179                 self.field_template = field_template
180         
181         def formfield(self, **kwargs):
182                 defaults = {
183                         'required': False,
184                         'label': capfirst(self.verbose_name),
185                         'help_text': self.help_text
186                 }
187                 if self.has_default():
188                         defaults['initial'] = self.default
189                 defaults.update(kwargs)
190                 return self.field_template.formfield(**defaults)
191         
192         def value_from_object(self, obj):
193                 """If the field template is a :class:`DateField` or a :class:`DateTimeField`, this will convert the default return value to a datetime instance."""
194                 value = super(JSONAttribute, self).value_from_object(obj)
195                 if isinstance(self.field_template, (models.DateField, models.DateTimeField)):
196                         try:
197                                 value = self.field_template.to_python(value)
198                         except ValidationError:
199                                 value = None
200                 return value
201         
202         def get_storage_value(self, value):
203                 """If ``value`` is a :class:`datetime.datetime` instance, this will convert it to a format which can be stored as correct JSON."""
204                 if isinstance(value, datetime.datetime):
205                         return value.strftime("%Y-%m-%d %H:%M:%S")
206                 if isinstance(value, datetime.date):
207                         return value.strftime("%Y-%m-%d")
208                 return value
209
210
211 class ForeignKeyAttribute(AttributeProxyField):
212         """
213         Handles an :class:`.Attribute` with a :class:`.ForeignKeyValue`.
214         
215         :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`ContentTypeLimiter <philo.utils>` to restrict the queryset for the :class:`ForeignKeyAttribute`.
216         
217         """
218         value_class = ForeignKeyValue
219         
220         def __init__(self, model, limit_choices_to=None, **kwargs):
221                 super(ForeignKeyAttribute, self).__init__(**kwargs)
222                 # Spoof being a rel from a ForeignKey for admin widgets.
223                 self.to = model
224                 if limit_choices_to is None:
225                         limit_choices_to = {}
226                 self.limit_choices_to = limit_choices_to
227         
228         def validate_value(self, value):
229                 if value is not None and not isinstance(value, self.to) :
230                         raise TypeError("The '%s' attribute can only be set to an instance of %s or None." % (self.name, self.to.__name__))
231         
232         def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
233                 defaults = {
234                         'queryset': self.to._default_manager.complex_filter(self.limit_choices_to)
235                 }
236                 defaults.update(kwargs)
237                 return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults)
238         
239         def value_from_object(self, obj):
240                 """Converts the default value type (a model instance) to a pk."""
241                 relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
242                 return getattr(relobj, 'pk', None)
243         
244         def get_related_field(self):
245                 # Spoof being a rel from a ForeignKey for admin widgets.
246                 return self.to._meta.pk
247
248
249 class ManyToManyAttribute(ForeignKeyAttribute):
250         """
251         Handles an :class:`.Attribute` with a :class:`.ManyToManyValue`.
252         
253         :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`ContentTypeLimiter <philo.utils>` to restrict the queryset for the :class:`ManyToManyAttribute`.
254         
255         """
256         value_class = ManyToManyValue
257         
258         def validate_value(self, value):
259                 if not isinstance(value, models.query.QuerySet) or value.model != self.to:
260                         raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.to.__name__))
261         
262         def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
263                 return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
264         
265         def value_from_object(self, obj):
266                 """Converts the default value type (a queryset) to a list of pks."""
267                 qs = super(ForeignKeyAttribute, self).value_from_object(obj)
268                 try:
269                         return qs.values_list('pk', flat=True)
270                 except:
271                         return []