DEV Community

Cover image for Django for Beginners #2 - The MTV Structure
Eric Hu
Eric Hu

Posted on • Edited on • Originally published at ericsdevblog.com

Django for Beginners #2 - The MTV Structure

Download source code here. ⬅️

Django is a web dev framework designed based on the MTV (Model-Template-View) structure. In this structure, the model is in charge of interacting with our database, each model should correspond to one database table. The template is the frontend part of the application, it is what the users are going to see. And finally, the view is the backend logic of the app, it is usually in charge of retrieving data from the database through the models, putting them in the corresponding view, and eventually returning the rendered template to the user. In this article, we will discuss how this architecture works in your Django application.

Let's start with the models

The model is one of the best features of Django. For other web frameworks, you need to create both a model and a migration file. The migration file is a schema for the database, which describes the structure (column names and column types) of the database. The model provides an interface that handles the data manipulation based on that schema.

But for Django, you only need a model, and the corresponding migration files can be generated with a simple command, saving you a lot of time.

Each app has one models.py file, and all the models related to the app should be defined inside. Each model corresponds to a migration file, which corresponds to a database table. To differentiate tables for different apps, a prefix will be automatically assigned to each table. For our blog app, the corresponding database table will have the prefix blog_.

Here is an example of a model:

blog/models.py

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
Enter fullscreen mode Exit fullscreen mode

You can then generate a migration file using the following command:

python manage.py makemigrations
Enter fullscreen mode Exit fullscreen mode

And the generated migration file should look like this:

blog/migrations/0001_initial.py

# Generated by Django 4.1.2 on 2022-10-19 23:13

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = []

    operations = [
        migrations.CreateModel(
            name="Person",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("first_name", models.CharField(max_length=30)),
                ("last_name", models.CharField(max_length=30)),
            ],
        ),
    ]

Enter fullscreen mode Exit fullscreen mode

The migration file is a schema that describes how the database table should look, you can use the following command to apply this schema:

python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

Your database (db.sqlite3) should look like this:

database blog person

As a beginner, you should never try to edit or delete the migration files, just let Django do everything for you unless you absolutely know what you are doing. In most cases, Django can detect the changes you've made in the models (even if you deleted something) and generate the migration files accordingly.

In our example, this model will create a database table person, and inside the table, there will be three columns, each named id, first_name, and last_name. The id column is created automatically, as seen in the migration file. The id column is, by default, used as the primary key for indexing.

CharField() is called a field type and it defines the type of the column. max_length is called a field option, and it specifies extra information about that column. You can find a reference of all field types and field options here.

Model field types and options

To save us some time, I will only introduce some most commonly used field types.

Field Type Description
BigAutoField Creates an integer column that automatically increments. Usually used for the id column.
BooleanField Creates a Boolean value column, with values True or False.
DateField and DateTimeField As their names suggest, adds dates and times.
FileField and ImageField Creates a column that stores the path, which points to the uploaded file or image.
IntegerField and BigIntegerField Integer has values from -2147483648 to 2147483647. Big integer has values from -9223372036854775808 to 9223372036854775807
SlugField Slug is usually a URL-friendly version of the title/name.
CharField and TextField CharField and TextField both create a column for storing strings, except TextField corresponds to a larger text box in the Django admin, which we'll talk about later.

And field options.

Field Option Description
blank Allows this field to have an empty entry.
choices Gives this field multiple choices, you'll see how this works after we get to Django Admin.
default Gives the field a default value.
unique Makes sure that every item in the column is unique. Usually used to slug and other fields that are supposed to have unique values.

Makes sure that every item in the column is unique. Usually used to slug and other fields that are supposed to have unique values.

Meta options

You can also add a Meta class inside the model class, which contains extra information about this model, such as database table name, ordering options, and human-readable singular and plural names.

class Category(models.Model):
    priority = models.IntegerField()

    class Meta:
        ordering = ["priority"]
        verbose_name_plural = "categories"
Enter fullscreen mode Exit fullscreen mode

Model methods

Model methods are functions defined inside the model class. These functions allow us to perform custom actions on the current instance of the model object. For example:

class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    birth_date = models.DateField()

    def baby_boomer_status(self):
        "Returns the person's baby-boomer status."
        import datetime
        if self.birth_date < datetime.date(1945, 8, 1):
            return "Pre-boomer"
        elif self.birth_date < datetime.date(1965, 1, 1):
            return "Baby boomer"
        else:
            return "Post-boomer"
