Introduction
Often when we build web applications, we want to keep a record of the items our web app users recently viewed. Our web app could be a Blog where we want to keep a record of the post a user ( Authenticated or Anonymous User ) recently viewed, or it could be an E-commerce website where we want to keep a record of the items a specific user recently viewed.
In this tutorial, I am going to show you how you can simply do that by implementing it on a Blog web app.
Perquisites
In this tutorial, I expect you to :
• have a little experience with Django Framework.
• have a basic understanding of git
.
HTTP Communication
Before we code the recently viewed
functionality, You need to understand that the communication between the server and the client( User or Web browser ) is via the Hypertext Transfer Protocol ( HTTP )
which is a stateless protocol.
A stateless Protocol
is just the technical jargon for saying the server does not retain information of the previous request made by the client.
Once a piece of information is sent from the server to the client, as a response to a request from the client, The information is used once and not retained.
Information about the recently viewed object is a form of retained information of the previous request made on the server. In other to establish Stateful communication
( one that retains information ), we need to implement that ourselves.
Django and Session
Django usessessions
to keep track of the state between the server ( site ) and the client( Web browser ). Sessions allow you to store data not worthy of being stored on the database like the recently viewed object data. The Django Session Framework allows you to store and retrieve data on a per-site-visitors.
Django makes use of cookies to identify each browser and its related session with a site I.e.
•There is a cookie that is stored on our browser and the cookie has an ID.
•The ID of the cookie is stored by default in the database by Django
•The information we want to store like recently viewed objects has a relationship with this ID on our Database.
Though in Django, session data is stored in the database by default, you can configure Django to store this data elsewhere like:
• File ( File-Based Session )
• Cache ( Cached Session )
• Cookies ( Cookie Based Session )
The major difference between all these different session implementations is performance-related, ( majorly Speed and Memory Based Performance ).
If you need an in-depth explanation about SESSION, there is a nicely penned documentation about it on the Django website.
Implementation
In this section, I will explain the logic and code implementation of the recently viewed post functionality in the Blog.
First, let's clone the Blog from my GitHub repository.
Cloning the Blog
From your terminal, clone the GitHub repository and make it the working directory:
$git clone -b Tutorial https://github.com/DrAnonymousNet/MyBlog.git
$cd MyBlog
You should have the file structure as shown below
In the MyBlog
folder, The blog folder houses the url.py
, settings.py
as well as the asgi and wsgi.py
files. The post
and the user
folder represents an application I started using the python manage.py startapp
command and they contain the views.py
file as well as other files that are created when you run the above command. The templates
folder houses the HTML file.
Installation
In your terminal, install all the required library for the project using the
pip install -r requirements.txt
Run
python manage.py makemigrations
andpython manage.py migrate
to create the database for our Installed app.Run
python manage.py createsuperuser
to create a super user.Fire up the development server with
python manage.py runserver
and check for errors. Hopefully, there is none.After installation, Create a handful number of dummy posts from the admin website.
Coding
One way to implement the recently viewed post features in our Blog app is to create a column for recently viewed data on the User Model, but this is not a data worthy of having a column on the user model because, by doing so, We won't be able to keep a record of the post viewed when a user is not authenticated ( Not logged In ). A session allows us to do that.
Enabling Session
To use session in any of our Django projects, we need to enable it as follow.
If you are following through with our Blog app, you can skip this section because session has been enabled.
• Edit the Middleware
in the settings.py
.Make sure it contains django.contrib.session.middleware.SessionMiddleware
. This is usually there by default, but in case it's not, add it back.
• Add django.contrib.sessions
to your INSTALLED_APPS in case it's not there.
• Run python manage.py makemigrations
if you just added the two lines above
• Run python manage.py migrate
to create the session database that stores our session data.
MIDDLEWARE = [
....
“django.contrib.sessions.middleware.SessionMiddleware”,
....
]
INSTALLED_APPS= [
....
‘django.contrib.sessions’,
....
]
By doing that, we are ready to use session
in our project.
Once the session is activated, The HTTP request Object created for every request
sent to the server —the first argument to all our view functions— has a session attribute we can work with. The session attribute is a python dictionary-like object
. It is a convention to use python string as dictionary keys on the request.session
.
We can create
new keys and values as shown below:
request.session[“viewed_post”] = “post 1”
request.session[“is_active”] = True
We can change
the value of existing keys as shown below:
request.session[“viewed_post”] = “post 2”
request.session[“is_active”] = False
We can also retrieve
the value of an existing key :
print(request.session[“viewed_post”])
If the key is not present, the above method returns a KeyError
. To give a fallback default value in case the key is not present, we can use the get
method:
print( request.session.get( “viewed_post”, “post_2” ))
The get
method returns the value of the viewed_post
if present or assigns the post_2
argument to the key.
And we can delete
an existing key:
del request.session.get( “viewed_post”, “post_2” )
Logic
In our recently viewed
implementation, I will keep track of the last five posts the current user viewed. The simple algorithm
or logic
to follow when a user viewed a post goes thus;
•Check if there is a recently_viewed
key in our request.session
.
• If false
, create the recently_viewed
key in the request.session
object of the present user and assign an empty list to store the last five recently viewed posts.
• Add the post id to the list
If there is a recently_viewed
key in the present user's request.session
object.
• Check if the current post is in the list of recently viewed post.
•If the current post is in the recently viewed post, we want to remove it and add it to index 0.
•If the post is not there, we want to insert it in the 0th index and check if the length of the recently viewed posts is greater than 5.
• If it's greater than 5, remove the last item.
Open the post/view.py
file and add the function below:
def recently_viewed( request, post_id ):
if not "recently_viewed" in request.session:
request.session["recently_viewed"] = []
request.session["recently_viewed"].append(post_id)
else:
if post_id in request.session["recently_viewed"]:
request.session["recently_viewed"].remove(post_id)
request.session["recently_viewed"].insert(0, post_id)
if len(request.session["recently_viewed"]) > 5:
request.session["recently_viewed"].pop()
request.session.modified =True
In the code
above, we pass the present users' request
and the current post ID post_id
to the recently_viewed
function and follow the above
algorithm.
The last line is important because Django only saves to the session database if any of the session dictionary value has been assigned or deleted. I.e when we alter the request.session.
For instance, the session is saved when the recently_viewed
key was created because we directly modified the request.session
:
request.session[“recently_viewed”] = []
However, subsequent operations like the append
, insert
, pop
, and remove
are not saved because we are modifying a key in the request.session
object — recently_viewed
key — not the request.session
object itself. We need to explicitly let the database know that we want to save those operations, hence we need to add request. session.modified =True
Call the recently_viewed function
in the post view
function as shown below:
def post(request, slug, id):
post = Post.objects.get(id=id)
category = Category.objects.all()
cat = Category.objects.get(slug=slug)
latests = Post.objects.filter(date_posted__isnull=False).order_by('-date_posted')[:3]
context = {}
recently_viewed(request, id)
if post.date_posted:
next = post.get_next_by_id()
previous = post.get_previous_by_id()
context = {
"previous": previous,
"next": next,
}
comment = Comment.objects.filter(post=post)
page_request_var, page, paginated_queryset = pagination(request, comment, num_per_page=10)
form = CommentForm()
if request.method == "POST":
form = CommentForm(request.POST)
if form.is_valid():
author = Author.objects.get(author=request.user)
form.instance.author = author
form.instance.post = post
form.save()
form = CommentForm()
cont = {"post": post,
"category": category,
"latests": latests,
"cat": cat, "form": form,
"queryset": paginated_queryset,
"page": page, "slug": slug,
"page_request_var": page_request_var,
}
for c in cont.keys():
context[c] = cont[c]
return render(request, "post.html", context)
Any time a post is requested, the above view function Is called. For every time the view function is called, the recently_viewed
function is also called with the current request
and the post id
.
In other to render the recently viewed posts, We will have to query the database for all the posts that are present in the request.session[“recently_viewed”]
. You can create a view specifically for that, or add it to an existing view. My blog project has an aside section as part of every page, I will just add it to the post view
Add
recently_viewed_qs = Post.objects.filter(pk__in=request.session.get("recently_viewed", []))
in the post view below the calling of the recently_viewed
function:
def post(request, slug, id):
….
recently_viewed(request, id)
recently_viewed_qs = Post.objects.filter(pk__in=request.session.get("recently_viewed", []))
….
return render(request, "post.html", context)
The line filter the post queryset to get the posts in the request.session[“recently_viewed”]
list, since we stored the id of each post in the list of request.session[“recently_viewed”]
. we can use the pk__in
attribute of the filter method
as shown above.
Also, we need to sort
the queryset based on how the posts are sorted in the request.session[“recently_viewed”]
list. By default, the filtered queryset is sorted by post ID in ascending order. We can use the Python built-in sorted function
with a defined key to sort it based on how the posts are arranged in the list. The key argument could be a function that returns the way we want the first argument to be sorted. This is a very good use case of the lambda function
.
Add recently_viewed_qs = sorted(recently_viewed_qs, key = lambda x: request.session[x.id])
to the post view function
below the two lines added above:
def post(request, slug, id):
….
recently_viewed(request, id)
recently_viewed_qs = Post.objects.filter(pk__in=request.session.get("recently_viewed", []))
recently_viewed_qs = sorted(recently_viewed_qs, key = lambda x: request.session[x.id])
….
return render(request, "post.html", context)
In the sorted function
, I passed the filtered queryset which is an iterable as the first argument, and passed the lambda function as the key.
In other to have access to the queryset stored in the recently_viewed_qs
variable in the HTML template, we need to add it to the context dictionary with a key which we will have access to in the post.html
file.
Let's add that to the context data in the post view function
:
def post(request, post_id){
.…
context ={
….
“recently_viewed”: recently_viewed_qs,
….
}
.…
return render(request, "post.html", context)
We will then add the code below to the templates/aside.html
which is included
— {% include 'aside.html' %}
— in the thetemplates/post.html
template:
<div class="widget latest-posts">
<header><h3 class="h6">Recently Viewed Posts</h3></header>
<div class="blog-posts">
{% for post in recently_viewed%}
<a href="{{post.get_absolute_url}}">
<div class="item d-flex align-items-center">
<div
class="image"><img src="{{post.thumbnail.url}}" alt="..."
class="img-fluid"></div>
<div class="title"><strong>{{post.title}}</strong>
<div class="d-flex align-items-center">
<div class="views"><i class="icon-eye"></i>{{post.views}}</div>
<div class="comments"><i class="icon-comment"></i>{{post.get_comment_count}}</div>
</div>
</div>
</div></a>
{% endfor %}
</div>
</div>
Using the Django Template Language Syntax, We loop through the recently_viewed
key we passed as context data from the post view function.
Now let's run the server with the python manage.py runserver
to see if all these work. Hopefully, there is no error.
Yeah !!!!, No Error. If you do it all correctly, there shouldn't be any error on your side also.
Let's open the website on our browser and click on a post.
That's it, our post is added to the recently viewed post section
despite that we are not logged in.
Let me view 4 more posts to make it 5 in total.
Notice that the last viewed post is at the top and the first viewed post is at the bottom. If I view one more post, the first viewed post ( Algorithm ) is removed from the list.
The Algorithm post is popped from the list since the list is greater than 5.
If I add a post that is already on the list, it is moved to the top.
The linked list post is moved to the top.
The recently viewed post data are stored despite that we are not logged in, all these data are retained when we log in but are flushed ( cleared ) when we log out. Though we can create a backup to retain the data when we log out but that's beyond the scope of this tutorial.
Conclusion
In this tutorial, you learned how to implement a recently viewed post feature on a Blog App we cloned from my Github repository. This approach can be applied on other projects like an E-commerce website where you want to keep record of the item a specific user viewed recently.
This use-case of session is just one of the numerous specific-user-tailored
data we can create using session.
The full code to the Blog App project i used in this tutorial can be found on GitHub.