40

In Django, we can get the time user last logged in by using Auth.User.last_login. That is only updated when the user logs in using his username/password. Suppose the user is already logged in and the authentication information is saved in a cookie, therefore is able to access the site without logging in. How can we get the date the user previously visited the site? This would be useful for queries such as getting the number of new records since the last visit.

user2233706
  • 6,148
  • 5
  • 44
  • 86
  • 1
    https://godjango.com/blog/record-last-access-not-just-last-login/ is more efficient since you don't do an extra lookup like in the middleware case. – Andrei-Niculae Petre Feb 21 '17 at 09:26

8 Answers8

37

Example model:

class User(models.Model):
    last_visit = models.DateTimeField(...)
    ...

Example middleware which will be executed for all logged-in users:

from django.utils.timezone import now

class SetLastVisitMiddleware(object):
    def process_response(self, request, response):
        if request.user.is_authenticated():
            # Update last visit time after request finished processing.
            User.objects.filter(pk=request.user.pk).update(last_visit=now())
        return response

Add the new middleware to Your settings.py:

MIDDLEWARE_CLASSES = (
    ...
    'path.to.your.SetLastVisitMiddleware',
    ...
)

Warning: not tested, but doesn't require external packages to be installed and it's only 5 lines of code.

See more in the docs about Middleware and custom user models (since Django 1.5)

HankMoody
  • 3,077
  • 1
  • 17
  • 38
  • 1
    You have to return the `response` object at the end of `process_response`. Although django-last-seen is a packaged solution, I went with this answer because with django-last-seen I have to remember to call `LastSeen.object.when` upon every request. Of course, I could put `LastSeen.object.when` in middleware, but then I might as well write my own middleware. – user2233706 Sep 03 '13 at 02:46
18

Here's a middleware that will keep track of user last activity and count separated by intervals of time. Using the interval creates discrete "sessions" which can be tracked/counted along with the benefit of minimizing writes to the database.

Every time an auth user performs a request, will hit the cache to find their last activity, and then update the cache with a new timestamp. If the activity has had a gap of at least "interval" time, then it will update the database timestamp.

from datetime import timedelta as td
from django.utils import timezone
from django.conf import settings
from django.db.models.expressions import F    
from <user profile path> import UserProfile  

class LastUserActivityMiddleware(object):
    KEY = "last-activity"

    def process_request(self, request):
        if request.user.is_authenticated():
            last_activity = request.session.get(self.KEY)

            # If key is old enough, update database.
            too_old_time = timezone.now() - td(seconds=settings.LAST_ACTIVITY_INTERVAL_SECS)
            if not last_activity or last_activity < too_old_time:
                UserProfile.objects.filter(user=request.user.pk).update(
                        last_login=timezone.now(),
                        login_count=F('login_count') + 1)

            request.session[self.KEY] = timezone.now()

        return None

Comments:

  1. How you define settings.LAST_ACTIVITY_INTERVAL_SECS determine what constitutes the interval of non-activity considered to be a new login.
  2. This updates a "UserProfile" object which I have 1:1 with my User objects, but you can update any object you please.
  3. Make sure to include this in settings.MIDDLEWARE_CLASSES.
  4. Note this middleware uses process_request not process_response otherwise depending on middleware order, APPEND_SLASH may cause request.user to be unavailable as discussed: Django: WSGIRequest' object has no attribute 'user' on some pages?
Community
  • 1
  • 1
John Lehmann
  • 7,975
  • 4
  • 58
  • 71
  • 5
    I had to add `from dateutil.parser import parse` and replace `if not last_activity or last_activity < too_old_time:` with `if not last_activity or parse(last_activity) < too_old_time:` .........and............ `request.session[self.KEY] = timezone.now()` with `request.session[self.KEY] = timezone.now().isoformat()` – Andrew Swihart Jun 09 '17 at 16:13
  • 5
    Forgot to mention why: Django sessions apparently require JSON serializable data, which datetimes like timezone.now() are not, so I had to store it as a string using datetime.isoformat() instead and parse that back into a datetime. – Andrew Swihart Jun 09 '17 at 16:59
  • Should i save this in **middleware.py** file ? – Lars Apr 17 '21 at 09:26
4

Taking into account @John Lehmann solution and @Andrew Swihart suggestions, I came up with this code for newer versions of Django (> 2.0):

from datetime import timedelta as td
from django.utils import timezone
from django.conf import settings
from django.db.models.expressions import F
from dateutil.parser import parse

from .models import Account


class AccountLoginMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.user.is_authenticated:
            last_activity = request.session.get('last-activity')

            too_old_time = timezone.now() - td(seconds=settings.LOGIN_INTERVAL)
            if not last_activity or parse(last_activity) < too_old_time:
                Account.objects.filter(username=request.user.username).update(
                    login_last=timezone.now(),
                    login_count=F('login_count') + 1)

            request.session['last-activity'] = timezone.now().isoformat()

        response = self.get_response(request)

        return response
