Skip to content
This repository was archived by the owner on Dec 5, 2023. It is now read-only.

Commit 175690d

Browse files
committed
Initial commit
0 parents  commit 175690d

15 files changed

+377
-0
lines changed

.hgignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# use glob syntax.
2+
syntax: glob
3+
*.pyc
4+
dev.db
5+
htmlcov
6+
.coverage
7+
dist
8+
MANIFEST

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Ferran Pegueroles

README.rst

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
================
2+
django-last-seen
3+
================
4+
5+
Keep trak of when a user has been last seen on a website.
6+
The last seen time is kept on the database
7+
8+
The app is ready for django 1.5, it uses the AUTH_USER_MODEL setting to get
9+
the user model,
10+
11+
Installation
12+
============
13+
14+
#. Install with ``pip install django-last-seen"`` or add ``"last_seen"``
15+
directory to your Python path.
16+
#. Add ``"last_seen"`` to the ``INSTALLED_APPS`` tuple found in your settings
17+
file.
18+
#. Add 'last_seen.middleware.LastSeenMiddleWare' to MIDDLEWARE_CLASSES tuple
19+
found in your settings file.
20+
#. Run ``manage.py syncdb`` to create the new tables
21+
22+
Usage
23+
=====
24+
25+
To get when a user has been last seen::
26+
27+
from last_seen.model import LastSeen
28+
29+
seen = LastSeen.object.when(user=user)
30+
31+
32+
To save a last seen user without the middleware::
33+
34+
from last_seen.model import LastSeen
35+
36+
# save with a special module
37+
LastSeen.object.when(user=user, module='forum')
38+
39+
Middleware
40+
==========
41+
42+
The provided middleware keeps track of when an authenticated user has been
43+
last seen on the site,
44+
45+
If you want to keep track of a user last seen on a part of a site, you can
46+
use a special module name and use::
47+
48+
from last_seen.model import LastSeen
49+
50+
# save with a special module
51+
LastSeen.object.when(user=user, module='forum')
52+
53+
Then to get the data::
54+
55+
from last_seen.model import LastSeen
56+
57+
# user last seen on any part of the site
58+
seen = LastSeen.object.when(user=user)
59+
60+
# user last seen on a module
61+
seen = LastSeen.object.when(user=user, module='forum')
62+
63+
Settings
64+
========
65+
66+
:LAST_SEEN_DEFAULT_MODULE: The default module used on the middleware. The
67+
default value is ``default``.
68+
69+
:LAST_SEEN_INTERVAL: How often is the last seen timestamp updated to the
70+
database. The default is 2 hours.
71+

last_seen/__init__.py

Whitespace-only changes.

last_seen/admin.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
from django.contrib import admin
3+
4+
from models import LastSeen
5+
6+
7+
class LastSeenAdmin(admin.ModelAdmin):
8+
list_filter = ('site', 'module', 'last_seen')
9+
search_fields = ('user__username',)
10+
list_display = ('site', 'module', 'user', 'last_seen')
11+
12+
13+
admin.site.register(LastSeen, LastSeenAdmin)

last_seen/middleware.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
3+
from models import user_seen
4+
5+
6+
class LastSeenMiddleWare(object):
7+
8+
def process_request(request):
9+
if request.user.is_autheticated():
10+
user_seen(request.user)
11+
12+
return None

last_seen/models.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import time
2+
from django.db import models
3+
from django.contrib.sites.models import Site
4+
from django.utils import timezone
5+
from django.core.cache import cache
6+
7+
import settings
8+
9+
10+
class LastSeenManager(models.Manager):
11+
def seen(self, user, module=settings.LAST_SEEN_DEFAULT_MODULE):
12+
args = {
13+
'user': user,
14+
'site': Site.objects.get_current(),
15+
'module': module,
16+
}
17+
updated = self.filter(**args).update(last_seen=timezone.now())
18+
if not updated:
19+
self.create(**args)
20+
21+
def when(self, user, module=None, site=None):
22+
args = {'user': user}
23+
if module:
24+
args['module'] = module
25+
if site:
26+
args['site'] = site
27+
return self.filter(**args).latest('last_seen').last_seen
28+
29+
30+
class LastSeen(models.Model):
31+
site = models.ForeignKey(Site)
32+
user = models.ForeignKey(settings.AUTH_USER_MODEL)
33+
module = models.CharField(default=settings.LAST_SEEN_DEFAULT_MODULE, max_length=20)
34+
last_seen = models.DateTimeField(default=timezone.now)
35+
36+
objects = LastSeenManager()
37+
38+
class Meta:
39+
unique_together = (('user', 'site', 'module'),)
40+
ordering = ('-last_seen',)
41+
42+
def __unicode__(self):
43+
return u"%s on %s" % (self.user, self.last_seen)
44+
45+
46+
def user_seen(user, module=settings.LAST_SEEN_DEFAULT_MODULE):
47+
# compute limit to update db
48+
cache_key = "last_seen:%s:%s" % (module, user.pk)
49+
limit = time.time() - settings.LAST_SEEN_INTERVAL
50+
seen = cache.get(cache_key)
51+
if not seen or seen < limit:
52+
# mark the database and the cache
53+
LastSeen.objects.seen(user, module=module)
54+
cache.set(cache_key, time.time())

