Index: django/db/models/fields/__init__.py
===================================================================
--- django/db/models/fields/__init__.py	(revision 11819)
+++ django/db/models/fields/__init__.py	(working copy)
@@ -49,6 +49,8 @@
 #     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 @@
         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 @@
         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 @@
         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 @@
 
 # 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 @@
 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 @@
         return super(DateField, self).formfield(**defaults)
 
 class DateTimeField(DateField):
+    """Date (with time)"""
+    
     def get_internal_type(self):
         return "DateTimeField"
 
@@ -583,7 +600,10 @@
         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 @@
         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 @@
         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 @@
         return "FilePathField"
 
 class FloatField(Field):
+    """Floating point number"""
+    
     empty_strings_allowed = False
 
     def get_db_prep_value(self, value):
@@ -691,7 +717,10 @@
         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 @@
         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 @@
         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 @@
         return super(NullBooleanField, self).formfield(**defaults)
 
 class PositiveIntegerField(IntegerField):
+    """Integer"""
+    
     def get_internal_type(self):
         return "PositiveIntegerField"
 
@@ -778,6 +815,8 @@
         return super(PositiveIntegerField, self).formfield(**defaults)
 
 class PositiveSmallIntegerField(IntegerField):
+    """Integer"""
+
     def get_internal_type(self):
         return "PositiveSmallIntegerField"
 
@@ -787,6 +826,8 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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)
Index: django/db/models/fields/related.py
===================================================================
--- django/db/models/fields/related.py	(revision 11819)
+++ django/db/models/fields/related.py	(working copy)
@@ -691,6 +691,8 @@
         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 @@
         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 @@
     })
 
 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)
Index: django/db/models/fields/files.py
===================================================================
--- django/db/models/fields/files.py	(revision 11819)
+++ django/db/models/fields/files.py	(working copy)
@@ -209,6 +209,8 @@
         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 @@
         super(ImageFieldFile, self).delete(save)
 
 class ImageField(FileField):
+    """File path"""
+    
     attr_class = ImageFieldFile
     descriptor_class = ImageFileDescriptor
 
Index: django/contrib/gis/db/models/fields/__init__.py
===================================================================
--- django/contrib/gis/db/models/fields/__init__.py	(revision 11819)
+++ django/contrib/gis/db/models/fields/__init__.py	(working copy)
@@ -30,7 +30,7 @@
     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 @@
 
 # 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'
Index: django/contrib/admindocs/views.py
===================================================================
--- django/contrib/admindocs/views.py	(revision 11819)
+++ django/contrib/admindocs/views.py	(working copy)
@@ -326,44 +326,21 @@
             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=''):
     """
     Return a list of views from a list of urlpatterns.
Index: django/contrib/admindocs/tests.py
===================================================================
--- django/contrib/admindocs/tests.py	(revision 0)
+++ django/contrib/admindocs/tests.py	(revision 0)
@@ -0,0 +1,36 @@
+import unittest
+import views
+from django.db.models import fields as builtin_fields
+import models
+
+
+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(models.CustomField()),
+            u'A custom field type'
+        )
+        self.assertEqual(
+            views.get_readable_field_data_type(models.DocstringLackingField()),
+            u'Field of type: DocstringLackingField'
+        )
+    
+    def test_multiline_custom_field_truncation(self):
+        self.assertEqual(
+            views.get_readable_field_data_type(models.ManyLineDocstringField()),
+            u'Many-line custom field'
+        )
Index: django/contrib/admindocs/models.py
===================================================================
--- django/contrib/admindocs/models.py	(revision 0)
+++ django/contrib/admindocs/models.py	(revision 0)
@@ -0,0 +1,14 @@
+
+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
Index: django/contrib/localflavor/us/models.py
===================================================================
--- django/contrib/localflavor/us/models.py	(revision 11819)
+++ django/contrib/localflavor/us/models.py	(working copy)
@@ -2,6 +2,7 @@
 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 @@
         return super(USStateField, self).formfield(**defaults)
 
 class PhoneNumberField(Field):
+    """Phone number"""
     def get_internal_type(self):
         return "PhoneNumberField"
 
Index: docs/howto/custom-model-fields.txt
===================================================================
--- docs/howto/custom-model-fields.txt	(revision 11819)
+++ docs/howto/custom-model-fields.txt	(working copy)
@@ -39,6 +39,8 @@
 something like this::
 
     class Hand(object):
+        """A hand of cards (bridge style)"""
+
         def __init__(self, north, east, south, west):
             # Input parameters are lists of cards ('Ah', '9s', etc)
             self.north = north
@@ -163,6 +165,8 @@
     from django.db import models
 
     class HandField(models.Field):
+        """A hand of cards (bridge style)"""
+
         def __init__(self, *args, **kwargs):
             kwargs['max_length'] = 104
             super(HandField, self).__init__(*args, **kwargs)
@@ -244,6 +248,8 @@
 For example::
 
     class HandField(models.Field):
+        """A hand of cards (bridge style)"""
+
         __metaclass__ = models.SubfieldBase
 
         def __init__(self, *args, **kwargs):
@@ -252,6 +258,21 @@
 This ensures that the :meth:`to_python` method, documented below, will always be
 called when the attribute is initialized.
 
+
+Documenting your Custom Field
+-----------------------------
+
+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.
+
 Useful methods
 --------------
 
