How To Implement A Recently Viewed Feature  In Your Django Web App

How To Implement A Recently Viewed Feature In Your Django Web App

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

Screenshot (19).png

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

  1. Create a virtual environment

  2. In your terminal, install all the required library for the project using the pip install -r requirements.txt

  3. Run python manage.py makemigrations and python manage.py migrate to create the database for our Installed app.

  4. Run python manage.py createsuperuser to create a super user.

  5. Fire up the development server with python manage.py runserver and check for errors. Hopefully, there is none.

  6. 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' requestand 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.

Screenshot (20).png

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. Image1

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.

20220508_124057

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.

20220508_124059

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.

20220508_124646

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.