diff --git a/README.md b/README.md index 2e6d68f..40f80d7 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`` has only been tested on Django 1.2 and 1.3, but probably works from 1.0 onwards. +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. -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 ------------------ - -1) Get the code: +5 Minute Install +---------------- - git clone git@github.com:RobCombs/django-locking.git +1) Install: -2) Install the django-locking python egg: - - cd django-locking - sudo python setup.py install + pip install git+https://github.com/jaylett/django-locking.git#egg=django-locking -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 -------- @@ -187,11 +147,24 @@ Example: ...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 +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. diff --git a/locking/admin.py b/locking/admin.py index da684b2..2e0e3e1 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): @@ -64,10 +65,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) @@ -140,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, }) @@ -173,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/managers.py b/locking/managers.py index e1b9ed4..e2df8d9 100644 --- a/locking/managers.py +++ b/locking/managers.py @@ -1,10 +1,12 @@ +import django 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,14 +14,16 @@ 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): + def get_queryset(self): timeout = point_of_timeout() return super(LockedManager, self).get_query_set().filter(_locked_at__gt=timeout, _locked_at__isnull=False) + 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)) \ 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/migrations/0001_initial.py b/locking/migrations/0001_initial.py index a0db825..6bde380 100644 --- a/locking/migrations/0001_initial.py +++ b/locking/migrations/0001_initial.py @@ -1,75 +1,29 @@ -# encoding: utf-8 -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models +# encoding: utf8 +from django.db import models, migrations +from django.conf import settings -class Migration(SchemaMigration): - def forwards(self, orm): - - # Adding model 'Lock' - db.create_table('locking_lock', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('_locked_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_column='locked_at')), - ('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'])), - ('_hard_lock', self.gf('django.db.models.fields.BooleanField')(default=False, db_column='hard_lock', blank=True)), - )) - db.send_create_signal('locking', ['Lock']) - def backwards(self, orm): - - # Deleting model 'Lock' - db.delete_table('locking_lock') +class Migration(migrations.Migration): - 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'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'locking.lock': { - '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']"}), - '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'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) - } - } - complete_apps = ['locking'] + 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/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/south_migrations/0001_initial.py b/locking/south_migrations/0001_initial.py new file mode 100644 index 0000000..8b7821d --- /dev/null +++ b/locking/south_migrations/0001_initial.py @@ -0,0 +1,66 @@ +# encoding: utf-8 +import datetime +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): + + # Adding model 'Lock' + db.create_table('locking_lock', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('_locked_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_column='locked_at')), + ('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[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']) + + def backwards(self, orm): + + # Deleting model 'Lock' + db.delete_table('locking_lock') + + models = { + 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'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'locking.lock': { + '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['%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'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + } + } + complete_apps = ['locking'] 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/south_migrations/__init__.py b/locking/south_migrations/__init__.py new file mode 100644 index 0000000..e69de29 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(); }); diff --git a/locking/tests/factories.py b/locking/tests/factories.py new file mode 100644 index 0000000..6e3b146 --- /dev/null +++ b/locking/tests/factories.py @@ -0,0 +1,7 @@ +import factory + +from .. import models + +class LockFactory(factory.Factory): + class Meta: + model = models.Lock diff --git a/locking/tests/old_tests.py b/locking/tests/old_tests.py new file mode 100644 index 0000000..33b1868 --- /dev/null +++ b/locking/tests/old_tests.py @@ -0,0 +1,320 @@ +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 +from locking.tests import models as testmodels + + +json_decode = json.JSONDecoder().decode + + +class AppTestCase(TestCase): + + fixtures = ['locking_scenario',] + + def setUp(self): + self.alt_story, self.story = testmodels.Story.objects.all() + users = User.objects.all() + self.user, self.alt_user = users + + def test_hard_lock(self): + # you can save a hard lock once (to initiate the lock) + # but after that saving without first unlocking raises an error + self.story.lock_for(self.user, hard_lock=True) + self.assertEquals(self.story.lock_type, "hard") + self.story.save() + self.assertRaises(models.ObjectLockedError, self.story.save) + + def test_soft_lock(self): + self.story.lock_for(self.user) + self.story.save() + self.assertEquals(self.story.lock_type, "soft") + self.story.save() + + def test_lock_for(self): + self.story.lock_for(self.user) + self.assertTrue(self.story.is_locked) + self.story.save() + self.assertTrue(self.story.is_locked) + + def test_lock_for_overwrite(self): + # we shouldn't be able to overwrite an active lock by another user + self.story.lock_for(self.alt_user) + self.assertRaises(models.ObjectLockedError, self.story.lock_for, self.user) + + def test_unlock(self): + self.story.lock_for(self.user) + self.story.unlock() + self.assertFalse(self.story.is_locked) + + def test_hard_unlock(self): + self.story.lock_for(self.user, hard_lock=True) + self.story.unlock_for(self.user) + self.assertFalse(self.story.is_locked) + self.story.unlock() + + def test_unlock_for_self(self): + self.story.lock_for(self.user) + self.story.unlock_for(self.user) + self.assertFalse(self.story.is_locked) + + def test_unlock_for_disallowed(self, hard_lock=False): + # we shouldn't be able to disengage a lock that was put in place by another user + self.story.lock_for(self.alt_user, hard_lock=hard_lock) + self.assertRaises(models.ObjectLockedError, self.story.unlock_for, self.user) + + def test_hard_unlock_for_disallowed(self): + self.test_unlock_for_disallowed(hard_lock=True) + + def test_lock_expiration(self): + self.story.lock_for(self.user) + self.assertTrue(self.story.is_locked) + 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 = timezone.now() - timedelta(days=1, seconds=1) + self.assertFalse(self.story.is_locked) + + def test_lock_seconds_remaining(self): + self.story.lock_for(self.user) + expected = locking_settings.LOCK_TIMEOUT + self.assertTrue(self.story.lock_seconds_remaining <= expected + 1 and + self.story.lock_seconds_remaining >= expected - 1, + "%d not close to %d" % ( + self.story.lock_seconds_remaining, expected)) + + def test_lock_seconds_remaining_half_timeout(self): + self.story.lock_for(self.user) + self.story._locked_at -= timedelta(seconds=(locking_settings.LOCK_TIMEOUT / 2)) + expected = locking_settings.LOCK_TIMEOUT / 2 + self.assertTrue(self.story.lock_seconds_remaining <= expected + 1 and + self.story.lock_seconds_remaining >= expected - 1, + "%d not close to %d" % ( + self.story.lock_seconds_remaining, expected)) + + def test_lock_seconds_remaining_day(self): + self.story.lock_for(self.user) + self.story._locked_at -= timedelta(days=1) + expected = locking_settings.LOCK_TIMEOUT - (24 * 60 * 60) + self.assertTrue(self.story.lock_seconds_remaining <= expected + 1 and + self.story.lock_seconds_remaining >= expected - 1, + "%d not close to %d" % ( + self.story.lock_seconds_remaining, expected)) + + def test_lock_applies_to(self): + self.story.lock_for(self.alt_user) + applies = self.story.lock_applies_to(self.user) + self.assertTrue(applies) + + def test_lock_doesnt_apply_to(self): + self.story.lock_for(self.user) + applies = self.story.lock_applies_to(self.user) + self.assertFalse(applies) + + def test_is_locked_by(self): + self.story.lock_for(self.user) + self.assertEquals(self.story.locked_by, self.user) + + def test_is_unlocked(self): + # this might seem like a silly test, but an object + # should be unlocked unless it has actually been locked + self.assertFalse(self.story.is_locked) + + def test_locking_bit_when_locking(self): + # when we've locked something, we should set an administrative + # bit so other developers can know a save will do a lock or + # unlock and respond to that information if they so wish. + self.story.content = "Blah" + self.assertEquals(self.story._state.locking, False) + self.story.lock_for(self.user) + self.assertEquals(self.story._state.locking, True) + self.story.save() + self.assertEquals(self.story._state.locking, False) + + def test_locking_bit_when_unlocking(self): + # when we've locked something, we should set an administrative + # bit so other developers can know a save will do a lock or + # unlock and respond to that information if they so wish. + self.story.content = "Blah" + self.assertEquals(self.story._state.locking, False) + self.story.lock_for(self.user) + self.story.unlock_for(self.user) + self.assertEquals(self.story._state.locking, True) + self.story.save() + self.assertEquals(self.story._state.locking, False) + + def test_unlocked_manager(self): + self.story.lock_for(self.user) + self.story.save() + self.assertEquals(testmodels.Story.objects.count(), 2) + self.assertEquals(testmodels.Story.unlocked.count(), 1) + self.assertEquals(testmodels.Story.unlocked.get(pk=self.alt_story.pk).pk, 1) + self.assertRaises(testmodels.Story.DoesNotExist, testmodels.Story.unlocked.get, pk=self.story.pk) + self.assertNotEquals(testmodels.Story.unlocked.all()[0].pk, self.story.pk) + + def test_locked_manager(self): + self.story.lock_for(self.user) + self.story.save() + self.assertEquals(testmodels.Story.objects.count(), 2) + self.assertEquals(testmodels.Story.locked.count(), 1) + self.assertEquals(testmodels.Story.locked.get(pk=self.story.pk).pk, 2) + self.assertRaises(testmodels.Story.DoesNotExist, testmodels.Story.locked.get, pk=self.alt_story.pk) + self.assertEquals(testmodels.Story.locked.all()[0].pk, self.story.pk) + + def test_managers(self): + self.story.lock_for(self.user) + self.story.save() + locked = testmodels.Story.locked.all() + unlocked = testmodels.Story.unlocked.all() + self.assertEquals(locked.count(), 1) + self.assertEquals(unlocked.count(), 1) + self.assertTrue(len(set(locked).intersection(set(unlocked))) == 0) + + +users = [ + # Stan is a superuser + { + "username": "Stan", + "password": "green pastures" + }, + # Fred has pretty much no permissions whatsoever + { + "username": "Fred", + "password": "pastures of green" + }, +] + + +class BrowserTestCase(TestCase): + + fixtures = ['locking_scenario',] + apps = ('locking.tests', 'django.contrib.auth', 'django.contrib.admin', ) + # REFACTOR: + # urls = 'locking.tests.urls' + + def setUp(self): + # some objects we might use directly, instead of via the client + self.story = story = testmodels.Story.objects.all()[0] + user_objs = User.objects.all() + self.user, self.alt_user = user_objs + # client setup + self.c = Client() + self.c.login(**users[0]) + # refactor: http://docs.djangoproject.com/en/dev/topics/testing/#urlconf-configuration + # is probably a smarter way to go about this + info = ('tests', 'story') + self.urls = { + "change": reverse('admin:%s_%s_change' % info, args=[story.pk]), + "changelist": reverse('admin:%s_%s_changelist' % info), + "lock": reverse('admin:%s_%s_lock' % info, args=[story.pk]), + "unlock": reverse('admin:%s_%s_unlock' % info, args=[story.pk]), + "is_locked": reverse('admin:%s_%s_lock_status' % info, args=[story.pk]), + "is_locked": reverse('admin:%s_%s_lock_js' % info, args=[story.pk]), + } + + def tearDown(self): + pass + + # Some terminology: + # - 'disallowed' is when the locking system does not allow a certain operation + # - 'unauthorized' is when Django does not permit a user to do something + # - 'unauthenticated' is when a user is logged out of Django + + def test_lock_when_allowed(self): + res = self.c.get(self.urls['lock']) + self.assertEquals(res.status_code, 200) + # reload our test story + story = testmodels.Story.objects.get(pk=self.story.id) + self.assertTrue(story.is_locked) + + def test_lock_when_logged_out(self): + self.c.logout() + res = self.c.get(self.urls['lock']) + self.assertEquals(res.status_code, 401) + + def test_lock_when_unauthorized(self): + # when a user doesn't have permission to change the model + # this tests the user_may_change_model decorator + self.c.logout() + self.c.login(**users[1]) + res = self.c.get(self.urls['lock']) + self.assertEquals(res.status_code, 401) + + def test_lock_when_does_not_apply(self): + # don't make a resource available to lock models that don't + # have locking enabled -- this tests the is_lockable decorator + obj = testmodels.Unlockable.objects.get(pk=1) + args = [obj._meta.app_label, obj._meta.module_name, obj.pk] + url = reverse(views.lock, args=args) + res = self.c.get(url) + self.assertEquals(res.status_code, 404) + + def test_lock_when_disallowed(self): + self.story.lock_for(self.alt_user) + self.story.save() + res = self.c.get(self.urls['lock']) + self.assertEquals(res.status_code, 403) + + def test_unlock_when_allowed(self): + self.story.lock_for(self.user) + self.story.save() + res = self.c.get(self.urls['unlock']) + self.assertEquals(res.status_code, 200) + # reload our test story + story = testmodels.Story.objects.get(pk=self.story.id) + self.assertFalse(story.is_locked) + + def test_unlock_when_disallowed(self): + self.story.lock_for(self.alt_user) + self.story.save() + res = self.c.get(self.urls['unlock']) + self.assertEquals(res.status_code, 403) + + def test_is_locked_when_applies(self): + self.story.lock_for(self.alt_user) + self.story.save() + res = self.c.get(self.urls['is_locked']) + res = json_decode(res.content) + self.assertTrue(res['applies']) + self.assertTrue(res['is_active']) + + def test_is_locked_when_self(self): + self.story.lock_for(self.user) + self.story.save() + res = self.c.get(self.urls['is_locked']) + res = json_decode(res.content) + self.assertFalse(res['applies']) + self.assertTrue(res['is_active']) + + def test_js_variables(self): + res = self.c.get(self.urls['js_variables']) + self.assertEquals(res.status_code, 200) + self.assertContains(res, locking_settings.LOCK_TIMEOUT) + + def test_admin_media(self): + res = self.c.get(self.urls['change']) + self.assertContains(res, 'admin.locking.js') + + def test_admin_changelist_when_locked(self): + self.story.lock_for(self.alt_user) + self.story.save() + res = self.c.get(self.urls['changelist']) + self.assertContains(res, 'locking/img/lock.png') + + def test_admin_changelist_when_locked_self(self): + self.test_lock_when_allowed() + res = self.c.get(self.urls['changelist']) + self.assertContains(res, 'locking/img/page_edit.png') + + def test_admin_changelist_when_unlocked(self): + res = self.c.get(self.urls['changelist']) + self.assertNotContains(res, 'locking/img') diff --git a/locking/tests/tests.py b/locking/tests/tests.py index bbcc0c5..2667b4e 100644 --- a/locking/tests/tests.py +++ b/locking/tests/tests.py @@ -1,319 +1,14 @@ -from datetime import datetime, timedelta -import json +from django.conf import settings +from django.test import TestCase +from django.utils import timezone -from django.core.urlresolvers import reverse -from django.test.client import Client -from django.contrib.auth.models import User +from .factories import LockFactory -from locking import models, views, settings as locking_settings -from locking.tests.utils import TestCase -from locking.tests import models as testmodels - -json_decode = json.JSONDecoder().decode - - -class AppTestCase(TestCase): - - fixtures = ['locking_scenario',] - - def setUp(self): - self.alt_story, self.story = testmodels.Story.objects.all() - users = User.objects.all() - self.user, self.alt_user = users - - def test_hard_lock(self): - # you can save a hard lock once (to initiate the lock) - # but after that saving without first unlocking raises an error - self.story.lock_for(self.user, hard_lock=True) - self.assertEquals(self.story.lock_type, "hard") - self.story.save() - self.assertRaises(models.ObjectLockedError, self.story.save) - - def test_soft_lock(self): - self.story.lock_for(self.user) - self.story.save() - self.assertEquals(self.story.lock_type, "soft") - self.story.save() - - def test_lock_for(self): - self.story.lock_for(self.user) - self.assertTrue(self.story.is_locked) - self.story.save() - self.assertTrue(self.story.is_locked) - - def test_lock_for_overwrite(self): - # we shouldn't be able to overwrite an active lock by another user - self.story.lock_for(self.alt_user) - self.assertRaises(models.ObjectLockedError, self.story.lock_for, self.user) - - def test_unlock(self): - self.story.lock_for(self.user) - self.story.unlock() - self.assertFalse(self.story.is_locked) - - def test_hard_unlock(self): - self.story.lock_for(self.user, hard_lock=True) - self.story.unlock_for(self.user) - self.assertFalse(self.story.is_locked) - self.story.unlock() - - def test_unlock_for_self(self): - self.story.lock_for(self.user) - self.story.unlock_for(self.user) - self.assertFalse(self.story.is_locked) - - def test_unlock_for_disallowed(self, hard_lock=False): - # we shouldn't be able to disengage a lock that was put in place by another user - self.story.lock_for(self.alt_user, hard_lock=hard_lock) - self.assertRaises(models.ObjectLockedError, self.story.unlock_for, self.user) - - def test_hard_unlock_for_disallowed(self): - self.test_unlock_for_disallowed(hard_lock=True) - - 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.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.assertFalse(self.story.is_locked) +class ExpirationTestCase(TestCase): def test_lock_seconds_remaining(self): - self.story.lock_for(self.user) - expected = locking_settings.LOCK_TIMEOUT - self.assertTrue(self.story.lock_seconds_remaining <= expected + 1 and - self.story.lock_seconds_remaining >= expected - 1, - "%d not close to %d" % ( - self.story.lock_seconds_remaining, expected)) - - def test_lock_seconds_remaining_half_timeout(self): - self.story.lock_for(self.user) - self.story._locked_at -= timedelta(seconds=(locking_settings.LOCK_TIMEOUT / 2)) - expected = locking_settings.LOCK_TIMEOUT / 2 - self.assertTrue(self.story.lock_seconds_remaining <= expected + 1 and - self.story.lock_seconds_remaining >= expected - 1, - "%d not close to %d" % ( - self.story.lock_seconds_remaining, expected)) - - def test_lock_seconds_remaining_day(self): - self.story.lock_for(self.user) - self.story._locked_at -= timedelta(days=1) - expected = locking_settings.LOCK_TIMEOUT - (24 * 60 * 60) - self.assertTrue(self.story.lock_seconds_remaining <= expected + 1 and - self.story.lock_seconds_remaining >= expected - 1, - "%d not close to %d" % ( - self.story.lock_seconds_remaining, expected)) - - def test_lock_applies_to(self): - self.story.lock_for(self.alt_user) - applies = self.story.lock_applies_to(self.user) - self.assertTrue(applies) - - def test_lock_doesnt_apply_to(self): - self.story.lock_for(self.user) - applies = self.story.lock_applies_to(self.user) - self.assertFalse(applies) - - def test_is_locked_by(self): - self.story.lock_for(self.user) - self.assertEquals(self.story.locked_by, self.user) - - def test_is_unlocked(self): - # this might seem like a silly test, but an object - # should be unlocked unless it has actually been locked - self.assertFalse(self.story.is_locked) - - def test_locking_bit_when_locking(self): - # when we've locked something, we should set an administrative - # bit so other developers can know a save will do a lock or - # unlock and respond to that information if they so wish. - self.story.content = "Blah" - self.assertEquals(self.story._state.locking, False) - self.story.lock_for(self.user) - self.assertEquals(self.story._state.locking, True) - self.story.save() - self.assertEquals(self.story._state.locking, False) - - def test_locking_bit_when_unlocking(self): - # when we've locked something, we should set an administrative - # bit so other developers can know a save will do a lock or - # unlock and respond to that information if they so wish. - self.story.content = "Blah" - self.assertEquals(self.story._state.locking, False) - self.story.lock_for(self.user) - self.story.unlock_for(self.user) - self.assertEquals(self.story._state.locking, True) - self.story.save() - self.assertEquals(self.story._state.locking, False) - - def test_unlocked_manager(self): - self.story.lock_for(self.user) - self.story.save() - self.assertEquals(testmodels.Story.objects.count(), 2) - self.assertEquals(testmodels.Story.unlocked.count(), 1) - self.assertEquals(testmodels.Story.unlocked.get(pk=self.alt_story.pk).pk, 1) - self.assertRaises(testmodels.Story.DoesNotExist, testmodels.Story.unlocked.get, pk=self.story.pk) - self.assertNotEquals(testmodels.Story.unlocked.all()[0].pk, self.story.pk) - - def test_locked_manager(self): - self.story.lock_for(self.user) - self.story.save() - self.assertEquals(testmodels.Story.objects.count(), 2) - self.assertEquals(testmodels.Story.locked.count(), 1) - self.assertEquals(testmodels.Story.locked.get(pk=self.story.pk).pk, 2) - self.assertRaises(testmodels.Story.DoesNotExist, testmodels.Story.locked.get, pk=self.alt_story.pk) - self.assertEquals(testmodels.Story.locked.all()[0].pk, self.story.pk) - - def test_managers(self): - self.story.lock_for(self.user) - self.story.save() - locked = testmodels.Story.locked.all() - unlocked = testmodels.Story.unlocked.all() - self.assertEquals(locked.count(), 1) - self.assertEquals(unlocked.count(), 1) - self.assertTrue(len(set(locked).intersection(set(unlocked))) == 0) - - -users = [ - # Stan is a superuser - { - "username": "Stan", - "password": "green pastures" - }, - # Fred has pretty much no permissions whatsoever - { - "username": "Fred", - "password": "pastures of green" - }, -] - - -class BrowserTestCase(TestCase): - - fixtures = ['locking_scenario',] - apps = ('locking.tests', 'django.contrib.auth', 'django.contrib.admin', ) - # REFACTOR: - # urls = 'locking.tests.urls' - - def setUp(self): - # some objects we might use directly, instead of via the client - self.story = story = testmodels.Story.objects.all()[0] - user_objs = User.objects.all() - self.user, self.alt_user = user_objs - # client setup - self.c = Client() - self.c.login(**users[0]) - # refactor: http://docs.djangoproject.com/en/dev/topics/testing/#urlconf-configuration - # is probably a smarter way to go about this - info = ('tests', 'story') - self.urls = { - "change": reverse('admin:%s_%s_change' % info, args=[story.pk]), - "changelist": reverse('admin:%s_%s_changelist' % info), - "lock": reverse('admin:%s_%s_lock' % info, args=[story.pk]), - "unlock": reverse('admin:%s_%s_unlock' % info, args=[story.pk]), - "is_locked": reverse('admin:%s_%s_lock_status' % info, args=[story.pk]), - "is_locked": reverse('admin:%s_%s_lock_js' % info, args=[story.pk]), - } - - def tearDown(self): - pass - - # Some terminology: - # - 'disallowed' is when the locking system does not allow a certain operation - # - 'unauthorized' is when Django does not permit a user to do something - # - 'unauthenticated' is when a user is logged out of Django - - def test_lock_when_allowed(self): - res = self.c.get(self.urls['lock']) - self.assertEquals(res.status_code, 200) - # reload our test story - story = testmodels.Story.objects.get(pk=self.story.id) - self.assertTrue(story.is_locked) - - def test_lock_when_logged_out(self): - self.c.logout() - res = self.c.get(self.urls['lock']) - self.assertEquals(res.status_code, 401) - - def test_lock_when_unauthorized(self): - # when a user doesn't have permission to change the model - # this tests the user_may_change_model decorator - self.c.logout() - self.c.login(**users[1]) - res = self.c.get(self.urls['lock']) - self.assertEquals(res.status_code, 401) - - def test_lock_when_does_not_apply(self): - # don't make a resource available to lock models that don't - # have locking enabled -- this tests the is_lockable decorator - obj = testmodels.Unlockable.objects.get(pk=1) - args = [obj._meta.app_label, obj._meta.module_name, obj.pk] - url = reverse(views.lock, args=args) - res = self.c.get(url) - self.assertEquals(res.status_code, 404) - - def test_lock_when_disallowed(self): - self.story.lock_for(self.alt_user) - self.story.save() - res = self.c.get(self.urls['lock']) - self.assertEquals(res.status_code, 403) - - def test_unlock_when_allowed(self): - self.story.lock_for(self.user) - self.story.save() - res = self.c.get(self.urls['unlock']) - self.assertEquals(res.status_code, 200) - # reload our test story - story = testmodels.Story.objects.get(pk=self.story.id) - self.assertFalse(story.is_locked) - - def test_unlock_when_disallowed(self): - self.story.lock_for(self.alt_user) - self.story.save() - res = self.c.get(self.urls['unlock']) - self.assertEquals(res.status_code, 403) - - def test_is_locked_when_applies(self): - self.story.lock_for(self.alt_user) - self.story.save() - res = self.c.get(self.urls['is_locked']) - res = json_decode(res.content) - self.assertTrue(res['applies']) - self.assertTrue(res['is_active']) - - def test_is_locked_when_self(self): - self.story.lock_for(self.user) - self.story.save() - res = self.c.get(self.urls['is_locked']) - res = json_decode(res.content) - self.assertFalse(res['applies']) - self.assertTrue(res['is_active']) - - def test_js_variables(self): - res = self.c.get(self.urls['js_variables']) - self.assertEquals(res.status_code, 200) - self.assertContains(res, locking_settings.LOCK_TIMEOUT) - - def test_admin_media(self): - res = self.c.get(self.urls['change']) - self.assertContains(res, 'admin.locking.js') - - def test_admin_changelist_when_locked(self): - self.story.lock_for(self.alt_user) - self.story.save() - res = self.c.get(self.urls['changelist']) - self.assertContains(res, 'locking/img/lock.png') - - def test_admin_changelist_when_locked_self(self): - self.test_lock_when_allowed() - res = self.c.get(self.urls['changelist']) - self.assertContains(res, 'locking/img/page_edit.png') - - def test_admin_changelist_when_unlocked(self): - res = self.c.get(self.urls['changelist']) - self.assertNotContains(res, 'locking/img') + lock = LockFactory(_locked_at=timezone.now()) + actual = lock.lock_seconds_remaining + expected = 120 # as set in runtests.py + self.assertAlmostEqual(expected, actual, delta=1) 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 diff --git a/locking/views.py b/locking/views.py index f7f9ff4..cdc6c2a 100644 --- a/locking/views.py +++ b/locking/views.py @@ -92,20 +92,20 @@ 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), }) - 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') 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', + }, +} 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 diff --git a/runtests.py b/runtests.py new file mode 100644 index 0000000..4d7aa6a --- /dev/null +++ b/runtests.py @@ -0,0 +1,35 @@ +import os.path +import subprocess +import sys + +import django +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', + }, + }, +) + +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', ]) +if failures: #pragma no cover + sys.exit(failures) diff --git a/setup.py b/setup.py index 4caacf6..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', @@ -22,5 +20,11 @@ download_url='http://www.github.com/RobCombs/django-locking/tarball/master', license='BSD', packages=find_packages(), - install_requires=['django-staticfiles'], + package_data={ + 'locking': [ + 'static/locking/css/*', + 'static/locking/js/*', + 'static/locking/img/*', + ], + }, )