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.
13 numbers = models.PositiveIntegerField()
15 class ThingProxy(Thing):
16 improvised = JSONAttribute(models.BooleanField)
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
29 __all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
32 ATTRIBUTE_REGISTRY = '_attribute_registry'
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
42 def actually_contribute_to_class(self, sender, **kwargs):
43 sender._entity_meta.add_proxy_field(self)
45 def contribute_to_class(self, cls, name):
46 from philo.models.base import Entity
47 if issubclass(cls, Entity):
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)
54 raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__)
56 def formfield(self, form_class=forms.CharField, **kwargs):
59 'label': capfirst(self.verbose_name),
60 'help_text': self.help_text
62 if self.has_default():
63 defaults['initial'] = self.default
64 defaults.update(kwargs)
65 return form_class(**defaults)
67 def value_from_object(self, obj):
68 return getattr(obj, self.name)
70 def has_default(self):
71 return self.default is not NOT_PROVIDED
74 class AttributeFieldDescriptor(object):
75 def __init__(self, field):
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]
83 def __get__(self, instance, owner):
87 if self.field.name not in instance.__dict__:
88 instance.__dict__[self.field.name] = instance.attributes[self.field.attribute_key]
90 return instance.__dict__[self.field.name]
92 def __set__(self, instance, value):
94 raise AttributeError("%s must be accessed via instance" % self.field.name)
96 self.field.validate_value(value)
97 instance.__dict__[self.field.name] = value
99 registry = self.get_registry(instance)
100 registry['added'].add(self.field)
101 registry['removed'].remove(self.field)
103 def __delete__(self, instance):
104 del instance.__dict__[self.field.name]
106 registry = self.get_registry(instance)
107 registry['added'].remove(self.field)
108 registry['removed'].add(self.field)
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()
116 from philo.models import Attribute
117 for field in registry['added']:
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
125 value_class = field.get_value_class()
126 if isinstance(attribute.value, value_class):
127 value = attribute.value
129 if isinstance(attribute.value, models.Model):
130 attribute.value.delete()
131 value = value_class()
133 value.set_value(field.value_from_object(instance))
136 attribute.value = value
138 del instance.__dict__[ATTRIBUTE_REGISTRY]
141 class AttributeField(EntityProxyField):
142 def __init__(self, attribute_key=None, **kwargs):
143 self.attribute_key = attribute_key
144 super(AttributeField, self).__init__(**kwargs)
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)
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)
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.")
164 def get_value_class(self):
165 raise NotImplementedError("get_value_class must be implemented by AttributeField subclasses.")
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
175 def validate_value(self, value):
178 def formfield(self, **kwargs):
181 'label': capfirst(self.verbose_name),
182 'help_text': self.help_text
184 if self.has_default():
185 defaults['initial'] = self.default
186 defaults.update(kwargs)
187 return self.field_template.formfield(**defaults)
189 def get_value_class(self):
190 from philo.models import JSONValue
193 # Not sure what this is doing - keep eyes open!
194 #def value_from_object(self, obj):
196 # return getattr(obj, self.name)
197 # except AttributeError:
201 class ForeignKeyAttribute(AttributeField):
202 def __init__(self, model, limit_choices_to=None, **kwargs):
203 super(ForeignKeyAttribute, self).__init__(**kwargs)
205 if limit_choices_to is None:
206 limit_choices_to = {}
207 self.limit_choices_to = limit_choices_to
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__))
213 def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
215 'queryset': self.model._default_manager.complex_filter(self.limit_choices_to)
217 defaults.update(kwargs)
218 return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults)
220 def get_value_class(self):
221 from philo.models import ForeignKeyValue
222 return ForeignKeyValue
224 #def value_from_object(self, obj):
226 # relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
227 # except AttributeError:
229 # return getattr(relobj, 'pk', None)
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__))
237 def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
238 return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
240 def get_value_class(self):
241 from philo.models import ManyToManyValue
242 return ManyToManyValue