An HTML Form is a group of one or more fields/widgets on a web page, which can be used to collect information from users for submission to a server. Forms are a flexible mechanism for collecting user input because there are suitable widgets for entering many different types of data, including text boxes, checkboxes, radio buttons, date pickers, etc.

Forms are also a relatively secure way of sharing data with the server, as they allow us to send data in POST requests with cross-site request forgery(CSRF) protection.

Working with forms can be complicated! Developers need to write HTML for the form, validate and properly sanitise entered data on the server (and possibly also in the browser), repost the form with error messages to inform users of any invalid fields, handle the data when it has successfully been submitted, and finally respond to the user in some way to indicate success.

Form fields¶

The Form class is the heart of Django’s form handling system. It specifies the fields in the form, their layout, display widgets, labels, initial values, valid values, and (once validated) the error messages associated with invalid fields. The class also provides methods for rendering itself in templates using predefined formats (tables, lists, etc.) or for getting the value of any element (enabling fine-grained manual rendering).

Declaring a Form

The declaration syntax for a Form is very similar to that for declaring a Model, and shares the same field types (and some similar parameters). This makes sense because in both cases we need to ensure that each field handles the right types of data, is constrained to valid data, and has a description for display/documentation.

Form data is stored in an application’s forms.py file,to create a Form, we import the forms library, derive from the Form class, and declare the form’s fields. A very basic email form class would be like this :

1
2
3
4
5
6
from django import forms

class EmailCommentsForm(forms.Form):
name = forms.CharField(max_length=50)
email = forms.EmailField(help_text="Enter a valid email address.")
comments = forms.CharField(required=False, widget=forms.Textarea)

Although the primary way you’ll use Field classes is in Form classes, you can also instantiate them and use them directly to get a better idea of how they work. Each Field instance has a clean() method, which takes a single argument and either raises a django.core.exceptions.ValidationError exception or returns the clean value:

1
2
3
4
5
6
7
8
from django import forms
f = forms.EmailField()
f.clean('foo@example.com')
# 'foo@example.com'
f.clean('invalid email address')
# Traceback (most recent call last):
# ...
# ValidationError: ['Enter a valid email address.']

Core field arguments

