From: Stephen Burrows Date: Fri, 13 May 2011 19:17:04 +0000 (-0400) Subject: Added/corrected docstrings for custom model fields and EntityProxyFields (now renamed... X-Git-Tag: philo-0.9~12^2~12 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/b4cd9f878117f7398442acb09d40b520f321a91d?ds=inline;hp=46997a51ed0ceb16ddbac712fbcb552a86cdc9e2 Added/corrected docstrings for custom model fields and EntityProxyFields (now renamed to AttributeProxyFields and combined with former AttributeField). Added docs for philo.forms. --- diff --git a/docs/conf.py b/docs/conf.py index f32576a..d6c70af 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,6 +21,9 @@ sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) os.environ['DJANGO_SETTINGS_MODULE'] = 'dummy-settings' +# Import loader so that loader_tags will be correctly added to builtins. Weird import situations... this is necessary for doc build to work. +from django.template import loader + # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. diff --git a/docs/forms.rst b/docs/forms.rst new file mode 100644 index 0000000..b2dfbb4 --- /dev/null +++ b/docs/forms.rst @@ -0,0 +1,12 @@ +Forms +===== + +.. automodule:: philo.forms.entities + :members: + + +Fields +++++++ + +.. automodule:: philo.forms.fields + :members: diff --git a/docs/index.rst b/docs/index.rst index 3e4b1e7..3ef9945 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,17 +9,18 @@ Welcome to Philo's documentation! Contents: .. toctree:: - :maxdepth: 2 - - intro - models/intro - exceptions - handling_requests - signals - validators - utilities - templatetags - loaders + :maxdepth: 2 + + intro + models/intro + exceptions + handling_requests + signals + validators + utilities + templatetags + forms + loaders Indices and tables ================== diff --git a/docs/models/fields.rst b/docs/models/fields.rst index 0b3d0f9..3092fa4 100644 --- a/docs/models/fields.rst +++ b/docs/models/fields.rst @@ -3,9 +3,19 @@ Custom Fields .. automodule:: philo.models.fields :members: + :exclude-members: JSONField, SlugMultipleChoiceField + + .. autoclass:: JSONField() + :members: + + .. autoclass:: SlugMultipleChoiceField() + :members: -EntityProxyFields ------------------ +AttributeProxyFields +-------------------- .. automodule:: philo.models.fields.entities - :members: \ No newline at end of file + :members: + + .. autoclass:: AttributeProxyField(attribute_key=None, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs) + :members: \ No newline at end of file diff --git a/philo/forms/entities.py b/philo/forms/entities.py index 5d34cce..ba72d7d 100644 --- a/philo/forms/entities.py +++ b/philo/forms/entities.py @@ -94,6 +94,10 @@ class EntityFormMetaclass(ModelFormMetaclass): class EntityForm(ModelForm): + """ + :class:`EntityForm` knows how to handle :class:`.Entity` instances - specifically, how to set initial values for :class:`.AttributeProxyField`\ s and save cleaned values to an instance on save. + + """ __metaclass__ = EntityFormMetaclass def __init__(self, *args, **kwargs): diff --git a/philo/forms/fields.py b/philo/forms/fields.py index 8bb5ce3..66b96ad 100644 --- a/philo/forms/fields.py +++ b/philo/forms/fields.py @@ -9,6 +9,7 @@ __all__ = ('JSONFormField',) class JSONFormField(forms.Field): + """A form field which is validated by :func:`philo.validators.json_validator`.""" default_validators = [json_validator] def clean(self, value): diff --git a/philo/models/fields/__init__.py b/philo/models/fields/__init__.py index eca3a12..efd315f 100644 --- a/philo/models/fields/__init__.py +++ b/philo/models/fields/__init__.py @@ -12,6 +12,7 @@ from philo.validators import TemplateValidator, json_validator class TemplateField(models.TextField): + """A :class:`TextField` which is validated with a :class:`.TemplateValidator`. ``allow``, ``disallow``, and ``secure`` will be passed into the validator's construction.""" def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs): super(TemplateField, self).__init__(*args, **kwargs) self.validators.append(TemplateValidator(allow, disallow, secure)) @@ -41,6 +42,7 @@ class JSONDescriptor(object): class JSONField(models.TextField): + """A :class:`TextField` which stores its value on the model instance as a python object and stores its value in the database as JSON. Validated with :func:`.json_validator`.""" default_validators = [json_validator] def get_attname(self): @@ -69,6 +71,7 @@ class JSONField(models.TextField): class SlugMultipleChoiceField(models.Field): + """Stores a selection of multiple items with unique slugs in the form of a comma-separated list.""" __metaclass__ = models.SubfieldBase description = _("Comma-separated slug field") diff --git a/philo/models/fields/entities.py b/philo/models/fields/entities.py index 3e96d13..cc8c398 100644 --- a/philo/models/fields/entities.py +++ b/philo/models/fields/entities.py @@ -1,12 +1,3 @@ -""" -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. - -Example:: - - class Thing(Entity): - numbers = models.PositiveIntegerField() - improvised = JSONAttribute(models.BooleanField) -""" import datetime from itertools import tee @@ -26,8 +17,23 @@ __all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute') ATTRIBUTE_REGISTRY = '_attribute_registry' -class EntityProxyField(object): - def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs): +class AttributeProxyField(object): + """ + :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. + + Example:: + + class Thing(Entity): + numbers = models.PositiveIntegerField() + improvised = JSONAttribute(models.BooleanField) + + :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. + + The remaining parameters have the same meaning as for ordinary model fields. + + """ + def __init__(self, attribute_key=None, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs): + self.attribute_key = attribute_key self.verbose_name = verbose_name self.help_text = help_text self.default = default @@ -36,8 +42,15 @@ class EntityProxyField(object): def actually_contribute_to_class(self, sender, **kwargs): sender._entity_meta.add_proxy_field(self) + setattr(sender, self.name, AttributeFieldDescriptor(self)) + opts = sender._entity_meta + if not hasattr(opts, '_has_attribute_fields'): + opts._has_attribute_fields = True + models.signals.post_save.connect(process_attribute_fields, sender=sender) def contribute_to_class(self, cls, name): + if self.attribute_key is None: + self.attribute_key = name if issubclass(cls, Entity): self.name = self.attname = name self.model = cls @@ -48,6 +61,10 @@ class EntityProxyField(object): raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__) def formfield(self, form_class=forms.CharField, **kwargs): + """ + Returns a form field capable of accepting values for the :class:`AttributeProxyField`. + + """ defaults = { 'required': False, 'label': capfirst(self.verbose_name), @@ -59,25 +76,34 @@ class EntityProxyField(object): return form_class(**defaults) def value_from_object(self, obj): - """The return value of this method will be used by the EntityForm as - this field's initial value.""" + """Returns the value of this field in the given model instance.""" return getattr(obj, self.name) def get_storage_value(self, value): - """Final conversion of `value` before it gets stored on an Entity instance. - This step is performed by the ProxyFieldForm.""" + """Final conversion of ``value`` before it gets stored on an :class:`.Entity` instance. This will be called during :meth:`.EntityForm.save`.""" return value + def validate_value(self, value): + "Raise an appropriate exception if ``value`` is not valid for this :class:`AttributeProxyField`." + pass + def has_default(self): + """Returns ``True`` if a default value was provided and ``False`` otherwise.""" return self.default is not NOT_PROVIDED def _get_choices(self): + """Returns the choices passed into the constructor.""" if hasattr(self._choices, 'next'): choices, self._choices = tee(self._choices) return choices else: return self._choices choices = property(_get_choices) + + @property + def value_class(self): + """Each :class:`AttributeProxyField` subclass can define a value_class to use for creation of new :class:`.AttributeValue`\ s""" + raise AttributeError("value_class must be defined on %s subclasses." % self.__class__.__name__) class AttributeFieldDescriptor(object): @@ -118,12 +144,14 @@ class AttributeFieldDescriptor(object): def process_attribute_fields(sender, instance, created, **kwargs): - """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 """ + """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.""" if ATTRIBUTE_REGISTRY in instance.__dict__: registry = instance.__dict__[ATTRIBUTE_REGISTRY] instance.attribute_set.filter(key__in=[field.attribute_key for field in registry['removed']]).delete() for field in registry['added']: + # TODO: Should this perhaps just use instance.attributes[field.attribute_key] = getattr(instance, field.name, None)? + # (Would eliminate the need for field.value_class.) try: attribute = instance.attribute_set.get(key=field.attribute_key) except Attribute.DoesNotExist: @@ -134,35 +162,13 @@ def process_attribute_fields(sender, instance, created, **kwargs): del instance.__dict__[ATTRIBUTE_REGISTRY] -class AttributeField(EntityProxyField): - def __init__(self, attribute_key=None, **kwargs): - self.attribute_key = attribute_key - super(AttributeField, self).__init__(**kwargs) - - def actually_contribute_to_class(self, sender, **kwargs): - super(AttributeField, self).actually_contribute_to_class(sender, **kwargs) - setattr(sender, self.name, AttributeFieldDescriptor(self)) - opts = sender._entity_meta - if not hasattr(opts, '_has_attribute_fields'): - opts._has_attribute_fields = True - models.signals.post_save.connect(process_attribute_fields, sender=sender) - - def contribute_to_class(self, cls, name): - if self.attribute_key is None: - self.attribute_key = name - super(AttributeField, self).contribute_to_class(cls, name) +class JSONAttribute(AttributeProxyField): + """ + Handles an :class:`.Attribute` with a :class:`.JSONValue`. - def validate_value(self, value): - "Confirm that the value is valid or raise an appropriate error." - pass + :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. - @property - def value_class(self): - raise AttributeError("value_class must be defined on AttributeField subclasses.") - - -class JSONAttribute(AttributeField): - """Handles an :class:`Attribute` with a :class:`JSONValue`.""" + """ value_class = JSONValue @@ -184,12 +190,14 @@ class JSONAttribute(AttributeField): return self.field_template.formfield(**defaults) def value_from_object(self, obj): + """If the field template is a :class:`DateField` or a :class:`DateTimeField`, this will convert the default return value to a datetime instance.""" value = super(JSONAttribute, self).value_from_object(obj) if isinstance(self.field_template, (models.DateField, models.DateTimeField)): value = self.field_template.to_python(value) return value def get_storage_value(self, value): + """If ``value`` is a :class:`datetime.datetime` instance, this will convert it to a format which can be stored as correct JSON.""" if isinstance(value, datetime.datetime): return value.strftime("%Y-%m-%d %H:%M:%S") if isinstance(value, datetime.date): @@ -197,12 +205,18 @@ class JSONAttribute(AttributeField): return value -class ForeignKeyAttribute(AttributeField): - """Handles an :class:`Attribute` with a :class:`ForeignKeyValue`.""" +class ForeignKeyAttribute(AttributeProxyField): + """ + Handles an :class:`.Attribute` with a :class:`.ForeignKeyValue`. + + :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`.ContentTypeLimiter` to restrict the queryset for the :class:`ForeignKeyAttribute`. + + """ value_class = ForeignKeyValue def __init__(self, model, limit_choices_to=None, **kwargs): super(ForeignKeyAttribute, self).__init__(**kwargs) + # Spoof being a rel from a ForeignKey for admin widgets. self.to = model if limit_choices_to is None: limit_choices_to = {} @@ -220,15 +234,22 @@ class ForeignKeyAttribute(AttributeField): return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults) def value_from_object(self, obj): + """Converts the default value type (a model instance) to a pk.""" relobj = super(ForeignKeyAttribute, self).value_from_object(obj) return getattr(relobj, 'pk', None) def get_related_field(self): - """Spoof being a rel from a ForeignKey.""" + # Spoof being a rel from a ForeignKey for admin widgets. return self.to._meta.pk class ManyToManyAttribute(ForeignKeyAttribute): + """ + Handles an :class:`.Attribute` with a :class:`.ManyToManyValue`. + + :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`ContentTypeLimiter ` to restrict the queryset for the :class:`ManyToManyAttribute`. + + """ value_class = ManyToManyValue def validate_value(self, value): @@ -239,6 +260,7 @@ class ManyToManyAttribute(ForeignKeyAttribute): return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs) def value_from_object(self, obj): + """Converts the default value type (a queryset) to a list of pks.""" qs = super(ForeignKeyAttribute, self).value_from_object(obj) try: return qs.values_list('pk', flat=True) diff --git a/philo/signals.py b/philo/signals.py index 558c6fe..13f6cd1 100644 --- a/philo/signals.py +++ b/philo/signals.py @@ -1,7 +1,7 @@ from django.dispatch import Signal -#: Sent whenever an Entity subclass has been "prepared" -- that is, after the processing necessary to make :mod:`EntityProxyFields ` work has been completed. This will fire after :obj:`django.db.models.signals.class_prepared`. +#: Sent whenever an Entity subclass has been "prepared" -- that is, after the processing necessary to make :mod:`.AttributeProxyField`\ s work has been completed. This will fire after :obj:`django.db.models.signals.class_prepared`. #: #: Arguments that are sent with this signal: #: