15.11.2024
Simple search lookups
Now that we have activated the PostgreSQL database, we can integrate PostgreSQL's full-text search into our project. This module has several search features.
# blog_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.sites",
"django.contrib.sitemaps",
"django.contrib.staticfiles",
"django.contrib.postgres",
]
Just by integrating the 'postgres' module you can create a simple search. I will show you an example in the python shell:
In [1]: from blog.models import Post
In [2]: Post.objects.filter(title__search='django')
Out[2]: <QuerySet [<Post: Django REST Framework (DRF) - permissions>, <Post: Django REST Framework (DRF) - ModelViewSets>, <Post: What is the difference between the template filter: `|linebreaks` and `|linebreaksbr` in Django Template?>]>
Building a search view
To integrate a full-text search into my blog project, I created a form with an attribute called 'query'. In our blog project, this form allows a user to search for blog articles by entering a word or longer text.
# bog/forms.py
from django import forms
class SearchForm(forms.Form):
query = forms.CharField()
In our view, we perform a query with the word or phrases entered by the user. We will use the SearchVector
class to query the body and title of the blog instances.
# blog/views.py
from django.contrib.postgres.search import SearchVector
from django.shortcuts import render
from blog.forms import SearchForm
from blog.models import Post
def post_search(request):
form = SearchForm()
query = None
results = []
if "query" in request.GET:
form = SearchForm(request.GET)
if form.is_valid():
query = form.cleaned_data["query"]
results = Post.published.annotate(
search=SearchVector("title", "body"),
).filter(search=query)
return render(
request, "post/search.html", {"form": form, "query": query, "results": results}
)
Legend:
- form = SearchForm()
: Instantiation of the SearchForm
.
- if "query" in request.GET:
: If the "query" attribute is inside the request object within the GET method, we check that the form is valid and store the results of the search in the results
variable.
- SearchVector()
: This class allows you to search multiple model attributes or even query within relationships such as ForeignKey
or ManyToMany
.
Now we need to create an HTML template to display the form on the website. The template contains several conditions. If no search has been performed or the page has just loaded, the template will display the initial form for the user to perform a search.
If the user has already searched for a word or phrase, it will either display the results of the search found, or it will display the message "No results found".
<!-- blog/templates/post/search.html -->
{% extends "base.html" %}
{% load blog_tags %}
{% block title %} Search {% endblock %}
{% block content %}
{% if query %}
<h1>Posts containing "{{ query }}"</h1>
<h3>
{% with results.count as total_results %}
Found {{ totoal_results }} result{{ total_results|pluralize }}
{% endwith %}
</h3>
{% for post in results %}
<h4>
<a href="{{ post.get_aboslute_url }}">
{{ post.title }}
</a>
</h4>
{{ post.body|markdown|truncatewords_html:12 }}
{% empty %}
<p>There are no results for your search.</p>
{% endfor %}
<hr>
<p><a href="{% url 'blog:post_search' %}">Search again</a></p>
{% else %}
<h1>Search for posts</h1>
<form method="get">
{{ form.as_p }}
<input type="submit" value="Search">
</form>
{% endif %}
{% endblock %}
Don't forget to create a URL pattern for the search view.
# blog/urls.py
from django.urls import path
from blog import views
app_name = "blog"
urlpatterns = [
name="post_feed"),
path("search/", views.post_search, name="post_search"),
]
Stemming and ranking results
# blog/views.py
from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank
from django.shortcuts import render
from blog.forms import SearchForm
from blog.models import Post
def post_search(request):
form = SearchForm()
query = None
results = []
if "query" in request.GET:
form = SearchForm(request.GET)
if form.is_valid():
query = form.cleaned_data["query"]
search_vector = SearchVector("title", "body")
search_query = SearchQuery(query)
results = (
Post.published.annotate(
search=search_vector, rank=SearchRank(search_vector, search_query)
)
.filter(search=query)
.order_by("-rank")
)
return render(
request, "post/search.html", {"form": form, "query": query, "results": results}
)
Stemming and removing stop words in different languages
# blog/views.py
from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank
from django.shortcuts import render
from blog.forms import SearchForm
from blog.models import Post
def post_search(request):
form = SearchForm()
query = None
results = []
if "query" in request.GET:
form = SearchForm(request.GET)
if form.is_valid():
query = form.cleaned_data["query"]
search_vector = SearchVector("title", "body", config="spanish")
search_query = SearchQuery(query, config="spanish")
results = (
Post.published.annotate(
search=search_vector, rank=SearchRank(search_vector, search_query)
)
.filter(search=query)
.order_by("-rank")
)
return render(
request, "post/search.html", {"form": form, "query": query, "results": results}
)
Weighting queries
# blog/views.py
from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank
from django.shortcuts import render
from blog.forms import SearchForm
from blog.models import Post
def post_search(request):
form = SearchForm()
query = None
results = []
if "query" in request.GET:
form = SearchForm(request.GET)
if form.is_valid():
query = form.cleaned_data["query"]
search_vector = SearchVector("title", weight="A") + SearchVector(
"body", weight="B"
)
search_query = SearchQuery(query)
results = (
Post.published.annotate(
search=search_vector, rank=SearchRank(search_vector, search_query)
)
.filter(rank__gte=0.3)
.order_by("-rank")
)
return render(
request, "post/search.html", {"form": form, "query": query, "results": results}
)
Full text search
https://docs.djangoproject.com/en/5.1/ref/contrib/postgres/search/
SearchVector
https://docs.djangoproject.com/en/5.1/ref/contrib/postgres/search/#searchvector
SearchVectorField
https://docs.djangoproject.com/en/5.1/ref/contrib/postgres/search/#performance
08.11.2024
Retrieving posts by similarity
I have previously implemented a tagging system (django-taggit) for blog posts, and there are a lot of interesting things you can do with tags. Tags allow you to categorise posts in a non-hierarchical way. Several tags will be shared by posts on similar topics. To retrieve similar posts to a specific post, follow these steps:
- Get all the tags for the current post.
- Get all posts tagged with any of these tags.
- Exclude the current post from this list to avoid recommending the same post.
- If there are two or more posts with the same number of tags, recommend the most recent post.
- Limit the query to the number of posts you want to recommend.
# blog/views.py
class PostDetailView(DetailView):
model = Post
template_name = "post/detail.html"
context_object_name = "post"
def get_object(self):
return get_object_or_404(
Post,
status=Post.Status.PUBLISHED,
slug=self.kwargs["slug"],
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["comments"] = self.object.comments.filter(active=True)
context["form"] = CommentForm()
context["similar_posts"] = self.get_similar_posts()
return context
def get_similar_posts(self):
post_tags_ids = self.object.tags.values_list("id", flat=True)
similar_posts = Post.published.filter(tags__in=post_tags_ids).exclude(
id=self.object.id
)
similar_posts = similar_posts.annotate(same_tags=Count("tags")).order_by(
"-same_tags", "-publish"
)[:4]
return similar_posts
Legend:
- post_tags_ids
: This variable stores the ids of all tags of the current post instance.
- similar_posts
: The first time the variable holds all published post instances, where the tags ids are inside the id list (post_tags_ids
) and exclude the current post instance.
- similar_posts
: The second time, this variable uses the Count aggregation function to generate a calculated field (same_tags
) that contains the number of tags shared by all the queried tags. And it orders the result by the number of shared tags in descending order and by publish date to display recent posts first for the posts with the same number of shared tags. And the result will be sliced to only 4 posts.
<!-- blog/templates/post/detail.html -->
<h2>Similar posts</h2>
{% for post in similar_posts %}
<p>
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
</p>
{% empty %}
There are no similar posts yet.
{% endfor %}
08.11.2024
Creating custom template tags and filter
The Django framework allows you to create your own template tags and filters. You have to integrate the custom template tag and filter file inside the Django application. You create a Python directory named: "templatetags" and create a file for your custom template tags and filter. Please keep in mind to stop the server and restart it after adding a custom template tag directory and file.
Create a custom template tags
A custom template tag can be a tag to execute a QuerySet or any server-side processing that you want to reuse across templates. There are two helper functions, which allow you to easily create template tags:
-
simple_tag
: It processes the given data and returns a string. -
inclusion_tag
: It processes the given data and returns a rendered template.
I created a simple template tag to retrieve the total posts that have been recently published on my blog.
# blog/templatetags/blog_tags.py
from django import template
from blog.models import Post
register = template.Library()
@register.simple_tag
def total_posts():
return Post.published.count()
Legend:
register = template.Library()
: Each module containing template tags must define a variable called register
to be a valid tag library. This variable is an instance of template.Library
, and it's used to register the application's template tags and filters.
@register.simple_tag
: This decorator registers this Python function as a custom template tag.
<!-- core/templates/base.html -->
{% load static blog_tags %}
<!DOCTYPE html>
<html lang="en">
<body>
<div id="sidebar">
<h2>Dori's Python Life in Words</h2>
<p>This is my blog. I've written {% total_posts %} posts so far.</p>
Legend:
- {% load blog_tags %}
: This template tag load your custom template tag file blog_tags
.
{% total_posts %}
: This template tag executes your custom template tag and displays the total number of posts.
Creating an inclusion template tag
With this custom template tag I want to display a list of posts in the sidebar of my blog. This template tag will render a template to display a list of links of the posts.
# blog/templatetags/blog_tags.py
from django import template
from blog.models import Post
register = template.Library()
@register.inclusion_tag("post/latest_posts.html")
def show_latest_posts(count=5):
latest_posts = Post.published.order_by("-publish")[:count]
return {"latest_posts": latest_posts}
Legend:
- inclusion_tag("post/latest_posts.html")
: The inclusion_tag()
method has the template as parameter which will be rendered by this custom tag.
- show_latest_posts(count=5)
: The show_latest_posts
function takes count as a parameter and the default value is 5. This function retrieves the 5 last published post instances.
We integrate the custom template tag in our base template:
<!-- core/templates/base.html -->
{% load static blog_tags %}
<h3>Latest posts</h3>
{% show_latest_posts 3 %}
And this custom template tag will display a list of 3 posts.
<!-- blog/templates/post/latest_posts.html-->
<ul>
{% for post in latest_posts %}
<li>
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
Implement custom template filter to support Markdown syntax
A custom template filter is a Python function that takes one or two parameters. Filters can be chained together. They return a value that can be displayed or used by another filter.
We create this custom template filter so that we can use Markdown syntax for the body of our blog posts. Markdown is a plain-text formatting syntax that's very easy to use and is intended to be converted to HTML.
I have installed Markdown with: python -m pip install markdown==3.6
.
# blog/templatetags/blog_tags.py
import markdown
from django import template
from django.utils.safestring import mark_safe
from blog.models import Post
register = template.Library()
@register.filter(name="markdown")
def markdown_format(text):
return mark_safe(markdown.markdown(text))
Legend:
- @register.filter(name="markdown")
: We register a filter with the filter()
method. Here we give the custom template filter the name 'markdown'.
- mark_safe()
: This function marks the result as safe HTML and to be rendered in the template. It is a Django function.
<!-- blog/templates/post/detail.html-->
{% extends "base.html" %}
{% load blog_tags %}
{% block content %}
<h1>{{ post.title }}</h1>
<p class="date">
Published {{ post.publish }} by {{ post.author }}
</p>
{{ post.body|markdown }}
<p>
{% endblock %}
08.11.2024
Adding a sitemap to the blog
A sitemap provides search engines with a structure of our website and helps them to index our website more effectively. It is a dynamically generated XML file.
The two Django frameworks (sites and sitemaps) need to be added to INSTALLED_APPS
.
#blog_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.sites",
"django.contrib.sitemaps",
"django.contrib.staticfiles",
# custom apps
"account",
"blog",
"core",
# packages/libraries:
"taggit",
]
# Sitemap framework
SITE_ID = 1
Legend:
- "django.contrib.sites": This allows you to associate objects with specific websites that run with your project.
- "django.contrib.sitemaps": This is the actual Django sitemap framework.
- SITE_ID
: This SITE_ID
is to use the django.contrib.sites
framework.
We need to perform migrations to implement the Django sitemap framework inside the database: python manage.py migrate
.
#blog/sitemaps.py
from django.contrib.sitemaps import Sitemap
from blog.models import Post
class PostSitemap(Sitemap):
changefreq = "weekly"
priority = 0.9
def items(self):
return Post.published.all()
def lastmod(self, obj):
return obj.updated
Legend:
- PostSitemap
: Is our custom sitemap, which we inherit from Sitemap class.
- changefreq
: This attribute indicates the frequency of changes to your post pages.
- priority
: This attribute indicates their importance in your website.
- items()
: This method defines the QuerySet of sitemap objects. It automatically calls the get_absolut_url()
method in the model.
- lastmod()
: This method retrieves each object from the items()
method and returns the last time the object was modified.
# blog_project/urls.py
from django.contrib import admin
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from blog.sitemaps import PostSitemap
sitemaps = {"posts": PostSitemap}
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls", namespace="blog")),
path(
"sitemap.xml",
sitemap,
{"sitemaps": sitemaps},
name="django.contrib.sitemaps.views.sitemap",
),
]
Legend:
- sitemaps
: This dictionary contains all custom sitemaps.
- path("sitemap.xml",...)
: This path defines a URL pattern that matches the sitemaps.xml
pattern and uses the sitemap view to display the XML output.
12.11.2024
Creating feeds for blog posts
Django has a built-in syndication feed framework that you can use to dynamically generate RSS or Atom feeds, similar to creating sitemaps using the site framework. A web feed is a data format (usually XML) that makes the most recently updated content available to the user.
# blog/feeds.py
import markdown
from django.contrib.syndication.views import Feed
from django.template.defaultfilters import truncatewords_html
from django.urls import reverse_lazy
from blog.models import Post
class LatestPostsFeed(Feed):
title = "Dori's Python Life in Words"
link = reverse_lazy("blog:post_list")
description = "New posts of my blog."
def items(self):
return Post.published.all()[:4]
def item_title(self, item):
return item.title
def item_description(self, item):
return truncatewords_html(markdown.markdown(item.body), 30)
def item_pubdate(self, item):
return item.publish
Legend:
- Feed
: The Feed class of the syndication feed framework.
- title
, link
and description
: These are attributes of the Feed class and correspond to the standard RSS elements.
- items()
: This method gets the objects to include in the feed, the last 4 published post instances.
- item_title()
, item_description()
and item_pubdate()
: These methods retrieve each object from the items()
method and return the title, 30 words of the description body and the publication date.
Now, let's create the a new URL pattern for the feed:
# blog/urls.py
from django.urls import path
from blog.feeds import LatestPostsFeed
app_name = "blog"
urlpatterns = [
path("latest/feed/", LatestPostsFeed(), name="post_feed"),
12./13.11.2024
Adding full-text search to the blog
For a blog application, a full-text search feature is important. A full-text search feature can perform complex searches, retrieve results by similarity, or by weighting terms based on how often they appear in the text or how important different fields are.
For the SQLite database, this feature is limited and Django does not support it out of the box. The PostgreSQL database has a full-text search feature built in. For this feature, it was time to integrate a PostgreSQL database into our project.
Installing PostgreSQL
To use a PostgreSQL database, I have pulled the docker hub image of the PostgreSQL database with: docker pull postgres:16.2
. After pulling the image I run a PostgreSQL container with:
docker run --name=blog_db -e POSTGRES_DB=blog -e POSTGRES_USER=blog -e POSTGRES_PASSWORD=super_secret -p 5432:5432 -d postgres:16.2
Legend
- docker run
: This Docker command will build the container and run it immediately.
- --name
: This flag is used to name the Docker container.
- -e
: This flag allows you to set environment variables, such as
- POSTGRES_DB
: This is an optional environment variable and can be used to set a name for the default database that is created when the image is first started. If not set, the POSTGRES_USER
value is used as the database name.
- POSTGRES_USER
: If POSTGRES_USER
is not set, the default POSTGRES_USER
is "postgres". This environment variable is also optional.
- POSTGRES_PASSWORD
: This is a required environment variable and sets the password for using the PostgreSQL database.
- -p
: This option binds the port to the container.
- -d
: This is a detach
mode, the container will run in the background and will not block a terminal window.
- postgres:16.2
: This is the name of the docker image with its tag. Where postgres
is the name of the Docker image and 16.2
is the tag.
To check if the container is running, you can use docker ps
. This command lists all running Docker containers with their current status and exposed ports. If you want to see all Docker containers, even the ones that have already stopped, you can use docker ps -a
.
The next step was to install the psycopg
PostgreSQL adapter for Python: python -m pip install psycopg==3.1.18
.
Dumping the existing data
All my previous data was stored in the SQLite database in my blog project. There is a way to dump all the data from the SQLite database, export that data, change the database in our blog project and import the data into the PostgreSQL database.
python manage.py dumpdata --indent=2 --output=blog_project.json --exclude=auth --exclude=contenttypes
Legend:
- dumpdata
: This command exports the data in the database to a file.
- --indent
: This flag specifies the number of indentation spaces to use in the output.
- --output
: This flag specifies a file to write the serialised data to.
- --exclude
: This flag excludes certain applications or models from being dumped.
Changing the database
The SQLite database configuration within the settings.py
file:
# blog_project/settings.py
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "blog-project.sqlite3",
}
}
This configuration has been changed to a PostgreSQL database configuration:
# blog_project/settings.py
from decouple import config
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": config("DB_NAME"),
"USER": config("DB_USER"),
"PASSWORD": config("DB_PASSWORD"),
"HOST": config("DB_HOST", 'localhost'),
"PORT": "5432",
}
}
The database configuration variables are stored in a local environment file called .env
.
# .env
# PostgreSQL
DB_NAME=blog
DB_USER=blog
DB_PASSWORD=super_secret
DB_HOST=localhost
To save the new database configuration, you need to migrate with python manage.py migrate
.
Loading the data into the new PostgreSQL database
We are going to load the data fixtures we created earlier into our new PostgreSQL database using:
python manage.py loaddata blog_project.json
Legend:
- loaddata
: This command searches for the specified file and loads the contents of the file (fixture) into the configured database.
Introduction
Project Overview:
This Django project is a comprehensive blog application designed to allow users to publish and interact with blog posts. The application serves as a platform to explore and implement Django's core features, while adhering to best practices for performance, maintainability, and user engagement. From initial development to deployment, the project showcases advanced functionalities and thoughtful optimizations, reflecting a thorough understanding of the Django framework.
Key Features:
-
Optimized Performance:
- Leveraged the
Meta
class for efficient model ordering and indexing. - Developed a custom Model Manager for enhanced query handling.
- Implemented pagination to streamline the display of blog posts.
- Leveraged the
-
SEO and URL Management:
- Designed SEO-friendly URLs using canonical tags to improve search engine visibility.
-
Email Integration:
- Configured Django’s email system and tested email functionality through the shell.
-
Enhanced Templating:
- Created custom template tags and filters to extend and customize the template layer.
-
Improved Indexing and Content Syndication:
- Added a sitemap to improve search engine indexing.
- Developed RSS feeds to enable content syndication for blog posts.
-
Robust Database Management:
- Used PostgreSQL for reliable and scalable database support.
- Incorporated Django fixtures for efficient data export and import.
-
Advanced Search Functionality:
- Implemented full-text search leveraging PostgreSQL capabilities for powerful and accurate content searches.
This project remains under continuous development, integrating new features and refinements to ensure a modern, user-friendly blogging experience.
07.10.2024
1. use AbstractUser instead of import User:
Subclassing AbstractUser
for a custom User
model in a Django project is considered good practice, as it provides flexibility and scalability for user management while maintaining compatibility with Django's built-in authentication system.
By subclassing AbstractUser
, you can add custom fields and methods to your User
model without losing any of Django's built-in authentication functionality. Even if you subclass AbstractUser
and pass
it as in my example, you'll still have the flexibility to customise your User
model in the future. You won't have to perform complex migrations that can lead to errors.
Advice:
Use the AbstractUser
subclass for your User
model, even if you might not need it later, but you never know. Avoid future problems.
# account/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
# blog_project/settings.py
# set the User model:
AUTH_USER_MODEL = "account.User"
- Link to Django documentation AbstractUser
07.10.2024
2. create a Post model:
For our blog application, we need a model to create Post
instances. The Post
model will have a title
, a slug
(which will improve the SEO friendliness of the blog), an author
, a body
where the content of the article is stored, a boolean publish
, when the blog post is created
, updated
and a status
.
# blog/models.py
from django.db import models
from django.urls import reverse
from django.utils import timezone
class Post(models.Model):
class Status(models.TextChoices):
DRAFT = "DF", "Draft"
PUBLISHED = "PB", "Published"
title = models.CharField(max_length=250)
slug = models.SlugField(max_length=250)
author = models.ForeignKey(
"account.User", on_delete=models.CASCADE, related_name="blog_posts"
)
body = models.TextField()
publish = models.DateTimeField(default=timezone.now)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=2, choices=Status, default=Status.DRAFT)
class Meta:
ordering = ["-publish"]
indexes = [
models.Index(fields=["publish"]),
]
def __str__(self):
return self.title
Legend:
Model class:
-
author
: is linked to ourUser
model with aForeignKey
relation. -
body
: is a normal DjangoTextField()
. It is a good idea to integrate a WYSIWYG rich text editor, likeCkEditor
, to improve the user experience. -
publish
: Here you can choose from several ways of saving the date and time of the Post instance when it is published. You can use the date and time of the location stored in your project withdefault=timezone.now
, or you can set the date and time to the location of the server withdb_default=Now()
(from django.db.models.functions import Now
). I have chosen to use the project date and time.
Meta class:
-
ordering
: theordering
attribute tells Django to sort the results by thepublish
field by default. The dash indicates a descending order of results. indexes
: in the DjangoMeta
class are used to define database indexes on one or more fields of a model to optimize query performance and ensure faster lookups.-
Links to the Django documentation:
Note for myself:
add CkEditor as Rich Text editor (https://medium.com/@yashnarsamiyev2/how-to-add-ckeditor-in-django-aa6de5a09862)
07.10.2024
3. Admin:
# blog/admin.py
from django.contrib import admin
from blog.models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "slug", "author", "publish", "status"]
list_filter = ["status", "created", "publish", "author"]
search_fields = ["title", "body"]
prepopulated_fields = {"slug": ("title",)}
raw_id_fields = ["author"]
date_hierarchy = "publish"
ordering = ["status", "publish"]
show_facets = admin.ShowFacets.ALWAYS
Note:
It is even possible to search for ForeignKey
relations with the search_fields
.
This is the syntax:
search_fields = ["foreign_key__related_fieldname"]
In our case I could use the FormeignKey
relation of user
to search for it, like:
search_fields = ["user__email"]
So, I would be able to search for a user
instance by email address.
Legend:
-
list_display
: By default, your admin panel will display the__str__()
representation of each object, and by using thelist_display
obtion, you can specify which attributes of the model are displayed in the admin panel. Note that aForeignKey
field will display the__str__()
representation of the related object,ManyToManyFields
are not supported and if you want to display aBooleanField
, Django will display a pretty 'yes' or 'no' or 'unknown' icon. -
list_filter
: When thelist_filter
option is set, you activate a filter option on the right-hand side of the admin panel. In my case, I used the list option to simply filter the filenames. search_fields
: Addingsearch_fields
adds a search bar at the top of the page to the admin panel. The fields used in the list ofsearch_fields
should be all kinds of text fields, such asCharField
orTextField
.-
Links to the Django documentation:
07.10.2024
4. creating a Model Manager:
A Model Manager is used when you don't just want to retrieve a single instance of a model with special criteria, but you want to fill an entire QuerySet with those special criteria. All instances of this model in the database are to be filtered using the special criteria.
There are several ways to create a Model Manager. You can create additional functions / methods for the Manager. Or you can create your own Model Manager and override the get_queryset()
method.
In our case, I have decided to override the get_queryset()
method in the custom Model Manager and now have two Manger available for filtering. We can use the Model Manager to filter all our Post instances by status "Published". So we are going to modify the initial QuerySet of our Model Manager.
# blog/models.py
from django.db import models
from django.urls import reverse
from django.utils import timezone
class PublishedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(status=Post.Status.PUBLISHED)
class Post(models.Model):
class Status(models.TextChoices):
DRAFT = "DF", "Draft"
PUBLISHED = "PB", "Published"
title = models.CharField(max_length=250)
slug = models.SlugField(max_length=250, unique_for_date="publish")
author = models.ForeignKey(
"account.User", on_delete=models.CASCADE, related_name="blog_posts"
)
body = models.TextField()
publish = models.DateTimeField(default=timezone.now)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=2, choices=Status, default=Status.DRAFT)
objects = models.Manager() # The default manager.
published = PublishedManager() # Our custom manager.
class Meta:
ordering = ["-publish"]
indexes = [
models.Index(fields=["publish"]),
]
def __str__(self):
return self.title
Legend:
-
get_queryset()
: The objects you want to retrieve are determined by theget_queryset()
method.
Now we have two managers. We can use them to retrieve, filter and manipulate our Post model instances. You can use Post.objects.all()
, this will give you a QuerySet with all the instances of the Post model. And if you use Post.published.all()
you will get all the instances of the Post model that have the status="Published"
.
- Link to Django documentation Model Manager
07.10.2024
5. create a canonical URL (get_absolute_url):
Canonicalisation in the context of URLs (a canonical URL) refers to the process of selecting a preferred URL when there are multiple URLs for the same or similar content.
This avoids duplicate content problems as it provides one "canonical" URL for similar or identical content on multiple URL's. If a site is accessible from multiple URLs, search engines may index them separately, which dilutes their SEO value.
In the Django model, we use the get_absolut_url()
method to create a canonical URL in Django.
# blog/models.py
from django.db import models
from django.urls import reverse
from django.utils import timezone
class PublishedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(status=Post.Status.PUBLISHED)
class Post(models.Model):
class Status(models.TextChoices):
DRAFT = "DF", "Draft"
PUBLISHED = "PB", "Published"
title = models.CharField(max_length=250)
slug = models.SlugField(max_length=250, unique_for_date="publish")
author = models.ForeignKey(
"account.User", on_delete=models.CASCADE, related_name="blog_posts"
)
body = models.TextField()
publish = models.DateTimeField(default=timezone.now)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=2, choices=Status, default=Status.DRAFT)
objects = models.Manager() # The default manager.
published = PublishedManager() # Our custom manager.
class Meta:
ordering = ["-publish"]
indexes = [
models.Index(fields=["publish"]),
]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse(
"blog:post_detail",
args=[self.slug],
)
Legend:
-
get_absolute_url()
: Theget_absolute_url()
method is used to define a canonical URL for a model instance in Django. By defining this method in a model, you can reference the URL of a specific object consistently throughout your templates and views. This approach centralizes the URL definition, making your code more organized and maintainable.
The get_absolute_url()
can be used as a reference to the detail template of a Post
instance.
# blog/templates/post/list.html
{% extends "base.html" %}
{% block title %} Django Blog {% endblock %}
{% block content %}
<h1>My Blog</h1>
{% for post in posts %}
<h2>
<a href="{{ post.get_absolute_url }}">
{{ post.title }}
</a>
</h2>
<p class="date">
Published {{ post.publish }} by {{ post.author }}
</p>
{{ post.body|truncatewords:30|linebreaks }}
{% endfor %}
{% endblock %}
Legend:
<a href="{{ post.get_absolute_url }}">
: The equivalent is<a href="{% url 'blog:post_detail' post.slug %}>
.Link to Django documentation
get_absolute_url()
07.10.2024
6. Environment Variables:
For developing a Django project, you don't need to think about environment variables, but if you want to deploy that project, you need to make sure you separate the environment variable.
For a more detailed description of the separation of environment variables, see this dev.to article
My way of separating the environment variables is to create multiple settings files. I usually have a settings.py
file, a settings-local.py
file (which is not pushed to GitHub or version control) and a settings-ci.py
file (these settings are for continuous integration with GitHub).
# blog_project/settings.py
import os
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DEBUG", "True").lower() == "false"
ALLOWED_HOSTS = (
os.environ.get("ALLOWED_HOSTS").split(",")
if os.environ.get("ALLOWED_HOSTS")
else []
)
# set the User
AUTH_USER_MODEL = "account.User"
# Email server configuration
EMAIL_HOST = "smtp.gmail.com"
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
EMAIL_PORT = 587
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL")
Legend:
-
os.environ.get()
: To develop my Django projects I use PyCharm. In PyCharm you can define your terminal environment variables in the PyCharm settings. I set theDJANGO_SETTINGS_MODULE=blog_project.settings-local
and every terminal I open will automatically use thesettings-local.py
file as a settings reference. So I useos.environ.get()
to get the variables defined in thesettings-local.py
file.
# blog_project/settings-local.py
from blog_project.settings import * # noqa
SECRET_KEY = "super_secret"
DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
EMAIL_HOST_USER = "name.lastname@gmail.com"
EMAIL_HOST_PASSWORD = "secret_password"
DEFAULT_FROM_EMAIL = "My Blog"
# only for development!!!
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
Legend:
-
SECRET_KEY
: ASECRET_KEY
, which should be set to a unique, unpredictable value and to a specific Django installation, is used for cryptographic signing. -
DEBUG
: is a boolean which turns on/off the debug mode of the Django project. -
ALLOWED_HOSTS
: is a list of strings that represent the host names or domain names that this Django site can serve. -
EMAIL_HOST_USER
: the user name to be used for the SMTP server defined inEMAIL_HOST
(defined insettings.py
file). -
EMAIL_HOST_PASSWORD
: the password to be used for the SMTP server defined inEMAIL_HOST
(defined insettings.py
file). When authenticating to the SMTP server, this setting is used in conjunction with EMAIL_HOST_USER. Django won't attempt authentication if either of these settings is empty. -
DEFAULT_FROM_EMAIL
: This is the default e-mail address for automated correspondence from the site manager. This address, which can be any format valid in the selected email sending protocol, is used in the "From:" header of outgoing emails. -
EMAIL_BACKEND
: selection of the e-mail backend but in our case only for development purpose. The console backend just writes the emails that would be sent to standard output instead of sending real emails.
- Link to Django documentation:
7. send a post via email:
09.10.2024
8. Creation of the Comment model:
Creating comments from other users is quite common for a blog application. We set relations to the Post model and to our User model. The User model is for authentication later in our process of creating this blog application.
create Comment model, explain why used user and post and why on_delete=models.SET_NULL
and on_delete=models.CASCADE
# blog/models.py
class Comment(models.Model):
body = models.TextField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True)
user = models.ForeignKey(
"account.User",
on_delete=models.SET_NULL,
null=True,
related_name="comment_users",
)
post = models.ForeignKey(
"blog.Post", on_delete=models.CASCADE, related_name="comments"
)
class Meta:
ordering = ["created"]
indexes = [models.Index(fields=["created"])]
@property
def get_full_name(self):
return f"{self.user.first_name} {self.user.last_name}"
def __str__(self):
return f"Comment by {self.get_full_name} on {self.post}"
Legend:
-
on_delete=models.SET_NULL
: To maintain data integrity, theon_delete
option determines how the database handles the deletion of referenced data. The on_delete behaviour controls what happens when the model (in our case the Comment instance) is deleted. -
on_delete=models.SET_NULL
: The optionSET_NULL
sets the ForeignKey to null, but we need to specifynull=True
in our model. When a User instance is deleted, the user field in the Comment instance is set toNULL
(i.e.None
) instead of deleting the comment itself. -
on_delete=models.CASCADE
: This option will delete the Comment instance(s) when the Post instance is deleted.
- Link to Django documentation:
We need to add the Comment model to the admin.py
file to display the Comment instances in the admin panel.
# blog/admin.py
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ["user", "post", "created", "active"]
list_filter = ["active", "created", "updated"]
search_fields = ["user__username", "user__email", "body"]
09.10.2024
9. create a comment ModelForm:
Django ModelForms is a helper class that allows you to create a Form class from an existing Django model.
A list of ModelForm.Meta class attributes:
-
model
: Specifies the model that the form is tied to (mandatory attribute for ModelForm). -
fields
orexclude
: Specify which fields to include or exclude from the form. -
formfield_callback
: A callable that customizes the form fields. -
widgets
: A dictionary to specify custom widgets for form fields. -
localized_fields
: A list of field names that should be localized. -
labels
: A dictionary of field labels. -
help_texts
: A dictionary of help texts for fields. -
error_messages
: A dictionary to specify custom error messages for fields. -
field_classes
: A dictionary to specify custom field classes for form fields.
A) The ModelForm:
# blog/forms.py
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ["user", "body"]
We have defined the model
for which we want to create a form. The Meta
class attribute model
is mandatory. And we define the two attribute names of the Comment
model: user
and body
. These two attributes will be used to create the form.
The ForeignKey
user is represented as a django.form.MultipleChoiceField
. You can select the user from a list of users in the form.
B) The ModelForm view:
Let's create the view. This view context contains the post that pk searched for, the form to create a comment, and the comment itself when submitted.
# blog/views.py
@require_POST
def post_comment(request, post_id):
post = get_object_or_404(Post, pk=post_id, status=Post.Status.PUBLISHED)
comment = None
form = CommentForm(data=request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.save()
return render(
request, "post/comment.html", {"post": post, "form": form, "comment": comment}
)
Legend:
-
@require_POST
: This decorator makes sure that the view will only accept requests from the POST method. -
comment = form.save(commit=False)
: Thesave()
method creates an instance of the model that the form is linked to and saves it to the database. If you call it usingcommit=False
, the model instance is created but not saved to the database. This allows us to modify the form object before finally saving it.
C) Create the redirect template comment.html
:
<!-- blog/templates/post/comment.html -->
{% extends "base.html" %}
{% block title %}Add a comment{% endblock %}
{% block content %}
{% if comment %}
<h2>Your comment has been added.</h2>
<p><a href="{{ post.get_absolute_url }}">Back to the post</a></p>
{% else %}
{% include "post/includes/comment_form.html" %}
{% endif %}
{% endblock %}
Legend:
-
{% include "post/includes/comment_form.html" %}
: This template tag loads a template and renders it using the current context of the template it is in.
D) Integrate the ModelForm and Comments in detail
template:
<!-- blog/template/post/detail.html -->
{% extends "base.html" %}
{% block title %} {{ post.title }} {% endblock %}
{% block content %}
<h1>{{ post.title }}</h1>
<p class="date">
Published {{ post.publish }} by {{ post.author }}
</p>
{{ post.body|linebreaks }}
<p>
<a href="{% url 'blog:post_share' post.id %}">Share this post.</a>
</p>
{% with comments.count as total_comments %}
<h2>
{{ total_comments }} comment{{ total_comments|pluralize }}
</h2>
{% endwith %}
{% for comment in comments %}
<div class="comment">
<p class="info">
Comment {{ forloop.counter }} by <strong>{{ comment.user.username }}</strong> {{ comment.created }}
</p>
{{ comment.body|linebreaks }}
</div>
{% empty %}
<p>There are no comments.</p>
{% endfor %}
{% include "post/includes/comment_form.html" %}
{% endblock %}
Legend:
-
{% with %}
: The{% with %}
template tag in Django is used to temporarily assign a value to a variable within a specific block of a template. You can reuse this value multiple times within the{% with %}
block. The{% with %}
template tag is useful when you want to avoid repetition, when you want to improve the readability of your template, and when you are working with complex values. -
{% empty %}
: The{% empty %}
template tag is an optional tag that can be used if the given array is empty or could not be found. -
|linebreaks
: The template filter|linebreaks
converts the entire text to a paragraph. If there is a single newline (\n
) in this plain text string, it converts the newline to an HTML linebreak (<br>
) tag. If you have two newline characters (\n\n
) in a plain text string, this template filter will create two paragraphs. -
{% include "post/includes/comment_form.html" %}
: This template tag loads a template and renders it using the current context of the template it is in.
E) Create the ModelForm template:
We have included the template for rendering the ModelForm in the detail HTML template, and now we need to create the template for displaying the ModelForm.
There are several ways to display the ModelForm:
E.1) Simple ModelForm rendering:
<!-- blog/templates/post/includes/comment_form.html -->
<h2>Add a comment</h2>
<form action="{% url 'blog:post_comment' post.id %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<p><input type="submit" value="Add comment"></p>
</form>
Legend:
-
{{ form.as_p }}
: With the built-in methodas_p
you display the ModelForm all in paragraphs.
E.2) Looping over the form fields:
<!-- blog/templates/post/includes/comment_form.html -->
<h2>Add a comment</h2>
<form action="{% url 'blog:post_comment' post.id %}" method="post">
{% csrf_token %}
{% for field in form %}
<div>
{{ field.errors }}
{{ field.lable_tag }} {{ field }}
<div>{{ field.help_text|safe }}</div>
</div>
{% endfor %}
</form>
Legend:
-
{{ field.errors }}
: Displays all containing validation errors corresponding to this field. -
{{ field.lable_tag }}
: Displays the lable of the field. -
{{ field }}
: Displays the Field instance from the form class. -
{{ field.help_text }}
: Displays the help texts corresponding to the field. -
{{ |safe }}
: Marks a string as not requiring any HTML escaping before output. If autoescaping is switched off, this filter has no effect.
E.3) Reusable field group templates:
<!-- blog/templates/post/includes/comment_form.html -->
<h2>Add a comment</h2>
<form action="{% url 'blog:post_comment' post.id %}" method="post">
{% csrf_token %}
<div>{{ form.user.as_field_group }}</div>
<div>{{ form.body.as_field_group }}</div>
<p><input type="submit" value="Add comment"></p>
</form>
Legend:
-
{{ form.user.as_field_group }}
: The as_field_group method renders the related elements of the field as a group. This group contains its label, widget, error and help text.
- Link to Django documentation:
- ModelForm
- require_POST decorator
- template tag
{% with %}
- template tag
{% empty %}
- template tag
{% include %}
- Looping over the form’s fields
- template filter
|safe
15.10.2024
10. Add django-taggit
package to project:
django-taggit
is a Django application/package used to add tags to blogs, articles etc. With this package it is really easy to add the tags and their functionality to our django project.
First we have to install the package:
pip install django-taggit
Little note: For my Django project I used the version 6.0.1 of django-taggit
.
Add taggit
in settings.py to INSTALLED_APPS
:
# blog_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# custom apps
"account",
"blog",
"core",
# packages/libraries:
"taggit",
]
After we add the django-taggit
to our model:
# blog/models.py
from django.db import models
from django.urls import reverse
from django.utils import timezone
from taggit.managers import TaggableManager
class Post(models.Model):
class Status(models.TextChoices):
DRAFT = "DF", "Draft"
PUBLISHED = "PB", "Published"
title = models.CharField(max_length=250)
slug = models.SlugField(max_length=250, unique_for_date="publish")
body = models.TextField()
publish = models.DateTimeField(default=timezone.now)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=2, choices=Status, default=Status.DRAFT)
author = models.ForeignKey(
"account.User", on_delete=models.CASCADE, related_name="blog_posts"
)
tags = TaggableManager()
Legend:
-
tags = TaggableManager()
: You add theTaggableManager()
to the model you want to add tags to.
When the TaggableManager
is added to the model, you have to migrate to
python manage.py makemigrations
python manage.py migrate
Legend:
-
makemigrations
: This command detects changes to your model, such as adding theTaggableManager()
field, and creates migration files. These files describe the database changes that are required, like creating new tables or fields. -
migrate
: This command applies the changes described in the migration files to the database, updating it to reflect the current state of your models.
Now, your setup is done, you can start creating tags in your shell
. Open your python shell with the command:
python manage.py shell
Personally, I use the package ipython
for my python shell.
In [1]: from blog.models import Post
In [2]: post = Post.objects.first()
In [3]: post
Out[3]: <Post: How to get the verbose_name of an attribute of a model>
In [4]: post.tags.add('django', 'django-model')
In [5]: post.tags.all()
Out[5]: <QuerySet [<Tag: django>, <Tag: django-model>]>
Just like this you can add tags to your model Post
.
After creating for one Post
instance these tags, we can display the tags in our listing template.
<!-- blog/templates/post/list.html -->
{% extends "base.html" %}
{% block title %} Django Blog {% endblock %}
{% block content %}
<h1>My Blog</h1>
{% for post in posts %}
<h2>
<a href="{{ post.get_absolute_url }}">
{{ post.title }}
</a>
</h2>
{% if post.tags.exists %}
<p class="tags">Tags: {{ post.tags.all|join:", " }}</p>
{% endif %}
<p class="date">
Published {{ post.publish }} by {{ post.author }}
</p>
{{ post.body|truncatewords:30|linebreaks }}
{% endfor %}
{% include "pagination.html" with page=page_obj %}
{% endblock %}
Legend:
-
{% if post.tags.exists %}
: Later, all ourPost
instances will have tags, but currently only onePost
instance has tags, so I check if the `Post' instance has tags. {{ |join:", " }}
: The template filter|join
is like the pythonjoin()
method. It displays the tags separated by comma.-
Links:
Top comments (0)