Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a4071c7
django.conf.urls.defaults was deprecated in 1.4, removed in 1.6.
jaylett Nov 5, 2013
7df29d7
Use timezone-aware datetimes.
jaylett Nov 5, 2013
79dd347
django-staticfiles was folded into Django 1.3.
jaylett May 1, 2015
390b9f2
Support Django >= 1.8 without losing < 1.6.
jaylett May 1, 2015
23d10a9
Use staticfiles to generate URL for our JS.
jaylett May 1, 2015
17429b7
Update HttpResponse params for Django 1.7+
npinchot May 1, 2015
dd78deb
List static files as package data.
jaylett May 1, 2015
7a43283
Remove long_description from package.
jaylett May 1, 2015
0985135
Update documentation to remotely match code.
jaylett Nov 5, 2013
6e39b7a
move broken tests out of the way
Nov 25, 2014
2a4c1e7
add factories
Nov 25, 2014
43a2bc8
add a test for lock_seconds_remaining
Nov 25, 2014
a2163f3
Make this test pass.
jaylett May 1, 2015
b17a39a
Simple test runner, Django 1.6+.
jaylett May 1, 2015
c4c7783
Drop Django 1.5 support.
jaylett May 1, 2015
c1addc4
Move migrations into South 1.0 location.
jaylett May 1, 2015
ba28109
Setup Django explicitly for tests (1.7 only).
jaylett May 1, 2015
562e4f7
Create Django 1.7 migration.
jaylett May 1, 2015
134c488
Note where the 120s figure actually comes from.
jaylett May 1, 2015
6fbb83e
Make 1.6 migration work with custom user models.
jaylett May 1, 2015
0b8a7e2
Display locking user using get_username().
jaylett May 1, 2015
c5b4bfa
Support Grappelli admin as well as Django default.
jaylett May 1, 2015
23dcdeb
Prepare for Django 1.8: queryset -> get_queryset.
jaylett May 15, 2015
b5e57e8
Document BC for South migrations, if you need them.
jaylett May 15, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 47 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------------

Expand All @@ -36,6 +43,7 @@ ________________________________________________________________________________


Consolidated username and lock icon into one column on change list page

Changes in settings:
----------------------------

Expand Down Expand Up @@ -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!

Expand All @@ -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
--------
Expand All @@ -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
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.
18 changes: 8 additions & 10 deletions locking/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand Down Expand Up @@ -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,
})
Expand All @@ -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
Expand Down
14 changes: 9 additions & 5 deletions locking/managers.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
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)
"""

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))
return super(UnlockedManager, self).get_query_set().filter(Q(_locked_at__lte=timeout) | Q(_locked_at__isnull=True))
98 changes: 26 additions & 72 deletions locking/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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,),
),
]
Loading