# Fluent in Django: Get to know Django models better

## Intro

Django is an MTV framework. Instead of MVC (Model, Views, Controller), it uses Model, Template, and View.
The View is a Python function that takes a Web request and returns a Web response (in MVC, this would be a Controller).
But the heart of your application is the **Model**.

**Django model** is a class that subclasses `django.db.models.Model`. It contains the essential fields and behaviors of your data.

Usually, each model maps to a single database table and each attribute of the model represents a database field.

In the introductory Django tutorials, you usually create a `Model` with few basic fields, you mess with it in the `View` and show it in a template.

But `Model` is and can do so much more.
It is the single, definitive source of information about your data. 
That means that all the logic about your data should be located in the `Model` (not in `View` as too often can be seen).


## Prerequisites

You will need the basic knowledge of Django. If you don't feel comfortable with Django yet, try [this beginner-friendly tutorial](https://girlthatlovestocode.com/fluent-in-django-first-steps).

## Initial setup

Start with setting up a new Django project:
```sh
$ mkdir django_models
$ cd django_models
$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install Django
(venv)$ django-admin startproject django_models .
```

Create a new app:
```sh
(venv)$ django-admin startapp tutorial
```

and register it:
```python
# django_models/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'tutorial',
]
```

Now for the `Model`. We will be working on a `Model` for a marathon app. 
Our `Model` will be called `Runner` and at the beginning, it will only have 3 fields. As our business logic will progress, so will our `Model`.


```python
# tutorial/models.py
from django.db import models


class Runner(models.Model):
    name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
```

For now, each runner will only have 3 fields:
- `name` is basic `CharField` with max of 50 characters (`max_length` is mandatory parameter for a `CharField`)
- `last_name` is basic `CharField` with a max of 50 characters
- `email` is an `EmailField` - that's a `CharField` that checks that the value is a valid email address using [EmailValidator](https://docs.djangoproject.com/en/3.2/ref/validators/#emailvalidator).

Our model doesn't include any additional logic.

Create and run the migrations:

```sh
(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate
```

