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