Split forms into containers, entities, and fields. Split attribute fields out from...
[philo.git] / models / fields / attributes.py
1 """
2 The Attributes defined in this file can be assigned as fields on a proxy of
3 a subclass of philo.models.Entity. They act like any other model fields,
4 but instead of saving their data to the database, they save it to
5 attributes related to a model instance. Additionally, a new attribute will
6 be created for an instance if and only if the field's value has been set.
7 This is relevant i.e. for passthroughs, where the value of the field may
8 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                 class Meta:
19                         proxy = True
20 """
21 from django import forms
22 from django.core.exceptions import FieldError
23 from django.db import models
24 from django.db.models.fields import NOT_PROVIDED
25 from django.utils.text import capfirst
26 from philo.signals import entity_class_prepared
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, *args, **kwargs):
37                 self.verbose_name = verbose_name
38                 self.help_text = help_text
39                 self.default = default
40                 self.editable = editable
41         
42         def actually_contribute_to_class(self, sender, **kwargs):
43                 sender._entity_meta.add_proxy_field(self)
44         
45         def contribute_to_class(self, cls, name):
46                 from philo.models.base import Entity
47                 if issubclass(cls, Entity):
48                         self.name = 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                 return getattr(obj, self.name)
69         
70         def has_default(self):
71                 return self.default is not NOT_PROVIDED
72
73
74 class AttributeFieldDescriptor(object):
75         def __init__(self, field):
76                 self.field = field
77         
78         def get_registry(self, instance):
79                 if ATTRIBUTE_REGISTRY not in instance.__dict__:
80                         instance.__dict__[ATTRIBUTE_REGISTRY] = {'added': set(), 'removed': set()}
81                 return instance.__dict__[ATTRIBUTE_REGISTRY]
82         
83         def __get__(self, instance, owner):
84                 if instance is None:
85                         return self
86                 
87                 if self.field.name not in instance.__dict__:
88                         instance.__dict__[self.field.name] = instance.attributes[self.field.attribute_key]
89                 
90                 return instance.__dict__[self.field.name]
91         
92         def __set__(self, instance, value):
93                 if instance is None:
94                         raise AttributeError("%s must be accessed via instance" % self.field.name)
95                 
96                 self.field.validate_value(value)
97                 instance.__dict__[self.field.name] = value
98                 
99                 registry = self.get_registry(instance)
100                 registry['added'].add(self.field)
101                 registry['removed'].remove(self.field)
102         
103         def __delete__(self, instance):
104                 del instance.__dict__[self.field.name]
105                 
106                 registry = self.get_registry(instance)
107                 registry['added'].remove(self.field)
108                 registry['removed'].add(self.field)
109
110
111 def process_attribute_fields(sender, instance, created, **kwargs):
112         if ATTRIBUTE_REGISTRY in instance.__dict__:
113                 registry = instance.__dict__[ATTRIBUTE_REGISTRY]
114                 instance.attribute_set.filter(key__in=[field.attribute_key for field in registry['removed']]).delete()
115                 
116                 from philo.models import Attribute
117                 for field in registry['added']:
118                         try:
119                                 attribute = self.attribute_set.get(key=field.key)
120                         except Attribute.DoesNotExist:
121                                 attribute = Attribute()
122                                 attribute.entity = instance
123                                 attribute.key = field.key
124                         
125                         value_class = field.get_value_class()
126                         if isinstance(attribute.value, value_class):
127                                 value = attribute.value
128                         else:
129                                 if isinstance(attribute.value, models.Model):
130                                         attribute.value.delete()
131                                 value = value_class()
132                         
133                         value.set_value(field.value_from_object(instance))
134                         value.save()
135                         
136                         attribute.value = value
137                         attribute.save()
138                 del instance.__dict__[ATTRIBUTE_REGISTRY]
139
140
141 class AttributeField(EntityProxyField):
142         def __init__(self, attribute_key=None, **kwargs):
143                 self.attribute_key = attribute_key
144                 super(AttributeField, self).__init__(**kwargs)
145         
146         def actually_contribute_to_class(self, sender, **kwargs):
147                 super(AttributeField, self).actually_contribute_to_class(sender, **kwargs)
148                 setattr(sender, self.name, AttributeFieldDescriptor(self))
149                 opts = sender._entity_meta
150                 if not hasattr(opts, '_has_attribute_fields'):
151                         opts._has_attribute_fields = True
152                         models.signals.post_save.connect(process_attribute_fields, sender=sender)
153                 
154         
155         def contribute_to_class(self, cls, name):
156                 if self.attribute_key is None:
157                         self.attribute_key = name
158                 super(AttributeField, self).contribute_to_class(cls, name)
159         
160         def validate_value(self, value):
161                 "Confirm that the value is valid or raise an appropriate error."
162                 raise NotImplementedError("validate_value must be implemented by AttributeField subclasses.")
163         
164         def get_value_class(self):
165                 raise NotImplementedError("get_value_class must be implemented by AttributeField subclasses.")
166
167
168 class JSONAttribute(AttributeField):
169         def __init__(self, field_template=None, **kwargs):
170                 super(JSONAttribute, self).__init__(**kwargs)
171                 if field_template is None:
172                         field_template = models.CharField(max_length=255)
173                 self.field_template = field_template
174         
175         def validate_value(self, value):
176                 pass
177         
178         def formfield(self, **kwargs):
179                 defaults = {
180                         'required': False,
181                         'label': capfirst(self.verbose_name),
182                         'help_text': self.help_text
183                 }
184                 if self.has_default():
185                         defaults['initial'] = self.default
186                 defaults.update(kwargs)
187                 return self.field_template.formfield(**defaults)
188         
189         def get_value_class(self):
190                 from philo.models import JSONValue
191                 return JSONValue
192         
193         # Not sure what this is doing - keep eyes open!
194         #def value_from_object(self, obj):
195         #       try:
196         #               return getattr(obj, self.name)
197         #       except AttributeError:
198         #               return None
199
200
201 class ForeignKeyAttribute(AttributeField):
202         def __init__(self, model, limit_choices_to=None, **kwargs):
203                 super(ForeignKeyAttribute, self).__init__(**kwargs)
204                 self.model = model
205                 if limit_choices_to is None:
206                         limit_choices_to = {}
207                 self.limit_choices_to = limit_choices_to
208         
209         def validate_value(self, value):
210                 if value is not None and not isinstance(value, self.model) :
211                         raise TypeError("The '%s' attribute can only be set to an instance of %s or None." % (self.name, self.model.__name__))
212         
213         def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
214                 defaults = {
215                         'queryset': self.model._default_manager.complex_filter(self.limit_choices_to)
216                 }
217                 defaults.update(kwargs)
218                 return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults)
219         
220         def get_value_class(self):
221                 from philo.models import ForeignKeyValue
222                 return ForeignKeyValue
223         
224         #def value_from_object(self, obj):
225         #       try:
226         #               relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
227         #       except AttributeError:
228         #               return None
229         #       return getattr(relobj, 'pk', None)
230
231
232 class ManyToManyAttribute(ForeignKeyAttribute):
233         def validate_value(self, value):
234                 if not isinstance(value, models.query.QuerySet) or value.model != self.model:
235                         raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.model.__name__))
236         
237         def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
238                 return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
239         
240         def get_value_class(self):
241                 from philo.models import ManyToManyValue
242                 return ManyToManyValue