last_seen/settings.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.conf import settings
2+
3+
4+
LAST_SEEN_DEFAULT_MODULE = getattr(settings, 'LAST_SEEN_DEFAULT_MODULE',
5+
'default')
6+
LAST_SEEN_INTERVAL = getattr(settings, 'LAST_SEEN_INTERVAL', 60 * 60 * 2)
7+
8+
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')

last_seen/tests.py

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import datetime
2+
import mock
3+
import time
4+
from django.test import TestCase
5+
from django.utils import timezone
6+
from django.contrib.auth.models import User
7+
from django.contrib.sites.models import Site
8+
from django.core.cache import cache
9+
10+
from last_seen.models import LastSeen, user_seen
11+
from last_seen import settings
12+
from last_seen import middleware
13+
14+
15+
class TestLastSeenModel(TestCase):
16+
17+
def test_unicode(self):
18+
user = User(username='testuser')
19+
ts = datetime.datetime(2013, 1, 1, 2, 3, 4)
20+
seen = LastSeen(user=user, last_seen=ts)
21+
self.assertIn('testuser', unicode(seen))
22+
self.assertIn('2013-01-01 02:03:04', unicode(seen))
23+
24+
25+
class TestLastSeenManager(TestCase):
26+
27+
@mock.patch('last_seen.models.LastSeen.objects.create')
28+
@mock.patch('last_seen.models.LastSeen.objects.filter')
29+
def test_seen(self, filter, create):
30+
user = User(username='testuser')
31+
filter.return_value.update.return_value = 1
32+
33+
LastSeen.objects.seen(user=user)
34+
35+
filter.assert_called_with(user=user,
36+
module=settings.LAST_SEEN_DEFAULT_MODULE,
37+
site=Site.objects.get_current())
38+
self.assertFalse(create.called)
39+
40+
@mock.patch('last_seen.models.LastSeen.objects.create')
41+
@mock.patch('last_seen.models.LastSeen.objects.filter')
42+
def test_seen_create(self, filter, create):
43+
user = User(username='testuser')
44+
filter.return_value.update.return_value = 0
45+
46+
LastSeen.objects.seen(user=user)
47+
48+
filter.assert_called_with(user=user,
49+
module=settings.LAST_SEEN_DEFAULT_MODULE,
50+
site=Site.objects.get_current())
51+
create.assert_called_with(user=user,
52+
module=settings.LAST_SEEN_DEFAULT_MODULE,
53+
site=Site.objects.get_current())
54+
55+
def test_when_non_existent(self):
56+
user = User(username='testuser', pk=1)
57+
self.assertRaises(LastSeen.DoesNotExist, LastSeen.objects.when, user)
58+
59+
@mock.patch('last_seen.models.LastSeen.objects.filter')
60+
def test_seen_defaults(self, filter):
61+
user = User(username='testuser')
62+
LastSeen.objects.when(user=user)
63+
64+
filter.assert_called_with(user=user)
65+
66+
@mock.patch('last_seen.models.LastSeen.objects.filter')
67+
def test_seen_module(self, filter):
68+
user = User(username='testuser')
69+
LastSeen.objects.when(user=user, module='mod')
70+
71+
filter.assert_called_with(user=user, module='mod')
72+
73+
@mock.patch('last_seen.models.LastSeen.objects.filter')
74+
def test_seen_site(self, filter):
75+
user = User(username='testuser')
76+
site = Site()
77+
LastSeen.objects.when(user=user, site=site)
78+
79+
filter.assert_called_with(user=user, site=site)
80+
81+
82+
class TestUserSeen(TestCase):
83+
84+
@mock.patch('last_seen.models.LastSeen.objects.seen')
85+
def test_user_seen(self, seen):
86+
user = User(username='testuser', pk=1)
87+
user_seen(user)
88+
seen.assert_called_with(user, module=settings.LAST_SEEN_DEFAULT_MODULE)
89+
90+
@mock.patch('last_seen.models.LastSeen.objects.seen')
91+
def test_user_seen_cached(self, seen):
92+
user = User(username='testuser', pk=1)
93+
module = 'test_mod'
94+
cache.set("last_seen:%s:%s" % (module, user.pk), time.time())
95+
user_seen(user, module=module)
96+
self.assertFalse(seen.called)
97+
98+
@mock.patch('last_seen.models.LastSeen.objects.seen')
99+
def test_user_seen_cache_expired(self, seen):
100+
user = User(username='testuser', pk=1)
101+
module = 'test_mod'
102+
cache.set("last_seen:%s:%s" % (module, user.pk),
103+
time.time() - (2 * settings.LAST_SEEN_INTERVAL))
104+
user_seen(user, module=module)
105+
seen.assert_called_with(user, module=module)
106+
107+
class TestMiddleWare(TestCase):
108+
pass