Enter fullscreen mode Exit fullscreen mode

When the baby_boomer_status() method is called, Django will examine the person's birth date and return the person's baby-boomer status.

Objects, methods, and properties are very important concepts in programming languages, and you can read this article for more information.

Model inheritance

In most web applications, you'll need more than one model, and some of them will have common fields. In this case, you can create a parent model which contains the common fields, and make other models inherit from the parent.

class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True

class Student(CommonInfo):
    home_group = models.CharField(max_length=5)
Enter fullscreen mode Exit fullscreen mode

Notice that CommonInfo is marked as an abstract model, which means this model doesn't really correspond to an individual model, instead, it is used as a parent to other models.

To verify this, generate a new migration file:

blog/migrations/0002_student.py

python manage.py makemigrations
Enter fullscreen mode Exit fullscreen mode
# Generated by Django 4.1.2 on 2022-10-19 23:28

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ("blog", "0001_initial"),
    ]

    operations = [
        migrations.CreateModel(
            name="Student",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("name", models.CharField(max_length=100)),
                ("age", models.PositiveIntegerField()),
                ("home_group", models.CharField(max_length=5)),
            ],
            options={
                "abstract": False,
            },
        ),
    ]

Enter fullscreen mode Exit fullscreen mode

As you can see, only a Student table is created.

Database relations

So far, we've only talked about how to create individual tables, however, in most applications, these tables aren't entirely independent, and there are usually relations between different tables. For instance, you could have a category that has multiple posts, a post that belongs to a specific user, etc. So how can you describe such relations in Django?

There are primarily three types of database relations, one-to-one, many-to-one, and many-to-many.

One-to-one relation

The one-to-one relationship should be the easiest to understand. For example, each person could have one phone, and each phone could belong to one person. We can describe this relation in the models like this:

class Person(models.Model):
    name = models.CharField(max_length=100)

class Phone(models.Model):
    person = models.OneToOneField('Person', on_delete=models.CASCADE)
Enter fullscreen mode Exit fullscreen mode

The OneToOneField field type works just like any other field type, except it takes at least two arguments. The first argument is the name of the model with whom this model has a relationship. And the second argument is on_delete, which defines the action Django will take when data is deleted. This has more to do with SQL than Django, so we are not going into this topic in detail, but if you are interested, here are some available values for on_delete.

You can now generate and apply migrations for these models and see what happens. If you run into problems running these commands, simply delete the db.sqlite3 file and the migration files to start over.

python manage.py makemigrations
Enter fullscreen mode Exit fullscreen mode
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

one to one relation

As you can see, the OneToOneField created a person_id column inside the blog_phone table, and this column will store the id of the person that owns this phone.

Many-to-one relation

Each category can have many posts, and each post belongs to one category. This relation is referred to as a Many-to-one relation.