j4n7
  • 590
  • 7
  • 9
3

The solution of @John Lehmann is wonderful. However, it requires using specific cache-based sessions settings to avoid database write on each request.

There are two options in cache-based sessions, backends.cache or backends.cached_db. The second one is a write-through cache, i.e. each modification to session data is written on both the database as well as cache. This provides persistency across restarts.

I have re-written the above to explicitly use the cache function and avoid many database writes.

from django.core.cache import cache
from django.utils import timezone
# other user model import

def last_visit_middleware(get_response):

    def middleware(request):
        """
        Save the time of last user visit
        """
        response = get_response(request)

        if request.session.session_key:
            key = "recently-seen-{}".format(request.session.session_key)
            recently_seen = cache.get(key)

            # is_authenticated hits db as it selects user row
            # so we will hit it only if user is not recently seen
            if not recently_seen and request.user.is_authenticated:
                UserAccount.objects.filter(id=request.user.id) \
                    .update(last_visit=timezone.now())

                visit_time = 60 * 30    # 30 minutes
                cache.set(key, 1, visit_time)

        return response

    return middleware

The records the time of last arrival or last visit. It does not record the time of last exit or "last seen".

Pratyush
  • 5,108
  • 6
  • 41
  • 63
1

I would go for django-last-seen

Usage:

from last_seen.model import LastSeen

seen = LastSeen.object.when(user=user)
PythonicNinja
  • 499
  • 4
  • 10
1

The same as John Lehmann's middleware, but rewritten as a function with Andrew Swihart's suggestions and tested on Django 2.2:

 def last_user_activity_middleware(get_response):

    def middleware(request):

        response = get_response(request)

        key = "last-activity"

        if request.user.is_authenticated:

            last_activity = request.session.get(key)

            # If key is old enough, update database.
            too_old_time = timezone.now() - td(seconds=60 * 60)
            if not last_activity or parse(last_activity) < too_old_time:
                MyUser.objects.filter(email=request.user).update(
                    last_visit=timezone.now(),
                    login_count=F('login_count') + 1)

            request.session[key] = timezone.now().isoformat()

        return response

    return middleware

Learn more about writing own middleware in official documentation: https://docs.djangoproject.com/en/2.2/topics/http/middleware/#writing-your-own-middleware

Nairum
  • 1,217
  • 1
  • 15
  • 36
1

This is my lastvisitmiddleware.py file which I have added in the settings.py file as a middleware

from django.utils.timezone import now
from myapp.models import UserLastVisit
from django.contrib.auth.models import User

class LastVisitMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.user.is_authenticated:
            # Update last visit time after request finished processing.
            user = User.objects.get(id=request.user.id)
            userLastVisit = UserLastVisit.objects.filter(user_id=user)
            if userLastVisit:
                userLastVisit.update(last_visit=now())
            else:
                UserLastVisit.objects.create(user_id=user, last_visit=now())

        response = self.get_response(request)
        return response

setings.py file

MIDDLEWARE = [
   ...
   'mysite.lastvisitmiddleware.LastVisitMiddleware',
   ...
]

models.py

class UserLastVisit(models.Model):
    user_id = models.ForeignKey(User, models.DO_NOTHING, db_column='user_id')
    last_visit = models.DateTimeField()

This solution worked for me. Now, every time a user visits the site, the UserLastVisit table will be updated with the latest last_visit. One problem in that is if user travel between different pages, then also the last visit will be updated. We can use a time range like 24 hrs or something like that to update it only once in that time range. It combines multiple approaches from the answers available on this thread

dchhitarka
  • 21
  • 1
  • 2
  • 7
0

I came up with yet another of the solutions above, using cache versions and expiration dates to determine an interval between which we do not want to update the "last seen" value:

# middleware.py
from django.utils.timezone import now
from django.core.cache import cache


class LastActivityMiddleware:
    """Records last activity of a user. This is different of user.last_login
    because you can be inactive without been logged out"""

    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self, request):
        """Records the timestamp of last activity of a user, every x minutes"""
        if request.user.is_authenticated:
            last_activity_timeout = 60*10  # 10min expiration
            user_last_activity_cache_key = f'{request.user.email}_last_activity'

            # in version 1 of cache key, there is an expiration date. This expiration date reprensent
            # the cooldown time for the last activity to be updated again, using the second version of the
            # key which store the latest activity timestamp value
            v2 = cache.get_or_set(user_last_activity_cache_key, now(), timeout=None, version=2)

            if not cache.get(user_last_activity_cache_key, version=1):
                cache.set(user_last_activity_cache_key, v2, timeout=last_activity_timeout, version=1)
                cache.set(user_last_activity_cache_key, now(), timeout=None, version=2)

        response = self.get_response(request)
        return response

Guillaume Lebreton
  • 2,586
  • 16
  • 25