initial commit for better sessions app
authorMaciej Tronowski <mtro@man.poznan.pl>
Mon, 7 Sep 2015 17:02:33 +0000 (19:02 +0200)
committerDawid Jagieła <lightnir@gmail.com>
Sat, 12 Sep 2015 10:09:14 +0000 (12:09 +0200)
 Functions:
 - expire user session after given period of time (warn first, option to extend session)
 - allow only one active session per user

15 files changed:
better_sessions/__init__.py [new file with mode: 0644]
better_sessions/admin.py [new file with mode: 0644]
better_sessions/apps.py [new file with mode: 0644]
better_sessions/context_processors.py [new file with mode: 0644]
better_sessions/middleware.py [new file with mode: 0644]
better_sessions/migrations/0001_initial.py [new file with mode: 0644]
better_sessions/migrations/__init__.py [new file with mode: 0644]
better_sessions/models.py [new file with mode: 0644]
better_sessions/settings.py [new file with mode: 0644]
better_sessions/signals.py [new file with mode: 0644]
better_sessions/static/better_sessions/better_sessions.js [new file with mode: 0644]
better_sessions/templates/better_sessions/alerts.html [new file with mode: 0644]
better_sessions/tests.py [new file with mode: 0644]
better_sessions/urls.py [new file with mode: 0644]
better_sessions/views.py [new file with mode: 0644]

