My dear reader, how are you? السلام عليكم
Be yourself; everyone else is already taken— Oscar Wilde
This is the third part of DjangoBlog tutorial series. This post extends our application with a tagging functionality and full-text search using PostgreSQL search features.
Few useful links to practically follow the project:
- DjangoBlog GitHub repository — DirectMe
- All other tutorials on Django-Blog — DirectMe
- Set your GitHub repo to DjangoBlog Part 2 using the following command:
git fetch origin 952756d3c3573aca30c14c34ca94782c474d0115
Assigning Category to Posts using tagging functionality
For this functionality, we will use a third-party Django module called django-taggit
(blogenv)Django-Blog-Application/Django-Blog$ pip3 install django-taggit # open config/settings.py and add the installed app INSTALLED_APPS = [ 'taggit', 'blog', 'django.contrib.admin', ... ... ] # now add tag instance to Post model as shown below # open blog/models.py and add from taggit.managers import TaggableManager class Post(models.Model): ... tags = TaggableManager() ... # finally add the migrations (blogenv)Django-Blog-Application/Django-Blog$ python3 manage.py makemigrations blog (blogenv)Django-Blog-Application/Django-Blog$ python3 manage.py migrate # you can manually first add a few tags from admin view at http://127.0.0.1:8000/admin/taggit/tag/ and then add them to view # open blog/templates/post/list.html and add <p class="tags">Tags: {{ post.tags.all|join:", " }}</p>
Add the tags in the list view as shown below
# open blog/views.py and update post_list view as below from taggit.models import Tag def post_list(request, tag_slug=None): posts = Post.published.all() tag = None if tag_slug: tag = get_object_or_404(Tag, slug=tag_slug) object_list = object_list.filter(tags__in=[tag]) paginator = Paginator(object_list, 3) page = request.GET.get('page') try: posts = paginator.page(page) except PageNotAnInteger: posts = paginator.page(1) except EmptyPage: posts = paginator.page(paginator.num_pages) return render(request, 'blog/post/list.html', {'page': page, 'posts': posts, 'tag': tag}) # note that we are adding the pagination in list view as well. See the functional pagination by running the source code of DjangoBlog on GitHub.
Let us now add a route to tags as shown below
# open blog/urls.py and add urlpatterns = [ path('category/<slug:tag_slug>/', views.post_list, name='post_list_by_tag'), ]
Now, update the templates as shown below
# open templates/post/list.html and extend it by adding the following program {% if tag %} <h2>Posts tagged with "{{ tag.name }}"</h2> {% endif %} <p class="tags"> Tags: {% for tag in post.tags.all %} <a href="{% url "blog:post_list_by_tag" tag.slug %}"> {{ tag.name }} </a> {% if not forloop.last %}, {% endif %} {% endfor %} </p>
Voila! This completes adding our tagging feature for this blog application.
Extracting posts by similarity
Using the number of tags that posts share with each other, we can now implement functionality to display similar posts. We will add the list of similar posts in the detailed view. Follow the steps given below for implementation.
# open blog/views.py and add the following in post_detail function def post_detail(request, year, month, day, post): # add it under comments program # get similar posts post_tags_ids = post.tags.values_list('id', flat=True) similar_posts = Post.published.filter(tags__in=post_tags_ids)\ .exclude(id=post.id) similar_posts = similar_posts.annotate(same_tags=Count('tags'))\ .order_by('-same_tags','-publish')[:4] return render(request, 'blog/post/detail.html', {'post': post, 'comments': comments, 'new_comment': new_comment, 'comment_form': comment_form 'similar_posts': similar_posts})
Add the template code as below
# open blog/templates/blog/post/detail.html and update with the code below <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 %}
full-text search using POSTGRESQL
Since our DjangoBlog is already configured with PostgreSQL, we will now use its search functionality as shown below.
# add the following in config/settings.py INSTALLED_APPS = [ 'taggit', 'blog', 'django.contrib.postgres', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ]
Add a search form
# open blog/forms.py and add class SearchForm(forms.Form): query = forms.CharField()
We will now add a search view as shown below
# open blog/view.py and add the following code from django.contrib.postgres.search import SearchVector from .forms import EmailPostForm, CommentForm, SearchForm 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.objects.annotate( similarity=TrigramSimilarity('title', query), ).filter(similarity__gt=0.3).order_by('-similarity') return render(request, 'blog/post/search.html', {'form': form, 'query': query, 'results': results})
Let us now add a route to search view
# open blog/urls.py and apend it with following urlpatterns = [ path('', views.post_list, name='post_list'), path('<int:year>/<int:month>/<int:day>/<slug:post>/', views.post_detail, name='post_detail'), path('<int:post_id>/share/', views.post_share, name='post_share'), path('category/<slug:tag_slug>/', views.post_list, name='post_list_by_tag'), path('search/', views.post_search, name='post_search'), ]
Finally, we need to add a template for the search view as shown below
# create blog/templates/blog/post/search.html and add the following program {% extends "base.html" %} {% block title %}Search{% endblock %} {% block content %} {% if query %} <h1>Posts containing "{{ query }}"</h1> <h3> {% with results.count as total_results %} Found {{ total_results }} result{{ total_results|pluralize }} {% endwith %} </h3> {% for post in results %} <h4><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h4> {{ post.body|truncatewords:5 }} {% empty %} <p>There are no results for your query.</p> {% endfor %} <p><a href="{% url "blog:post_search" %}">Search again</a></p> {% else %} <h1>Search for posts</h1> <form action="." method="get"> {{ form.as_p }} <input type="submit" value="Search"> </form> {% endif %} {% endblock %}
If you run the development server at this point in time you should be able to see the added functionalities at http://127.0.0.1:8000/blog/
I hope you find this tutorial useful. If you find any errors or feel any need for improvement, let me know in your comments below.
Signing off for today. Stay tuned and I will see you next week! Happy learning.