From: Maciej Tronowski Date: Mon, 7 Sep 2015 17:02:33 +0000 (+0200) Subject: initial commit for better sessions app X-Git-Tag: v1.1~21 X-Git-Url: http://mmka.chem.univ.gda.pl/gitweb/?a=commitdiff_plain;h=857b0e17b0431749b81c1aff136cb4d59e38ff1e;p=qcg-portal.git initial commit for better sessions app Functions: - expire user session after given period of time (warn first, option to extend session) - allow only one active session per user --- diff --git a/better_sessions/__init__.py b/better_sessions/__init__.py new file mode 100644 index 0000000..78393fb --- /dev/null +++ b/better_sessions/__init__.py @@ -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 index 0000000..cdd3655 --- /dev/null +++ b/better_sessions/admin.py @@ -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 index 0000000..32151e9 --- /dev/null +++ b/better_sessions/apps.py @@ -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 index 0000000..3a290d6 --- /dev/null +++ b/better_sessions/context_processors.py @@ -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 index 0000000..81a09bd --- /dev/null +++ b/better_sessions/middleware.py @@ -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 index 0000000..fa7ac1b --- /dev/null +++ b/better_sessions/migrations/0001_initial.py @@ -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 index 0000000..e69de29 diff --git a/better_sessions/models.py b/better_sessions/models.py new file mode 100644 index 0000000..c7edeed --- /dev/null +++ b/better_sessions/models.py @@ -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 index 0000000..7e666b4 --- /dev/null +++ b/better_sessions/settings.py @@ -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 index 0000000..1553d50 --- /dev/null +++ b/better_sessions/signals.py @@ -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 index 0000000..e6979cb --- /dev/null +++ b/better_sessions/static/better_sessions/better_sessions.js @@ -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 index 0000000..8160df1 --- /dev/null +++ b/better_sessions/templates/better_sessions/alerts.html @@ -0,0 +1,8 @@ + + + diff --git a/better_sessions/tests.py b/better_sessions/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/better_sessions/tests.py @@ -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 index 0000000..8fc3b04 --- /dev/null +++ b/better_sessions/urls.py @@ -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 index 0000000..97d520c --- /dev/null +++ b/better_sessions/views.py @@ -0,0 +1,5 @@ +from django.http.response import JsonResponse + + +def ping(request): + return JsonResponse({'status': 'ok' if request.user.is_authenticated() else 'expired'})