Create Django Models for the Blog API
===========================================
Before jumping into coding, it is a good idea to create some design and document it.
The minimum required documentation for our project is a database diagram for our Blog API project.
There are many tools on the market for creating diagrams, but I believe the best way is to use diagram as code tools.
`Mermaid `_ provides very good support for creating diagrams using code-like description.
Mermaid also provides `Mermaid Live Editor `_ - an online tool for creating diagrams.
1. Setup Sphinx for Mermaid
2. Create E-R diagram for our Blog API
Pre-requisites
++++++++++++++++++++
Requires the :doc:`custom-django-user-model` guide to be completed.
Guide code:
+++++++++++++++++
- `create-django-models `_ branch in the GitHub `repository `_.
Setup Sphinx for Mermaid
+++++++++++++++++++++++++++
Install Sphinx extension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add ``sphinxcontrib-mermaid`` to the documentation dependencies in :file:`docs/requirements.txt`:
.. code-block:: text
recommonmark
sphinx
sphinx-autobuild
sphinxcontrib-mermaid
Update the project dependencies installation:
.. code-block:: bash
pip install -U -r requirements.txt
Configure Sphinx to use the extension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add the ``sphinxcontrib.mermaid`` extension to the list of extensions in the :file:`docs/conf.py` file:
.. code-block:: python
extensions = [
"recommonmark",
"sphinxcontrib.mermaid", # new
]
Create a simple diagram
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Open a Sphinx document and add following content:
.. code-block:: text
.. mermaid::
sequenceDiagram
participant Alice
participant Bob
Alice->John: Hello John, how are you?
loop Healthcheck
John->John: Fight against hypochondria
end
Note right of John: Rational thoughts
prevail...
John-->Alice: Great!
John->Bob: How about you?
Bob-->John: Jolly good!
After building the project documentation, you should be able to see:
.. image:: images/sample-mermaid-sequence-diagram.png
:alt: Sample Mermaid sequence diagram
:align: center
Configure ReadTheDocs for Mermaid
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order to be able to generate Mermaid diagrams in PDF files, you need to update the :file:`.readthedocs.yaml`:
.. code-block:: yaml
build:
os: ubuntu-22.04
tools:
python: "3.12"
nodejs: "19"
jobs:
post_install:
- npm install -g @mermaid-js/mermaid-cli
In your documentation directory add file puppeteer-config.json with contents:
.. code-block:: json
{
"args": ["--no-sandbox"]
}
In your documentation :file:`docs/conf.py` file, add:
.. code-block:: python
mermaid_params = ['-p', 'puppeteer-config.json']
Create E-R diagram for our Blog API
++++++++++++++++++++++++++++++++++++++++++++
To learn more about E-R diagrams with Mermaid, go to Mermaid documentation on `Entity Relationship Diagrams `_.
Instead of Mermaid, you could try using plant uml through the `PlantUML extension `_.
Here is the diagram I came with:
.. image:: images/blog-api-model-e-r-diagram.png
:alt: Blog API E-R Diagram
:align: center
.. code-block:: text
.. mermaid::
---
title: Blog API Model
---
erDiagram
Post ||--o{ Comment : "has"
Post }o--|| User : "created by"
Post }o--o{ Tag : has
Create Posts App
+++++++++++++++++++++++
.. code-block::
python manage.py startapp posts
.. code-block:: python
# django_project/settings.py
INSTALLED_APPS = [
# ..............
# Local
"accounts.apps.AccountsConfig",
"posts.apps.PostsConfig", # new
]
Create Posts Model
+++++++++++++++++++++++
.. code-block:: python
# posts/models.py
from django.conf import settings
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=50)
body = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
.. code-block:: bash
python manage.py makemigrations posts
python manage.py migrate
.. code-block:: python
# posts/admin.py
from django.contrib import admin
from .models import Post
admin.site.register(Post)
Practice with Posts
~~~~~~~~~~~~~~~~~~~~~~~~
Let's create a user:
.. code-block:: bash
python manage.py shell
>>> from accounts.models import CustomUser
>>> CustomUser.objects.create(username="ivan")
Let's add some posts:
.. code-block:: bash
python manage.py shell
>>> from accounts.models import CustomUser
>>> from posts.models import Post
>>> u = CustomUser.objects.get(username="ivan")
>>> Post.objects.create(title="Hello, World!", body="It's a lovely day, isn't it?", author=u)
>>> Post.objects.create(title="Second Post", body="This is my second post.", author=u)
Create Comments Model
+++++++++++++++++++++++++++
.. code-block:: python
# posts/models.py
# ....
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
body = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
label = truncate_with_elipsis(self.body, 50)
return f"{self.author.username}: {label}"
.. code-block:: bash
python manage.py makemigrations posts
python manage.py migrate
.. code-block:: python
# posts/admin.py
from django.contrib import admin
from .models import Comments, Post
admin.site.register(Post)
admin.site.register(Comment)
Practice with Comments
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
python manage.py shell
>>> from accounts.models import CustomUser
>>> from posts.models import Comment, Post
>>> u = CustomUser.objects.get(username="ivan")
>>> p = Post.objects.get(title="Second Post")
>>> Comment.objects.create(body="comment 1", author=u,post=p)
Create Tags Model
+++++++++++++++++++++++++++++++
.. code-block:: python
# posts/models.py
# ....
class Post(models.Model):
title = models.CharField(max_length=50)
body = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag', related_name='posts', through='PostTag') # new
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
# .......
class Tag(models.Model):
name = models.CharField(max_length=30, unique=True)
def __str__(self):
return self.name
class PostTag(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
class Meta:
unique_together = ('post', 'tag')
The relationship between Post and Tag could be specified also without link table. In this case Django will automatically create a link table:
.. code-block:: python
tags = models.ManyToManyField('Tag', related_name='posts')
.. code-block:: python
# posts/admin.py
from django.contrib import admin
from .models import Comments, Post
admin.site.register(Post)
admin.site.register(Comment)
admin.site.register(Tag) # new
admin.site.register(PostTag) # new
.. code-block:: bash
python manage.py makemigrations posts
python manage.py migrate
Practice with Tags
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
python manage.py shell
>>> from posts.models import Post, Tag
>>> t1 = Tag.objects.create(name="Tag 1")
>>> t2 = Tag.objects.create(name="Tag 2")
>>> p = Post.objects.get(title="Second Post")
>>> # Tags can be assigned by instance reference
>>> p.tags.set([t1, t2])
>>> p.tags.all()
, ]>
>>> # Tags can be assigned by ID
>>> p.tags.set([t1.id])
]>
>>> # Attempt to assign a tag multiple times is assigning it only once
>>> p.tags.set([t1, t1])
]>