From 0986a4d2e147926fc1ee62fb67406d0a39fbe776 Mon Sep 17 00:00:00 2001 From: Karen Tracey Date: Sat, 12 Dec 2009 20:25:41 +0000 Subject: [PATCH] Fixed #7977: Fixed admindocs to use docstrings instead of a static array to locate type information. Thanks J. Clifford Dyer. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11833 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/admindocs/models.py | 1 + django/contrib/admindocs/tests/__init__.py | 36 +++++++++++++ django/contrib/admindocs/tests/fields.py | 13 +++++ django/contrib/admindocs/views.py | 49 +++++------------ .../contrib/gis/db/models/fields/__init__.py | 9 +++- django/contrib/localflavor/us/models.py | 2 + django/db/models/fields/__init__.py | 52 +++++++++++++++++++ django/db/models/fields/files.py | 4 ++ django/db/models/fields/related.py | 11 ++-- 9 files changed, 137 insertions(+), 40 deletions(-) create mode 100644 django/contrib/admindocs/models.py create mode 100644 django/contrib/admindocs/tests/__init__.py create mode 100644 django/contrib/admindocs/tests/fields.py diff --git a/django/contrib/admindocs/models.py b/django/contrib/admindocs/models.py new file mode 100644 index 00000000000..a9f813a4cb1 --- /dev/null +++ b/django/contrib/admindocs/models.py @@ -0,0 +1 @@ +# Empty models.py to allow for specifying admindocs as a test label. diff --git a/django/contrib/admindocs/tests/__init__.py b/django/contrib/admindocs/tests/__init__.py new file mode 100644 index 00000000000..a091ebe122f --- /dev/null +++ b/django/contrib/admindocs/tests/__init__.py @@ -0,0 +1,36 @@ +import unittest +from django.contrib.admindocs import views +import fields + +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' + ) diff --git a/django/contrib/admindocs/tests/fields.py b/django/contrib/admindocs/tests/fields.py new file mode 100644 index 00000000000..5cab3627c63 --- /dev/null +++ b/django/contrib/admindocs/tests/fields.py @@ -0,0 +1,13 @@ +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.""" + +class DocstringLackingField(models.Field): + pass diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index 571f393ff8a..d04030e9f03 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -326,43 +326,20 @@ def get_return_data_type(func_name): return 'Integer' return '' -# Maps Field objects to their human-readable data types, as strings. -# Column-type strings can contain format strings; they'll be interpolated -# against the values of Field.__dict__ before being output. -# If a column type is set to None, it won't be included in the output. -DATA_TYPE_MAPPING = { - 'AutoField' : _('Integer'), - 'BooleanField' : _('Boolean (Either True or False)'), - 'CharField' : _('String (up to %(max_length)s)'), - 'CommaSeparatedIntegerField': _('Comma-separated integers'), - 'DateField' : _('Date (without time)'), - 'DateTimeField' : _('Date (with time)'), - 'DecimalField' : _('Decimal number'), - 'EmailField' : _('E-mail address'), - 'FileField' : _('File path'), - 'FilePathField' : _('File path'), - 'FloatField' : _('Floating point number'), - 'ForeignKey' : _('Integer'), - 'ImageField' : _('File path'), - 'IntegerField' : _('Integer'), - 'IPAddressField' : _('IP address'), - 'ManyToManyField' : '', - 'NullBooleanField' : _('Boolean (Either True, False or None)'), - 'OneToOneField' : _('Relation to parent model'), - 'PhoneNumberField' : _('Phone number'), - 'PositiveIntegerField' : _('Integer'), - 'PositiveSmallIntegerField' : _('Integer'), - 'SlugField' : _('String (up to %(max_length)s)'), - 'SmallIntegerField' : _('Integer'), - 'TextField' : _('Text'), - 'TimeField' : _('Time'), - 'URLField' : _('URL'), - 'USStateField' : _('U.S. state (two uppercase letters)'), - 'XMLField' : _('XML text'), -} - def get_readable_field_data_type(field): - return DATA_TYPE_MAPPING[field.get_internal_type()] % field.__dict__ + """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.""" + + 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__ + } 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 b2dacc85d76..cdbc67757cb 100644 --- a/django/contrib/gis/db/models/fields/__init__.py +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -30,7 +30,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' @@ -257,22 +257,29 @@ class GeometryField(SpatialBackend.Field): # The OpenGIS Geometry Type Fields class PointField(GeometryField): + """Point""" geom_type = 'POINT' class LineStringField(GeometryField): + """Line string""" geom_type = 'LINESTRING' class PolygonField(GeometryField): + """Polygon""" geom_type = 'POLYGON' class MultiPointField(GeometryField): + """Multi-point""" geom_type = 'MULTIPOINT' class MultiLineStringField(GeometryField): + """Multi-line string""" geom_type = 'MULTILINESTRING' class MultiPolygonField(GeometryField): + """Multi polygon""" geom_type = 'MULTIPOLYGON' class GeometryCollectionField(GeometryField): + """Geometry collection""" geom_type = 'GEOMETRYCOLLECTION' diff --git a/django/contrib/localflavor/us/models.py b/django/contrib/localflavor/us/models.py index 5158da4e87c..9465126db7d 100644 --- a/django/contrib/localflavor/us/models.py +++ b/django/contrib/localflavor/us/models.py @@ -2,6 +2,7 @@ from django.conf import settings from django.db.models.fields import Field class USStateField(Field): + """U.S. state (two uppercase letters)""" def get_internal_type(self): return "USStateField" @@ -18,6 +19,7 @@ class USStateField(Field): return super(USStateField, self).formfield(**defaults) class PhoneNumberField(Field): + """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 a3007a7f660..05238cf797e 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -49,6 +49,8 @@ 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 @@ -340,7 +342,10 @@ class Field(object): return getattr(obj, self.attname) class AutoField(Field): + """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 @@ -370,7 +375,10 @@ class AutoField(Field): return None class BooleanField(Field): + """Boolean (Either True or False)""" + empty_strings_allowed = False + def __init__(self, *args, **kwargs): kwargs['blank'] = True if 'default' not in kwargs and not kwargs.get('null'): @@ -413,6 +421,8 @@ class BooleanField(Field): return super(BooleanField, self).formfield(**defaults) class CharField(Field): + """String (up to %(max_length)s)""" + def get_internal_type(self): return "CharField" @@ -434,6 +444,8 @@ class CharField(Field): # TODO: Maybe move this into contrib, because it's specialized. class CommaSeparatedIntegerField(CharField): + """Comma-separated integers""" + def formfield(self, **kwargs): defaults = { 'form_class': forms.RegexField, @@ -449,7 +461,10 @@ class CommaSeparatedIntegerField(CharField): ansi_date_re = re.compile(r'^\d{4}-\d{1,2}-\d{1,2}$') class DateField(Field): + """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. @@ -524,6 +539,8 @@ class DateField(Field): return super(DateField, self).formfield(**defaults) class DateTimeField(DateField): + """Date (with time)""" + def get_internal_type(self): return "DateTimeField" @@ -583,7 +600,10 @@ class DateTimeField(DateField): return super(DateTimeField, self).formfield(**defaults) class DecimalField(Field): + """Decimal number""" + empty_strings_allowed = False + 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) @@ -637,6 +657,8 @@ class DecimalField(Field): return super(DecimalField, self).formfield(**defaults) class EmailField(CharField): + """E-mail address""" + def __init__(self, *args, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 75) CharField.__init__(self, *args, **kwargs) @@ -647,6 +669,8 @@ class EmailField(CharField): return super(EmailField, self).formfield(**defaults) class FilePathField(Field): + """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) @@ -666,6 +690,8 @@ class FilePathField(Field): return "FilePathField" class FloatField(Field): + """Floating point number""" + empty_strings_allowed = False def get_db_prep_value(self, value): @@ -691,7 +717,10 @@ class FloatField(Field): return super(FloatField, self).formfield(**defaults) class IntegerField(Field): + """Integer""" + empty_strings_allowed = False + def get_db_prep_value(self, value): if value is None: return None @@ -715,7 +744,10 @@ class IntegerField(Field): return super(IntegerField, self).formfield(**defaults) class IPAddressField(Field): + """IP address""" + empty_strings_allowed = False + def __init__(self, *args, **kwargs): kwargs['max_length'] = 15 Field.__init__(self, *args, **kwargs) @@ -729,7 +761,10 @@ class IPAddressField(Field): return super(IPAddressField, self).formfield(**defaults) class NullBooleanField(Field): + """Boolean (Either True, False or None)""" + empty_strings_allowed = False + def __init__(self, *args, **kwargs): kwargs['null'] = True Field.__init__(self, *args, **kwargs) @@ -769,6 +804,8 @@ class NullBooleanField(Field): return super(NullBooleanField, self).formfield(**defaults) class PositiveIntegerField(IntegerField): + """Integer""" + def get_internal_type(self): return "PositiveIntegerField" @@ -778,6 +815,8 @@ class PositiveIntegerField(IntegerField): return super(PositiveIntegerField, self).formfield(**defaults) class PositiveSmallIntegerField(IntegerField): + """Integer""" + def get_internal_type(self): return "PositiveSmallIntegerField" @@ -787,6 +826,8 @@ class PositiveSmallIntegerField(IntegerField): return super(PositiveSmallIntegerField, self).formfield(**defaults) class SlugField(CharField): + """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. @@ -803,10 +844,14 @@ class SlugField(CharField): return super(SlugField, self).formfield(**defaults) class SmallIntegerField(IntegerField): + """Integer""" + def get_internal_type(self): return "SmallIntegerField" class TextField(Field): + """Text""" + def get_internal_type(self): return "TextField" @@ -816,7 +861,10 @@ class TextField(Field): return super(TextField, self).formfield(**defaults) class TimeField(Field): + """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: @@ -888,6 +936,8 @@ class TimeField(Field): return super(TimeField, self).formfield(**defaults) class URLField(CharField): + """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 @@ -899,6 +949,8 @@ class URLField(CharField): return super(URLField, self).formfield(**defaults) class XMLField(TextField): + """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 aab4f3789f1..7689d64dcfa 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -209,6 +209,8 @@ 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 @@ -323,6 +325,8 @@ class ImageFieldFile(ImageFile, FieldFile): super(ImageFieldFile, self).delete(save) class ImageField(FileField): + """File path""" + attr_class = ImageFieldFile descriptor_class = ImageFileDescriptor diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index f5e69ab6a7a..fcdda22cc66 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -691,6 +691,8 @@ class ManyToManyRel(object): return self.to._meta.pk class ForeignKey(RelatedField, Field): + """Foreign Key (type determined by related field)""" + empty_strings_allowed = False def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): try: @@ -788,12 +790,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.""" + def __init__(self, to, to_field=None, **kwargs): kwargs['unique'] = True super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) @@ -847,6 +850,8 @@ def create_many_to_many_intermediary_model(field, klass): }) class ManyToManyField(RelatedField, Field): + """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)