manage.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env python
2+
import os
3+
import sys
4+
5+
if __name__ == "__main__":
6+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
7+
8+
from django.core.management import execute_from_command_line
9+
10+
execute_from_command_line(sys.argv)

runtests.sh

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
3+
MODULES=${1:-last_seen}
4+
5+
coverage run --branch ./manage.py test $MODULES || exit 2
6+
coverage report --include="./last_seen/*" --omit="./*/migrations/*"
7+
coverage html --include="./last_seen/*" --omit="./*/migrations/*"

setup.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env python
2+
3+
from distutils.core import setup
4+
5+
__author__ = u'Ferran Pegueroles'
6+
__copyright__ = u'Copyright 2013, Ferran Pegueroles'
7+
__credits__ = [u'Ferran Pegueroles']
8+
9+
10+
__license__ = 'GPL'
11+
__version__ = '0.1'
12+
__email__ = '[email protected]'
13+
14+
15+
long_description = open('README.rst').read()
16+
17+
18+
setup(
19+
name='django-last-seen',
20+
version=__version__,
21+
url='http://bitbucket.org/ferranp/django-last-seen',
22+
author=__author__,
23+
author_email=__email__,
24+
license='GPL',
25+
packages=['last_seen'],
26+
description='Keep track of when a user has been last seen',
27+
long_description=long_description,
28+
classifiers=['Development Status :: 5 - Production/Stable',
29+
'Environment :: Web Environment',
30+
'Framework :: Django',
31+
'Intended Audience :: Developers',
32+
'License :: OSI Approved :: GNU General Public License (GPL)',
33+
'Topic :: Internet :: WWW/HTTP :: Dynamic Content']
34+
)

test_project/__init__.py

Whitespace-only changes.

test_project/settings.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Django settings for test_project project.
2+
3+
DEBUG = True
4+
TEMPLATE_DEBUG = DEBUG
5+
6+
DATABASES = {
7+
'default': {
8+
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
9+
'NAME': ':memory:', # Or path to database file if using sqlite3.
10+
'USER': '', # Not used with sqlite3.
11+
'PASSWORD': '', # Not used with sqlite3.
12+
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
13+
'PORT': '', # Set to empty string for default. Not used with sqlite3.
14+
}
15+
}
16+
17+
SITE_ID = 1
18+
19+
ROOT_URLCONF = 'test_project.urls'
20+
21+
INSTALLED_APPS = (
22+
'django.contrib.auth',
23+
'django.contrib.contenttypes',
24+
'django.contrib.sessions',
25+
'django.contrib.sites',
26+
'django.contrib.messages',
27+
'django.contrib.staticfiles',
28+
# Uncomment the next line to enable the admin:
29+
'django.contrib.admin',
30+
# Uncomment the next line to enable admin documentation:
31+
# 'django.contrib.admindocs',
32+
'last_seen'
33+
)
34+

test_project/urls.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from django.conf.urls import patterns, include, url
2+
3+
# Uncomment the next two lines to enable the admin:
4+
from django.contrib import admin
5+
admin.autodiscover()
6+
7+
urlpatterns = patterns('',
8+
# Examples:
9+
# url(r'^$', 'test_project.views.home', name='home'),
10+
# url(r'^test_project/', include('test_project.foo.urls')),
11+
12+
# Uncomment the admin/doc line below to enable admin documentation:
13+
# url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
14+
15+
# Uncomment the next line to enable the admin:
16+
url(r'^admin/', include(admin.site.urls)),
17+
)

0 commit comments

Comments
 (0)