2 from itertools import tee
4 from django import forms
5 from django.core.exceptions import FieldError
6 from django.db import models
7 from django.db.models.fields import NOT_PROVIDED
8 from django.utils.text import capfirst
10 from philo.models import ManyToManyValue, JSONValue, ForeignKeyValue, Attribute, Entity
11 from philo.signals import entity_class_prepared
14 __all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
17 ATTRIBUTE_REGISTRY = '_attribute_registry'
20 class AttributeProxyField(object):
22 :class:`AttributeProxyField`\ s can be assigned as fields on a subclass of :class:`philo.models.base.Entity`. They act like any other model fields, but instead of saving their data to the model's table, they save it to :class:`.Attribute`\ s related to a model instance. Additionally, a new :class:`.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:`.PassthroughAttributeMapper`\ s and :class:`.TreeAttributeMapper`\ s, where even an :class:`.Attribute` with a value of ``None`` will prevent a passthrough.
27 numbers = models.PositiveIntegerField()
28 improvised = JSONAttribute(models.BooleanField)
30 :param attribute_key: The key of the attribute that will be used to store this field's value, if it is different than the field's name.
32 The remaining parameters have the same meaning as for ordinary model fields.
35 def __init__(self, attribute_key=None, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs):
36 self.attribute_key = attribute_key
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 []
43 def actually_contribute_to_class(self, sender, **kwargs):
44 sender._entity_meta.add_proxy_field(self)
45 setattr(sender, self.name, AttributeFieldDescriptor(self))
46 opts = sender._entity_meta
47 if not hasattr(opts, '_has_attribute_fields'):
48 opts._has_attribute_fields = True
49 models.signals.post_save.connect(process_attribute_fields, sender=sender)
51 def contribute_to_class(self, cls, name):
52 if self.attribute_key is None:
53 self.attribute_key = name
54 if issubclass(cls, Entity):
55 self.name = self.attname = name
57 if self.verbose_name is None and name:
58 self.verbose_name = name.replace('_', ' ')
59 entity_class_prepared.connect(self.actually_contribute_to_class, sender=cls)
61 raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__)
63 def formfield(self, form_class=forms.CharField, **kwargs):
65 Returns a form field capable of accepting values for the :class:`AttributeProxyField`.
70 'label': capfirst(self.verbose_name),
71 'help_text': self.help_text
73 if self.has_default():
74 defaults['initial'] = self.default
75 defaults.update(kwargs)
76 return form_class(**defaults)
78 def value_from_object(self, obj):
79 """Returns the value of this field in the given model instance."""
80 return getattr(obj, self.name)
82 def get_storage_value(self, value):
83 """Final conversion of ``value`` before it gets stored on an :class:`.Entity` instance. This will be called during :meth:`.EntityForm.save`."""
86 def validate_value(self, value):
87 "Raise an appropriate exception if ``value`` is not valid for this :class:`AttributeProxyField`."
90 def has_default(self):
91 """Returns ``True`` if a default value was provided and ``False`` otherwise."""
92 return self.default is not NOT_PROVIDED
94 def _get_choices(self):
95 """Returns the choices passed into the constructor."""
96 if hasattr(self._choices, 'next'):
97 choices, self._choices = tee(self._choices)
101 choices = property(_get_choices)
104 def value_class(self):
105 """Each :class:`AttributeProxyField` subclass can define a value_class to use for creation of new :class:`.AttributeValue`\ s"""
106 raise AttributeError("value_class must be defined on %s subclasses." % self.__class__.__name__)
109 class AttributeFieldDescriptor(object):
110 def __init__(self, field):
113 def get_registry(self, instance):
114 if ATTRIBUTE_REGISTRY not in instance.__dict__:
115 instance.__dict__[ATTRIBUTE_REGISTRY] = {'added': set(), 'removed': set()}
116 return instance.__dict__[ATTRIBUTE_REGISTRY]
118 def __get__(self, instance, owner):
122 if self.field.name not in instance.__dict__:
123 instance.__dict__[self.field.name] = instance.attributes.get(self.field.attribute_key, None)
125 return instance.__dict__[self.field.name]
127 def __set__(self, instance, value):
129 raise AttributeError("%s must be accessed via instance" % self.field.name)
131 self.field.validate_value(value)
132 instance.__dict__[self.field.name] = value
134 registry = self.get_registry(instance)
135 registry['added'].add(self.field)
136 registry['removed'].discard(self.field)
138 def __delete__(self, instance):
139 del instance.__dict__[self.field.name]
141 registry = self.get_registry(instance)
142 registry['added'].discard(self.field)
143 registry['removed'].add(self.field)
146 def process_attribute_fields(sender, instance, created, **kwargs):
147 """This function is attached to each :class:`Entity` subclass's post_save signal. Any :class:`Attribute`\ s managed by :class:`AttributeProxyField`\ s which have been removed will be deleted, and any new attributes will be created."""
148 if ATTRIBUTE_REGISTRY in instance.__dict__:
149 registry = instance.__dict__[ATTRIBUTE_REGISTRY]
150 instance.attribute_set.filter(key__in=[field.attribute_key for field in registry['removed']]).delete()
152 for field in registry['added']:
153 # TODO: Should this perhaps just use instance.attributes[field.attribute_key] = getattr(instance, field.name, None)?
154 # (Would eliminate the need for field.value_class.)
156 attribute = instance.attribute_set.get(key=field.attribute_key)
157 except Attribute.DoesNotExist:
158 attribute = Attribute()
159 attribute.entity = instance
160 attribute.key = field.attribute_key
161 attribute.set_value(value=getattr(instance, field.name, None), value_class=field.value_class)
162 del instance.__dict__[ATTRIBUTE_REGISTRY]
165 class JSONAttribute(AttributeProxyField):
167 Handles an :class:`.Attribute` with a :class:`.JSONValue`.
169 :param field_template: A django form field instance that will be used to guide rendering and interpret values. For example, using :class:`django.forms.BooleanField` will make this field render as a checkbox.
173 value_class = JSONValue
175 def __init__(self, field_template=None, **kwargs):
176 super(JSONAttribute, self).__init__(**kwargs)
177 if field_template is None:
178 field_template = models.CharField(max_length=255)
179 self.field_template = field_template
181 def formfield(self, **kwargs):
184 'label': capfirst(self.verbose_name),
185 'help_text': self.help_text
187 if self.has_default():
188 defaults['initial'] = self.default
189 defaults.update(kwargs)
190 return self.field_template.formfield(**defaults)
192 def value_from_object(self, obj):
193 """If the field template is a :class:`DateField` or a :class:`DateTimeField`, this will convert the default return value to a datetime instance."""
194 value = super(JSONAttribute, self).value_from_object(obj)
195 if isinstance(self.field_template, (models.DateField, models.DateTimeField)):
196 value = self.field_template.to_python(value)
199 def get_storage_value(self, value):
200 """If ``value`` is a :class:`datetime.datetime` instance, this will convert it to a format which can be stored as correct JSON."""
201 if isinstance(value, datetime.datetime):
202 return value.strftime("%Y-%m-%d %H:%M:%S")
203 if isinstance(value, datetime.date):
204 return value.strftime("%Y-%m-%d")
208 class ForeignKeyAttribute(AttributeProxyField):
210 Handles an :class:`.Attribute` with a :class:`.ForeignKeyValue`.
212 :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`.ContentTypeLimiter` to restrict the queryset for the :class:`ForeignKeyAttribute`.
215 value_class = ForeignKeyValue
217 def __init__(self, model, limit_choices_to=None, **kwargs):
218 super(ForeignKeyAttribute, self).__init__(**kwargs)
219 # Spoof being a rel from a ForeignKey for admin widgets.
221 if limit_choices_to is None:
222 limit_choices_to = {}
223 self.limit_choices_to = limit_choices_to
225 def validate_value(self, value):
226 if value is not None and not isinstance(value, self.to) :
227 raise TypeError("The '%s' attribute can only be set to an instance of %s or None." % (self.name, self.to.__name__))
229 def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
231 'queryset': self.to._default_manager.complex_filter(self.limit_choices_to)
233 defaults.update(kwargs)
234 return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults)
236 def value_from_object(self, obj):
237 """Converts the default value type (a model instance) to a pk."""
238 relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
239 return getattr(relobj, 'pk', None)
241 def get_related_field(self):
242 # Spoof being a rel from a ForeignKey for admin widgets.
243 return self.to._meta.pk
246 class ManyToManyAttribute(ForeignKeyAttribute):
248 Handles an :class:`.Attribute` with a :class:`.ManyToManyValue`.
250 :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`ContentTypeLimiter <philo.utils>` to restrict the queryset for the :class:`ManyToManyAttribute`.
253 value_class = ManyToManyValue
255 def validate_value(self, value):
256 if not isinstance(value, models.query.QuerySet) or value.model != self.to:
257 raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.to.__name__))
259 def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
260 return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
262 def value_from_object(self, obj):
263 """Converts the default value type (a queryset) to a list of pks."""
264 qs = super(ForeignKeyAttribute, self).value_from_object(obj)
266 return qs.values_list('pk', flat=True)