The arguments that are common to most fields are listed below :

  • required: If True, the field may not be left blank or given a None value. Fields are required by default, so you would set required=False to allow blank values in the form.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    from django import forms
    f = forms.CharField()
    f.clean('foo')
    # 'foo'
    f.clean('')
    f.clean(None)
    # Traceback (most recent call last):
    # ...
    # ValidationError: ['This field is required.']
    f.clean(' ')
    # ' '
    f.clean(True)
    # 'True'

    f2 = forms.CharField(required=False)
    f2.clean('')
    # ''
    f2.clean(None)
    # ''
  • label: The label to use when rendering the field in HTML. If a label is not specified, Django will generate one from the field name by capitalizing the first letter and replacing underscores with spaces .

    Here’s a full example Form that implements label for two of its fields. We’ve specified auto_id=False to simplify the output:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from django import forms
    class CommentForm(forms.Form):
    name = forms.CharField(label='Your name')
    url = forms.URLField(label='Your website', required=False)
    comment = forms.CharField()
    f = CommentForm(auto_id=False)
    print(f)
    # <tr><th>Your name:</th><td><input type="text" name="name" required></td></tr>
    # <tr><th>Your website:</th><td><input type="url" name="url"></td></tr>
    # <tr><th>Comment:</th><td><input type="text" name="comment" required></td></tr>
  • label_suffix: By default a colon is displayed after the label . This argument allows you to specify a different suffix containing other character.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class ContactForm(forms.Form):
    age = forms.IntegerField()
    nationality = forms.CharField()
    captcha_answer = forms.IntegerField(label='2 + 2', label_suffix=' =')
    f = ContactForm(label_suffix='?')
    print(f.as_p())
    # <p><label for="id_age">Age?</label> <input id="id_age" name="age" type="number" required></p>
    # <p><label for="id_nationality">Nationality?</label> <input id="id_nationality" name="nationality" type="text" required></p>
    # <p><label for="id_captcha_answer">2 + 2 =</label> <input id="id_captcha_answer" name="captcha_answer" type="number" required></p>

    Use the form separately in templates:

    1
    2
    3
    4
    5
    6
    7
    8
    {{ f.as_p }}

    {% for field in f %}
    <div>
    {{ field.errors }}
    {{ field.label_tag }} {{ field }}
    </div>
    {% endfor %}
  • initial: The initial value for the field when the form is displayed.

    The use-case for this is when you want to display an “empty” form in which a field is initialized to a particular value. For example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from django import forms
    class CommentForm(forms.Form):
    name = forms.CharField(initial='Your name')
    url = forms.URLField(initial='http://')
    comment = forms.CharField()
    f = CommentForm(auto_id=False)
    print(f)
    # <tr><th>Name:</th><td><input type="text" name="name" value="Your name" required></td></tr>
    # <tr><th>Url:</th><td><input type="url" name="url" value="http://" required></td></tr>
    # <tr><th>Comment:</th><td><input type="text" name="comment" required></td></tr>
    f.is_bound
    # False

    You may be thinking, why not just pass a dictionary of the initial values as data when displaying the form? Well, if you do that, you’ll trigger validation, and the HTML output will include any validation errors:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class CommentForm(forms.Form):
    name = forms.CharField()
    url = forms.URLField()
    comment = forms.CharField()
    default_data = {'name': 'Your name', 'url': 'http://'}
    f = CommentForm(default_data, auto_id=False)
    print(f)
    # <tr><th>Name:</th><td><input type="text" name="name" value="Your name" required></td></tr>
    # <tr><th>Url:</th><td><ul class="errorlist"><li>Enter a valid URL.</li></ul><input type="url" # name="url" value="http://" required></td></tr>
    # <tr><th>Comment:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input # type="text" name="comment" required></td></tr>
    f.is_bound
    # True

    This is why initial values are only displayed for unbound forms. For bound forms, the HTML output will use the bound data.

    A Form instance is either bound to a set of data, or unbound.

    If it’s bound to a set of data, it’s capable of validating that data and rendering the form as HTML with the data displayed in the HTML.
    If it’s unbound, it cannot do validation (because there’s no data to validate!), but it can still render the blank form as HTML.

    Also note that initial values are not used as “fallback” data in validation if a particular field’s value is not given. initial values are only intended for initial form display:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class CommentForm(forms.Form):
    name = forms.CharField(initial='Your name')
    url = forms.URLField(initial='http://')
    comment = forms.CharField()
    data = {'name': '', 'url': '', 'comment': 'Foo'}
    f = CommentForm(data)
    f.is_valid()
    # False
    ## The form does *not* fall back to using the initial values.

    Access the errors attribute to get a dictionary of error messages

    The form’s data will be validated the first time either you call is_valid() or access errors.

    1
    2
    3
    4
    5
    6
    f.errors
    # {'url': ['This field is required.'], 'name': ['This field is required.']}
    f.errors.as_data()
    # {'name': [ValidationError(['This field is required.'])], 'url': [ValidationError(['This field is required.'])], 'comment': [ValidationError(['This field is required.'])]}
    f.errors.as_json()
    # '{"name": [{"message": "This field is required.", "code": "required"}], "url": [{"message": "This field is required.", "code": "required"}], "comment": [{"message": "This field is required.", "code": "required"}]}'

    Instead of a constant, you can also pass any callable:

    1
    2
    3
    4
    5
    import datetime
    class DateForm(forms.Form):
    day = forms.DateField(initial=datetime.date.today)
    print(DateForm())
    # <tr><th>Day:</th><td><input type="text" name="day" value="12/23/2008" required><td></tr>

    The callable will be evaluated only when the unbound form is displayed, not when it is defined.

  • widget: The display widget to use.

    The widget argument lets you specify a Widget class to use when rendering this Field. See Widgets for more information.

  • help_text : Additional text that can be displayed in forms to explain how to use the field.

    Here’s a full example Form that implements help_text for two of its fields. We’ve specified auto_id=False to simplify the output:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    from django import forms
    class HelpTextContactForm(forms.Form):
    subject = forms.CharField(max_length=100, help_text='100 characters max.')
    message = forms.CharField()
    sender = forms.EmailField(help_text='A valid email address, please.')
    cc_myself = forms.BooleanField(required=False)
    f = HelpTextContactForm(auto_id=False)
    print(f.as_table())
    # <tr><th>Subject:</th><td><input type="text" name="subject" maxlength="100" required><br><span class="helptext">100 characters max.</span></td></tr>
    # <tr><th>Message:</th><td><input type="text" name="message" required></td></tr>
    # <tr><th>Sender:</th><td><input type="email" name="sender" required><br>A valid email address, please.</td></tr>
    # <tr><th>Cc myself:</th><td><input type="checkbox" name="cc_myself"></td></tr>
    print(f.as_ul())
    # <li>Subject: <input type="text" name="subject" maxlength="100" required> <span class="helptext">100 characters max.</span></li>
    # <li>Message: <input type="text" name="message" required></li>
    # <li>Sender: <input type="email" name="sender" required> A valid email address, please.</li>
    # <li>Cc myself: <input type="checkbox" name="cc_myself"></li>
    print(f.as_p())
    # <p>Subject: <input type="text" name="subject" maxlength="100" required> <span class="helptext">100 characters max.</span></p>
    # <p>Message: <input type="text" name="message" required></p>
    # <p>Sender: <input type="email" name="sender" required> A valid email address, please.</p>
    # <p>Cc myself: <input type="checkbox" name="cc_myself"></p>

    data = {'subject': 'hello',
    'message': 'Hi there',
    'sender': 'foo@example.com',
    'cc_myself': True}
    f = HelpTextContactForm(data)
    f.is_valid()
    # True
  • error_messages: A list of error messages for the field. You can override these with your own messages if needed.

    The error_messages argument lets you override the default messages that the field will raise. Pass in a dictionary with keys matching the error messages you want to override.

    And here is a custom error message:

    1
    2
    3
    4
    5
    name = forms.CharField(error_messages={'required': 'Please enter your name'})
    name.clean('')
    # Traceback (most recent call last):
    # ...
    # ValidationError: ['Please enter your name']
  • validators: A list of functions that will be called on the field when it is validated.

    See the validators documentation for more information.

  • localize: Enables the localization of form data input .

    See the format localization documentation for more information.

  • disabled: The field is displayed but its value cannot be edited if this is True. The default is False.

