In my previous article on Model Relationships, I described how Django implements one-to-one, one-to-many and many-to-many model relationships using the OneToOneField
, ForeignKey
and ManyToManyField
. Today I'm going to document how to leverage the power of object-oriented programming when writing Django models and views.
In a Django app that plans an employee's tasks and meetings, much like a calendar app, we would like to be able to:
- provide a directory listing of employees
- schedule tasks for an employee
- schedule meetings for employees
- discover the pending tasks and meetings for the current day when an employee logs in
I'm going to highlight the five Django models that are involved in this app -- Person
, Bio
, Email
, Task
and Meeting
. Basically, a Person
has a one-to-one relationship with Bio
and Email
, one-to-many relationship with Task
and many-to-many relationship with Meeting
.
Let's revisit the MTV (Model-Template-View) paradigm that Django is based on. Recall that the model is the central component encapsulating data and behavior, the view defines which data from the model is to be presented to the user and the template defines how data from the view is presented to the user.
As you can see from the illustration, there is a pair of tightly-coupled entities: model-view and template-view. The model and view interact with each other; so do the template and view. But the template does not communicate with the model at all as we separate an application's user interface from the application logic. The Django view acts as an intermediary between the model and the template instead.
So, if we want to present an HTML page, person_detail.html, with information compiled from Person
, Email
, Bio
, Task
and Meeting
, we would need a Django view, such as PersonDetailView
, to collect all this information for us and pass it to the Django template described in person_detail.html.
There is a lot of information presented in the Django template. Let's examine each one.
- The name,
Raya
, is saved in aPerson
model instance. - The email,
[email protected]
is saved in anEmail
model instance. - The image and the text,
Project Manager Extraordinaire
, are saved in aBio
model instance. - The
Tasks Due Today
information is saved in one or moreTask
model instances. - The
Today's Meetings
information is saved in one or moreMeeting
model instances.
Let's explore what the Django view does to compile all this information.
Django provides two ways to implement views -- function or class. In this app, the Django view was implemented using a class, PersonDetailView
, that derives from Django's class-based generic view, DetailView
. Django provides generic views such as DetailView
and ListView
because they are common views for presenting detailed information or a directory listing. The main benefits of inheriting a class-based view is to reuse code whenever possible and focus on the specific needs of our application.
Earlier, I mentioned that the view and the template are tightly coupled. This means that the template does not interact with the model at all and uses whatever information it needs to display from the view itself. This information is captured in a Python dictionary of key-value pairs returned by the generic view's method, .get_context_data()
, also known as the context
. A sample context
may have the following information:
context
{
'object': <Person: Raya>,
'person': <Person: Raya>,
'view': <planner.views.PersonDetailView object at 0x0000000004A6B970>
}
where the object
and person
both represent an instance of the model Person
and the view
is the instance of PersonDetailView
inherited from DetailView
. A sample implementation of PersonDetailView
may look like this:
class PersonDetailView(DetailView):
template_name = "planner/person_detail.html" # This tells Django which template to use
model = Person # This tells Django which model to use
This simple implementation would be enough if all one needs to present is the details of the Person
model instance. However, we can extend the context
by adding more key-value pairs. To do this, we need to override the default implementation of .get_context_data()
. In this demo app, the person_detail.html template has additional information about tasks and meetings. So, here's my implementation of PersonDetailView
.
class PersonDetailView(DetailView):
template_name = "planner/person_detail.html" # This tells Django which template to use
model = Person # This tells Django which model to use
def get_context_data(self, **kwargs): # override the default implementation
context = super().get_context_data(**kwargs)
person = Person.objects.get(pk=self.kwargs['pk'])
context['meetings_today'] = person.meetings_today()
context['tasks_due_today'] = person.tasks_due_today()
# assert False # Uncomment this line to display the values of `context` on the browser
return context
Notice that I added two keys, meetings_today
and tasks_due_today
, to context
by assigning them the return values of person.meetings_today()
and person.tasks_due_today()
. These are custom methods that exhibit specific behavior of the Person
model in addition to many others. They basically return a Django QuerySet
of model instances that meet specific conditions.
Here is the implementation of these methods:
class Person(models.Model):
...
# return all tasks due today
def tasks_due_today(self):
return self.task_set.filter(deadline=date.today())
# return all meetings scheduled for today
def meetings_today(self):
return self.meeting_set.filter(participants__name=self.name).filter(date=date.today())
By encapsulating calls to the Django Model QuerySet API within the model as much as possible, we promote modularization and code reuse. Remember that adding methods to a Django model does not require any database migration as doing so doesn't affect the database schema at all.
Notice also the statement, assert False
, that is commented in the code sample above. This statement is useful for debugging and will display traceback information on your browser even if there is no error. You can obtain the context
information that is passed from the view to the template in the traceback.
For example:
context
{'meetings_today': <QuerySet [<Meeting: Manager Sync, Meeting for Managers Only, Wed, 2021-02-17, 09:30:00 with <QuerySet [<Person: Raya>, <Person: Kim>]>>, <Meeting: Onboarding, Onboarding with Raya and Kenya, Wed, 2021-02-17, 17:15:10 with <QuerySet [<Person: Raya>, <Person: Kenya>]>>]>,
'object': <Person: Raya>,
'person': <Person: Raya>,
'tasks_due_today': <QuerySet []>,
'view': <planner.views.PersonDetailView object at 0x0000000004A6B970>}
Notice that meetings_today
and tasks_due_today
have been added to the context
. They are each assigned a QuerySet
of Meeting
and Task
model instances respectively.
We can apply useful object-oriented techniques such as adding custom methods when implementing our Django models and inheriting our views from Django's class-based generic views. By leveraging our knowledge of object-oriented design and programming, we can save a lot of precious time from reusing and reducing the code we write and simplifying our code maintenance. If you have benefited from this article, kindly give positive feedback and share with others too. Thank you for reading!