Added release notes for 0.9.1.
[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
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                         value = self.field_template.to_python(value)
197                 return value
198         
199         def get_storage_value(self, value):
200                 """If ``value`` is a :class:`datetime.datetime` instance, this will convert it to a format which can be stored as correct JSON."""
201                 if isinstance(value, datetime.datetime):
202                         return value.strftime("%Y-%m-%d %H:%M:%S")
203                 if isinstance(value, datetime.date):
204                         return value.strftime("%Y-%m-%d")
205                 return value
206
207
208 class ForeignKeyAttribute(AttributeProxyField):
209         """
210         Handles an :class:`.Attribute` with a :class:`.ForeignKeyValue`.
211         
212         :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`ContentTypeLimiter <philo.utils>` to restrict the queryset for the :class:`ForeignKeyAttribute`.
213         
214         """
215         value_class = ForeignKeyValue
216         
217         def __init__(self, model, limit_choices_to=None, **kwargs):
218                 super(ForeignKeyAttribute, self).__init__(**kwargs)
219                 # Spoof being a rel from a ForeignKey for admin widgets.
220                 self.to = model
221                 if limit_choices_to is None:
222                         limit_choices_to = {}
223                 self.limit_choices_to = limit_choices_to
224         
225         def validate_value(self, value):
226                 if value is not None and not isinstance(value, self.to) :
227                         raise TypeError("The '%s' attribute can only be set to an instance of %s or None." % (self.name, self.to.__name__))
228         
229         def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
230                 defaults = {
231                         'queryset': self.to._default_manager.complex_filter(self.limit_choices_to)
232                 }
233                 defaults.update(kwargs)
234                 return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults)
235         
236         def value_from_object(self, obj):
237                 """Converts the default value type (a model instance) to a pk."""
238                 relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
239                 return getattr(relobj, 'pk', None)
240         
241         def get_related_field(self):
242                 # Spoof being a rel from a ForeignKey for admin widgets.
243                 return self.to._meta.pk
244
245
246 class ManyToManyAttribute(ForeignKeyAttribute):
247         """
248         Handles an :class:`.Attribute` with a :class:`.ManyToManyValue`.
249         
250         :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`ContentTypeLimiter <philo.utils>` to restrict the queryset for the :class:`ManyToManyAttribute`.
251         
252         """
253         value_class = ManyToManyValue
254         
255         def validate_value(self, value):
256                 if not isinstance(value, models.query.QuerySet) or value.model != self.to:
257                         raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.to.__name__))
258         
259         def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
260                 return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
261         
262         def value_from_object(self, obj):
263                 """Converts the default value type (a queryset) to a list of pks."""
264                 qs = super(ForeignKeyAttribute, self).value_from_object(obj)
265                 try:
266                         return qs.values_list('pk', flat=True)
267                 except:
268                         return []