Backend Forms¶
The backend system is using a model view form in order to allow users to edit properties of a model.
A model may declare which model view form is used for the backend.
In addition, Cubane extends Django’s form implementation by providing a number of additional features such as tabs, form configuration and dynamic form field visibility.
Model View Form¶
In order to edit a model instance, the corresponding backend view needs to know what form to use for this purpose. A form can be either declared by the model itself or the model view.
In the latter case, the form that is used for editing model instance can be
declared via the form
declaration on the model view class as the following
example demonstrates:
from cubane.views import ModelView
from models import Book
from forms import BookForm
class BookView(ModelView):
template_path = 'cubane/backend/'
model = Book
form = BookForm
In this case, the model view BookView
will use the form BookForm
when
editing or creating model instances of Book
.
In some cases you may not implement a model view by yourself, which is why you can also declare the default form used by a model view through the model instead:
from django.db import models
from cubane.models import DateTimeBase
class Book(DateTimeBase):
class Meta:
verbose_name = 'Book'
verbose_name_plural = 'Books'
class Listing:
columns = ['title', 'isbn']
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, db_index=True)
isbn = models.CharField(max_length=32, db_index=True, unique=True)
@classmethod
def get_form(cls):
from forms import BookForm
return BookForm
def __unicode__(self):
return self.title
The class method get_form
should return the class of the form that is able
of representing instances of the model. A model view representing Books will then
determine the form by calling the very same class method on the model class
unless a form has been declared by the model view itself.
See also
Please refer to the Model Forms section in order to learn more about how model forms can be declared and customised for the backend system.
Model View Filter Form¶
A model view may provide filter options by which records of a particular model can be filtered by individual attributes. When working with books, for example, you may want to allow users of the backend system to filter books by specific properties such as book title, author or ISBN.
A model filter form is used for this purpose. It is usually presented within a side column which can be extended by the user to see all options available.
If no filter form is declared, then the regular model form is used instead, which is sufficient in most cases. However, in some cases, you may want to use a different form that extends the model form in a specific way.
A filter form can be declared either by the model itself or by the corresponding model view. For the latter case, you can simply declare the filter form in a similar way as the model form is declared:
from cubane.views import ModelView
from models import Book
from forms import BookForm
class BookView(ModelView):
template_path = 'cubane/backend/'
model = Book
form = BookForm
filter_form = BookFilterForm
In this example, the BookForm
is used when creating or editing books.
However, when filtering books, the BookFilterForm
is used instead.
In some cases you may not implement a model view by yourself, which is why you can declare a model filter form via the model in a similar way how you would declare a model view:
from django.db import models
from cubane.models import DateTimeBase
class Book(DateTimeBase):
class Meta:
verbose_name = 'Book'
verbose_name_plural = 'Books'
class Listing:
columns = ['title', 'isbn']
filter_by = ['title', slug', 'isbn']
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, db_index=True)
isbn = models.CharField(max_length=32, db_index=True, unique=True)
@classmethod
def get_form(cls):
from forms import BookForm
return BookForm
@classmethod
def get_filter_form(cls):
from forms import BookFilterForm
return BookFilterForm
def __unicode__(self):
return self.title
In this example, the BookForm
is used for creating or editing instances of
books, but the BookFilterForm
is used for filtering books instead unless a
filter form has been declared by the corresponding model view directly.
Note
Please note that ultimately the model listing option filter_by
dictates the fields that are presented to users as part of the filter form,
but those fields obviously need to be present in the form.
You can read more about the filter_by
options as part of the
Model View Options section.
Model Forms¶
For Cubane’s backend system, a model form is a form that derives itself from
cubane.forms.BaseForm
or cubane.forms.BaseModelForm
and is
able to represent a model for the purpose of creating, editing or filtering
model instances.
The base class cubane.forms.BaseForm
derives itself from Django’s
django.forms.Form
class but adds additional support for various
aspects of editing model instances and customising the presentation and
behaviour of the form.
When working with models, you would probably want to use the class
cubane.forms.BaseModelForm
instead, which derives itself from Django’s
django.forms.ModelForm
and therefore provides the ability to generate
form fields based on the model automatically.
The BookForm
from previous examples may be declared in the following way:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
In addition to this simple form, Cubane allows for a number of options to extend the form - in particular for the purpose of the backend system.
Form Tabs¶
Form fields can be organised into tabs, where each tab contains one or more form fields. The user is presented with a number of tabs and can switch between multiple tabs.
Tabs are declared via the tabs
option as part of the Meta
class in the
following way:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
tabs = [
{
'title': 'Book Name',
'fields': [
'title',
'slug',
]
}, {
'title': 'Properties',
'fields': [
'isbn',
'recommended',
'price'
]
}
]
As the example demonstrates, tabs are declared as a list of dictionaries, where
the title
key declares the name of the tab and fields
a list of
individual form fields that are presented on that tab in the given order.
For the last example, the form for editing book instances is organised into two separate tabs, one for editing the title and slug fields and another for editing further properties such as ISBN, recommendation and price.
The order in which tabs are presented is the same as they are declared. Further, form fields within each tab are presented in the order in which they are listed for each tab.
Note
When using tabs, all required fields have to be listed on some tab. The system will generate an error message if a required field is not listed for any tab.
Tabs can be inherited from a base class, where additional fields are added to existing tabs or new tabs are introduced as the following example demonstrates:
from cubane.forms import BaseModelForm
from models import Book
class ExtendedBookForm(BookForm):
class Meta:
model = Book
fields = '__all__'
tabs = [
{
'title': 'Book Name',
'fields': [
'barcode:after(isbn)',
'author',
]
}, {
'title': 'Properties:as(Details)',
'fields': [
'rrp'
]
}, {
'title': 'Content',
'fields': [
'description'
]
}
]
The example demonstrates three different mechanisms by which tabs can be extended. In general, a derived form will always inherit all tabs that were declared in the base class.
In addition, the parent class may override existing properties using one of the following principals:
1. New form fields can be added to the end of any existing tab by simply
matching the name of an existing tab. In the example, the field author
is
added to the end of the tab with the name Book Name
.
2. A new or existing field can be inserted or moved before
or after
any
existing field. In the example, a new field with the name barcode
is
inserted into the Book Name
tab after the existing field isbn
. The
format is <field>:before(<ref-field>)
for inserting the field before
another field and <field>:after(<ref-field>)
for inserting the field
after another field.
3. A new or existing field can be inserted or moved to a new tab that did not
exist before. In the example, the new field description
is being added to a
new tab with the name Content
.
4. A tab can be renamed by using the format <tab-name>:as(<new-tab-name>)
when referencing a tab. In the example, the tab with the name Properties
is
renamed to Details
. In addition, a new field with the name rrp
is being
added to the tab; however, a tab can be renamed without adding fields to it.
Note
Fields cannot be removed since the form is deriving from an existing form
and cannot change the behaviour of the form itself via the declaration of
tabs
. Of course, it is always possible to remove form fields dynamically
during its construction or configuration of the form.
Currently, tabs can be renamed but cannot be removed or merged with other tabs.
Form Sections¶
Form fields may be grouped into sections. A section is a group of form fields for which a name has been declared. Visually, form sections are usually rendered as two columns where sections are distributed among those columns evenly.
Form sections are available for tabbed and non-tabbed forms alike and are
declared via the sections
field of the Meta
class as the following
example demonstrates:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
tabs = [
{
'title': 'Book Name and Details',
'fields': [
'title',
'slug',
'isbn',
'recommended',
'price'
]
}
]
sections = {
'title': 'Title and Slug',
'isbn': 'Details'
}
The sections
property is declared as a dictionary where the key is
referring to the name of an existing form field and the value is describing the
name of the section. A section is then grouping any number of form fields until
the next form section or the end of the form or tab.
In the previous example, the form fields title
and slug
are grouped
into a section called Title and Slug
and the remaining form fields are
grouped into a section with the name Details
.
When naming individual form fields by using the tabs
property for example,
then sections can also be created within the list of fields like the next
example demonstrates:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
tabs = [
{
'title': 'Book Name and Details',
'fields': [
':Title and Slug',
'title',
'slug',
':Details',
'isbn',
'recommended',
'price'
]
}
]
A section is introduced by using a colon character (:
) as the first
character of the section name.
When using non-tabbed forms, form fields and sections can be declared via the
section_fields
property:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
section_fields = [
':Title and Slug',
'title',
'slug',
':Details',
'isbn',
'recommended',
'price'
]
In this case, the form does not have any tabs but presents the same fields with the same sections in the same order as the previous example.
A section may also have additional text information or help description which
can be declared by using a pipe (|
) character:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
layout = FormLayout.COLUMNS
section_fields = [
':Title and Slug|Enter the name and the unique slug.',
'title',
'slug',
':Details|Enter additional details.',
'isbn',
'recommended',
'price'
]
In this example, the section field Title and Slug
will have an additional
help text associated with it, which is Enter the name and the unique slug.
.
Form Layout¶
A form is usually rendered in two columns where form sections are distributed among those columns evenly like in the following example:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
layout = FormLayout.COLUMNS
section_fields = [
':Title and Slug',
'title',
'slug',
':Details',
'isbn',
'recommended',
'price'
]
The layout of the form is explicitly set to FormLayout.COLUMNS
. If the form
should not be presented as columns then a flat layout can be specified instead:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
layout = FormLayout.FLAT
section_fields = [
':Title and Slug',
'title',
'slug',
':Details',
'isbn',
'recommended',
'price'
]
A flat layout is still presenting form sections but does not arrange them within columns.
When using a column layout, individual sections can be forced to take up two
columns at once by using the explanation mark (!
) as the first character of
the name of the section.
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
layout = FormLayout.COLUMNS
section_fields = [
'!:Title and Slug',
'title',
'slug',
'!:Details',
'isbn',
'recommended',
'price'
]
In this case, both sections will span across two columns deliberately.
Form Configuration¶
When using forms, you can add code that gets executed in order to configure the form before it is being used:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
def configure(form, request, instance=None, edit=True):
super(BookForm, self).configure(request, instance, edit)
self.remove_field('recommended')
self.update_sections()
The BaseFormMixin.configure()
method is called before a form is being
validated and can be used to dynamically change the form. Typically the
configure method is used to:
- Setup the queryset for model choice fields
- Remove or add form fields
- Making form fields required or not required
The given instance
argument is representing the model instance that is
being edited or created via the form. The edit
property is True
if a
model instance is being edited rather than created.
Various helper methods are available for forms, in particular
BaseFormMixin.remove_field()
for removing form fields and
BaseFormMixin.update_sections()
for re-arranging form sections, in
particular after form fields have been removed.
See also
Please refer to BaseFormMixin
for more information about form
utility methods.
Form Field Validation¶
Cubane provides the helper method FormBaseMixin.field_error()
when
raising form validation errors for a particular form field - in particular when
validating multiple form fields at once.
Usually, a ValidationError
exception is raised within a clean method that is
specific to a particular form field. However, when validating form data by
overriding the generic clean method of the form then ValidationError cannot be
raised. Instead, the FormBaseMixin.field_error()
can be used to trigger a
form field-specific validation error message:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
def clean(self):
d = super(BookForm, self).clean()
recommended = d.get('recommended')
rrp = d.get('rrp')
if recommended and not rrp:
self.field_error('rrp', 'This field is required for recommended books.')
return d
Form Browse¶
When referring to other entities in the form of ForeignKey, then the corresponding form field is usually a drop-down field listing all related model instances to choose from.
In order to integrate this better into Cubane, such ModelChoiceField can be extended to use a widget that allows browsing for model instances by attaching a browse button next to the drop-down field.
The BrowseSelect
widget can be used in the following way:
from cubane.forms import BaseModelForm
from cubane.backend.forms import BrowseSelect
from models import Book, Author
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
widgets = {
'author': BrowseSelect(model=Author)
}
For image-based models that are derived from Media
, the alternative
widget BrowseSelectThumbnail
can provide a better experience since it
renders a thumbnail preview of the image that is being selected:
from cubane.forms import BaseModelForm
from cubane.backend.forms import BrowseSelectThumbnail
from cubane.media.models import Media
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
widgets = {
'cover_image': BrowseSelectThumbnail(model=Media)
}
See also
In addition to those widgets, a form field implementation is also provided
via BrowseField
and BrowseThumbnailField
.
Form Visibility Rules¶
Form visibility rules allow for conditional-based control of the visibility of one or more form fields. For example, Only if a certain tick box is ticked, additional fields may be presented and required.
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
visibility = [
FormVisibility(field='recommended', value=True, visible=[
'rrp',
'recommended_since'
], required=[
'rrp'
]),
]
In this example, the fields rrp
and recommended_since
are both only
visible if the field recommended
is True
(e.g. checked). Assuming that
the field rrp
is not mandatory by default, it is required in the case that
the field recommended
is True
.
Form visibility can also be nested and linked like in the following example:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
visibility = [
FormVisibility(field='recommended', value=True, visible=[
'rrp',
'recommended_since'
], required=[
'rrp'
]),
FormVisibility(field='rrp', compare='>', value=0, visible=[
'recommended_since'
])
]
Here, the field recommended_since
is only visible if the field
recommended
is True
and the field rrp
is greater than 0
.
Form visibility can also have multiple predicates, in which case the
constructor of FormVisibility
takes a list of
FormVisibilityPredicate
instances:
from cubane.forms import BaseModelForm
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
visibility = [
FormVisibility([
FormVisibilityPredicate(field='recommended', value=True),
FormVisibilityPredicate(field='price', compare='>', value=0)
], visible=[
'rrp'
])
]
In this example, the field rrp
is only visible if the field recommended
is True
and the field price
is greater than 0
.
Form Blueprint Rules¶
A blueprint is based around the idea of auto-completing parts of a form by choosing from a list of possible options. For example, an invoice system for a bookstore may allow us to create line items by choosing a book. Form fields such as description, tax and price are then pre-populated based on data that is associated with the ebook record that has been chosen.
A blueprint does exactly that and is only applicable for related model instances, usually represented as foreign keys in the model and model choice fields in the form.
from cubane.forms import BaseModelForm
from cubane.backend.forms import BrowseSelect
from models import Book, LineItem
class LineItemForm(BaseModelForm):
class Meta:
model = LineItem
fields = '__all__'
widgets = {
'book': BrowseSelect(model=Book)
}
blueprints = {
'book': [
'description',
'tax',
'price'
]
}
We assume that the model LineItem
has a foreign key field to the Book
model via the property book
. Therefore the corresponding model form will
have a model choice field form with the same name. We also provide a browse
field widget to make choosing a book conveniently.
The blueprint rule is based on the field book
. Whenever it changes, the
fields description
, tax
and price
from the chosen book instance are
fetched and then copied over to the corresponding form fields of the line item
form.
Multiple blueprint rules for multiple form fields can be expressed in a similar way. Also, one or multiple form fields can be pre-populated this way.
Sometimes, the form field of the related model instance is not matching up the field of the hosting form perfectly, in this case, an actual assignment can be described in the following way:
from cubane.forms import BaseModelForm
from cubane.backend.forms import BrowseSelect
from models import Book, LineItem
class LineItemForm(BaseModelForm):
class Meta:
model = LineItem
fields = '__all__'
widgets = {
'book': BrowseSelect(model=Book)
}
blueprints = {
'book': [
'item_description = description',
'item_tax = tax',
'item_price = price'
]
}
Here we assume that all relevant fields of the LineItem
model are prefixed
with item_
, which is why we expressed the blueprint rules as assignments.
Form Limit Rules¶
Form limitation rules are designed to give interactive feedback about form
field limitations as the user is typing. For example, we assume that a form
field has a recommended max. character length of 120
characters, then a
form limitation rule can help to present this information to users:
from cubane.forms import BaseModelForm, FormInputLimit
from models import Book
class BookForm(BaseModelForm):
class Meta:
model = Book
fields = '__all__'
limits = {
'title': FormInputLimit(120)
}
A FormInputLimit
describes a max. character limit for a form field -
in this case for the field title
.