The has_changed() method is used to determine if the field value has changed from the initial value. Returns True or False.

See the Form.has_changed() documentation for more information.

Built-in Field classes

Naturally, the forms library comes with a set of Field classes that represent common validation needs. This section documents each built-in field.

For each field, we describe the default widget used if you don’t specify widget. We also specify the value returned when you provide an empty value.

BooleanField

  • Default widget: CheckboxInput

    • input_type: 'checkbox'
    • template_name: 'django/forms/widgets/checkbox.html'
    • Renders as: <input type="checkbox" ...>

    Takes one optional argument:

    • check_test

      A callable that takes the value of the CheckboxInput and returns True if the checkbox should be checked for that value.

  • Empty value: False

  • Normalizes to: A Python True or False value.

  • Validates that the value is True (e.g. the check box is checked) if the field has required=True.

  • Error message keys: required

If you want to include a boolean in your form that can be either True or False (e.g. a checked or unchecked checkbox), you must remember to pass in required=False when creating the BooleanField.

CharField

  • Default widget: TextInput
    • input_type: 'text'
    • template_name: 'django/forms/widgets/text.html'
    • Renders as: <input type="text" ...>
  • Empty value: Whatever you’ve given as empty_value.
  • Normalizes to: A string.
  • Uses MaxLengthValidator and MinLengthValidator if max_length and min_length are provided. Otherwise, all inputs are valid.
  • Error message keys: required, max_length, min_length

Has four optional arguments for validation:

  • max_length / min_length

    If provided, these arguments ensure that the string is at most or at least the given length.

  • strip

    If True , the value will be stripped of leading and trailing whitespace.

  • empty_value

    The value to use to represent “empty”. Defaults to an empty string.

ChoiceField

  • Default widget: Select
    • template_name: 'django/forms/widgets/select.html'
    • option_template_name: 'django/forms/widgets/select_option.html'
    • Renders as: <select><option ...>...</select>
  • Empty value: '' (an empty string)
  • Normalizes to: A string.
  • Validates that the given value exists in the list of choices.
  • Error message keys: required, invalid_choice

The invalid_choice error message may contain %(value)s, which will be replaced with the selected choice.

Also see TypedChoiceField, MultipleChoiceField, TypedMultipleChoiceField

DateField/DateTimeField/TimeField

  • Default widget: DateInput
    • input_type: 'text'
    • template_name: 'django/forms/widgets/date.html(datetime.html/time.html)'
    • Renders as: <input type="text" ...>
  • Empty value: None
  • Normalizes to: A Python datetime.date/datetime.datetime object.
  • Validates that the given value is a datetime format.
  • Error message keys: required, invalid

Takes one optional argument:

  • input_formats

    A list of formats used to attempt to convert a string to a valid datetime.date object.e.g. YYYY-MM-DD (2016-11-06), MM/DD/YYYY (02/26/2016), MM/DD/YY (10/25/16), and will be rendered using the default widget.

