Code


Version 6 (modified by nnonexistent@…, 6 years ago) (diff)

Fix after [6333]

The default SessionMiddleware that comes with Django lets you pick if you want all sessions to be browser-length or persistent, if you want to use different types of sessions based on a users preference (e.g. the good old "Remember Me" check-box) you're gonna have to write your own middleware. Fortunately it's pretty easy, this page documents my attempt, I don't like the "DualSession" name much, but it's the best I could come up with.

from django.conf import settings
from django.contrib.sessions.models import Session
from django.utils.cache import patch_vary_headers
import datetime

class DualSessionMiddleware(object):
    """Session middleware that allows you to turn individual browser-length 
    sessions into persistent sessions and vice versa.
    
    This middleware can be used to implement the common "Remember Me" feature
    that allows individual users to decide when their session data is discarded.
    If a user ticks the "Remember Me" check-box on your login form create
    a persistent session, if they don't then create a browser-length session.
    
    This middleware replaces SessionMiddleware, to enable this middleware:
    - Add this middleware to the MIDDLEWARE_CLASSES setting in settings.py, 
      replacing the SessionMiddleware entry.
    - In settings.py add this setting: 
      PERSISTENT_SESSION_KEY = 'sessionpersistent'
    - Tweak any other regular SessionMiddleware settings (see the sessions doc),
      the only session setting that's ignored by this middleware is 
      SESSION_EXPIRE_AT_BROWSER_CLOSE. 
      
    Once this middleware is enabled all sessions will be browser-length by
    default.
    
    To make an individual session persistent simply do this:
    
    session[settings.PERSISTENT_SESSION_KEY] = True
    
    To make a persistent session browser-length again simply do this:
    
    session[settings.PERSISTENT_SESSION_KEY] = False
    """
    
    def process_request(self, request):
        engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
        request.session = engine.SessionStore(request.COOKIES.get(settings.SESSION_COOKIE_NAME, None))

    def process_response(self, request, response):
        # If request.session was modified, or if response.session was set, save
        # those changes and set a session cookie.
        patch_vary_headers(response, ('Cookie',))
        try:
            modified = request.session.modified
        except AttributeError:
            pass
        else:
            if modified or settings.SESSION_SAVE_EVERY_REQUEST:
                session_key = request.session.session_key or Session.objects.get_new_session_key()
                
                if not request.session.get(settings.PERSISTENT_SESSION_KEY, False):
                    # session will expire when the user closes the browser
                    max_age = None
                    expires = None
                else:
                    max_age = settings.SESSION_COOKIE_AGE
                    expires = datetime.datetime.strftime(datetime.datetime.utcnow() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE), "%a, %d-%b-%Y %H:%M:%S GMT")
                
                new_session = Session.objects.save(session_key, 
                                                   request.session._session,
                                                   datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE))
                response.set_cookie(settings.SESSION_COOKIE_NAME, session_key,
                                    max_age = max_age, expires = expires, 
                                    domain = settings.SESSION_COOKIE_DOMAIN,
                                    secure = settings.SESSION_COOKIE_SECURE or None)
        return response

I think the class doc provides all the installation and deployment info you'll need (just make sure you read the Django docs about middleware first). Here's a sample login view that makes use of this middleware to implement the "Remember Me" check-box.

from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect
from mysite.accounts.forms import LoginForm

def login(request):
    """Display and processs the login form."""
    no_cookies = False
    account_disabled = False
    invalid_login = False
    redirect_to = request.REQUEST.get(REDIRECT_FIELD_NAME, '')
    if request.method == 'POST':
        if request.session.test_cookie_worked():
            request.session.delete_test_cookie()
            form = LoginForm(request.POST)
            if form.is_valid():
                user = auth.authenticate(username = form.cleaned_data['username'],
                                         password = form.cleaned_data['password'])
                if user:
                    if user.is_active:
                        auth.login(request, user)
                        request.session[settings.PERSISTENT_SESSION_KEY] = form.cleaned_data['remember_user']
                        # login successful, redirect
                        return HttpResponseRedirect('/')
                    else:
                        account_disabled = True
                else:
                    invalid_login = True
        else:
            no_cookies = True
            form = None
    else:
        form = LoginForm()
    
    # cookie must be successfully set/retrieved for the form to be processed    
    request.session.set_test_cookie()
    return render_to_response('accounts/login.html', 
                              { 'no_cookies': no_cookies,
                                'account_disabled': account_disabled,
                                'invalid_login': invalid_login,
                                'form': form,
                                REDIRECT_FIELD_NAME: redirect_to },
                              context_instance = RequestContext(request))

And here's the sample login form used by the view.

from django import newforms as forms
from django.newforms import widgets
from django.contrib.auth.models import User

class LoginForm(forms.Form):
    """Login form for users."""
    username = forms.RegexField(r'^[a-zA-Z0-9_]{1,30}$',
                                max_length = 30,
                                min_length = 1,
                                error_message = 'Must be 1-30 alphanumeric characters or underscores.')
    password = forms.CharField(min_length = 6, 
                               max_length = 128, 
                               widget = widgets.PasswordInput,
                               label = 'Password')
    remember_user = forms.BooleanField(required = False, 
                                       label = 'Remember Me')
    
    def clean(self):
        try:
            user = User.objects.get(username__iexact = self.cleaned_data['username'])
        except User.DoesNotExist, KeyError:
            raise forms.ValidationError('Invalid username, please try again.')
        
        if not user.check_password(self.cleaned_data['password']):
            raise forms.ValidationError('Invalid password, please try again.')
        
        return self.cleaned_data

If you've noticed anything that could be done better please point it out. This hasn't been tested in production yet, so you should run your own tests to make sure everything is working as expected.

Other Middleware