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

Commit cac28db

Browse files
committed
Be sure that LAST_SEEN_INTERVAL is used.
LAST_SEEN_INTERVAL must be used independently of the cache timeout and independetly of if there is any cache. Solves #1
1 parent d8a1b58 commit cac28db

File tree

4 files changed

+145
-34
lines changed

4 files changed

+145
-34
lines changed

last_seen/models.py

+29-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import time
2+
import datetime
23
from django.db import models
34
from django.contrib.sites.models import Site
45
from django.utils import timezone
@@ -13,12 +14,16 @@ class LastSeenManager(models.Manager):
1314
1415
Provides 2 utility methods
1516
"""
16-
def seen(self, user, module=settings.LAST_SEEN_DEFAULT_MODULE, site=None):
17+
def seen(self, user, module=settings.LAST_SEEN_DEFAULT_MODULE, site=None,
18+
force=False):
1719
"""
1820
Mask an user last on database seen with optional module and site
1921
2022
If module not provided uses LAST_SEEN_DEFAULT_MODULE from settings
2123
If site not provided uses current site
24+
25+
The last seen object is only updates is LAST_SEEN_INTERVAL seconds
26+
passed from last update or force=True
2227
"""
2328
if not site:
2429
site = Site.objects.get_current()
@@ -27,9 +32,18 @@ def seen(self, user, module=settings.LAST_SEEN_DEFAULT_MODULE, site=None):
2732
'site': site,
2833
'module': module,
2934
}
30-
updated = self.filter(**args).update(last_seen=timezone.now())
31-
if not updated:
32-
self.create(**args)
35+
seen, created = self.get_or_create(**args)
36+
if created:
37+
return seen
38+
39+
# if we get the object, see if we need to update
40+
limit = timezone.now() - \
41+
datetime.timedelta(seconds=settings.LAST_SEEN_INTERVAL)
42+
if seen.last_seen < limit or force:
43+
seen.last_seen = timezone.now()
44+
seen.save()
45+
46+
return seen
3347

3448
def when(self, user, module=None, site=None):
3549
args = {'user': user}
@@ -81,9 +95,14 @@ def user_seen(user, module=settings.LAST_SEEN_DEFAULT_MODULE, site=None):
8195
limit = time.time() - settings.LAST_SEEN_INTERVAL
8296
seen = cache.get(cache_key)
8397
if not seen or seen < limit:
84-
# mark the database and the cache
85-
LastSeen.objects.seen(user, module=module, site=site)
86-
cache.set(cache_key, time.time())
98+
# mark the database and the cache, if interval is cleared force
99+
# database write
100+
if seen == -1:
101+
LastSeen.objects.seen(user, module=module, site=site, force=True)
102+
else:
103+
LastSeen.objects.seen(user, module=module, site=site)
104+
timeout = settings.LAST_SEEN_INTERVAL
105+
cache.set(cache_key, time.time(), timeout)
87106

88107

89108
def clear_interval(user):
@@ -92,10 +111,10 @@ def clear_interval(user):
92111
93112
Usefuf if you want to force a database write for an user
94113
"""
95-
keys = []
114+
keys = {}
96115
for last_seen in LastSeen.objects.filter(user=user):
97116
cache_key = get_cache_key(last_seen.site, last_seen.module, user)
98-
keys.append(cache_key)
117+
keys[cache_key] = -1
99118

100119
if keys:
101-
cache.delete_many(keys)
120+
cache.set_many(keys)

last_seen/tests.py

+93-23
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.contrib.auth.models import User
66
from django.contrib.sites.models import Site
77
from django.core.cache import cache
8+
from django.utils import timezone
89

910
from last_seen.models import LastSeen, user_seen, clear_interval
1011
from last_seen import settings
@@ -23,45 +24,99 @@ def test_unicode(self):
2324

2425
class TestLastSeenManager(TestCase):
2526

26-
@mock.patch('last_seen.models.LastSeen.objects.create', autospec=True)
27-
@mock.patch('last_seen.models.LastSeen.objects.filter', autospec=True)
28-
def test_seen(self, filter, create):
29-
user = User(username='testuser')
30-
filter.return_value.update.return_value = 1
27+
@mock.patch('last_seen.models.LastSeen.objects.get_or_create',
28+
autospec=True)
29+
def test_seen(self, get_or_create):
30+
user = User(username='testuser', pk=1)
31+
lastseen = mock.Mock(LastSeen)
32+
get_or_create.return_value = (lastseen, True)
3133