Also see SplitDateTimeField.

DecimalField

  • Default widget: NumberInputwhen Field.localize is False, else TextInput.
    • input_type: 'number'
    • template_name: 'django/forms/widgets/number.html'
    • Renders as: <input type="number" ...>
  • Empty value: None
  • Normalizes to: A Python decimal.
  • Validates that the given value is a decimal. Uses MaxValueValidator and MinValueValidator if max_value and min_value are provided. Leading and trailing whitespace is ignored.
  • Error message keys: required, invalid, max_value, min_value, max_digits, max_decimal_places, max_whole_digits

The max_value and min_value error messages may contain %(limit_value)s, which will be substituted by the appropriate limit. Similarly, the max_digits, max_decimal_places and max_whole_digits error messages may contain %(max)s.

Takes four optional arguments:

  • max_value/min_value

These control the range of values permitted in the field, and should be given as decimal.Decimal values.

  • max_digits

    The maximum number of digits (those before the decimal point plus those after the decimal point, with leading zeros stripped) permitted in the value.

  • decimal_places

    The maximum number of decimal places permitted.

Also see https://docs.djangoproject.com/en/3.1/ref/forms/widgets/#numberinput and https://docs.djangoproject.com/en/3.1/ref/forms/fields/#integerfield

DurationField

See https://docs.djangoproject.com/en/3.1/ref/forms/fields/#durationfield

EmailField

  • Default widget: EmailInput
    • input_type: 'email'
    • template_name: 'django/forms/widgets/email.html'
    • Renders as: <input type="email" ...>
  • Empty value: Whatever you’ve given as empty_value.
  • Normalizes to: A string.
  • Uses EmailValidator to validate that the given value is a valid email address, using a moderately complex regular expression.
  • Error message keys: required, invalid

Has three optional arguments max_length, min_length, and empty_value which work just as they do for CharField.

FileField

  • Default widget: ClearableFileInput
    • template_name: 'django/forms/widgets/clearable_file_input.html'
    • Renders as: <input type="file" ...> with an additional checkbox input to clear the field’s value, if the field is not required and has initial data.
  • Empty value: None
  • Normalizes to: An UploadedFile object that wraps the file content and file name into a single object.
  • Can validate that non-empty file data has been bound to the form.
  • Error message keys: required, invalid, missing, empty, max_length

Has two optional arguments for validation, max_length and allow_empty_file. If provided, these ensure that the file name is at most the given length, and that validation will succeed even if the file content is empty.

To learn more about the UploadedFile object, see the file uploads documentation.

When you use a FileField in a form, you must also remember to bind the file data to the form.

The max_length error refers to the length of the filename. In the error message for that key, %(max)d will be replaced with the maximum filename length and %(length)d will be replaced with the current filename length.

FilePathField

See https://docs.djangoproject.com/en/3.1/ref/forms/fields/#filepathfield

ImageField

  • Default widget: ClearableFileInput
  • Empty value: None
  • Normalizes to: An UploadedFile object that wraps the file content and file name into a single object.
  • Validates that file data has been bound to the form. Also uses FileExtensionValidator to validate that the file extension is supported by Pillow.
  • Error message keys: required, invalid, missing, empty, invalid_image

Using an ImageField requires that Pillow is installed with support for the image formats you use. If you encounter a corrupt image error when you upload an image, it usually means that Pillow doesn’t understand its format. To fix this, install the appropriate library and reinstall Pillow.

After the field has been cleaned and validated, the UploadedFile object will have an additional image attribute containing the Pillow Image instance used to check if the file was a valid image. Pillow closes the underlying file descriptor after verifying an image, so whilst non-image data attributes, such as format, height, and width, are available, methods that access the underlying image data, such as getdata() or getpixel(), cannot be used without reopening the file. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from PIL import Image
from django import forms
from django.core.files.uploadedfile import SimpleUploadedFile
class ImageForm(forms.Form):
img = forms.ImageField()
file_data = {'img': SimpleUploadedFile('test.png', <file data>)}
form = ImageForm({}, file_data)
## Pillow closes the underlying file descriptor.
form.is_valid()
# True
image_field = form.cleaned_data['img']
image_field.image
# <PIL.PngImagePlugin.PngImageFile image mode=RGBA size=191x287 at 0x7F5985045C18>
image_field.image.width
# 191
image_field.image.height
# 287
image_field.image.format
# 'PNG'
image_field.image.getdata()
# Raises AttributeError: 'NoneType' object has no attribute 'seek'.
image = Image.open(image_field)
image.getdata()
# <ImagingCore object at 0x7f5984f874b0>