diff --git a/better_sessions/__init__.py b/better_sessions/__init__.py
new file mode 100644 (file)
index 0000000..78393fb
--- /dev/null
@@ -0,0 +1 @@
+default_app_config = 'better_sessions.apps.BetterSessionsConfig'
diff --git a/better_sessions/admin.py b/better_sessions/admin.py
new file mode 100644 (file)
index 0000000..cdd3655
--- /dev/null
@@ -0,0 +1,16 @@
+from django.contrib import admin
+
+from .models import UserSession
+
+
+@admin.register(UserSession)
+class UserSessionAdmin(admin.ModelAdmin):
+    list_display = ('user', 'key', 'created', 'updated')
+    list_filter = ('user',)
+    date_hierarchy = 'updated'
+    search_fields = ('user__username', 'key')
+    fields = ('user', 'key', 'created', 'updated')
+    readonly_fields = ('created', 'updated')
+
+    def get_queryset(self, request):
+        return super(UserSessionAdmin, self).get_queryset(request).select_related('user')
diff --git a/better_sessions/apps.py b/better_sessions/apps.py
new file mode 100644 (file)
index 0000000..32151e9
--- /dev/null
@@ -0,0 +1,9 @@
+from django.apps import AppConfig
+
+
+class BetterSessionsConfig(AppConfig):
+    name = 'better_sessions'
+    verbose_name = "Better Sessions"
+
+    def ready(self):
+        import signals
diff --git a/better_sessions/context_processors.py b/better_sessions/context_processors.py
new file mode 100644 (file)
index 0000000..3a290d6
--- /dev/null
@@ -0,0 +1,5 @@
+from .settings import WARN_AFTER, EXPIRE_AFTER
+
+
+def settings(_):
+    return {'session_warn_after': WARN_AFTER, 'session_expire_after': EXPIRE_AFTER}
diff --git a/better_sessions/middleware.py b/better_sessions/middleware.py
new file mode 100644 (file)
index 0000000..81a09bd
--- /dev/null
@@ -0,0 +1,22 @@
+import time
+
+from django.contrib.auth import logout
+
+from .settings import EXPIRE_AFTER
+
+
+class BetterSessionsMiddleware(object):
+    def process_request(self, request):
+        """ Update last activity time or logout. """
+        if request.user.is_authenticated():
+            now = time.time()
+            last_activity = request.session.get('last_activity', now)
+
+            print repr(now), repr(last_activity), now - last_activity
+
+            if now - last_activity > EXPIRE_AFTER:
+                print 'expired!'
+                logout(request)
+            else:
+                print 'prolong'
+                request.session['last_activity'] = now
diff --git a/better_sessions/migrations/0001_initial.py b/better_sessions/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..fa7ac1b
--- /dev/null
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='UserSession',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('key', models.CharField(max_length=40, verbose_name='Klucz sesji')),
+                ('created', models.DateTimeField(auto_now_add=True, verbose_name='Utworzono')),
+                ('updated', models.DateTimeField(auto_now=True, verbose_name='Uaktualniono')),
+                ('user', models.OneToOneField(related_name='session', verbose_name='U\u017cytkownik', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ('-updated',),
+                'verbose_name': 'Sesja u\u017cytkownika',
+                'verbose_name_plural': 'Sesje u\u017cytkownik\xf3w',
+            },
+        ),
+    ]
diff --git a/better_sessions/migrations/__init__.py b/better_sessions/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/better_sessions/models.py b/better_sessions/models.py
new file mode 100644 (file)
index 0000000..c7edeed
--- /dev/null
@@ -0,0 +1,19 @@
+# coding=utf-8
+from django.conf import settings
+from django.db import models
+from django.contrib.sessions.models import Session
+
+
+class UserSession(models.Model):
+    user = models.OneToOneField(settings.AUTH_USER_MODEL, verbose_name=u"Użytkownik", related_name='session')
+    key = models.CharField(u"Klucz sesji", max_length=40)
+    created = models.DateTimeField(u"Utworzono", auto_now_add=True)
+    updated = models.DateTimeField(u"Uaktualniono", auto_now=True)
+
+    class Meta:
+        verbose_name = u"Sesja użytkownika"
+        verbose_name_plural = u"Sesje użytkowników"
+        ordering = ('-updated',)
+
+    def __unicode__(self):
+        return '{} - {}'.format(self.user, self.key)
diff --git a/better_sessions/settings.py b/better_sessions/settings.py
new file mode 100644 (file)
index 0000000..7e666b4
--- /dev/null
@@ -0,0 +1,17 @@
+import warnings
+
+from django.conf import settings
+
+__all__ = ['EXPIRE_AFTER', 'WARN_AFTER']
+
+# Limit number of active session per user to only one
+SINGLE_SESSION = getattr(settings, 'BETTER_SESSIONS_SINGLE_SESSION', True)
+
+# Time (in seconds) before the user should be warned that its session will expire because of inactivity
+WARN_AFTER = getattr(settings, 'BETTER_SESSIONS_WARN_AFTER', 30)
+
+# Time (in seconds) before the user should be logged out if inactive
+EXPIRE_AFTER = getattr(settings, 'BETTER_SESSIONS_EXPIRE_AFTER', 45)
+
+if not getattr(settings, 'SESSION_EXPIRE_AT_BROWSER_CLOSE', False):
+    warnings.warn('settings.SESSION_EXPIRE_AT_BROWSER_CLOSE is not True')
diff --git a/better_sessions/signals.py b/better_sessions/signals.py
new file mode 100644 (file)
index 0000000..1553d50
--- /dev/null
@@ -0,0 +1,20 @@
+from django.contrib.auth import user_logged_in
+from django.contrib.sessions.models import Session
+
+from better_sessions.models import UserSession
+
+from .settings import SINGLE_SESSION
+
+
+def post_user_login(sender, request, user, **kwargs):
+    try:
+        Session.objects.filter(session_key=user.session.key).delete()
+    except UserSession.DoesNotExist:
+        user.session = UserSession()
+
+    user.session.key = request.session.session_key
+    user.session.save()
+
+
+if SINGLE_SESSION:
+    user_logged_in.connect(post_user_login)
diff --git a/better_sessions/static/better_sessions/better_sessions.js b/better_sessions/static/better_sessions/better_sessions.js
new file mode 100644 (file)
index 0000000..e6979cb
--- /dev/null
@@ -0,0 +1,81 @@
+"use strict";
+
+var psnc = psnc || {};
+
+psnc.BetterSession = function (options) {
+    $.extend(this, this.defaults, options);
+
+    // account for network delays and rendering time
+    this.warnAfter -= 5;
+    this.expireAfter -= 5;
+
+    this.postponeWarn = _.debounce(this.warn, this.warnAfter * 1000);
+    this.postponeExpire = _.debounce(this.expire, this.expireAfter * 1000);
+
+    $(this.warnSel).find('button').on('click', $.proxy(this.ping, this));
+
+    // update session whenever user loaded sth with ajax...
+    $(document).ajaxComplete($.proxy(this.update, this, true));
+    // ...or in another window
+    $(window).on('storage', $.proxy(this.update, this));
+
+    this.update(true);
+
+    return this;
+};
+
+psnc.BetterSession.prototype = {
+    defaults: {
+        expired: false,
+        warnAfter: 3300,
+        expireAfter: 3600,
+        key: 'lastActivity',
+        pingUrl: '/session/ping/',
+        warnSel: '#better-sessions-warn',
+        expireSel: '#better-sessions-expire',
+        timerSel: '#better-sessions-timer'
+    },
+
+    update: _.throttle(function (local) {
+        if (this.expired) return;
+
+        $(this.warnSel).hide();
+        clearInterval(this.timer);
+
+        // postpone expire message and warning
+        this.postponeWarn();
+        this.postponeExpire();
+
+        // update timestamp only if it is local event
+        if (local === true)
+            localStorage.setItem(this.key, new Date());
+    }, 1000),
+
+    ping: function () {
+        var self = this;
+        $.post(this.pingUrl, {}, function(response) {
+            // session is implicitly extended after every ajax request
+            if (response.status === 'expired')
+                self.expire();
+        }, 'json');
+    },
+
+    warn: function () {
+        this.timer = setInterval($.proxy(this.updateTimer, this), 1000);
+        this.updateTimer();
+        $(this.warnSel).show();
+    },
+
+    expire: function () {
+        this.expired = true;
+
+        $(this.warnSel).hide();
+        $(this.expireSel).show();
+        clearInterval(this.timer);
+    },
+
+    updateTimer: function() {
+        var remaining = Date.parse(localStorage.getItem(this.key)) - new Date() + (this.expireAfter * 1000);
+        $(this.timerSel).text((humanizeDuration(remaining, {language: 'pl', round: true}) || 'chwilę'))
+    }
+};
diff --git a/better_sessions/templates/better_sessions/alerts.html b/better_sessions/templates/better_sessions/alerts.html
new file mode 100644 (file)
index 0000000..8160df1
--- /dev/null
@@ -0,0 +1,8 @@
+<div id="better-sessions-warn" class="alert alert-warning" role="alert">
+    <strong>Uwaga!</strong> Twoja sesja wygaśnie za <span id="better-sessions-timer"></span>.
+    Kliknij <button class="btn btn-default">tutaj</button> aby przedłużyć sesję.
+</div>
+
+<div id="better-sessions-expire" class="alert alert-danger" role="alert" hidden>
+    <strong>Uwaga!</strong> Twoja sesja wygasła. Przeładowanie strony może spowodować utratę danych wprowadzonych w formularzach.
+</div>
diff --git a/better_sessions/tests.py b/better_sessions/tests.py
new file mode 100644 (file)
index 0000000..7ce503c
--- /dev/null
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/better_sessions/urls.py b/better_sessions/urls.py
new file mode 100644 (file)
index 0000000..8fc3b04
--- /dev/null
@@ -0,0 +1,7 @@
+from django.conf.urls import patterns, url
+
+from better_sessions.views import ping
+
+urlpatterns = patterns('',
+   url('ping/$', ping, name='ping')
+)
diff --git a/better_sessions/views.py b/better_sessions/views.py
new file mode 100644 (file)
index 0000000..97d520c
--- /dev/null
@@ -0,0 +1,5 @@
+from django.http.response import JsonResponse
+
+
+def ping(request):
+    return JsonResponse({'status': 'ok' if request.user.is_authenticated() else 'expired'})