3234
LastSeen.objects.seen(user=user)
3335

34-
filter.assert_called_with(user=user,
36+
get_or_create.assert_called_with(user=user,
3537
module=settings.LAST_SEEN_DEFAULT_MODULE,
3638
site=Site.objects.get_current())
37-
self.assertFalse(create.called)
39+
self.assertFalse(lastseen.save.called)
3840

39-
@mock.patch('last_seen.models.LastSeen.objects.create', autospec=True)
40-
@mock.patch('last_seen.models.LastSeen.objects.filter', autospec=True)
41-
def test_seen_no_default(self, filter, create):
42-
user = User(username='testuser')
41+
@mock.patch('last_seen.models.LastSeen.objects.get_or_create',
42+
autospec=True)
43+
def test_seen_no_default(self, get_or_create):
44+
user = User(username='testuser', pk=1)
4345
site = Site(pk=2)
44-
filter.return_value.update.return_value = 1
46+
get_or_create.return_value = (None, True)
4547

4648
LastSeen.objects.seen(user=user, site=site, module="test")
4749

48-
filter.assert_called_with(user=user, module="test", site=site)
49-
self.assertFalse(create.called)
50+
get_or_create.assert_called_with(user=user, module="test", site=site)
5051

51-
@mock.patch('last_seen.models.LastSeen.objects.create', autospec=True)
52-
@mock.patch('last_seen.models.LastSeen.objects.filter', autospec=True)
53-
def test_seen_create(self, filter, create):
52+
@mock.patch('last_seen.models.LastSeen.objects.get_or_create',
53+
autospec=True)
54+
def test_seen_create(self, get_or_create):
5455
user = User(username='testuser')
55-
filter.return_value.update.return_value = 0
56+
lastseen = mock.Mock(LastSeen)
57+
get_or_create.return_value = (lastseen, True)
5658

5759
LastSeen.objects.seen(user=user)
5860

59-
filter.assert_called_with(user=user,
61+
get_or_create.assert_called_with(user=user,
62+
module=settings.LAST_SEEN_DEFAULT_MODULE,
63+
site=Site.objects.get_current())
64+
self.assertFalse(lastseen.save.called)
65+
66+
@mock.patch('last_seen.models.LastSeen.objects.get_or_create',
67+
autospec=True)
68+
def test_seen_update(self, get_or_create):
69+
user = User(username='testuser')
70+
lastseen = mock.Mock(LastSeen)
71+
# force last seen old
72+
old_time = timezone.now() - \
73+
datetime.timedelta(seconds=(settings.LAST_SEEN_INTERVAL * 2))
74+
lastseen.last_seen = old_time
75+
get_or_create.return_value = (lastseen, False)
76+
77+
ret = LastSeen.objects.seen(user=user)
78+
79+
get_or_create.assert_called_with(user=user,
80+
module=settings.LAST_SEEN_DEFAULT_MODULE,
81+
site=Site.objects.get_current())
82+
self.assertTrue(lastseen.save.called)
83+
self.assertNotEqual(ret.last_seen, old_time)
84+
85+
@mock.patch('last_seen.models.LastSeen.objects.get_or_create',
86+
autospec=True)
87+
def test_seen_update_forced(self, get_or_create):
88+
user = User(username='testuser')
89+
lastseen = mock.Mock(LastSeen)
90+
# force last seen old
91+
old_time = timezone.now()
92+
lastseen.last_seen = old_time
93+
get_or_create.return_value = (lastseen, False)
94+
95+
ret = LastSeen.objects.seen(user=user, force=True)
96+
97+
get_or_create.assert_called_with(user=user,
6098
module=settings.LAST_SEEN_DEFAULT_MODULE,
6199
site=Site.objects.get_current())
62-
create.assert_called_with(user=user,
100+
self.assertTrue(lastseen.save.called)
101+
self.assertNotEqual(ret.last_seen, old_time)
102+
103+
@mock.patch('last_seen.models.LastSeen.objects.get_or_create',
104+
autospec=True)
105+
def test_seen_found_not_updated(self, get_or_create):
106+
user = User(username='testuser')
107+
lastseen = mock.Mock(LastSeen)
108+
# force last seen old
109+
old_time = timezone.now()
110+
lastseen.last_seen = old_time
111+
get_or_create.return_value = (lastseen, False)
112+
113+
ret = LastSeen.objects.seen(user=user)
114+
115+
get_or_create.assert_called_with(user=user,
63116
module=settings.LAST_SEEN_DEFAULT_MODULE,
64117
site=Site.objects.get_current())
118+
self.assertFalse(lastseen.save.called)
119+
self.assertEqual(ret.last_seen, old_time)
65120

66121
def test_when_non_existent(self):
67122
user = User(username='testuser', pk=1)
@@ -92,9 +147,13 @@ def test_seen_site(self, filter):
92147

93148
class TestUserSeen(TestCase):
94149

150+
def setUp(self):
151+
[cache.delete(key) for key in cache._cache.keys()]
152+
95153
@mock.patch('last_seen.models.LastSeen.objects.seen', autospec=True)
96154
def test_user_seen(self, seen):
97-
user = User(username='testuser', pk=1)
155+
user = User(username='testuser', pk=999)
156+
98157
user_seen(user)
99158
site = Site.objects.get_current()
100159
seen.assert_called_with(user, module=settings.LAST_SEEN_DEFAULT_MODULE,
@@ -140,8 +199,8 @@ def test_clear_interval(self, cache, filter):
140199
clear_interval(user)
141200

142201
filter.assert_called_with(user=user)
143-
expected = ['last_seen:1:mod1:1', 'last_seen:1:mod2:1']
144-
cache.delete_many.assert_called_with(expected)
202+
expected = {'last_seen:1:mod1:1': -1, 'last_seen:1:mod2:1': -1}
203+
cache.set_many.assert_called_with(expected)
145204

146205
@mock.patch('last_seen.models.LastSeen.objects.filter', autospec=True)
147206
@mock.patch('last_seen.models.cache', autospec=True)
@@ -154,6 +213,17 @@ def test_clear_interval_none(self, cache, filter):
154213
filter.assert_called_with(user=user)
155214
self.assertFalse(cache.delete_many.called)
156215

216+
def test_clear_interval_works(self):
217+
user = User.objects.create(username='testuser')
218+
219+
user_seen(user)
220+
when1 = LastSeen.objects.when(user=user)
221+
clear_interval(user)
222+
user_seen(user)
223+
when2 = LastSeen.objects.when(user=user)
224+
225+
self.assertNotEqual(when1, when2)
226+
157227

158228
class TestMiddleware(TestCase):
159229

test_project/settings.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
DATABASES = {
77
'default': {
88
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
9-
'NAME': ':memory:', # Or path to database file if using sqlite3.
9+
'NAME': 'test.db', # Or path to database file if using sqlite3.
1010
'USER': '', # Not used with sqlite3.
1111
'PASSWORD': '', # Not used with sqlite3.
1212
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
@@ -33,3 +33,15 @@
3333
'last_seen',
3434
)
3535

36+
STATIC_URL = "/static/"
37+
38+
MIDDLEWARE_CLASSES = (
39+
'django.middleware.common.CommonMiddleware',
40+
'django.contrib.sessions.middleware.SessionMiddleware',
41+
'django.middleware.csrf.CsrfViewMiddleware',
42+
'django.middleware.locale.LocaleMiddleware',
43+
'django.contrib.auth.middleware.AuthenticationMiddleware',
44+
'django.contrib.messages.middleware.MessageMiddleware',
45+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
46+
'last_seen.middleware.LastSeenMiddleware',
47+
)

test_project/urls.py

+10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
from django.conf.urls import patterns, include, url
2+
from last_seen.models import clear_interval
3+
from django.shortcuts import redirect
24

35
# Uncomment the next two lines to enable the admin:
46
from django.contrib import admin
57
admin.autodiscover()
68

9+
10+
def clear(request):
11+
""" Testing view to force clear interval of user"""
12+
clear_interval(request.user)
13+
return redirect("/admin")
14+
15+
716
urlpatterns = patterns('',
817
# Examples:
918
# url(r'^$', 'test_project.views.home', name='home'),
@@ -14,4 +23,5 @@
1423

1524
# Uncomment the next line to enable the admin:
1625
url(r'^admin/', include(admin.site.urls)),
26+
url(r'^clear/', clear),
1727
)

0 commit comments

Comments
 (0)