Additionally, UploadedFile.content_type will be updated with the image’s content type if Pillow can determine it, otherwise it will be set to None.

JSONField

See https://docs.djangoproject.com/en/3.1/ref/forms/fields/#jsonfield

GenericIPAddressField

See https://docs.djangoproject.com/en/3.1/ref/forms/fields/#genericipaddressfield

NullBooleanField

  • Default widget: NullBooleanSelect

    • template_name: 'django/forms/widgets/select.html'
    • option_template_name: 'django/forms/widgets/select_option.html'

    Select widget with options ‘Unknown’, ‘Yes’ and ‘No’

  • Empty value: None

  • Normalizes to: A Python True, False or None value.

  • Validates nothing (i.e., it never raises a ValidationError).

NullBooleanField may be used with widgets such as Selector RadioSelectby providing the widget choices:

1
2
3
4
5
6
7
8
9
NullBooleanField(
widget=Select(
choices=[
('', 'Unknown'),
(True, 'Yes'),
(False, 'No'),
]
)
)

RegexField

  • Default widget: TextInput
  • Empty value: Whatever you’ve given as empty_value.
  • Normalizes to: A string.
  • Uses RegexValidator to validate that the given value matches a certain regular expression.
  • Error message keys: required, invalid

Takes one required argument:

  • regex: A regular expression specified either as a string or a compiled regular expression object.

Also takes max_length, min_length, strip, and empty_value which work just as they do for CharField.

  • strip:Defaults to False. If enabled, stripping will be applied before the regex validation.

SlugField

  • Default widget: TextInput
  • Empty value: Whatever you’ve given as empty_value.
  • Normalizes to: A string.
  • Uses validate_slug or validate_unicode_slug to validate that the given value contains only letters, numbers, underscores, and hyphens.
  • Error messages: required, invalid

This field is intended for use in representing a model SlugField in forms.

Takes two optional parameters:

  • allow_unicode: A boolean instructing the field to accept Unicode letters in addition to ASCII letters. Defaults to False.

  • empty_value: the value to use to represent “empty”. Defaults to an empty string.

URLField

  • Default widget: URLInput
    • input_type: 'url'
    • template_name: 'django/forms/widgets/url.html'
    • Renders as: <input type="url" ...>
  • Empty value: Whatever you’ve given as empty_value.
  • Normalizes to: A string.
  • Uses URLValidator to validate that the given value is a valid URL.
  • Error message keys: required, invalid

Has three optional arguments max_length, min_length, and empty_value which work just as they do for CharField.

UUIDField

See https://docs.djangoproject.com/en/3.1/ref/forms/fields/#uuidfield

ComboField

  • Default widget: TextInput
  • Empty value: '' (an empty string)
  • Normalizes to: A string.
  • Validates the given value against each of the fields specified as an argument to the ComboField.
  • Error message keys: required, invalid

Takes one extra required argument:

  • fields

    The list of fields that should be used to validate the field’s value (in the order in which they are provided).

    1
    2
    3
    4
    5
    6
    7
    8
    from django.forms import ComboField
    f = ComboField(fields=[CharField(max_length=20), EmailField()])
    f.clean('test@example.com')
    'test@example.com'
    f.clean('longemailaddress@example.com')
    # Traceback (most recent call last):
    # ...
    # ValidationError: ['Ensure this value has at most 20 characters (it has 28).']
1

MultiValueField

See https://docs.djangoproject.com/en/3.1/ref/forms/fields/#multivaluefield

ModelChoiceField

  • Default widget: Select
  • Empty value: None
  • Normalizes to: A model instance.
  • Validates that the given id exists in the queryset.
  • Error message keys: required, invalid_choice

Allows the selection of a single model object, suitable for representing a foreign key.

Note that the default widget for ModelChoiceField becomes impractical when the number of entries increases. You should avoid using it for more than 100 items.

A single argument is required:

  • queryset

    A QuerySet of model objects from which the choices for the field are derived and which is used to validate the user’s selection. It’s evaluated when the form is rendered.

ModelChoiceField also takes two optional arguments:

  • empty_label

    By default the <select> widget used by ModelChoiceField will have an empty choice at the top of the list. You can change the text of this label (which is "---------" by default) with the empty_label attribute, or you can disable the empty label entirely by setting empty_label to None:

    1
    2
    3
    4
    5
    # A custom empty label
    field1 = forms.ModelChoiceField(queryset=..., empty_label="(Nothing)")

    # No empty label
    field2 = forms.ModelChoiceField(queryset=..., empty_label=None)