class Category(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    category = models.ForeignKey('Category', on_delete=models.CASCADE)
Enter fullscreen mode Exit fullscreen mode

ForeignKey will create a category_id column in the blog_post table, which stores the id of the category that this post belongs to.

Many-to-many relation

Many-to-many relation is slightly more complicated. For example, every article could have multiple tags, and each tag could have multiple articles.

class Tag(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    tags = models.ManyToManyField('Tag')
Enter fullscreen mode Exit fullscreen mode

Instead of creating a new column, this code will create a new table called post_tags, which contains two columns, post_id, and tag_id. This allows you to locate all the tags associated with a particular post, and vice versa.

To make things clearer, imagine we have a table like this:

post_id tag_id
1 1
2 1
3 2
1 2
2 3

For a post with id=1, there are two tags, each with id=1 and id=2. If we want to do things backward and find posts through a tag, we can see that for a tag with id=2, there are two posts, id=3 and id=1.

Now the view layer

The view layer is one of the most important components in a Django application, it is where we write all the backend logic. The most common thing a view is supposed to do is retrieve data from the database through the corresponding model, process the retrieved data, put them in the corresponding location in the template, and finally, render and return that template back to the user.

Of course, retrieving data is not the only thing we can do with a view function. In most web applications, there are four most basic operations we can do with data, create, read, update and delete them. These operations put together are often referred to as CRUD. We are going to investigate all of them in this tutorial.

We've already discussed models in the previous section, but we still don't know how to retrieve or store data using the model. Django offers us a very simple API to help us with that, which is called the QuerySet.

Suppose that this is your model:

blog/models.py

class Category(models.Model):
    name = models.CharField(max_length=100)


class Tag(models.Model):
    name = models.CharField(max_length=200)


class Post(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()
    pub_date = models.DateField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    tags = models.ManyToManyField(Tag)
Enter fullscreen mode Exit fullscreen mode

With the help of QuerySet, you can manipulate data through this model inside the view functions, which are located inside the blog/views.py file.

Creating and saving data

Let's say you are trying to create a new category, this is what you can do:

# import the Category model
from blog.models import Category

# create a new instance of Category
category = Category(name="New Category")

# save the newly created category to database
category.save()
Enter fullscreen mode Exit fullscreen mode

This should be very easy for you to understand if you are familiar with the concept of object-oriented programming. If not, here is an article on that subject.

In this example, we simply created a new instance of the Category object and used the save() method, which belongs to that object, to save the information to the database.

Now, what about relations? For example, there is a many-to-one relationship between category and post, which we defined using the ForeignKey() field. We assume there are enough records in the database.

from blog.models import Category, Post

# Post.objects.get(pk=1) is how we retrieve the post with pk=1,
# where pk stands for primary key, which is usually the id unless otherwise specified.
post = Post.objects.get(pk=1)

# retrieve the category with name="New Category"
new_category = Category.objects.get(name="New Category")

# assign new_category to the post's category field and save it
post.category = new_category
post.save()
Enter fullscreen mode Exit fullscreen mode

There is also a many-to-many relation between posts and tags.

from blog.models import Tag, Post

post1 = Post.objects.get(pk=1) # Retrieve post 1

tag1 = Tag.objects.get(pk=1) # Retrieve tag 1
tag2 = Tag.objects.get(pk=2) # Retrieve tag 2
tag3 = Tag.objects.get(pk=3) # Retrieve tag 3
tag4 = Tag.objects.get(pk=4) # Retrieve tag 4
tag5 = Tag.objects.get(pk=5) # Retrieve tag 5

post.tags.add(tag1, tag2, tag3, tag4, tag5) # Add tag 1-5 to post 1
Enter fullscreen mode Exit fullscreen mode

Retrieving data

Retrieving objects is slightly more complicated than what we've just seen. Imagine we have thousands of records in our database. How do we find one particular record, if we don't know the id? Or, what if we want a collection of records that fits particular criteria instead of one record?

QuerySet Methods

QuerySet methods allow you to retrieve data based on certain criteria. And they can be accessed using the attribute objects. We've already seen an example, get(), which is used to retrieve one particular record.

first_tag = Tag.objects.get(pk=1)
new_category = Category.objects.get(name="New Category")
Enter fullscreen mode Exit fullscreen mode

You can also retrieve all records using the all() method.

Post.objects.all()
Enter fullscreen mode Exit fullscreen mode

The all() method returns what we call a QuerySet, it is a collection of records. And you can further refine that collection by chaining a filter() or exclude() method.

Post.objects.all().filter(pub_date__year=2006)
Enter fullscreen mode Exit fullscreen mode

This will return all the posts that are published in the year 2006. And pub_date__year is called a field lookup, we'll discuss this topic in detail later.

Or we can exclude the posts that are published in the year 2006.

Post.objects.all().exclude(pub_date__year=2006)
Enter fullscreen mode Exit fullscreen mode

Besides get(), all(), filter() and exclude(), there are lots of other QuerySet methods just like them. We can't talk about all of them here, but if you are interested, here is a full list of all QuerySet methods.

Field Lookups

Field Lookups are the keyword arguments for methods get(), filter() and exclude(). If you are familiar with SQL clauses, they work just like the SQL WHERE clause. And they take the form fieldname__lookuptype=value. Notice that it is a double underscore in between.

Post.objects.all().filter(pub_date__lte='2006-01-01')
Enter fullscreen mode Exit fullscreen mode

In this example, pub_date is the field name, and lte is the lookup type, which means less than or equal to. This code will return all the posts where the pub_date is less than or equal to 2006-01-01.

Here is a list of all field lookups you can use.

Field lookups can also be used to find records that have a relationship with the current record. For example:

Post.objects.filter(category__name='Django')
Enter fullscreen mode Exit fullscreen mode

This line of code will return all posts that belong to the category whose name is "Django".

This works backward too. For instance, we can return all the categories that have at least one post whose title contains the word "Django".

Category.objects.filter(post__title__contains='Django')
Enter fullscreen mode Exit fullscreen mode

We can go across multiple relations as well.

Category.objects.filter(post__author__name='Admin')
Enter fullscreen mode Exit fullscreen mode

This will return all categories that own posts, which are published by the user Admin. In fact, you can chain as many relationships as you want.

Deleting Objects

The method we use to delete a record is conveniently named delete(). The following code will delete the post that has pk=1.

post = Post.objects.get(pk=1)
post.delete()
Enter fullscreen mode Exit fullscreen mode

We can also delete multiple records together.

Post.objects.filter(pub_date__year=2005).delete()
Enter fullscreen mode Exit fullscreen mode

This will delete all posts that are published in the year 2005.

However, what if the record we are deleting relates to another record? For example, here we are trying to delete a category with multiple posts.

category = Category.objects.get(pk=1)
category.delete()
Enter fullscreen mode Exit fullscreen mode

By default, Django emulates the behaviour of the SQL constraint ON DELETE CASCADE, which means all the posts that belong to this category also be deleted. If you wish to change that, you can change the on_delete option to something else. Here is a reference of all available options for on_delete.

The view function

So far, we've only seen some snippets showing what you can do inside a view function, but what does a complete view function look like? Well, here is an example. In Django, all the views are defined inside the views.py file.

from django.shortcuts import render
from blog.models import Post

# Create your views here.
def my_view(request):
    posts = Post.objects.all()

    return render(request, 'blog/index.html', {
        'posts': posts,
    })
Enter fullscreen mode Exit fullscreen mode

There are two things you need to pay attention to in this example.

First, notice that this view function takes an input request. This variable request is an HttpRequest object, and it is automatically passed to the view from our URL dispatcher. If you are not familiar with HTTP, you should go through this article first.

The request contains a lot of information about the current HTTP request. For example, we can access the HTTP request method and write different codes for different methods.

if request.method == 'GET':
    do_something()
elif request.method == 'POST':
    do_something_else()
Enter fullscreen mode Exit fullscreen mode

Here is a list of all the information you can access from the request.

Second, notice that a shortcut called render() is imported, and it is then used to pass the variable posts to the template blog/index.html.

This is called a shortcut because, by default, you are supposed to load the template with the loader() method, render that template with the retrieved data, and return an HttpResponse object. Django simplified this process with the render() shortcut. To make life easier for you, I'm not going to talk about the complex way here, since we will not use it in this tutorial anyway.

Here is a list of all shortcut functions in Django.

The Django template system

Now, let's talk about templates. The template layer is the frontend part of a Django application, which is why the template files are all HTML code since they are what you see in the browser, but things are slightly more complicated than that. If it contains only HTML codes, the entire website would be static, and that is not what we want. So the template would have to tell the view function where to put the retrieved data.

Configurations

Before we start, there is something you need to change in the settings.py, you must tell Django where you are putting the template files.

First, create the templates directory. I choose to put it under the root directory of the project, but you can move it somewhere else if you want.

.
├── blog
├── db.sqlite3
├── djangoBlog
├── env
├── manage.py
├── mediafiles
├── staticfiles
└── templates
Enter fullscreen mode Exit fullscreen mode

Go to settings.py and find TEMPLATES.

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            'templates',
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
Enter fullscreen mode Exit fullscreen mode

Change the DIRS option, which points to the template folder. Now let's verify that this setting works. Create a new URL pattern that points to a test() view.

djangoBlog/urls.py

from django.urls import path
from blog import views

urlpatterns = [
    path('test/', views.test),
]
Enter fullscreen mode Exit fullscreen mode

Create the test() view:

blog/views.py

def test(request):
    return render(request, 'test.html')
Enter fullscreen mode Exit fullscreen mode

Go to the templates folder and create a test.html template:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Test Page</title>
  </head>
  <body>
    <p>This is a test page.</p>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Start the dev server and go to http://127.0.0.1:8000/.

Django template

The Django template language

Now let's discuss Django's template engine in detail. Recall that we can send data from the view to the template like this:

def test(request):
    return render(request, 'test.html', {
        'name': 'Jack'
    })
Enter fullscreen mode Exit fullscreen mode

The string 'Jack' is assigned to the variable name and passed to the template. And inside the template, we can display the name using double curly braces, {{ }}.

<p>Hello, {{ name }}</p>
Enter fullscreen mode Exit fullscreen mode

Refresh the browser, and you will see the output.

Display Data

However, in most cases, the data that is passed to the template is not a simple string. For example:

def test(request):
    post = Post.objects.get(pk=1)

    return render(request, 'test.html', {
        'post': post
    })
Enter fullscreen mode Exit fullscreen mode

In this case, the post variable is, in fact, a dictionary. You can access the items in that dictionary like this in the template:

{{ post.title }}
{{ post.content }}
{{ post.pub_date }}
Enter fullscreen mode Exit fullscreen mode

Filters

Filters transform the values of variables. For example, we have a variable django, with the value 'the web framework for perfectionists with deadlines'. If we put a title filter on this variable:

{{ django|title }}
Enter fullscreen mode Exit fullscreen mode

The template will be rendered into:

The Web Framework For Perfectionists With Deadlines
Enter fullscreen mode Exit fullscreen mode

Here is a full list of all built-in filters in Django.

Tags

Tags add programming language features such as flow control and loops to HTML code, which will save us a lot of time and resources, we don't have to write the same code over and over again. All tags are defined using {% %}.

For example, this is a for loop:

<ul>
    {% for athlete in athlete_list %}
        <li>{{ athlete.name }}</li>
    {% endfor %}
</ul>
Enter fullscreen mode Exit fullscreen mode

This is an if statement:

{% if somevar == "x" %}
  This appears if variable somevar equals the string "x"
{% endif %}
Enter fullscreen mode Exit fullscreen mode

And this is an if-else statement:

{% if athlete_list %}
    Number of athletes: {{ athlete_list|length }}
{% elif athlete_in_locker_room_list %}
    Athletes should be out of the locker room soon!
{% else %}
    No athletes.
{% endif %}
Enter fullscreen mode Exit fullscreen mode

Here is a full list of all built-in tags in Django. There are lots of other useful filters and tags in Django template system, we'll talk about them as we encounter specific problems in the future.

The inheritance system

The primary benefit of using the Django template is that you do not need to write the same code over and over again. For example, in a typical web application, there is usually a navigation bar and a footer, which will appear on every page. Repeating all of these code on every page will make it very difficult for maintenance. Django offers us a very easy way to solve this problem.

Let’s create a layout.html file in the templates folder. As the name suggests, this is the place where we define the layout of our template. To make this example easier to read, I skipped the code for the footer and navbar.

layout.html

<!DOCTYPE html>
<html>
<head>
    {% block meta %} {% endblock %}
    <!-- Import CSS here -->
</head>
<body>

<div class="container">
    <!-- Put the navbar here -->

    {% block content %} {% endblock %}

    <!-- Put the footer here -->
</div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Notice that in this file, we defined two blocks, meta and content, using the {% block ... %} tag. To use this layout, we define a home.html template.

home.html

{% extends 'layout.html' %}

{% block meta %}
    <title>Page Title</title>
    <meta charset="UTF-8">
    <meta name="description" content="Free Web tutorials">
    <meta name="keywords" content="HTML, CSS, JavaScript">
    <meta name="author" content="John Doe">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
{% endblock %}

{% block content %}
<p>This is the content section.</p>
    {% include 'vendor/sidebar.html' %}
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

When this template is called, Django will first find the layout.html file, and fill out the meta and content blocks with information on this home.html page.

Also, notice there is something else in this template. {% include 'vendor/sidebar.html' %} tells Django to look for the templates/vendor/sidebar.html and place it here.

sidebar.html

<p>This is the sidebar.</p>
Enter fullscreen mode Exit fullscreen mode

It is not exactly a sidebar, but we can use it to demonstrate this inheritance system works. Also make sure your view is correct.

from django.shortcuts import render

def home(request):
    return render(request, 'home.html')
Enter fullscreen mode Exit fullscreen mode

And make sure your URL dispatcher points to this view.

path('home/', views.home),
Enter fullscreen mode Exit fullscreen mode

Open your browser and go to http://127.0.0.1:8000/home, and you should see the following page:

template layout

If you liked this article, please also check out my other tutorials:

Top comments (0)