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