1
2
3
4
5
6
7
8
9
10
11
12
13

Note that if a `ModelChoiceField` is required and has a default initial value, no empty choice is created (regardless of the value of `empty_label`).

- `to_field_name`

This optional argument is used to specify the field to use as the value of the choices in the field’s widget. Be sure it’s a unique field for the model, otherwise the selected value could match more than one object. By default it is set to `None`, in which case the primary key of each object will be used. For example:

```python
# No custom to_field_name
field1 = forms.ModelChoiceField(queryset=...)

# to_field_name provided
field2 = forms.ModelChoiceField(queryset=..., to_field_name="name")

would yield:

1
2
3
4
5
6
7
8
9
10
11
<select id="id_field1" name="field1">
<option value="obj1.pk">Object1</option>
<option value="obj2.pk">Object2</option>
...
</select>

<select id="id_field2" name="field2">
<option value="obj1.name">Object1</option>
<option value="obj2.name">Object2</option>
...
</select>

ModelChoiceField also has the attribute:

  • iterator

    The iterator class used to generate field choices from queryset. By default, ModelChoiceIterator.

The __str__() method of the model will be called to generate string representations of the objects for use in the field’s choices. To provide customized representations, subclass ModelChoiceField and override label_from_instance. This method will receive a model object and should return a string suitable for representing it. For example:

1
2
3
4
5
from django.forms import ModelChoiceField

class MyModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return "My Object #%i" % obj.id

Also see https://docs.djangoproject.com/en/3.1/ref/forms/fields/#modelmultiplechoicefield.

Using a Form and function view¶

Validation

Django provides numerous places where you can validate your data. The easiest way to validate a single field is to override the method clean_**<fieldname>**() for the field you want to check.

The example will use a function-based view and a Form class.

Update your forms.py file so it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import datetime

from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _

class ChangeDateForm(forms.Form):
new_date = forms.DateField(help_text="Enter a new date between now and 4 weeks (default 3).")

def clean_new_date(self):
data = self.cleaned_data['new_date']

# Check if a date is not in the past.
if data < datetime.date.today():
raise ValidationError(_('Invalid date - new date in past'))

# Check if a date is in the allowed range (+4 weeks from today).
if data > datetime.date.today() + datetime.timedelta(weeks=4):
raise ValidationError(_('Invalid date - new date more than 4 weeks ahead'))

# Remember to always return the cleaned data.
return data

There are two important things to note. The first is that we get our data using self.cleaned_data['new_date'] and that we return this data whether or not we change it at the end of the function. This step gets us the data “cleaned” and sanitized of potentially unsafe input using the default validators, and converted into the correct standard type for the data (in this case a Python datetime.datetime object).

The second point is that if a value falls outside our range we raise a ValidationError, specifying the error text that we want to display in the form if an invalid value is entered. The example above also wraps this text in one of Django’s translation functions ugettext_lazy() (imported as _()), which is good practice if you want to translate your site later.

View

For forms that use a POST request to submit information to the server, the most common pattern is for the view to test against the POST request type (if request.method == 'POST':) to identify form validation requests and GET (using an else condition) to identify the initial form creation request.

If you want to submit your data using a GET request then a typical approach for identifying whether this is the first or subsequent view invocation is to read the form data (e.g. to read a hidden value in the form).

By convention, we use the POSTrequest approach. The code fragment below shows the (very standard) pattern for this sort of function view.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from .forms import RatingModelForm
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse
import datetime

@login_required
def MovieRatingUpdate(request, pk):
# get the current `Rating` (),if this does not exist, the view will immediately exit and the page will display a "not found" error
r = get_object_or_404(Rating, pk=pk)

# If this is a POST request then process the Form data
if request.method == 'POST':

# Create a form instance and populate it with data from the request (binding):
form = RatingModelForm(request.POST)

# Check if the form is valid:
if form.is_valid():
# process the data in form.cleaned_data as required (here we just write it to the model due_back field)
r.rating_date = form.cleaned_data['rating_date']
r.rating_movie = form.cleaned_data['rating_movie']
r.rating_status = form.cleaned_data['rating_status']
r.rating_comment = form.cleaned_data['rating_comment']
r.save()

# redirect to a new URL:
return HttpResponseRedirect(reverse('movie:my-watched') )

# If this is a GET (or any other method) create the default form.
else:
form = RatingModelForm(instance=r)
# rating_date_initial = datetime.date.today()
# form = RatingModelForm(initial={'rating_date': rating_date_initial})

context = {
'form': form,
'r': r,
}

return render(request, 'movie/movie_ratings_edit.html', context)

First, we import our form (RatingModelForm) and a number of other useful objects/methods used in the body of the view function:

  • get_object_or_404(): Returns a specified object from a model based on its primary key value, and raises an Http404 exception (not found) if the record does not exist.
  • HttpResponseRedirect: This creates a redirect to a specified URL (HTTP status code 302).
  • reverse(): This generates a URL from a URL configuration name and a set of arguments. It is the Python equivalent of the url tag that we’ve been using in our templates.

Important: While you can also access the form data directly through the request (for example, request.POST['rating_date'] or request.GET['rating_date'] if using a GET request), this is NOT recommended. The cleaned data is sanitized, validated, and converted into Python-friendly types.

That’s everything needed for the form handling itself, but we still need to restrict access to the view to MovieRatingUpdate. We should probably create a new permission in Rating("can_edit"), but, to keep things simple here, we just use the @permission_required function decorator with our existing can_edit_ratingpermission.

The final view is therefore as shown below. Please copy this into the bottom of views.py.

1
2
3
4
5
6
7
8
import datetime

from django.contrib.auth.decorators import permission_required
...

@permission_required('movie.can_edit_rating')
def MovieRatingUpdate(request, pk):
...

The template

Create the template referenced in the view (book_renew_librarian.html) and copy the code below into it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{% extends 'movie/movie_base.html' %}
{% load movie_tags %}
{% load crispy_forms_tags %}
{% block sidebar %}
{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center align-items-center">
<table class="col-6">
...
</table>
<hr>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="" method="post" class="col-6">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" value="Submit" class="btn btn-primary">
</form>
{{ form.media }}
</div>
</div>
{% endblock %}

{% block js %}
{% endblock js %}

The form code is relatively simple and we use crispy_forms to beautify the forms .

First we declare the form tags, specifying where the form is to be submitted (action) and the method for submitting the data (in this case an “HTTP POST”) .

The {% csrf_token %} added just inside the form tags is part of Django’s cross-site forgery protection. Add the {% csrf_token %} to every Django template you create that uses POST to submit data. This will reduce the chance of forms being hijacked by malicious users.

https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)

1
2
<!-- Use novalidate attribute to break up the default Browser Validation-->
<form action="" novalidate>

It is also possible to have complete control over the rendering of each part of the form, by indexing its properties using dot notation. So, for example, we can access a number of separate items for our renewal_date field:

  • {{ form.rating_date}}: The whole field.
  • {{ form.rating_date.errors }}: The list of errors.
  • {{ form.rating_date.id_for_label }}: The id of the label.
  • {{ form.rating_date.help_text }}: The field help text.

ModelForms

Creating a Form class using the approach described above is very flexible, allowing you to create whatever sort of form page you like and associate it with any model or models.

However, if you just need a form to map the fields of a single model then your model will already define most of the information that you need in your form: fields, labels, help text, etc. Rather than recreating the model definitions in your form, it is easier to use the ModelForm helper class to create the form from your model.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class RatingModelForm(ModelForm):
def clean_rating_date(self):
data = self.cleaned_data['rating_date']

if data > datetime.date.today():
raise ValidationError(_('Invalid date - The date is in the future.'))

# Remember to always return the cleaned data.
return data

class Meta:
model = Rating
# you can include all fields using `fields = '__all__'`, or you can use `exclude` (instead of `fields`) to specify the fields *not* to include from the model
fields = ['rating_movie','rating_date','rating_status','rating_comment']
labels = {'rating_date': _('Movie watched date')}
help_texts = {'rating_date': _('Enter a valid Movie watched date (default today).')}

The rest of the information comes from the model field definitions (e.g. labels, widgets, help text, error messages).

If these aren’t quite right, then we can override them in our class Meta, specifying a dictionary containing the field to change and its new value.

Generic editing views¶

The form handling algorithm we used in our function view example above represents an extremely common pattern in form editing views. Django abstracts much of this “boilerplate” for you, by creating generic editing views for creating, editing, and deleting views based on models. Not only do these handle the “view” behavior, but they automatically create the form class (a ModelForm) for you from the model.

In this section we’re going to use generic editing views to create pages to add functionality to create, edit, and delete Director records — effectively providing a basic reimplementation of parts of the Admin site (this could be useful if you need to offer admin functionality in a more flexible way that can be provided by the admin site).

Views

Open the views file views.py and append the following code block to the bottom of it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy

class DirectorCreate(LoginRequiredMixin, CreateView):
model = Director
fields = '__all__'
initial = {'director_date_edited': datetime.datetime.now()}
template_name = 'movie/movie_director_form.html'
success_msg = 'director successfully created.'

class DirectorUpdate(LoginRequiredMixin, UpdateView):
model = Director
fields = '__all__'
template_name = 'movie/movie_director_form.html'

class DirectorDelete(LoginRequiredMixin, DeleteView):
model = Director
success_url = reverse_lazy('movie:directors')
template_name = 'movie/movie_director_del_confirm.html'

That seems not good for DirectorCreate, because we define the director_date_edited as required in model.py:

1
2
3
director_date_edited = models.DateTimeField('date edited',default=timezone.now())
## Change to ==>
director_date_edited = models.DateTimeField('date edited',blank = True, null = True)

Then, modify the DirectorCreate, thats it!

1
2
3
4
5
6
7
8
9
10
11
class DirectorCreate(LoginRequiredMixin, CreateView):
model = Director
fields = '__all__'
exclude =['director_date_edited']
template_name = 'movie/movie_director_form.html'
success_msg = 'director successfully created.'
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.director_date_edited = datetime.datetime.now()
self.object.save()
return super(DirectorCreate, self).form_valid(form)

As you can see, to create, update, or delete the views you need to derive from CreateView, UpdateView, and DeleteView (respectively) and then define the associated model.

You can specify an alternative redirect location by explicitly declaring parameter success_url (as done for the DirectorDelete class).

The DirectorDelete class doesn’t need to display any of the fields, so these don’t need to be specified. You do however need to specify the success_url, because there is no obvious default value for Django to use. In this case, we use the reverse_lazy() function to redirect to our directors list after an author has been deleted — reverse_lazy() is a lazily executed version of reverse(), used here because we’re providing a URL to a class-based view attribute.

Templates

The “create” and “update” views use the same template by default, which will be named after your model: model_name**_form.html** (you can change the suffix to something other than _form using the template_name_suffix field in your view, e.g. template_name_suffix = 'movie_director_form.html' or 'template_name = 'movie/movie_director_form.html')

Create the template file movie_director_form.html and copy in the text below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{% extends 'movie/movie_base.html' %}
{% load movie_tags %}
{% load crispy_forms_tags %}
{% block title %}
Directors
{% endblock %}
{% block content %}
<div class="container h-100">
<div class="row justify-content-center align-items-center h-100 ">
</form>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-row">
<div class="form-group col-md-6 mb-0">
{{ form.director_name|as_crispy_field }}
</div>
<div class="form-group col-md-6 mb-0">
{{ form.director_name2|as_crispy_field }}
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6 mb-0">
{{ form.director_name_CN|as_crispy_field }}
</div>
<div class="form-group col-md-6 mb-0">
{{ form.director_name_CN2|as_crispy_field }}
</div>
</div>
{{ form.director_picture|as_crispy_field }}
{{ form.director_biography|as_crispy_field }}
...
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
{% endblock %}

This is similar to our previous forms and renders the fields using a table. Note also how again we declare the {% csrf_token %} to ensure that our forms are resistant to CSRF attacks.

The “delete” view expects to find a template named with the format model_name**_confirm_delete.html** (again, you can change the suffix using template_name_suffix in your view).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{% extends 'movie/movie_base.html' %}
{% load i18n %}
{% load movie_tags %}

{% block title %}
{{ super }} Directors | Delete
{% endblock %}
{% block content %}

<h1>Delete Director</h1>

<p>Are you sure you want to delete the director: {{ director }} ?</p>

<form action="" method="POST" id='director-delete'>
{% csrf_token %}
<button class="btn btn-danger" type = "submit">Yes, delete.</button>
</form>

{% endblock %}

URL configurations

Open your URL configuration file and add the following configuration to the bottom of the file:

1
2
3
4
5
urlpatterns += [  
path('director/create/', views.DirectorCreate.as_view(), name='director_create'),
path('director/<int:pk>/update/', views.DirectorUpdate.as_view(), name='director_update'),
path('director/<int:pk>/delete/', views.DirectorDelete.as_view(), name='director_delete'),
]

There is nothing particularly new here! You can see that the views are classes, and must hence be called via .as_view(), and you should be able to recognize the URL patterns in each case. We must use pk as the name for our captured primary key value, as this is the parameter name expected by the view classes.

REFERENCES