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.
7 numbers = models.PositiveIntegerField()
8 improvised = JSONAttribute(models.BooleanField)
11 from itertools import tee
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
19 from philo.models import ManyToManyValue, JSONValue, ForeignKeyValue, Attribute, Entity
20 from philo.signals import entity_class_prepared
23 __all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
26 ATTRIBUTE_REGISTRY = '_attribute_registry'
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 []
37 def actually_contribute_to_class(self, sender, **kwargs):
38 sender._entity_meta.add_proxy_field(self)
40 def contribute_to_class(self, cls, name):
41 if issubclass(cls, Entity):
42 self.name = self.attname = name
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)
48 raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__)
50 def formfield(self, form_class=forms.CharField, **kwargs):
53 'label': capfirst(self.verbose_name),
54 'help_text': self.help_text
56 if self.has_default():
57 defaults['initial'] = self.default
58 defaults.update(kwargs)
59 return form_class(**defaults)
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)
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."""
71 def has_default(self):
72 return self.default is not NOT_PROVIDED
74 def _get_choices(self):
75 if hasattr(self._choices, 'next'):
76 choices, self._choices = tee(self._choices)
80 choices = property(_get_choices)
83 class AttributeFieldDescriptor(object):
84 def __init__(self, field):
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]
92 def __get__(self, instance, owner):
96 if self.field.name not in instance.__dict__:
97 instance.__dict__[self.field.name] = instance.attributes.get(self.field.attribute_key, None)
99 return instance.__dict__[self.field.name]
101 def __set__(self, instance, value):
103 raise AttributeError("%s must be accessed via instance" % self.field.name)
105 self.field.validate_value(value)
106 instance.__dict__[self.field.name] = value
108 registry = self.get_registry(instance)
109 registry['added'].add(self.field)
110 registry['removed'].discard(self.field)
112 def __delete__(self, instance):
113 del instance.__dict__[self.field.name]
115 registry = self.get_registry(instance)
116 registry['added'].discard(self.field)
117 registry['removed'].add(self.field)
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()
126 for field in registry['added']:
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 attribute.set_value(value=getattr(instance, field.name, None), value_class=field.value_class)
134 del instance.__dict__[ATTRIBUTE_REGISTRY]
137 class AttributeField(EntityProxyField):
138 def __init__(self, attribute_key=None, **kwargs):
139 self.attribute_key = attribute_key
140 super(AttributeField, self).__init__(**kwargs)
142 def actually_contribute_to_class(self, sender, **kwargs):
143 super(AttributeField, self).actually_contribute_to_class(sender, **kwargs)
144 setattr(sender, self.name, AttributeFieldDescriptor(self))
145 opts = sender._entity_meta
146 if not hasattr(opts, '_has_attribute_fields'):
147 opts._has_attribute_fields = True
148 models.signals.post_save.connect(process_attribute_fields, sender=sender)
150 def contribute_to_class(self, cls, name):
151 if self.attribute_key is None:
152 self.attribute_key = name
153 super(AttributeField, self).contribute_to_class(cls, name)
155 def validate_value(self, value):
156 "Confirm that the value is valid or raise an appropriate error."
160 def value_class(self):
161 raise AttributeError("value_class must be defined on AttributeField subclasses.")
164 class JSONAttribute(AttributeField):
165 """Handles an :class:`Attribute` with a :class:`JSONValue`."""
167 value_class = JSONValue
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 formfield(self, **kwargs):
178 'label': capfirst(self.verbose_name),
179 'help_text': self.help_text
181 if self.has_default():
182 defaults['initial'] = self.default
183 defaults.update(kwargs)
184 return self.field_template.formfield(**defaults)
186 def value_from_object(self, obj):
187 value = super(JSONAttribute, self).value_from_object(obj)
188 if isinstance(self.field_template, (models.DateField, models.DateTimeField)):
189 value = self.field_template.to_python(value)
192 def get_storage_value(self, value):
193 if isinstance(value, datetime.datetime):
194 return value.strftime("%Y-%m-%d %H:%M:%S")
195 if isinstance(value, datetime.date):
196 return value.strftime("%Y-%m-%d")
200 class ForeignKeyAttribute(AttributeField):
201 """Handles an :class:`Attribute` with a :class:`ForeignKeyValue`."""
202 value_class = ForeignKeyValue
204 def __init__(self, model, limit_choices_to=None, **kwargs):
205 super(ForeignKeyAttribute, self).__init__(**kwargs)
207 if limit_choices_to is None:
208 limit_choices_to = {}
209 self.limit_choices_to = limit_choices_to
211 def validate_value(self, value):
212 if value is not None and not isinstance(value, self.to) :
213 raise TypeError("The '%s' attribute can only be set to an instance of %s or None." % (self.name, self.to.__name__))
215 def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
217 'queryset': self.to._default_manager.complex_filter(self.limit_choices_to)
219 defaults.update(kwargs)
220 return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults)
222 def value_from_object(self, obj):
223 relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
224 return getattr(relobj, 'pk', None)
226 def get_related_field(self):
227 """Spoof being a rel from a ForeignKey."""
228 return self.to._meta.pk
231 class ManyToManyAttribute(ForeignKeyAttribute):
232 value_class = ManyToManyValue
234 def validate_value(self, value):
235 if not isinstance(value, models.query.QuerySet) or value.model != self.to:
236 raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.to.__name__))
238 def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
239 return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
241 def value_from_object(self, obj):
242 qs = super(ForeignKeyAttribute, self).value_from_object(obj)
244 return qs.values_list('pk', flat=True)