Added/corrected docstrings for custom model fields and EntityProxyFields (now renamed...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Fri, 13 May 2011 19:17:04 +0000 (15:17 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Fri, 13 May 2011 20:09:45 +0000 (16:09 -0400)
docs/conf.py
docs/forms.rst [new file with mode: 0644]
docs/index.rst
docs/models/fields.rst
philo/forms/entities.py
philo/forms/fields.py
philo/models/fields/__init__.py
philo/models/fields/entities.py
philo/signals.py

index f32576a..d6c70af 100644 (file)
@@ -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 (file)
index 0000000..b2dfbb4
--- /dev/null
@@ -0,0 +1,12 @@
+Forms
+=====
+
+.. automodule:: philo.forms.entities
+       :members:
+
+
+Fields
+++++++
+
+.. automodule:: philo.forms.fields
+       :members:
index 3e4b1e7..3ef9945 100644 (file)
@@ -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
 ==================
index 0b3d0f9..3092fa4 100644 (file)
@@ -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
index 5d34cce..ba72d7d 100644 (file)
@@ -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):
index 8bb5ce3..66b96ad 100644 (file)
@@ -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):
index eca3a12..efd315f 100644 (file)
@@ -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")
        
index 3e96d13..cc8c398 100644 (file)
@@ -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 <philo.utils>` 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)
index 558c6fe..13f6cd1 100644 (file)
@@ -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 <philo.models.fields.entities>` 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:
 #: