From 2b5730873bd29e5bb449df5800dea610c462786a Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 14 May 2011 16:29:39 +0000 Subject: [PATCH] Added ability to describe grouping of form fields in the same row to the `fields` ModelAdmin attribute. git-svn-id: http://code.djangoproject.com/svn/django/trunk@16225 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/admin/validation.py | 77 +++++++++---------- docs/ref/contrib/admin/index.txt | 48 ++++++++---- .../regressiontests/admin_validation/tests.py | 7 +- 3 files changed, 76 insertions(+), 56 deletions(-) diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 1d57686dbc..b4f2b9d0dc 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -222,6 +222,40 @@ def validate_inline(cls, parent, parent_model): if hasattr(cls, "readonly_fields"): check_readonly_fields(cls, cls.model, cls.model._meta) +def validate_fields_spec(cls, model, opts, flds, label): + """ + Validate the fields specification in `flds` from a ModelAdmin subclass + `cls` for the `model` model. `opts` is `model`'s Meta inner class. + Use `label` for reporting problems to the user. + + The fields specification can be a ``fields`` option or a ``fields`` + sub-option from a ``fieldsets`` option component. + """ + for fields in flds: + # The entry in fields might be a tuple. If it is a standalone + # field, make it into a tuple to make processing easier. + if type(fields) != tuple: + fields = (fields,) + for field in fields: + if field in cls.readonly_fields: + # Stuff can be put in fields that isn't actually a + # model field if it's in readonly_fields, + # readonly_fields will handle the validation of such + # things. + continue + check_formfield(cls, model, opts, label, field) + try: + f = opts.get_field(field) + except models.FieldDoesNotExist: + # If we can't find a field on the model that matches, + # it could be an extra field on the form. + pass + if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: + raise ImproperlyConfigured("'%s.%s' " + "can't include the ManyToManyField field '%s' because " + "'%s' manually specifies a 'through' model." % ( + cls.__name__, label, field, field)) + def validate_base(cls, model): opts = model._meta @@ -238,23 +272,7 @@ def validate_base(cls, model): # fields if cls.fields: # default value is None check_isseq(cls, 'fields', cls.fields) - for field in cls.fields: - if field in cls.readonly_fields: - # Stuff can be put in fields that isn't actually a model field - # if it's in readonly_fields, readonly_fields will handle the - # validation of such things. - continue - check_formfield(cls, model, opts, 'fields', field) - try: - f = opts.get_field(field) - except models.FieldDoesNotExist: - # If we can't find a field on the model that matches, - # it could be an extra field on the form. - continue - if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: - raise ImproperlyConfigured("'%s.fields' can't include the ManyToManyField " - "field '%s' because '%s' manually specifies " - "a 'through' model." % (cls.__name__, field, field)) + validate_fields_spec(cls, model, opts, cls.fields, 'fields') if cls.fieldsets: raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) if len(cls.fields) > len(set(cls.fields)): @@ -273,30 +291,7 @@ def validate_base(cls, model): raise ImproperlyConfigured("'fields' key is required in " "%s.fieldsets[%d][1] field options dict." % (cls.__name__, idx)) - for fields in fieldset[1]['fields']: - # The entry in fields might be a tuple. If it is a standalone - # field, make it into a tuple to make processing easier. - if type(fields) != tuple: - fields = (fields,) - for field in fields: - if field in cls.readonly_fields: - # Stuff can be put in fields that isn't actually a - # model field if it's in readonly_fields, - # readonly_fields will handle the validation of such - # things. - continue - check_formfield(cls, model, opts, "fieldsets[%d][1]['fields']" % idx, field) - try: - f = opts.get_field(field) - if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: - raise ImproperlyConfigured("'%s.fieldsets[%d][1]['fields']' " - "can't include the ManyToManyField field '%s' because " - "'%s' manually specifies a 'through' model." % ( - cls.__name__, idx, field, field)) - except models.FieldDoesNotExist: - # If we can't find a field on the model that matches, - # it could be an extra field on the form. - pass + validate_fields_spec(cls, model, opts, fieldset[1]['fields'], "fieldsets[%d][1]['fields']" % idx) flattened_fieldsets = flatten_fieldsets(cls.fieldsets) if len(flattened_fieldsets) > len(set(flattened_fieldsets)): raise ImproperlyConfigured('There are duplicate field(s) in %s.fieldsets' % cls.__name__) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index fb48ba8119..8c7a69ceaf 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -160,27 +160,45 @@ subclass:: .. attribute:: ModelAdmin.fields - Use this option as an alternative to ``fieldsets`` if the layout does not - matter and if you want to only show a subset of the available fields in the - form. For example, you could define a simpler version of the admin form for - the ``django.contrib.flatpages.FlatPage`` model as follows:: + If you need to achieve simple changes in the layout of fields in the forms + of the "add" and "change" pages like only showing a subset of the available + fields, modifying their order or grouping them in rows you can use the + ``fields`` option (for more complex layout needs see the + :attr:`~ModelAdmin.fieldsets` option described in the next section). For + example, you could define a simpler version of the admin form for the + ``django.contrib.flatpages.FlatPage`` model as follows:: class FlatPageAdmin(admin.ModelAdmin): fields = ('url', 'title', 'content') - In the above example, only the fields 'url', 'title' and 'content' will be - displayed, sequentially, in the form. + In the above example, only the fields ``url``, ``title`` and ``content`` + will be displayed, sequentially, in the form. .. versionadded:: 1.2 ``fields`` can contain values defined in :attr:`ModelAdmin.readonly_fields` to be displayed as read-only. + .. versionadded:: 1.4 + + To display multiple fields on the same line, wrap those fields in their own + tuple. In this example, the ``url`` and ``title`` fields will display on the + same line and the ``content`` field will be displayed below them in its + own line:: + + class FlatPageAdmin(admin.ModelAdmin): + fields = (('url', 'title'), 'content') + .. admonition:: Note This ``fields`` option should not be confused with the ``fields`` - dictionary key that is within the ``fieldsets`` option, as described in - the previous section. + dictionary key that is within the :attr:`~ModelAdmin.fieldsets` option, + as described in the next section. + + If neither ``fields`` nor :attr:`~ModelAdmin.fieldsets` options are present, + Django will default to displaying each field that isn't an ``AutoField`` and + has ``editable=True``, in a single fieldset, in the same order as the fields + are defined in the model. .. attribute:: ModelAdmin.fieldsets @@ -213,9 +231,10 @@ subclass:: .. image:: _images/flatfiles_admin.png - If ``fieldsets`` isn't given, Django will default to displaying each field - that isn't an ``AutoField`` and has ``editable=True``, in a single - fieldset, in the same order as the fields are defined in the model. + If neither ``fieldsets`` nor :attr:`~ModelAdmin.fields` options are present, + Django will default to displaying each field that isn't an ``AutoField`` and + has ``editable=True``, in a single fieldset, in the same order as the fields + are defined in the model. The ``field_options`` dictionary can have the following keys: @@ -229,9 +248,10 @@ subclass:: 'fields': ('first_name', 'last_name', 'address', 'city', 'state'), } - To display multiple fields on the same line, wrap those fields in - their own tuple. In this example, the ``first_name`` and - ``last_name`` fields will display on the same line:: + Just like with the :attr:`~ModelAdmin.fields` option, to display + multiple fields on the same line, wrap those fields in their own + tuple. In this example, the ``first_name`` and ``last_name`` fields + will display on the same line:: { 'fields': (('first_name', 'last_name'), 'address', 'city', 'state'), diff --git a/tests/regressiontests/admin_validation/tests.py b/tests/regressiontests/admin_validation/tests.py index 6fbdc8040e..a80820cb67 100644 --- a/tests/regressiontests/admin_validation/tests.py +++ b/tests/regressiontests/admin_validation/tests.py @@ -201,7 +201,7 @@ class ValidationTestCase(TestCase): validate, BookAdmin, Book) - def test_cannon_include_through(self): + def test_cannot_include_through(self): class FieldsetBookAdmin(admin.ModelAdmin): fieldsets = ( ('Header 1', {'fields': ('name',)}), @@ -212,6 +212,11 @@ class ValidationTestCase(TestCase): validate, FieldsetBookAdmin, Book) + def test_nested_fields(self): + class NestedFieldsAdmin(admin.ModelAdmin): + fields = ('price', ('name', 'subtitle')) + validate(NestedFieldsAdmin, Book) + def test_nested_fieldsets(self): class NestedFieldsetAdmin(admin.ModelAdmin): fieldsets = (