diff --git a/django/contrib/admindocs/tests/__init__.py b/django/contrib/admindocs/tests/__init__.py index a091ebe122..b4526c6a7e 100644 --- a/django/contrib/admindocs/tests/__init__.py +++ b/django/contrib/admindocs/tests/__init__.py @@ -1,36 +1,30 @@ import unittest -from django.contrib.admindocs import views import fields - +from django.contrib.admindocs import views from django.db.models import fields as builtin_fields + class TestFieldType(unittest.TestCase): def setUp(self): pass - + def test_field_name(self): self.assertRaises(AttributeError, views.get_readable_field_data_type, "NotAField" ) - + def test_builtin_fields(self): self.assertEqual( views.get_readable_field_data_type(builtin_fields.BooleanField()), u'Boolean (Either True or False)' ) - + def test_custom_fields(self): self.assertEqual( views.get_readable_field_data_type(fields.CustomField()), u'A custom field type' ) self.assertEqual( - views.get_readable_field_data_type(fields.DocstringLackingField()), - u'Field of type: DocstringLackingField' - ) - - def test_multiline_custom_field_truncation(self): - self.assertEqual( - views.get_readable_field_data_type(fields.ManyLineDocstringField()), - u'Many-line custom field' + views.get_readable_field_data_type(fields.DescriptionLackingField()), + u'Field of type: DescriptionLackingField' ) diff --git a/django/contrib/admindocs/tests/fields.py b/django/contrib/admindocs/tests/fields.py index 5cab3627c6..e59498afb5 100644 --- a/django/contrib/admindocs/tests/fields.py +++ b/django/contrib/admindocs/tests/fields.py @@ -1,13 +1,7 @@ from django.db import models class CustomField(models.Field): - """A custom field type""" - -class ManyLineDocstringField(models.Field): - """Many-line custom field - - This docstring has many lines. Lorum ipsem etc. etc. Four score - and seven years ago, and so on and so forth.""" + description = "A custom field type" -class DocstringLackingField(models.Field): +class DescriptionLackingField(models.Field): pass diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index d04030e9f0..bcf8353287 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -327,19 +327,11 @@ def get_return_data_type(func_name): return '' def get_readable_field_data_type(field): - """Returns the first line of a doc string for a given field type, if it - exists. Fields' docstrings can contain format strings, which will be - interpolated against the values of Field.__dict__ before being output. - If no docstring is given, a sensible value will be auto-generated from - the field's class name.""" + """Returns the description for a given field type, if it exists, + Fields' descriptions can contain format strings, which will be interpolated + against the values of field.__dict__ before being output.""" - if field.__doc__: - doc = field.__doc__.split('\n')[0] - return _(doc) % field.__dict__ - else: - return _(u'Field of type: %(field_type)s') % { - 'field_type': field.__class__.__name__ - } + return field.description % field.__dict__ def extract_views_from_urlpatterns(urlpatterns, base=''): """ diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py index cdbc67757c..2e846f8b81 100644 --- a/django/contrib/gis/db/models/fields/__init__.py +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -1,3 +1,4 @@ +from django.utils.translation import ugettext_lazy as _ from django.contrib.gis import forms # Getting the SpatialBackend container and the geographic quoting method. from django.contrib.gis.db.backend import SpatialBackend, gqn @@ -30,7 +31,7 @@ def get_srid_info(srid): return _srid_cache[srid] class GeometryField(SpatialBackend.Field): - """The base GIS field -- maps to the OpenGIS Specification Geometry type.""" + "The base GIS field -- maps to the OpenGIS Specification Geometry type." # The OpenGIS Geometry name. geom_type = 'GEOMETRY' @@ -38,6 +39,8 @@ class GeometryField(SpatialBackend.Field): # Geodetic units. geodetic_units = ('Decimal Degree', 'degree') + description = _("The base GIS field -- maps to the OpenGIS Specification Geometry type.") + def __init__(self, verbose_name=None, srid=4326, spatial_index=True, dim=2, **kwargs): """ The initialization function for geometry fields. Takes the following @@ -257,29 +260,29 @@ class GeometryField(SpatialBackend.Field): # The OpenGIS Geometry Type Fields class PointField(GeometryField): - """Point""" geom_type = 'POINT' + description = _("Point") class LineStringField(GeometryField): - """Line string""" geom_type = 'LINESTRING' + description = _("Line string") class PolygonField(GeometryField): - """Polygon""" geom_type = 'POLYGON' + description = _("Polygon") class MultiPointField(GeometryField): - """Multi-point""" geom_type = 'MULTIPOINT' + description = _("Multi-point") class MultiLineStringField(GeometryField): - """Multi-line string""" geom_type = 'MULTILINESTRING' + description = _("Multi-line string") class MultiPolygonField(GeometryField): - """Multi polygon""" geom_type = 'MULTIPOLYGON' + description = _("Multi polygon") class GeometryCollectionField(GeometryField): - """Geometry collection""" geom_type = 'GEOMETRYCOLLECTION' + description = _("Geometry collection") diff --git a/django/contrib/localflavor/us/models.py b/django/contrib/localflavor/us/models.py index 3e755da306..18386697d7 100644 --- a/django/contrib/localflavor/us/models.py +++ b/django/contrib/localflavor/us/models.py @@ -1,16 +1,21 @@ from django.conf import settings +from django.utils.translation import ugettext_lazy as _ from django.db.models.fields import Field, CharField from django.contrib.localflavor.us.us_states import STATE_CHOICES - + class USStateField(CharField): - """U.S. state (two uppercase letters)""" + + description = _("U.S. state (two uppercase letters)") + def __init__(self, *args, **kwargs): kwargs['choices'] = STATE_CHOICES kwargs['max_length'] = 2 super(USStateField, self).__init__(*args, **kwargs) - + class PhoneNumberField(Field): - """Phone number""" + + description = _("Phone number") + def get_internal_type(self): return "PhoneNumberField" diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 05238cf797..b5fd30e47c 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -49,8 +49,6 @@ class FieldDoesNotExist(Exception): # getattr(obj, opts.pk.attname) class Field(object): - """Base class for all field types""" - # Designates whether empty strings fundamentally are allowed at the # database level. empty_strings_allowed = True @@ -61,6 +59,13 @@ class Field(object): creation_counter = 0 auto_creation_counter = -1 + # Generic field type description, usually overriden by subclasses + def _description(self): + return _(u'Field of type: %(field_type)s') % { + 'field_type': self.__class__.__name__ + } + description = property(_description) + def __init__(self, verbose_name=None, name=None, primary_key=False, max_length=None, unique=False, blank=False, null=False, db_index=False, rel=None, default=NOT_PROVIDED, editable=True, @@ -342,10 +347,8 @@ class Field(object): return getattr(obj, self.attname) class AutoField(Field): - """Integer""" - + description = ugettext_lazy("Integer") empty_strings_allowed = False - def __init__(self, *args, **kwargs): assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__ kwargs['blank'] = True @@ -375,10 +378,8 @@ class AutoField(Field): return None class BooleanField(Field): - """Boolean (Either True or False)""" - empty_strings_allowed = False - + description = ugettext_lazy("Boolean (Either True or False)") def __init__(self, *args, **kwargs): kwargs['blank'] = True if 'default' not in kwargs and not kwargs.get('null'): @@ -421,8 +422,7 @@ class BooleanField(Field): return super(BooleanField, self).formfield(**defaults) class CharField(Field): - """String (up to %(max_length)s)""" - + description = ugettext_lazy("String (up to %(max_length)s)") def get_internal_type(self): return "CharField" @@ -444,8 +444,7 @@ class CharField(Field): # TODO: Maybe move this into contrib, because it's specialized. class CommaSeparatedIntegerField(CharField): - """Comma-separated integers""" - + description = ugettext_lazy("Comma-separated integers") def formfield(self, **kwargs): defaults = { 'form_class': forms.RegexField, @@ -461,10 +460,8 @@ class CommaSeparatedIntegerField(CharField): ansi_date_re = re.compile(r'^\d{4}-\d{1,2}-\d{1,2}$') class DateField(Field): - """Date (without time)""" - + description = ugettext_lazy("Date (without time)") empty_strings_allowed = False - def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs): self.auto_now, self.auto_now_add = auto_now, auto_now_add #HACKs : auto_now_add/auto_now should be done as a default or a pre_save. @@ -539,8 +536,7 @@ class DateField(Field): return super(DateField, self).formfield(**defaults) class DateTimeField(DateField): - """Date (with time)""" - + description = ugettext_lazy("Date (with time)") def get_internal_type(self): return "DateTimeField" @@ -600,10 +596,8 @@ class DateTimeField(DateField): return super(DateTimeField, self).formfield(**defaults) class DecimalField(Field): - """Decimal number""" - empty_strings_allowed = False - + description = ugettext_lazy("Decimal number") def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs): self.max_digits, self.decimal_places = max_digits, decimal_places Field.__init__(self, verbose_name, name, **kwargs) @@ -657,8 +651,7 @@ class DecimalField(Field): return super(DecimalField, self).formfield(**defaults) class EmailField(CharField): - """E-mail address""" - + description = ugettext_lazy("E-mail address") def __init__(self, *args, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 75) CharField.__init__(self, *args, **kwargs) @@ -669,8 +662,7 @@ class EmailField(CharField): return super(EmailField, self).formfield(**defaults) class FilePathField(Field): - """File path""" - + description = ugettext_lazy("File path") def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs): self.path, self.match, self.recursive = path, match, recursive kwargs['max_length'] = kwargs.get('max_length', 100) @@ -690,9 +682,8 @@ class FilePathField(Field): return "FilePathField" class FloatField(Field): - """Floating point number""" - empty_strings_allowed = False + description = ugettext_lazy("Floating point number") def get_db_prep_value(self, value): if value is None: @@ -717,10 +708,8 @@ class FloatField(Field): return super(FloatField, self).formfield(**defaults) class IntegerField(Field): - """Integer""" - empty_strings_allowed = False - + description = ugettext_lazy("Integer") def get_db_prep_value(self, value): if value is None: return None @@ -744,10 +733,8 @@ class IntegerField(Field): return super(IntegerField, self).formfield(**defaults) class IPAddressField(Field): - """IP address""" - empty_strings_allowed = False - + description = ugettext_lazy("IP address") def __init__(self, *args, **kwargs): kwargs['max_length'] = 15 Field.__init__(self, *args, **kwargs) @@ -761,10 +748,8 @@ class IPAddressField(Field): return super(IPAddressField, self).formfield(**defaults) class NullBooleanField(Field): - """Boolean (Either True, False or None)""" - empty_strings_allowed = False - + description = ugettext_lazy("Boolean (Either True, False or None)") def __init__(self, *args, **kwargs): kwargs['null'] = True Field.__init__(self, *args, **kwargs) @@ -804,8 +789,7 @@ class NullBooleanField(Field): return super(NullBooleanField, self).formfield(**defaults) class PositiveIntegerField(IntegerField): - """Integer""" - + description = ugettext_lazy("Integer") def get_internal_type(self): return "PositiveIntegerField" @@ -815,8 +799,7 @@ class PositiveIntegerField(IntegerField): return super(PositiveIntegerField, self).formfield(**defaults) class PositiveSmallIntegerField(IntegerField): - """Integer""" - + description = ugettext_lazy("Integer") def get_internal_type(self): return "PositiveSmallIntegerField" @@ -826,8 +809,7 @@ class PositiveSmallIntegerField(IntegerField): return super(PositiveSmallIntegerField, self).formfield(**defaults) class SlugField(CharField): - """String (up to %(max_length)s)""" - + description = ugettext_lazy("String (up to %(max_length)s)") def __init__(self, *args, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 50) # Set db_index=True unless it's been set manually. @@ -844,14 +826,12 @@ class SlugField(CharField): return super(SlugField, self).formfield(**defaults) class SmallIntegerField(IntegerField): - """Integer""" - + description = ugettext_lazy("Integer") def get_internal_type(self): return "SmallIntegerField" class TextField(Field): - """Text""" - + description = ugettext_lazy("Text") def get_internal_type(self): return "TextField" @@ -861,10 +841,8 @@ class TextField(Field): return super(TextField, self).formfield(**defaults) class TimeField(Field): - """Time""" - + description = ugettext_lazy("Time") empty_strings_allowed = False - def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs): self.auto_now, self.auto_now_add = auto_now, auto_now_add if auto_now or auto_now_add: @@ -936,8 +914,7 @@ class TimeField(Field): return super(TimeField, self).formfield(**defaults) class URLField(CharField): - """URL""" - + description = ugettext_lazy("URL") def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 200) self.verify_exists = verify_exists @@ -949,8 +926,7 @@ class URLField(CharField): return super(URLField, self).formfield(**defaults) class XMLField(TextField): - """XML text""" - + description = ugettext_lazy("XML text") def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs): self.schema_path = schema_path Field.__init__(self, verbose_name, name, **kwargs) diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 7689d64dcf..e6497f0441 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -209,8 +209,6 @@ class FileDescriptor(object): instance.__dict__[self.field.name] = value class FileField(Field): - """File path""" - # The class to wrap instance attributes in. Accessing the file object off # the instance will always return an instance of attr_class. attr_class = FieldFile @@ -218,6 +216,8 @@ class FileField(Field): # The descriptor to use for accessing the attribute off of the class. descriptor_class = FileDescriptor + description = ugettext_lazy("File path") + def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs): for arg in ('primary_key', 'unique'): if arg in kwargs: @@ -325,10 +325,9 @@ class ImageFieldFile(ImageFile, FieldFile): super(ImageFieldFile, self).delete(save) class ImageField(FileField): - """File path""" - attr_class = ImageFieldFile descriptor_class = ImageFileDescriptor + description = ugettext_lazy("File path") def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs): self.width_field, self.height_field = width_field, height_field diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 95a4a4e6e3..122ddd4ce5 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -682,9 +682,8 @@ class ManyToManyRel(object): return self.to._meta.pk class ForeignKey(RelatedField, Field): - """Foreign Key (type determined by related field)""" - empty_strings_allowed = False + description = ugettext_lazy("Foreign Key (type determined by related field)") def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): try: to_name = to._meta.object_name.lower() @@ -773,13 +772,13 @@ class ForeignKey(RelatedField, Field): return rel_field.db_type() class OneToOneField(ForeignKey): - """One-to-one relationship - + """ A OneToOneField is essentially the same as a ForeignKey, with the exception that always carries a "unique" constraint with it and the reverse relation always returns the object pointed to (since there will only ever be one), - rather than returning a list.""" - + rather than returning a list. + """ + description = ugettext_lazy("One-to-one relationship") def __init__(self, to, to_field=None, **kwargs): kwargs['unique'] = True super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) @@ -794,8 +793,7 @@ class OneToOneField(ForeignKey): return super(OneToOneField, self).formfield(**kwargs) class ManyToManyField(RelatedField, Field): - """Many-to-many relationship""" - + description = ugettext_lazy("Many-to-many relationship") def __init__(self, to, **kwargs): try: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index f407c2bfad..1475aaf62d 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -5,6 +5,7 @@ Writing custom model fields =========================== .. versionadded:: 1.0 +.. currentmodule:: django.db.models Introduction ============ @@ -165,7 +166,8 @@ behave like any existing field, so we'll subclass directly from from django.db import models class HandField(models.Field): - """A hand of cards (bridge style)""" + + description = "A hand of cards (bridge style)" def __init__(self, *args, **kwargs): kwargs['max_length'] = 104 @@ -248,7 +250,8 @@ simple: make sure your field subclass uses a special metaclass: For example:: class HandField(models.Field): - """A hand of cards (bridge style)""" + + description = "A hand of cards (bridge style)" __metaclass__ = models.SubfieldBase @@ -262,16 +265,17 @@ called when the attribute is initialized. Documenting your Custom Field ----------------------------- +.. class:: django.db.models.Field + +.. attribute:: description + As always, you should document your field type, so users will know what it is. -The best way to do this is to simply provide a docstring for it. This will -automatically be picked up by ``django.contrib.admindocs``, if you have it -installed, and the first line of it will show up as the field type in the -documentation for any model that uses your field. In the above examples, it -will show up as 'A hand of cards (bridge style)'. Note that if you provide a -more verbose docstring, only the first line will show up in -``django.contrib.admindocs``. The full docstring will, of course, still be -available through ``pydoc`` or the interactive interpreter's ``help()`` -function. +In addition to providing a docstring for it, which is useful for developers, +you can also allow users of the admin app to see a short description of the +field type via the ``django.contrib.admindocs`` application. To do this simply +provide descriptive text in a ``description`` class attribute of your custom field. +In the above example, the type description displayed by the ``admindocs`` application +for a ``HandField`` will be 'A hand of cards (bridge style)'. Useful methods --------------