In my last article, Leveraging OO in Models and Views, I described how we can maximize object-oriented programming in implementing custom behavior in our Django models to hide model-specific QuerySet API and to take advantage of Django's class-based generic views, such as DetailView
to reuse code and minimize development time.
Recall that in Django's MTV (Model-Template-View) architecture, the model is the central component encapsulating data and behavior, the view defines which data from the model will be presented to the user and the template takes over how the data from the view will be presented in HTML. A typical application not only presents read-only data, but also lets a user create new data and update existing data. Creating and updating information is typically done in an HTML Form which is defined within the <form>...</form>
tag. Django facilitates data collection by letting the developer define a form inside a template. In this article, I'm going to document how to leverage one of Django's class-based generic-editing views to update a single model instance. By doing this, we can continue to reuse and reduce code.
We continue to use a demo Django app that lets a user plans tasks and meetings. For this article, we are focusing only on three models, Person
, Bio
, and Email
. Person
has a one-to-one relationship with Bio
and Email
.
The code that describes these models are as follows:
# Person has one-to-one relationship with Email and Bio
class Person(models.Model):
STATUS = [
('AWAY', 'I am away'),
('BUSY', 'I am busy'),
('MEETING', 'I am in a meeting'),
('IDLE', 'I am available')
]
name = models.CharField(max_length=20)
status = models.CharField(max_length=10, choices=STATUS, default='IDLE')
age = models.IntegerField(default=0)
...
# Bio has a one-to-one relationship with Person, but can be NULL
class Bio(models.Model):
bio = models.TextField()
image = models.URLField()
person = models.OneToOneField(Person, on_delete=models.CASCADE)
...
# Email has one-to-one relationship with Person
class Email(models.Model):
address = models.EmailField()
person = models.OneToOneField(Person, on_delete=models.CASCADE)
...
Selected fields of these three models can be viewed in PersonDetailView
, a subclass of DetailView
that belongs to Django's class-based generic display views.
Recall that the code for PersonDetailView
is:
from django.views.generic.detail import DetailView
from planner.models import Person, Bio, Email
class PersonDetailView(DetailView):
template_name = "planner/person_detail.html"
model = Person
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
person = get_object_or_404(Person,pk=self.kwargs['pk'])
context['meetings_today'] = person.meetings_today()
context['tasks_due_today'] = person.tasks_due_today()
return context
The PersonDetailView
passes the information stored in context
to the template, person_detail.html. The template extracts the information it needs from context
and formats them accordingly for display.
Focus on the red highlighted box in the screenshot above where three pencil icons, , appear next to three field instances, Raya
, Project Manager
and [email protected]
.
Clicking on the pencil icon next to Raya
will bring us to a template containing a form for editing a single model instance. This form is generated by a special view, PersonUpdateView
, corresponding to the Person
model instance for Raya
.
Notice that the three HTML input elements correspond to the Person
model fields of name
, status
and age
. This is made possible by defining PersonUpdateView
as a subclass of Django's UpdateView
, a generic editing view that displays a form for editing an existing model instance.
from django.views.generic.edit import UpdateView
from planner.models import Person, Bio, Email
class PersonUpdateView(UpdateView):
model = Person
fields = '__all__'
template_name_suffix = '_update_form'
In the code example above, we define the model
that this view corresponds to (Person
), select all the fields of the model that we want to display by assigning __all__
to fields
and provide a suffix name, _update_form
, to template_name_suffix
. By default, Django will use a template whose suffix is _form
and the full template name for updating the Person
model would be person_form.html. By providing an explicit suffix of _update_form
, we are telling Django to expect a template whose name is person_update_form.html instead.
The URL path corresponding to this view is defined in the app's urls.py file. For example:
urlpatterns = [
...
path('person/edit/<int:pk>/', PersonUpdateView.as_view(), name='person_update_form'),
]
Notice that pk
is the primary key associated with the Person
model instance. We chose to name our URL pattern to coincide with the template name, person_update_form
.
A sample template, person_update_form.html, may look like this:
{% extends "planner/base.html" %}
{% block title %}{{ title }}{% endblock title %}
{% block content %}
<h1>Information about <a href="{% url 'planner:person_detail' person.pk %}">{{ person.name }}</a></h1>
<form method="post">
{% csrf_token %}
<table>
{{ form }}
</table>
<br>
<button>Save</button>
</form>
{% endblock content %}
Clicking on the pencil icon next to Project Manager
will bring us to another form generated by BioUpdateView
, a subclass of Django's UpdateView
as well.
Notice that the two HTML input elements on the form correspond to the Bio
model fields of bio
and image
. Let's take a look at the content of BioUpdateView
.
class BioUpdateView(UpdateView):
model = Bio
fields = ['bio', 'image']
template_name_suffix = '_update_form'
We assign the model Bio
to model
and explicitly define the template name to have a suffix of _update_form
. But unlike PersonUpdateView
, we only want to display selected fields, bio
and image
. If we had assigned __all__
to fields
, the form returned by the view will also include a list of Person
model instances because there is a OneToOneField
(person
) in the Bio
model that is related to Person
. Since we only want to relate the bio
and image
fields to Raya
and not to another Person
model instance, we restrict the fields we want to display in fields
.
The URL path corresponding to this view is defined in the app's urls.py file. For example:
urlpatterns = [
...
path('bio/edit/<int:pk>/', BioUpdateView.as_view(), name='bio_update_form'),
]
The URL pattern in BioUpdateView
follows the same convention as the one in PersonUpdateView
.
Clicking on the pencil icon next to [email protected]
will lead us to a form generated by EmailUpdateView
, another subclass of Django's UpdateView
.
Notice that the only HTML input element on the form corresponds to the address
field of the Email
model. The content of EmailUpdateView
is as follows.
class EmailUpdateView(UpdateView):
model = Email
fields = ['address']
template_name_suffix = '_update_form'
Like BioUpdateView
, we explicitly name the field we want to display on the form. We assign address
(instead of __all__
) to fields
to hide the OneToOneField
(person
) in Email
which would display a select list of Person
model instances in the form. We define the model
to be Email
and override the template's default name to have a suffix of _update_form
.
The URL path corresponding to this view is defined in the app's urls.py file. For example:
urlpatterns = [
...
path('email/edit/<int:pk>/', EmailUpdateView.as_view(), name='email_update_form'),
]
The URL pattern in EmailUpdateView
follows the same convention as the one in PersonUpdateView
.
Django provides helpful class-based generic views for both displaying and editing models, such as DisplayView
and UpdateView
respectively. Using these views shorten the amount of code that we need to write and I would highly recommend using these views whenever possible.
In the next article, I will address a situation where we want to combine editing the model instances together on the same template instead of three separate ones. Hope to see you there and thanks for reading.