2 from itertools import tee
4 from django import forms
5 from django.core.exceptions import FieldError, ValidationError
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)):
197 value = self.field_template.to_python(value)
198 except ValidationError:
202 def get_storage_value(self, value):
203 """If ``value`` is a :class:`datetime.datetime` instance, this will convert it to a format which can be stored as correct JSON."""
204 if isinstance(value, datetime.datetime):
205 return value.strftime("%Y-%m-%d %H:%M:%S")
206 if isinstance(value, datetime.date):
207 return value.strftime("%Y-%m-%d")
211 class ForeignKeyAttribute(AttributeProxyField):
213 Handles an :class:`.Attribute` with a :class:`.ForeignKeyValue`.
215 :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`ContentTypeLimiter <philo.utils>` to restrict the queryset for the :class:`ForeignKeyAttribute`.
218 value_class = ForeignKeyValue
220 def __init__(self, model, limit_choices_to=None, **kwargs):
221 super(ForeignKeyAttribute, self).__init__(**kwargs)
222 # Spoof being a rel from a ForeignKey for admin widgets.
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 """Converts the default value type (a model instance) to a pk."""
241 relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
242 return getattr(relobj, 'pk', None)
244 def get_related_field(self):
245 # Spoof being a rel from a ForeignKey for admin widgets.
246 return self.to._meta.pk
249 class ManyToManyAttribute(ForeignKeyAttribute):
251 Handles an :class:`.Attribute` with a :class:`.ManyToManyValue`.
253 :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`ContentTypeLimiter <philo.utils>` to restrict the queryset for the :class:`ManyToManyAttribute`.
256 value_class = ManyToManyValue
258 def validate_value(self, value):
259 if not isinstance(value, models.query.QuerySet) or value.model != self.to:
260 raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.to.__name__))
262 def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
263 return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
265 def value_from_object(self, obj):
266 """Converts the default value type (a queryset) to a list of pks."""
267 qs = super(ForeignKeyAttribute, self).value_from_object(obj)
269 return qs.values_list('pk', flat=True)