From a4071c74585a78bb3739b2f13d4f09b851b4a5a1 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Tue, 5 Nov 2013 16:22:36 +0000 Subject: [PATCH 01/24] django.conf.urls.defaults was deprecated in 1.4, removed in 1.6. --- locking/admin.py | 5 +---- locking/tests/urls.py | 2 +- locking/urls.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/locking/admin.py b/locking/admin.py index da684b2..20a2664 100644 --- a/locking/admin.py +++ b/locking/admin.py @@ -64,10 +64,7 @@ def get_urls(self): admin:%(app_label)s_%(object_name)s_lock_status admin:%(app_label)s_%(object_name)s_lock_js """ - try: - from django.conf.urls.defaults import patterns, url - except ImportError: - from django.conf.urls import patterns, url + from django.conf.urls import patterns, url def wrap(view): curried_view = curry(view, self) diff --git a/locking/tests/urls.py b/locking/tests/urls.py index 0270c76..6c36c1f 100644 --- a/locking/tests/urls.py +++ b/locking/tests/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import patterns, include +from django.conf.urls import patterns, include from django.contrib import admin diff --git a/locking/urls.py b/locking/urls.py index 7731ccc..cac7ce5 100644 --- a/locking/urls.py +++ b/locking/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import patterns +from django.conf.urls import patterns from warnings import warn From 7df29d74c08386a953cf9712b76ea83ef4f04641 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Tue, 5 Nov 2013 16:23:20 +0000 Subject: [PATCH 02/24] Use timezone-aware datetimes. --- locking/managers.py | 7 ++++--- locking/models.py | 7 ++++--- locking/tests/tests.py | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/locking/managers.py b/locking/managers.py index e1b9ed4..09e6089 100644 --- a/locking/managers.py +++ b/locking/managers.py @@ -1,10 +1,11 @@ from django.db.models import Q, Manager from locking import settings as locking_settings import datetime +from django.utils import timezone """ LOCKED - if (datetime.today() - self.locked_at).seconds < LOCK_TIMEOUT: + if (timezone.now() - self.locked_at).seconds < LOCK_TIMEOUT: self.locked_at < (NOW - TIMEOUT) @@ -12,7 +13,7 @@ def point_of_timeout(): delta = datetime.timedelta(seconds=locking_settings.LOCK_TIMEOUT) - return datetime.datetime.now() - delta + return timezone.now() - delta class LockedManager(Manager): def get_query_set(self): @@ -22,4 +23,4 @@ def get_query_set(self): class UnlockedManager(Manager): def get_query_set(self): timeout = point_of_timeout() - return super(UnlockedManager, self).get_query_set().filter(Q(_locked_at__lte=timeout) | Q(_locked_at__isnull=True)) \ No newline at end of file + return super(UnlockedManager, self).get_query_set().filter(Q(_locked_at__lte=timeout) | Q(_locked_at__isnull=True)) diff --git a/locking/models.py b/locking/models.py index eb5161b..473e1cc 100644 --- a/locking/models.py +++ b/locking/models.py @@ -2,6 +2,7 @@ from datetime import datetime from django.db import models +from django.utils import timezone # Forward compat with Django 1.5's custom user models from django.conf import settings @@ -142,7 +143,7 @@ def is_locked(self): """ if not isinstance(self.locked_at, datetime): return False - return datetime.now() < self.lock_expiration_time + return timezone.now() < self.lock_expiration_time @property @@ -169,7 +170,7 @@ def lock_seconds_remaining(self): """ if not self.locked_at: return 0 - locked_delta = datetime.now() - self.locked_at + locked_delta = timezone.now() - self.locked_at # If the lock has already expired, there are 0 seconds remaining if locking_settings.TIME_UNTIL_EXPIRATION < locked_delta: return 0 @@ -208,7 +209,7 @@ def lock_for(self, user, hard_lock=True, lock_duration=None, override=False): else: raise ObjectLockedError("This object is already locked by another" " user. May not override, except through the `unlock` method.") - locked_at = datetime.now() + locked_at = timezone.now() if lock_duration: locked_at += lock_duration - locking_settings.TIME_UNTIL_EXPIRATION self._locked_at = locked_at diff --git a/locking/tests/tests.py b/locking/tests/tests.py index bbcc0c5..33b1868 100644 --- a/locking/tests/tests.py +++ b/locking/tests/tests.py @@ -1,9 +1,10 @@ -from datetime import datetime, timedelta +from datetime import timedelta import json from django.core.urlresolvers import reverse from django.test.client import Client from django.contrib.auth.models import User +from django.utils import timezone from locking import models, views, settings as locking_settings from locking.tests.utils import TestCase @@ -74,13 +75,13 @@ def test_hard_unlock_for_disallowed(self): def test_lock_expiration(self): self.story.lock_for(self.user) self.assertTrue(self.story.is_locked) - self.story._locked_at = datetime.today() - timedelta(minutes=locking_settings.LOCK_TIMEOUT+1) + self.story._locked_at = timezone.now() - timedelta(minutes=locking_settings.LOCK_TIMEOUT+1) self.assertFalse(self.story.is_locked) def test_lock_expiration_day(self): self.story.lock_for(self.user) self.assertTrue(self.story.is_locked) - self.story._locked_at = datetime.today() - timedelta(days=1, seconds=1) + self.story._locked_at = timezone.now() - timedelta(days=1, seconds=1) self.assertFalse(self.story.is_locked) def test_lock_seconds_remaining(self): From 79dd347e1de54712472626eb7440a16d44da4d5b Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 10:14:18 +0100 Subject: [PATCH 03/24] django-staticfiles was folded into Django 1.3. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 4caacf6..619832f 100644 --- a/setup.py +++ b/setup.py @@ -22,5 +22,4 @@ download_url='http://www.github.com/RobCombs/django-locking/tarball/master', license='BSD', packages=find_packages(), - install_requires=['django-staticfiles'], ) From 390b9f2cb853e8fa1e5b9fee630de5e2c2726ab8 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 10:33:45 +0100 Subject: [PATCH 04/24] Support Django >= 1.8 without losing < 1.6. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on npinchot’s PR to RobCombs, but with BC from the Django release notes for 1.6. --- locking/managers.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/locking/managers.py b/locking/managers.py index 09e6089..6af1ace 100644 --- a/locking/managers.py +++ b/locking/managers.py @@ -1,3 +1,4 @@ +import django from django.db.models import Q, Manager from locking import settings as locking_settings import datetime @@ -15,12 +16,20 @@ def point_of_timeout(): delta = datetime.timedelta(seconds=locking_settings.LOCK_TIMEOUT) return timezone.now() - delta + class LockedManager(Manager): - def get_query_set(self): + def get_queryset(self): timeout = point_of_timeout() return super(LockedManager, self).get_query_set().filter(_locked_at__gt=timeout, _locked_at__isnull=False) + if django.VERSION < (1, 6): + get_query_set = get_queryset + + class UnlockedManager(Manager): - def get_query_set(self): + def get_queryset(self): timeout = point_of_timeout() return super(UnlockedManager, self).get_query_set().filter(Q(_locked_at__lte=timeout) | Q(_locked_at__isnull=True)) + + if django.VERSION < (1, 6): + get_query_set = get_queryset From 23d10a9b6e92944893fbdc5eaae036df104a5e20 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 10:40:44 +0100 Subject: [PATCH 05/24] Use staticfiles to generate URL for our JS. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Otherwise you can’t reliably use more sophisticated features of the storage backends, like hashed URLs as implemented by CachedFilesMixin. Since you should be either using that approach, or prefixing your static fileset with something unique per release (say a version number or the git SHA) then we drop the “?v=6” version suffix on our Javascript at the same time. --- locking/admin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locking/admin.py b/locking/admin.py index 20a2664..1ae9594 100644 --- a/locking/admin.py +++ b/locking/admin.py @@ -7,6 +7,7 @@ from django.contrib import admin from django import forms +from django.contrib.staticfiles.templatetags.staticfiles import static from django.core.urlresolvers import reverse from django.utils import html as html_utils from django.utils.functional import curry @@ -37,10 +38,10 @@ class LockableAdminMixin(object): def media(self): return super(LockableAdminMixin, self).media + forms.Media(**{ 'js': ( - locking_settings.STATIC_URL + "locking/js/admin.locking.js?v=6", + static("locking/js/admin.locking.js"), ), 'css': { - 'all': (locking_settings.STATIC_URL + 'locking/css/locking.css',), + 'all': (static('locking/css/locking.css'),), }}) def locking_media(self, obj=None): From 17429b7a71960a149b191f56755e360992fd1781 Mon Sep 17 00:00:00 2001 From: Nate Pinchot Date: Fri, 1 May 2015 10:49:41 +0100 Subject: [PATCH 06/24] Update HttpResponse params for Django 1.7+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed 'mimetype' param to 'content_type’; this is supported from Django 1.5, so removes support for 1.4 and earlier. --- README.md | 2 +- locking/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2e6d68f..f7762aa 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Django has seen great adoption in the content management sphere, especially amon ``django-locking`` makes sure no two users can edit the same content at the same time, preventing annoying overwrites and lost time. Find the repository and download the code at http://github.com/stdbrouw/django-locking -``django-locking`` has only been tested on Django 1.2 and 1.3, but probably works from 1.0 onwards. +``django-locking`` is intended to support Django >= 1.5. Documentation ------------- diff --git a/locking/views.py b/locking/views.py index f7f9ff4..799d713 100644 --- a/locking/views.py +++ b/locking/views.py @@ -105,7 +105,7 @@ def render_lock_status(request, lock=None, status=200): 'locked_by_name': locked_by_name, 'applies': lock.lock_applies_to(request.user), }) - return HttpResponse(json_encode(data), mimetype='application/json', status=status) + return HttpResponse(json_encode(data), content_type='application/json', status=status) def lock_status(model_admin, request, object_id, extra_context=None, **kwargs): @@ -145,4 +145,4 @@ def locking_js(model_admin, request, object_id, extra_context=None): ? DJANGO_LOCKING : {{}}; DJANGO_LOCKING.config = {config_data} """).strip().format(config_data=json_encode(js_vars)) - return HttpResponse(response_js, mimetype='application/x-javascript') + return HttpResponse(response_js, content_type='application/x-javascript') From dd78deb88f267451c9c4c2180e6b6e5ad6275090 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 10:58:54 +0100 Subject: [PATCH 07/24] List static files as package data. --- setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup.py b/setup.py index 619832f..4736772 100644 --- a/setup.py +++ b/setup.py @@ -22,4 +22,11 @@ download_url='http://www.github.com/RobCombs/django-locking/tarball/master', license='BSD', packages=find_packages(), + package_data={ + 'locking': [ + 'static/locking/css/*', + 'static/locking/js/*', + 'static/locking/img/*', + ], + }, ) From 7a43283a4f8f5cbc6b9b77c68b5165a5968c0e00 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 10:59:46 +0100 Subject: [PATCH 08/24] Remove long_description from package. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since we don’t ship README.md, we can’t rely on it in setup.py or sdists won’t actually install. Which is bad. --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 4736772..951e719 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,9 @@ import os from setuptools import setup, find_packages -README = os.path.join(os.path.dirname(__file__), 'README.md') -long_description = open(README).read() + setup(name='django-locking', version='2.2.13', description=("Prevents users from doing concurrent editing in Django. Works out of the box in the admin interface, or you can integrate it with your own apps using a public API."), - long_description=long_description, classifiers=['Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Framework :: Django', From 0985135edc8b066065c3a74beb0a72febe6ce012 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Tue, 5 Nov 2013 16:51:33 +0000 Subject: [PATCH 09/24] Update documentation to remotely match code. --- README.md | 101 ++++++++++++++---------------------------------------- 1 file changed, 26 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index f7762aa..15f467c 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,21 @@ Django has seen great adoption in the content management sphere, especially amon ``django-locking`` makes sure no two users can edit the same content at the same time, preventing annoying overwrites and lost time. Find the repository and download the code at http://github.com/stdbrouw/django-locking -``django-locking`` is intended to support Django >= 1.5. +This version of ``django-locking`` is intended to work with Django 1.5-1.6, although it's only been tested cursorily and probably shouldn't be relied on without reading the code, then having a good sit down with a cup of tea and a hard think. -Documentation -------------- -Forked from the Django Locking plugin at stdbrouw/django-locking, this code features the cream of the crop for django-locking combining features from over 4 repos! +Credit +------ + +This code is basically a composition of the following repos with a taste of detailed descretion from me. Credit goes out to the following authors and repos for their contributions: + +https://github.com/stdbrouw/django-locking +https://github.com/runekaagaard/django-locking +https://github.com/theatlantic/django-locking +https://github.com/ortsed/django-locking + +Major features +============== -New features added to this fork -=============================== Changes on change list pages ---------------------------- @@ -36,6 +43,7 @@ ________________________________________________________________________________ Consolidated username and lock icon into one column on change list page + Changes in settings: ---------------------------- @@ -75,81 +83,33 @@ Refactored and cleaned up code for easier maintainability Simplified installation by coupling common functionality into base admin/form/model classes -10 Minute Install ------------------ +5 Minute Install +---------------- -1) Get the code: +1) Install: - git clone git@github.com:RobCombs/django-locking.git + pip install git+https://github.com/jaylett/django-locking.git#egg=django-locking -2) Install the django-locking python egg: - - cd django-locking - sudo python setup.py install - -3) Add locking to the list of INSTALLED_APPS in project settings file: +2) Add locking to the list of INSTALLED_APPS in project settings file; you also need `django.contrib.staticfiles` (probably already there): - INSTALLED_APPS = ('locking',) + INSTALLED_APPS = ('locking', 'django.contrib.staticfiles') -4) Add the following url mapping to your urls.py file: - - urlpatterns = patterns('', - (r'^admin/ajax/', include('locking.urls')), - ) - -5) Add locking to the admin files that you want locking for: +3) Add locking to the admin files that you want locking for: from locking.admin import LockableAdmin class YourAdmin(LockableAdmin): list_display = ('get_lock_for_admin') -6) Add warning and expiration time outs to your Django settings file: +4) Add warning and expiration time outs to your Django settings file: LOCKING = {'time_until_expiration': 120, 'time_until_warning': 60} - -7) Build the Lock table in the database: +5) Build the Lock table in the database: django-admin.py/manage.py migrate locking (For south users. Recommended approach) OR django-admin.py/manage.py syncdb (For non south users) -8) Install django-locking media: - - cp -r django-locking/locking/media/locking $your static media directory - -Note: This is the step where people usually get lost. -Just start up your django server and look for the 200/304s http responses when the server attempts to load the media -as you navigate to a model change list/view page where you've enabled django-locking. If you see 404s, you put the media in the wrong directory! - -You should see something like this in the django server console: - -[02/May/2012 15:33:20] "GET /media/static/locking/css/locking.css HTTP/1.1" 304 0 - -[02/May/2012 15:33:20] "GET /media/static/web/common/javascript/jquery-1.4.4.min.js HTTP/1.1" 304 0 - -[02/May/2012 15:33:20] "GET /media/static/locking/js/jquery.url.packed.js HTTP/1.1" 304 0 - -[02/May/2012 15:33:21] "GET /admin/ajax/variables.js HTTP/1.1" 200 114 - -[02/May/2012 15:33:21] "GET /media/static/locking/js/admin.locking.js?v=1 HTTP/1.1" 304 0 - -[02/May/2012 15:33:21] "GET /admin/ajax/redirects/medleyobjectredirect/14/is_locked/?_=1335987201245 HTTP/1.1" 200 0 - -[02/May/2012 15:33:21] "GET /admin/ajax/redirects/medleyobjectredirect/14/lock/?_=1335987201295 HTTP/1.1" 200 0 - - -You can also hit the media directly for troubleshooting your django-locking media installation: -http://www.local.wsbradio.com:8000/media/static/locking/js/admin.locking.js -If the url resolves, then you've completed this step correctly! -Basically, the code refers to the media like so. That's why you needed to do this step. - - class Media: - js = ( 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js', - 'static/locking/js/jquery.url.packed.js', - "/admin/ajax/variables.js", - "static/locking/js/admin.locking.js?v=1") - css = {"all": ("static/locking/css/locking.css",) - } +Note that Django's built-in staticfiles cannot serve from an egg, so don't clone from a repo and try to install things that way (unless you create an sdist first). Do the above and you should be fine. That's it! @@ -162,8 +122,8 @@ You'll see locks in the interface similar to the screen shots above. You can also look at your server console and you'll see the client making ajax calls to the django server checking for locks like so: - [04/May/2012 15:15:09] "GET /admin/ajax/redirects/medleyobjectredirect/14/is_locked/?_=1336158909826 HTTP/1.1" 200 0 - [04/May/2012 15:15:09] "GET /admin/ajax/redirects/medleyobjectredirect/14/lock/?_=1336158909858 HTTP/1.1" 200 0 + [04/May/2012 15:15:09] "GET /admin/editorial/dispatch/14/locking_variables.js HTTP/1.1" 200 488 + [04/May/2012 15:15:09] "GET /admin/editorial/dispatch/14/lock/?_=1383670147604 HTTP/1.1" 200 200 Optional -------- @@ -186,12 +146,3 @@ Example: self.cleaned_data = super(MedleyRedirectForm, self).clean() ...some code return self.cleaned_data - -CREDIT ------- -This code is basically a composition of the following repos with a taste of detailed descretion from me. Credit goes out to the following authors and repos for their contributions -and my job for funding this project: -https://github.com/stdbrouw/django-locking -https://github.com/runekaagaard/django-locking -https://github.com/theatlantic/django-locking -https://github.com/ortsed/django-locking \ No newline at end of file From 6e39b7a9bb372a0a23c256314fc2c13eaca7d97f Mon Sep 17 00:00:00 2001 From: Daniel Craigmile Date: Mon, 24 Nov 2014 18:37:45 -0600 Subject: [PATCH 10/24] move broken tests out of the way --- locking/tests/{tests.py => old_tests.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename locking/tests/{tests.py => old_tests.py} (100%) diff --git a/locking/tests/tests.py b/locking/tests/old_tests.py similarity index 100% rename from locking/tests/tests.py rename to locking/tests/old_tests.py From 2a4c1e7675b3b6f4f8000b76005b0e66c2fc73ac Mon Sep 17 00:00:00 2001 From: Daniel Craigmile Date: Mon, 24 Nov 2014 18:38:00 -0600 Subject: [PATCH 11/24] add factories --- locking/factories.py | 7 +++++++ requirements.txt | 3 +++ 2 files changed, 10 insertions(+) create mode 100644 locking/factories.py create mode 100644 requirements.txt diff --git a/locking/factories.py b/locking/factories.py new file mode 100644 index 0000000..26be9c4 --- /dev/null +++ b/locking/factories.py @@ -0,0 +1,7 @@ +import factory + +from . import models + +class LockFactory(factory.Factory): + class Meta: + model = models.Lock diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..629aed4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# Dev requirements +Django<1.8 +factory_boy==2.5.2 From 43a2bc84510befb6c1ec42a5ca00b307c63cc836 Mon Sep 17 00:00:00 2001 From: Daniel Craigmile Date: Mon, 24 Nov 2014 18:39:54 -0600 Subject: [PATCH 12/24] add a test for lock_seconds_remaining --- locking/tests/tests.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 locking/tests/tests.py diff --git a/locking/tests/tests.py b/locking/tests/tests.py new file mode 100644 index 0000000..3e3802a --- /dev/null +++ b/locking/tests/tests.py @@ -0,0 +1,14 @@ +from locking.factories import LockFactory +from django.conf import settings +from datetime import datetime +from django.test import TestCase + +class ExpirationTestCase(TestCase): + + def test_foo(self): + lock = LockFactory(_locked_at=datetime.now()) + settings.LOCKING['time_until_expiration'] = 100 + actual = lock.lock_seconds_remaining + expected = 100 + # allow for the time it's taking the test to run: + self.assertAlmostEqual(expected, actual, delta=1) From a2163f3d1a9de7f0ac6b80c9cdb24bcc3b740bb2 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 11:38:07 +0100 Subject: [PATCH 13/24] Make this test pass. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * can’t override time_until_expiration setting because locking hides its actual settings away (and we can’t use @override_settings) consequently just test against the default * move test factories somewhere sensible * timezone-aware datetimes --- locking/{ => tests}/factories.py | 2 +- locking/tests/tests.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) rename locking/{ => tests}/factories.py (81%) diff --git a/locking/factories.py b/locking/tests/factories.py similarity index 81% rename from locking/factories.py rename to locking/tests/factories.py index 26be9c4..6e3b146 100644 --- a/locking/factories.py +++ b/locking/tests/factories.py @@ -1,6 +1,6 @@ import factory -from . import models +from .. import models class LockFactory(factory.Factory): class Meta: diff --git a/locking/tests/tests.py b/locking/tests/tests.py index 3e3802a..ed90b2b 100644 --- a/locking/tests/tests.py +++ b/locking/tests/tests.py @@ -1,14 +1,14 @@ -from locking.factories import LockFactory from django.conf import settings -from datetime import datetime from django.test import TestCase +from django.utils import timezone + +from .factories import LockFactory + class ExpirationTestCase(TestCase): - def test_foo(self): - lock = LockFactory(_locked_at=datetime.now()) - settings.LOCKING['time_until_expiration'] = 100 + def test_lock_seconds_remaining(self): + lock = LockFactory(_locked_at=timezone.now()) actual = lock.lock_seconds_remaining - expected = 100 - # allow for the time it's taking the test to run: + expected = 120 # must match default in locking.settings self.assertAlmostEqual(expected, actual, delta=1) From b17a39ae6489916868a65f19c80a42ac22531909 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 11:49:35 +0100 Subject: [PATCH 14/24] Simple test runner, Django 1.6+. --- runtests.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 runtests.py diff --git a/runtests.py b/runtests.py new file mode 100644 index 0000000..4681dab --- /dev/null +++ b/runtests.py @@ -0,0 +1,30 @@ +import os.path +import subprocess +import sys +from django.conf import settings + +settings.configure( + DEBUG=True, + INSTALLED_APPS=[ + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'locking', + ], + SECRET_KEY='empty', + LOCKING={ + 'time_until_expiration': 120, + 'time_until_warning': 60 + }, + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'test', + }, + }, +) + +from django.test.runner import DiscoverRunner +test_runner = DiscoverRunner(verbosity=1, failfast=False) +failures = test_runner.run_tests(['locking', ]) +if failures: #pragma no cover + sys.exit(failures) From c4c7783812070d8a86911f76a7fbba140ccfc252 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 11:50:22 +0100 Subject: [PATCH 15/24] Drop Django 1.5 support. --- locking/managers.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/locking/managers.py b/locking/managers.py index 6af1ace..e2df8d9 100644 --- a/locking/managers.py +++ b/locking/managers.py @@ -22,14 +22,8 @@ def get_queryset(self): timeout = point_of_timeout() return super(LockedManager, self).get_query_set().filter(_locked_at__gt=timeout, _locked_at__isnull=False) - if django.VERSION < (1, 6): - get_query_set = get_queryset - class UnlockedManager(Manager): def get_queryset(self): timeout = point_of_timeout() return super(UnlockedManager, self).get_query_set().filter(Q(_locked_at__lte=timeout) | Q(_locked_at__isnull=True)) - - if django.VERSION < (1, 6): - get_query_set = get_queryset From c1addc48629f9a56ac232b5f10465d008237250f Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 11:57:21 +0100 Subject: [PATCH 16/24] Move migrations into South 1.0 location. In preparation for creating Django 1.7 migrations for this app. --- locking/{migrations => south_migrations}/0001_initial.py | 0 ...ield_lock_app__del_field_lock_entry_id__del_field_lock_mode.py | 0 locking/{migrations => south_migrations}/__init__.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename locking/{migrations => south_migrations}/0001_initial.py (100%) rename locking/{migrations => south_migrations}/0002_auto__del_field_lock_app__del_field_lock_entry_id__del_field_lock_mode.py (100%) rename locking/{migrations => south_migrations}/__init__.py (100%) diff --git a/locking/migrations/0001_initial.py b/locking/south_migrations/0001_initial.py similarity index 100% rename from locking/migrations/0001_initial.py rename to locking/south_migrations/0001_initial.py diff --git a/locking/migrations/0002_auto__del_field_lock_app__del_field_lock_entry_id__del_field_lock_mode.py b/locking/south_migrations/0002_auto__del_field_lock_app__del_field_lock_entry_id__del_field_lock_mode.py similarity index 100% rename from locking/migrations/0002_auto__del_field_lock_app__del_field_lock_entry_id__del_field_lock_mode.py rename to locking/south_migrations/0002_auto__del_field_lock_app__del_field_lock_entry_id__del_field_lock_mode.py diff --git a/locking/migrations/__init__.py b/locking/south_migrations/__init__.py similarity index 100% rename from locking/migrations/__init__.py rename to locking/south_migrations/__init__.py From ba28109d06d4fc467942f28d65a91bc344c41716 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 11:58:19 +0100 Subject: [PATCH 17/24] Setup Django explicitly for tests (1.7 only). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Otherwise the app registry won’t be ready, and probably some other things. --- runtests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/runtests.py b/runtests.py index 4681dab..4d7aa6a 100644 --- a/runtests.py +++ b/runtests.py @@ -1,6 +1,8 @@ import os.path import subprocess import sys + +import django from django.conf import settings settings.configure( @@ -23,6 +25,9 @@ }, ) +if hasattr(django, 'setup'): + django.setup() + from django.test.runner import DiscoverRunner test_runner = DiscoverRunner(verbosity=1, failfast=False) failures = test_runner.run_tests(['locking', ]) From 562e4f78a827d9706ff15af22a274343a9d37f39 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 12:03:48 +0100 Subject: [PATCH 18/24] Create Django 1.7 migration. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also include manage.py & manage-specific settings file, just for making migrations. Feels a bit grotty, but it’s a pain to make migrations otherwise. --- README.md | 2 +- locking/migrations/0001_initial.py | 29 +++++++++++++++++++++++++++++ locking/migrations/__init__.py | 0 manage.py | 10 ++++++++++ manage_settings.py | 12 ++++++++++++ 5 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 locking/migrations/0001_initial.py create mode 100644 locking/migrations/__init__.py create mode 100644 manage.py create mode 100644 manage_settings.py diff --git a/README.md b/README.md index 15f467c..2225292 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Django has seen great adoption in the content management sphere, especially amon ``django-locking`` makes sure no two users can edit the same content at the same time, preventing annoying overwrites and lost time. Find the repository and download the code at http://github.com/stdbrouw/django-locking -This version of ``django-locking`` is intended to work with Django 1.5-1.6, although it's only been tested cursorily and probably shouldn't be relied on without reading the code, then having a good sit down with a cup of tea and a hard think. +This version of ``django-locking`` is intended to work with Django 1.6-1.7, although it's only been tested cursorily and probably shouldn't be relied on without reading the code, then having a good sit down with a cup of tea and a hard think. Credit ------ diff --git a/locking/migrations/0001_initial.py b/locking/migrations/0001_initial.py new file mode 100644 index 0000000..6bde380 --- /dev/null +++ b/locking/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# encoding: utf8 +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '__first__'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Lock', + fields=[ + (u'id', models.AutoField(verbose_name=u'ID', serialize=False, auto_created=True, primary_key=True)), + ('content_type', models.ForeignKey(to='contenttypes.ContentType', to_field=u'id')), + ('object_id', models.PositiveIntegerField()), + ('_locked_at', models.DateTimeField(null=True, editable=False, db_column='locked_at')), + ('_locked_by', models.ForeignKey(db_column='locked_by', to_field=u'id', editable=False, to=settings.AUTH_USER_MODEL, null=True)), + ('_hard_lock', models.BooleanField(default=False, editable=False, db_column='hard_lock')), + ], + options={ + u'ordering': ('-_locked_at',), + }, + bases=(models.Model,), + ), + ] diff --git a/locking/migrations/__init__.py b/locking/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..a12ef4a --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "manage_settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/manage_settings.py b/manage_settings.py new file mode 100644 index 0000000..b15627e --- /dev/null +++ b/manage_settings.py @@ -0,0 +1,12 @@ +INSTALLED_APPS=[ + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'locking', +] +SECRET_KEY='empty' +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'test', + }, +} From 134c488a6a9c6c46309ee2d37b4694413866c146 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 12:03:59 +0100 Subject: [PATCH 19/24] Note where the 120s figure actually comes from. --- locking/tests/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locking/tests/tests.py b/locking/tests/tests.py index ed90b2b..2667b4e 100644 --- a/locking/tests/tests.py +++ b/locking/tests/tests.py @@ -10,5 +10,5 @@ class ExpirationTestCase(TestCase): def test_lock_seconds_remaining(self): lock = LockFactory(_locked_at=timezone.now()) actual = lock.lock_seconds_remaining - expected = 120 # must match default in locking.settings + expected = 120 # as set in runtests.py self.assertAlmostEqual(expected, actual, delta=1) From 6fbb83eeeb034f55d240393b7f134e833ca4f22d Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 12:54:01 +0100 Subject: [PATCH 20/24] Make 1.6 migration work with custom user models. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.7 migration looks like it’ll work out of the box (less surprisingly). Credit for approach: http://kevindias.com/writing/django-custom-user-models-south-and-reusable-apps/ --- locking/south_migrations/0001_initial.py | 51 ++++++++++-------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/locking/south_migrations/0001_initial.py b/locking/south_migrations/0001_initial.py index a0db825..8b7821d 100644 --- a/locking/south_migrations/0001_initial.py +++ b/locking/south_migrations/0001_initial.py @@ -3,6 +3,15 @@ from south.db import db from south.v2 import SchemaMigration from django.db import models +from django.conf import settings +from django.contrib.auth import get_user_model + +User = get_user_model() + +# With the default User model these will be 'auth.User' and 'auth.user' +# so instead of using orm['auth.User'] we can use orm[user_orm_label] +user_orm_label = '%s.%s' % (User._meta.app_label, User._meta.object_name) +user_model_label = '%s.%s' % (User._meta.app_label, User._meta.module_name) class Migration(SchemaMigration): def forwards(self, orm): @@ -14,7 +23,7 @@ def forwards(self, orm): ('app', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)), ('model', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)), ('entry_id', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)), - ('_locked_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='working_on_locking_lock', null=True, db_column='locked_by', to=orm['auth.User'])), + ('_locked_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='working_on_locking_lock', null=True, db_column='locked_by', to=orm[user_orm_label])), ('_hard_lock', self.gf('django.db.models.fields.BooleanField')(default=False, db_column='hard_lock', blank=True)), )) db.send_create_signal('locking', ['Lock']) @@ -25,34 +34,16 @@ def backwards(self, orm): db.delete_table('locking_lock') models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + user_model_label: { + 'Meta': { + 'object_name': User.__name__, + 'db_table': "'%s'" % User._meta.db_table + }, + User._meta.pk.attname: ( + 'django.db.models.fields.AutoField', [], + {'primary_key': 'True', + 'db_column': "'%s'" % User._meta.pk.column} + ), }, 'contenttypes.contenttype': { 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, @@ -65,7 +56,7 @@ def backwards(self, orm): 'Meta': {'object_name': 'Lock'}, '_hard_lock': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_column': "'hard_lock'", 'blank': 'True'}), '_locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_column': "'locked_at'"}), - '_locked_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'working_on_locking_lock'", 'null': 'True', 'db_column': "'locked_by'", 'to': "orm['auth.User']"}), + '_locked_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'working_on_locking_lock'", 'null': 'True', 'db_column': "'locked_by'", 'to': "orm['%s']" % user_orm_label}), 'app': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), 'entry_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), From 0b8a7e20d920ef96b015263ac58bd13a228d0fee Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 13:14:36 +0100 Subject: [PATCH 21/24] Display locking user using get_username(). This is part of the 1.5+ custom user model API. .username is not. --- locking/admin.py | 4 ++-- locking/views.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/locking/admin.py b/locking/admin.py index 1ae9594..f973b77 100644 --- a/locking/admin.py +++ b/locking/admin.py @@ -171,11 +171,11 @@ def get_lock_for_admin(self, obj): locked_by_name = lock.locked_by.get_full_name() if locked_by_name: locked_by_name = u"%(username)s (%(fullname)s)" % { - 'username': lock.locked_by.username, + 'username': lock.locked_by.get_username(), 'fullname': locked_by_name, } else: - locked_by_name = lock.locked_by.username + locked_by_name = lock.locked_by.get_username() if lock.locked_by.pk == current_user_id: msg = _(u"You own this lock for %s longer") % until diff --git a/locking/views.py b/locking/views.py index 799d713..cdc6c2a 100644 --- a/locking/views.py +++ b/locking/views.py @@ -92,16 +92,16 @@ def render_lock_status(request, lock=None, status=200): locked_by_name = lock.locked_by.get_full_name() if locked_by_name: locked_by_name = u"%(username)s (%(fullname)s)" % { - 'username': lock.locked_by.username, + 'username': lock.locked_by.get_username(), 'fullname': locked_by_name, } else: - locked_by_name = lock.locked_by.username + locked_by_name = lock.locked_by.get_username() data.update({ 'lock_pk': lock.pk, - 'current_user': getattr(request.user, 'username', None), + 'current_user': request.user.get_username(), 'is_active': lock.is_locked, - 'locked_by': getattr(lock.locked_by, 'username', None), + 'locked_by': lock.locked_by and lock.locked_by.get_username() or None, 'locked_by_name': locked_by_name, 'applies': lock.lock_applies_to(request.user), }) From c5b4bfaa071febdd63e186bbffe146b5148e7cba Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 1 May 2015 13:45:29 +0100 Subject: [PATCH 22/24] Support Grappelli admin as well as Django default. --- locking/static/locking/js/admin.locking.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locking/static/locking/js/admin.locking.js b/locking/static/locking/js/admin.locking.js index 08e51e3..9db7f2d 100644 --- a/locking/static/locking/js/admin.locking.js +++ b/locking/static/locking/js/admin.locking.js @@ -321,7 +321,7 @@ var DJANGO_LOCKING = DJANGO_LOCKING || {}; }; $(document).ready(function() { - var $target = $("#content-inner, #content").eq(0); + var $target = $("#content-inner, #content, #grp-content").eq(0); var $notificationElement = $('
').prependTo($target); $notificationElement.djangoLocking(); }); From 23dcdeb30749f79215b66b83a6423ff8ba9cad06 Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 15 May 2015 18:05:55 +0100 Subject: [PATCH 23/24] Prepare for Django 1.8: queryset -> get_queryset. --- locking/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locking/admin.py b/locking/admin.py index f973b77..2e0e3e1 100644 --- a/locking/admin.py +++ b/locking/admin.py @@ -138,14 +138,14 @@ def save_model(self, request, obj, *args, **kwargs): lock.unlock_for(request.user) super(LockableAdminMixin, self).save_model(request, obj, *args, **kwargs) - def queryset(self, request): + def get_queryset(self, request): """ Extended queryset method which adds a custom SQL select column, `_locking_user_pk`, which is set to the pk of the current request's user instance. Doing this allows us to access the user id by obj._locking_user_pk for any object returned from this queryset. """ - qs = super(LockableAdminMixin, self).queryset(request) + qs = super(LockableAdminMixin, self).get_queryset(request) return qs.extra(select={ '_locking_user_pk': "%d" % request.user.pk, }) From b5e57e84bbb276eff801a3d514ba9954c1a39fca Mon Sep 17 00:00:00 2001 From: James Aylett Date: Fri, 15 May 2015 18:06:12 +0100 Subject: [PATCH 24/24] Document BC for South migrations, if you need them. --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 2225292..40f80d7 100644 --- a/README.md +++ b/README.md @@ -146,3 +146,25 @@ Example: self.cleaned_data = super(MedleyRedirectForm, self).clean() ...some code return self.cleaned_data + +Migration dependencies under South +---------------------------------- + +In order for tests to work you need to ensure that the migration which +creates the ``Lock`` model runs after your user model is created. This +should generally work if you put the ``locking`` app after whatever +app provides the user model (``auth.user`` or your own app if you're +using a custom user model), but if you don't want to do that or it +doesn't work you'll want to add a reverse dependency from the +migration in your app that creates the user model. + +For South v1.0 you want to do the following: + + class Migration(SchemaMigration): + + needed_by = ( + ("locking", "0001_initial"), + ) + +Django 1.7's migrations system takes care of this for you +automatically.