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.
13 numbers = models.PositiveIntegerField()
15 class ThingProxy(Thing):
16 improvised = JSONAttribute(models.BooleanField)
19 from itertools import tee
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
27 from philo.models import ManyToManyValue, JSONValue, ForeignKeyValue, Attribute, Entity
28 from philo.signals import entity_class_prepared
31 __all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
34 ATTRIBUTE_REGISTRY = '_attribute_registry'
37 class EntityProxyField(object):
38 def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs):
39 self.verbose_name = verbose_name
40 self.help_text = help_text
41 self.default = default
42 self.editable = editable
43 self._choices = choices or []
45 def actually_contribute_to_class(self, sender, **kwargs):
46 sender._entity_meta.add_proxy_field(self)
48 def contribute_to_class(self, cls, name):
49 if issubclass(cls, Entity):
50 self.name = self.attname = name
52 if self.verbose_name is None and name:
53 self.verbose_name = name.replace('_', ' ')
54 entity_class_prepared.connect(self.actually_contribute_to_class, sender=cls)
56 raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__)
58 def formfield(self, form_class=forms.CharField, **kwargs):
61 'label': capfirst(self.verbose_name),
62 'help_text': self.help_text
64 if self.has_default():
65 defaults['initial'] = self.default
66 defaults.update(kwargs)
67 return form_class(**defaults)
69 def value_from_object(self, obj):
70 """The return value of this method will be used by the EntityForm as
71 this field's initial value."""
72 return getattr(obj, self.name)
74 def get_storage_value(self, value):
75 """Final conversion of `value` before it gets stored on an Entity instance.
76 This step is performed by the ProxyFieldForm."""
79 def has_default(self):
80 return self.default is not NOT_PROVIDED
82 def _get_choices(self):
83 if hasattr(self._choices, 'next'):
84 choices, self._choices = tee(self._choices)
88 choices = property(_get_choices)
91 class AttributeFieldDescriptor(object):
92 def __init__(self, field):
95 def get_registry(self, instance):
96 if ATTRIBUTE_REGISTRY not in instance.__dict__:
97 instance.__dict__[ATTRIBUTE_REGISTRY] = {'added': set(), 'removed': set()}
98 return instance.__dict__[ATTRIBUTE_REGISTRY]
100 def __get__(self, instance, owner):
104 if self.field.name not in instance.__dict__:
105 instance.__dict__[self.field.name] = instance.attributes.get(self.field.attribute_key, None)
107 return instance.__dict__[self.field.name]
109 def __set__(self, instance, value):
111 raise AttributeError("%s must be accessed via instance" % self.field.name)
113 self.field.validate_value(value)
114 instance.__dict__[self.field.name] = value
116 registry = self.get_registry(instance)
117 registry['added'].add(self.field)
118 registry['removed'].discard(self.field)
120 def __delete__(self, instance):
121 del instance.__dict__[self.field.name]
123 registry = self.get_registry(instance)
124 registry['added'].discard(self.field)
125 registry['removed'].add(self.field)
128 def process_attribute_fields(sender, instance, created, **kwargs):
129 if ATTRIBUTE_REGISTRY in instance.__dict__:
130 registry = instance.__dict__[ATTRIBUTE_REGISTRY]
131 instance.attribute_set.filter(key__in=[field.attribute_key for field in registry['removed']]).delete()
133 for field in registry['added']:
135 attribute = instance.attribute_set.get(key=field.attribute_key)
136 except Attribute.DoesNotExist:
137 attribute = Attribute()
138 attribute.entity = instance
139 attribute.key = field.attribute_key
141 value_class = field.value_class
142 if isinstance(attribute.value, value_class):
143 value = attribute.value
145 if isinstance(attribute.value, models.Model):
146 attribute.value.delete()
147 value = value_class()
149 value.set_value(getattr(instance, field.name, None))
152 attribute.value = value
154 del instance.__dict__[ATTRIBUTE_REGISTRY]
157 class AttributeField(EntityProxyField):
158 def __init__(self, attribute_key=None, **kwargs):
159 self.attribute_key = attribute_key
160 super(AttributeField, self).__init__(**kwargs)
162 def actually_contribute_to_class(self, sender, **kwargs):
163 super(AttributeField, self).actually_contribute_to_class(sender, **kwargs)
164 setattr(sender, self.name, AttributeFieldDescriptor(self))
165 opts = sender._entity_meta
166 if not hasattr(opts, '_has_attribute_fields'):
167 opts._has_attribute_fields = True
168 models.signals.post_save.connect(process_attribute_fields, sender=sender)
170 def contribute_to_class(self, cls, name):
171 if self.attribute_key is None:
172 self.attribute_key = name
173 super(AttributeField, self).contribute_to_class(cls, name)
175 def validate_value(self, value):
176 "Confirm that the value is valid or raise an appropriate error."
180 def value_class(self):
181 raise AttributeError("value_class must be defined on AttributeField subclasses.")
184 class JSONAttribute(AttributeField):
185 value_class = JSONValue
187 def __init__(self, field_template=None, **kwargs):
188 super(JSONAttribute, self).__init__(**kwargs)
189 if field_template is None:
190 field_template = models.CharField(max_length=255)
191 self.field_template = field_template
193 def formfield(self, **kwargs):
196 'label': capfirst(self.verbose_name),
197 'help_text': self.help_text
199 if self.has_default():
200 defaults['initial'] = self.default
201 defaults.update(kwargs)
202 return self.field_template.formfield(**defaults)
204 def value_from_object(self, obj):
205 value = super(JSONAttribute, self).value_from_object(obj)
206 if isinstance(self.field_template, (models.DateField, models.DateTimeField)):
207 value = self.field_template.to_python(value)
210 def get_storage_value(self, value):
211 if isinstance(value, datetime.datetime):
212 return value.strftime("%Y-%m-%d %H:%M:%S")
213 if isinstance(value, datetime.date):
214 return value.strftime("%Y-%m-%d")
218 class ForeignKeyAttribute(AttributeField):
219 value_class = ForeignKeyValue
221 def __init__(self, model, limit_choices_to=None, **kwargs):
222 super(ForeignKeyAttribute, self).__init__(**kwargs)
224 if limit_choices_to is None:
225 limit_choices_to = {}
226 self.limit_choices_to = limit_choices_to
228 def validate_value(self, value):
229 if value is not None and not isinstance(value, self.to) :
230 raise TypeError("The '%s' attribute can only be set to an instance of %s or None." % (self.name, self.to.__name__))
232 def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
234 'queryset': self.to._default_manager.complex_filter(self.limit_choices_to)
236 defaults.update(kwargs)
237 return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults)
239 def value_from_object(self, obj):
240 relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
241 return getattr(relobj, 'pk', None)
243 def get_related_field(self):
244 """Spoof being a rel from a ForeignKey."""
245 return self.to._meta.pk
248 class ManyToManyAttribute(ForeignKeyAttribute):
249 value_class = ManyToManyValue
251 def validate_value(self, value):
252 if not isinstance(value, models.query.QuerySet) or value.model != self.to:
253 raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.to.__name__))
255 def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
256 return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
258 def value_from_object(self, obj):
259 qs = super(ForeignKeyAttribute, self).value_from_object(obj)
261 return qs.values_list('pk', flat=True)