Wednesday, July 20, 2011

Django: Using Caching to Track Online Users

Recently I wanted a simple solution to track whether a user is online on a given Django site.  The definition of "online" on a site is kind of ambiguous, so I'll define that a user is considered to be online if they have made any request to the site in the last five minutes.

I found that one approach is to use Django's caching framework to track when a user last accessed the site.  For example, upon each request, I can have a middleware set the current time as a cache value associated with a given user.  This allows us to store some basic information about logged-in user's online state without having to hit the database on each request and easily retrieve it by accessing the cache.

My approach below.  Comments welcome.

In settings.py:
# add the middleware that you are about to create to settings
MIDDLEWARE_CLASSES = (
    ....
    'middleware.activeuser_middleware.ActiveUserMiddleware',
    ....
)

# Setup caching per Django docs. In actuality, you'd probably use memcached instead of local memory.
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'default-cache'
    }
}

# Number of seconds of inactivity before a user is marked offline
USER_ONLINE_TIMEOUT = 300

# Number of seconds that we will keep track of inactive users for before 
# their last seen is removed from the cache
USER_LASTSEEN_TIMEOUT = 60 * 60 * 24 * 7

In activeuser_middleware.py:
import datetime
from django.core.cache import cache
from django.conf import settings

class ActiveUserMiddleware:

    def process_request(self, request):
        current_user = request.user
        if request.user.is_authenticated():
            now = datetime.datetime.now()
            cache.set('seen_%s' % (current_user.username), now, 
                           settings.USER_LASTSEEN_TIMEOUT)

In your UserProfile module or some other model associated with the user:
class UserProfile(models.Model):
    ....
    ....

    def last_seen(self):
        return cache.get('seen_%s' % self.user.username)

    def online(self):
        if self.last_seen():
            now = datetime.datetime.now()
            if now > self.last_seen() + datetime.timedelta(
                         seconds=settings.USER_ONLINE_TIMEOUT):
                return False
            else:
                return True
        else:
            return False

Then in the template where you want to display whether the user is online or not:
{% with request.user.get_profile as profile %}

 <table>
   <tr><th>Last Seen</th><td>{% if profile.last_seen %}{{ profile.last_seen|timesince }}{% else %}awhile{% endif %} ago</td></tr>
   <tr><th>Online</th><td>{{ profile.online }}</td></tr>
 </table>

{% endwith %}

Pros:
 - Simple solution
 - Doesn't need to hit the database for saving the timestamp each request

Cons:
  - Last user access times are cleared if the server is rebooted or cache is reset
  - Last user access times are accessible only as long as they exist in the cache.

Friday, July 8, 2011

Django Permission TemplateTag

In a previous post, I wrote about a way to keep track of user permissions on a model instance.  For example, I suggested that each model have a permissions subclass that could be instantiated with a user instance passed as a constructor argument.  Methods on that permissions class could then be called to determine if that user has permission to perform various actions.

I also suggested that the threadlocals module could then be used to pass in the user instance to the permissions object in the Django template.  However, from various readings, I get the impression that threadlocals may not be the best thing for passing arguments in a template function.   Therefore, I decided to use a more traditional route of creating a template tag to do something similar.

I created a template tag that lets you surround a block of HTML code to hide or show the contents based on the return value of the permission function.  The tag below basically says, "if the logged in user has 'can_edit_group' permission on the given 'group' object instance, then display the Edit link".

Reference the original post for details.

In the Django template
{% load permission_tags %}
{% permission request.user can_edit_group on group %}
<a href="">Edit</a>
{% endpermission %}

Here is the templatetag definition that fits the example above.

In templatetags/permissions_tags.py
from django import template
register = template.Library()

def permission(parser, token):
    try:
        # get the arguments passed to the template tag; 
        # first argument is the tag name
        tag_name, username, permission, onkeyword, object = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError("%r tag requires exactly 4 arguments" % token.contents.split()[0])
    # look for the 'endpermission' terminator tag
    nodelist = parser.parse(('endpermission',))
    parser.delete_first_token()
    return PermissionNode(nodelist, username, permission, object)


class PermissionNode(template.Node):
    def __init__(self, nodelist, user, permission, object):
        self.nodelist = nodelist
        # evaluate the user instance as a variable and store
        self.user = template.Variable(user)
        # store the permission string
        self.permission = permission
        # evaluate the object instance as a variable and store
        self.object = template.Variable(object)

    def render(self, context):
        user_inst = self.user.resolve(context)
        object_inst = self.object.resolve(context)
        
        # create a new permissions object by calling a permissions 
        # factory method of the model class
        permissions_obj = object_inst.permissions(user_inst)
        
        content = self.nodelist.render(context)
        
        if hasattr(permissions_obj, self.permission):
            # check to see if the permissions object has the permissions method
            # provided in the template tag
            perm_func = getattr(permissions_obj, self.permission)
            # execute that permissions method
            if perm_func():
                return content 
        return ""

register.tag('permission', permission)

This tag currently works like an 'if' template tag and shows/hides anything wrapped between the permission and endpermission tags.  A future goal may be to make this work like an if/else tag so I can specify an else condition.