> If you got lost during the initial setup, I encourage you to go [back to basics](https://girlthatlovestocode.com/fluent-in-django-first-steps).

We won't be dealing with templates or views, so this concludes the basic setup.
However, we need to be able to see the data somewhere, so include the Runner model in the admin panel.

```python
# tutorial/admin.py

from django.contrib import admin
from .models import Runner


admin.site.register(Runner)
```

Create a superuser and run the server:
```sh
(venv)$ python manage.py createsuperuser
(venv)$ python manage.py runserver
```

## UUID

[Universally unique identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier)  is a 128-bit number, usually represented as 32 hexadecimal characters separated by four hyphens.
The probability that a UUID will be duplicated is not zero, but it is close enough to zero to be negligible.

ID that Django uses out-of-the-box is incremented sequentially. That means that the 5th registered user has an id 5 and the 6th one has id 6.
So, if I register and figure out that my id is 17, I know there were 16 people registered before me and I can try to get to their data by using their id. That makes your application very vulnerable.

That's where UUID comes in handy. UUID key is randomly generated, so it doesn't carry any information as of how many people are registered to the page or what their id might be.

Django has a special field, called [UUIDField](https://docs.djangoproject.com/en/3.2/ref/models/fields/) for storing UUIDs.

```python
# tutorial/models.py

import uuid
from django.db import models

class Runner(models.Model):
    name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
```
You added a new id field -- `UUIDField` and you made that field `PRIMARY_KEY` for the `Runner` table.
`UUID` can be built in [5 different ways](https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions), but you'll probably want a unique and randomly generated version - version 4.
You set it as a default with `default=uuid.uuid4`.
You also don't want anyone to change that field, so you've set `editable` to `False`.

Run the migrations:

```sh
(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate
```

Now login with your superuser and add a test runner in the admin and see how it looks on the list of objects - that weird number in the name of the object is a `UUID`. If you add another runner, the string will be different.

Before adding the uuid the name of the object looked like that:

![before_uuid.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620116910304/n5ME9-1jL.png)

And after adding the uuid, it looks like that:

![uuid.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620116791511/JSpB9sTBa.png)

> If you added any object before you've added the UUID, your database will fall apart. Before adding a new primary key, delete all the test object you added.
> 
> If you need to add a new primary key in production, things tend to get bloody - so don't do that. Think ahead and start with adding `UUID` at the beginning of the project.

## Choices

Imagine this amazing marathon runner that last year won the Berlin marathon. Will you squish him between someone who's there just for fun and someone who decided 6 months ago that he'll run the marathon? Of course, not.
Marathon start is divided into zones - the faster you are, the closer to the start line you are. If you don't have any previous results, you're in the 5th zone. And if all you do is eat, sleep and run, you're in the first zone.

It would be useful if we'd have a zone saved in our database for each of the runners. 
We could use an integer field, but then the user would be able to enter any number, even 100. We need to limit the entries to the 5 choices they have.

Since Django 3.0, there is a Choices class that extends [Python’s Enum types](https://docs.python.org/3/library/enum.html) with extra constraints and functionality.

> **Python Enumeration type**
> 
> ![enumerated_type.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620116819472/86ou5w1ap.png)
> 
> An enumeration is a set of symbolic names (members) bound to unique, constant values.
> 
> Enumeration can be iterated over. Members are immutable and can be compared.
> They are constants, so the names should be uppercase.

```python
# tutorial/models.py

import uuid
from django.db import models


class Runner(models.Model):

    # this is new:
    class Zone(models.IntegerChoices):
        ZONE_1 = 1, 'Less than 3.10'
        ZONE_2 = 2, 'Less than 3.25'
        ZONE_3 = 3, 'Less than 3.45'
        ZONE_4 = 4, 'Less than 4 hours'
        ZONE_5 = 5, 'More than 4 hours'

    name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    start_zone = models.PositiveSmallIntegerField(choices=Zone.choices, default=Zone.ZONE_5, help_text="What was your best time on the marathon in last 2 years?") # this is new
```

We created a Choice class (an enumeration) called `Zone` that has 5 members. 
The first part (eg. `ZONE_1`) is the name of the enumeration member. The second part (eg. `1`) is the actual value.
The third part (eg. `'Less than 3.10'`) is a human-readable name, label. This is what you'll see in the admin. 

The Choices class has two subclasses - `IntegerChoices` and `TextChoices`. Since we're only interested in the numbers of the zones, we used `IntegerChoices` (it doesn't matter that constant names and labels are string, you choose the type based on the values).

Now we need to connect those choices to a database field. Because we provided `IntegerChoices`, the field has to be one of the integer fields.
Because we know there'll always be only a handful of zone possibilities, we chose `PositiveSmallIntegerField`. 
We connected the Choice class to the field with `choices=Zone.choices`.
We selected the default value for the field and here you can see how we access one of the members (`Zone.ZONE_5`).
The new addition is also `help_text` - this will show either in form or in the admin to help the user know what data they need to enter.

Run the migrations:
```sh
(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate
```

You should be logged in from before, open the form to add another runner, and see what changed.

![start_zone_added.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620116856823/u5DQ0zZyk.png)

Here you can see the help text you provided and the human-readable labels from the `Zone` class.

## \__str__()

If you open http://127.0.0.1:8000/admin/tutorial/runner/, you'll see the list of the runners you've added (you should at least have one), but you have no idea about the runners. 

![before_str.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620116956545/96wLDu298.png)

You can just see an object with some id. If you'd want to edit someone, you'd need to open each of the objects and find the one with the right name and surname.
The method, that takes care of how the objects look when presented in a string (like on the list of runners) is the `__str__` method.

`__str__` is a Python method that returns a string representation of any object. This is what Django uses to display model instances as a plain string.

> Django documentation says:
> 
> "You’ll always want to define this method; the default isn’t very helpful at all."

Let's obey Django creators and change the representation of the object to something more readable:

```python
import uuid
from django.db import models


class Runner(models.Model):

    class Zone(models.IntegerChoices):
        ZONE_1 = 1, 'Less than 3.10'
        ZONE_2 = 2, 'Less than 3.25'
        ZONE_3 = 3, 'Less than 3.45'
        ZONE_4 = 4, 'Less than 4 hours'
        ZONE_5 = 5, 'More than 4 hours'

    name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    start_zone = models.PositiveSmallIntegerField(choices=Zone.choices, default=Zone.ZONE_5, help_text="What was your best time on the marathon in last 2 years?")

    # this is new:
    def __str__(self):
        return '%s %s' % (self.name, self.last_name)
```
Here you're using Python string formatting to create a string out of two variables - name and last name.

> You don't need to use only variables to create the object's string representation.
> If you want the runners presented in a last_name, name - zone form, you can do this:
> 
> ```python
> def __str__(self):
>   return '%s, %s - %s' % (self.last_name, self.name, self.start_zone)
> ```

Refresh the runners' admin page, and now it's way easier to navigate through all the runners:

![str.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620116969978/qfsrqotjB.png)

> **Why don't I have to run the migrations for this to work?**
> 
> Migrations are Django's way to propagate changes you make to your models to your database schema.
> So you need to run the migrations only when the changes you made, impact your database.
> Here you only changed the string representation of an object and that doesn't impact it, so there is no need to run the migration.



## Meta

As I mentioned at the beginning of this post, Django `Model` is more than just a class with a bunch of fields.
Any additional business logic has a place in the model. 

But anything that's not a field, is considered metadata and has a special place inside the model - inside the inner `Meta` class.

`Meta` class is optional, but inside it, you can change the ordering, set the verbose name, add permissions...

### verbose names

I used a simple name for the class, but maybe I'd like to use a more descriptive name for the users.

Also, if you noticed, in the Django admin, your class with a singular name (Runner) is transformed to plural (Runners) - what if the plural is different than just added "s" (mouse -> mice)?

You can set both, singular and plural human-readable names.

```python
import uuid
from django.db import models


class Runner(models.Model):

    class Zone(models.IntegerChoices):
        ZONE_1 = 1, 'Less than 3.10'
        ZONE_2 = 2, 'Less than 3.25'
        ZONE_3 = 3, 'Less than 3.45'
        ZONE_4 = 4, 'Less than 4 hours'
        ZONE_5 = 5, 'More than 4 hours'

    name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    start_zone = models.PositiveSmallIntegerField(choices=Zone.choices, default=Zone.ZONE_5, help_text="What was your best time on the marathon in last 2 years?")

    # this is new:
    class Meta:
        verbose_name = "Runner 42k"
        verbose_name_plural = "Runners 42k"

    def __str__(self):
        return '%s %s' % (self.name, self.last_name)
```

You've set the `verbose_name` and `verbose_name_plural` to something different in the `Meta` class.

If you refresh the admin, you'll see that both changed.

![verbose_name.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620116984046/ggQ_9gHd5.png)

### ordering

Inside class `Meta`, you can select the default ordering of the objects when the list of objects will be retained.

There's a great chance, that whatever you'll do with the Runner objects, you'll care in which zone they are.
So it would be a good idea to order them by the zone.

To be able to see that they are sorted by zone, do 2 things:
1. add 4 more runners from different zones in no particular order.
2. Change the `__str__` method, so it will also show the zone

```python
import uuid
from django.db import models


class Runner(models.Model):

    class Zone(models.IntegerChoices):
        ZONE_1 = 1, 'Less than 3.10'
        ZONE_2 = 2, 'Less than 3.25'
        ZONE_3 = 3, 'Less than 3.45'
        ZONE_4 = 4, 'Less than 4 hours'
        ZONE_5 = 5, 'More than 4 hours'

    name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    start_zone = models.PositiveSmallIntegerField(choices=Zone.choices, default=Zone.ZONE_5, help_text="What was your best time on the marathon in last 2 years?")

    class Meta:
        verbose_name = "Runner 42k"
        verbose_name_plural = "Runners 42k"
        ordering = ["start_zone"] # this is new

    def __str__(self):
        return '%s %s %s' % (self.name, self.last_name, self.start_zone) # we added start_zone to the string
```
Before the order was set:

![before_order.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620116999369/YYTg9sqs7.png)

After `ordering = ["start_zone"]` was added:

![after_order.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620117017908/CtFmwkYQT.png)

> It is possible to order by more than one criterion. You could sort it first by start_zone and then alphabetically by name. You could also sort it in reversed order, so the zone 5 runners are at the top (you add `-` inside the string before the name to reverse it).
> ```python
> class Meta:
>   ordering = ["-start_zone", "name"]
> ```
> 
> ![different_order.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620117034465/1aQTJUZ42.png)

### constraints


#### CheckConstraint

Because long runs for youngsters are advised against, the organizer permits only runners that will be of age this calendar year.
You have special [constraints classes](https://docs.djangoproject.com/en/3.2/ref/models/constraints/) for that purpose.

You need to add a constraint that won't allow people under 18 (or with their 18th birthday coming this year) to register.

```python
import uuid
import datetime

from django.db import models


class Runner(models.Model):

    class Zone(models.IntegerChoices):
        ZONE_1 = 1, 'Less than 3.10'
        ZONE_2 = 2, 'Less than 3.25'
        ZONE_3 = 3, 'Less than 3.45'
        ZONE_4 = 4, 'Less than 4 hours'
        ZONE_5 = 5, 'More than 4 hours'

    name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    start_zone = models.PositiveSmallIntegerField(choices=Zone.choices, default=Zone.ZONE_5, help_text="What was your best time on the marathon in last 2 years?")
    year_born = models.PositiveSmallIntegerField(default=1990) # new field added

    class Meta:
        verbose_name = "Runner 42k"
        verbose_name_plural = "Runners 42k"
        ordering = ["-start_zone", "name"]
        constraints = [ # constraints added
            models.CheckConstraint(check=models.Q(year_born__lte=datetime.date.today().year-18), name='will_be_of_age'),
        ]

    def __str__(self):
        return '%s %s %s' % (self.name, self.last_name, self.start_zone)
```

To be able to check if the birth year is valid, you need to add the field `year_born` to your `Runner` model.

Inside class `Meta`, you add constraints in a form of a list. Since you're **checking** if a person is old enough, you're using `CheckConstraint`.

`CheckConstraint` consist of two mandatory parts:
- check - A [Q object](https://docs.djangoproject.com/en/3.2/ref/models/querysets/#q-objects) or boolean expression that specifies the check you want the constraint to enforce.
- name - An unique name for the constraint

> `Q` encapsulates filters as objects that can then be combined logically (using `&` and `|`), thus making it possible to use conditions in database-related operations.

The check in our case checks if the year entered in the field `year_born` is less or equal(`year_born__lte=`) to today's year minus 18 years(`datetime.date.today().year-18`).

Run the migrations:
```sh
(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate
```

Try to add a runner with `year_born` 2010. You'll get an integrity error:

![age_constraint_error.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620117051678/QgNRHxzHx.png)


#### UniqueConstraint

Let's add another constraint. Because there have been some mixups in previous runs, the organizer wants to forbid 2 persons with the same name start in the same zone.

You need to add the constraint that will prevent adding another runner with the same name, surname, and start_zone.
A person with the same name can start in another zone.

```python
import uuid
import datetime

from django.db import models


class Runner(models.Model):

    class Zone(models.IntegerChoices):
        ZONE_1 = 1, 'Less than 3.10'
        ZONE_2 = 2, 'Less than 3.25'
        ZONE_3 = 3, 'Less than 3.45'
        ZONE_4 = 4, 'Less than 4 hours'
        ZONE_5 = 5, 'More than 4 hours'

    name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    start_zone = models.PositiveSmallIntegerField(choices=Zone.choices, default=Zone.ZONE_5, help_text="What was your best time on the marathon in last 2 years?")
    year_born = models.PositiveSmallIntegerField(default=1990)

    class Meta:
        verbose_name = "Runner 42k"
        verbose_name_plural = "Runners 42k"
        ordering = ["-start_zone", "name"]
        constraints = [
            models.CheckConstraint(check=models.Q(year_born__lte=datetime.date.today().year-18), name='will_be_of_age'),
            models.UniqueConstraint(fields=['name', 'last_name', 'start_zone'], name='unique_person') # new constraint added
        ]

    def __str__(self):
        return '%s %s %s' % (self.name, self.last_name, self.start_zone)
```

Since you want a set of fields to be *unique*, you're using `UniqueConstraint`. You have to specify which combination of fields you want to be unique in a list (in our case `fields=['name', 'last_name', 'start_zone']`), and as in the previous case, you have to set a unique name for the constraint.

Try to add another person with the same name, surname, and in the same zone.
This is what you'll get:

![unique_error.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620117065077/Xw2tkPn2m.png)

If you change any of those fields, the record will be added to DB without any problem.

> **Why do I get different errors?**
> 
> You might have noticed that in the case of a unique constraint, there was a notice inside Django admin, and in the case of age constraint, you've been redirected to an error page.
> 
> That's because, in `UniqueConstraints`, you were comparing fields that were already there, so the error happened prior to saving the record to the database. But in the case of `CheckConstraints`, you were dealing with the database (`Q()` represents an SQL condition).


## change the save method

There will be times when you'll want to overwrite the predefined save method. Maybe you'll want to add an additional field (eg. creation date), change the format (eg. string to date), or prevent a certain type of data.

If you want to start running in the start zone that is not the slowest (nr. 5 in our case), you have to have a previous record to prove that you can run so fast and you won't obstruct other runners.
We'll add a field for entering the previous record and in the custom save method check if the field has value.
If not, the runner will automatically be assigned to zone nr. 5, regardless of what they chose.

```python
import uuid
import datetime

from django.db import models


class Runner(models.Model):

    class Zone(models.IntegerChoices):
        ZONE_1 = 1, 'Less than 3.10'
        ZONE_2 = 2, 'Less than 3.25'
        ZONE_3 = 3, 'Less than 3.45'
        ZONE_4 = 4, 'Less than 4 hours'
        ZONE_5 = 5, 'More than 4 hours'

    name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    start_zone = models.PositiveSmallIntegerField(choices=Zone.choices, default=Zone.ZONE_5, help_text="What was your best time on the marathon in last 2 years?")
    year_born = models.PositiveSmallIntegerField(default=1990)
    best_run_time = models.DurationField(default=datetime.timedelta(hours=4)) # new field added

    class Meta:
        verbose_name = "Runner 42k"
        verbose_name_plural = "Runners 42k"
        ordering = ["-start_zone", "name"]
        constraints = [
            models.CheckConstraint(check=models.Q(year_born__lte=datetime.date.today().year-18), name='will_be_of_age'),
            models.UniqueConstraint(fields=['name', 'last_name', 'start_zone'], name='unique_person')
        ]

    def __str__(self):
        return '%s %s %s' % (self.name, self.last_name, self.start_zone)
    
    # this is new:
    def save(self, *args, **kwargs):

        if self.best_run_time >= datetime.timedelta(hours=4):
            self.start_zone = self.Zone.ZONE_5

        super(Runner, self).save(*args, **kwargs)
```

You added a new field, `best_run_time`. Here we're also introducing a new type of field - `DurationField`.

`DurationField` is a field for storing periods of time - modeled in Python by `timedelta`.
In Django admin, it's shown as an empty field, which is a little impractical, so we provided the default value, so you can edit it easily.
We've automatically set it to 4 hours with Python's `timedelta` class.

> You **can't** compare instances `DurationField` to instances of `DateTimeField` (the only exception is if using PostgreSQL). 


At the bottom, you've overwritten the `save` method. You check if the `best_run_time` is more than or equal to 4 hours (`datetime.timedelta(hours=4)`) and if it is, you automatically set the `start_zone` to zone 5 (`self.start_zone = self.Zone.ZONE_5`).

Run the migrations:
```sh
(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate
```

And try to add a runner with zone selected to something less than 5. Leave the `best_run_time` as it is (4 hours).
The runner will save without any problems, but if you check the added runner, you'll see that their start zone changed to "More than 4 hours".

Prior to saving:

![save_method_prior_save.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620117088559/VLf95nCwT.png)

After saving:

![save_after_save.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1620117100846/7xC4WNN4m.png)

As you can see, the start zone has changed. If Charlie's `best_run_time` would be less than 4 hours, his selected start zone would be left as he selected it.


## Conclusion

Django models are very powerful and they hide much more than you got to know here.

Nevertheless, you got to know a lot about Django models:
- less known fields
    - `EmailField`
    - `UUIDField`
    - `DurationField`
- how to add Choices field with the newest Django practice, using enumerators
- how to change the method for the string representation of the object
- how to work with the model's metadata:
    - verbose names
    - ordering
    - constraints
- how to overwrite the default `save()` method

Needless to say, some of the use cases are inappropriate for production. You can't prohibit a runner with the same name from running or automatically assign a runner who forgot to enter their best time to the slowest zone without notifying them.
However, those cases are real-life enough to give you a fair idea of what you might use them for in your real-life application.

<sub>Image by wal_172619 from Pixabay</sub>


