rparent / django-lock-tokens Goto Github PK
View Code? Open in Web Editor NEWA Django application that provides a locking mechanism to prevent concurrency editing.
License: MIT License
A Django application that provides a locking mechanism to prevent concurrency editing.
License: MIT License
models.py:
class ParentModel(LockableModel):
# some fields
class ChildModel(ParentModel):
# some extra fields
admin.py:
class ParentModelAdmin(LockableModelAdmin):
list_display = ['function_link_name']
list_display_links = None
# some other stuff
def function_link_name(self, obj):
if obj.is_locked():
return mark_safe(_('Someone is already editing %(name)s') % {'name': str(obj)})
else:
change_url = reverse('admin:xxxx_change', args=(obj.id,))
return mark_safe('<a class="grp-button" href="' + change_url + '" target="blank">' + str(obj) + '</a>')
class ChildModelAdmin(ParentModelAdmin):
# some other stuff here
When trying to load the changelist of ChildModel in the admin I get a Recursion Error:
"RecursionError: maximum recursion depth exceeded while calling a Python object"
ChildModel seems to be calling ParentModel, which in turn calls ChildModel etc.
My partner (@matthijskooijman) helped me kind of understand this issue and has a sort-of-working solution.
The offending line was obj = getattr(instance, key)
. Which for ChildModel looks up the attribute parentmodel_ptr
, which resolves to a ParentModel object which gets a call to getattr which follows the reverse link to the child and in turn gets a ChildModel object to call getattr on etc.
In utils.py
, changes in class_or_bound_method
, DLTModelProxyBase.__call__
and DLTModelProxyBase._bind_methods
def class_or_bound_method(function):
cm = classmethod(function)
cm._bind_at_instanciation = True
cm._orig_f = function
return cm
class DLTModelProxyBase(models.base.ModelBase):
def __call__(cls, *args, **kwargs):
instance = super(DLTModelProxyBase, cls).__call__(*args, **kwargs)
cls._bind_methods(instance, instance.__class__)
return instance
@classmethod
def _bind_methods(cls, instance, instance_class):
for (key, value) in instance_class.__dict__.items():
if getattr(value, '_bind_at_instanciation', False):
obj = getattr(instance, key)
setattr(instance, key, cls._get_bound_method(obj, instance))
for base in instance_class.__bases__:
cls._bind_methods(instance, base)
(Sidenote: instantiation is spelled wrong with a 'c'.)
This solution is not perfect and @matthijskooijman wants to take a better look at it later. For now this lets me continue the work I was doing because the error is gone, but without a fix and new release of django-lock-tokens I can not put it on the production server so I am looking forward to a neat solution. (Unfortunately my understanding of Python is not sufficient to help with this :-s )
It would be nice to understand the pros and cons of this package's token-based approach relative to other approaches (saxix/django-concurrency and gavinwahl/django-optimistic-lock).
See, e.g.,the discussion at https://github.com/gavinwahl/django-optimistic-lock#comparison-to-django-concurrency
Thanks!
Since my custom change_form template does not play well with the template provided by django-lock-tokens (see #4) I am trying to set and remove locks in the view that shows and handles the form.
On top of the view I have:
if check_for_session(schedule, request.session): # Returns True if lock was not found or was expired
try:
lock_for_session(schedule, request.session)
except AlreadyLockedError:
messages.error(request, ugettext_lazy('Someone else has locked the object you want to work on, you can not work on it at the moment.'), extra_tags='bg-danger')
return HttpResponseRedirect(reverse('admin:pe_schedule_changelist'))
Then some irrelevant code.
And just before redirecting after saving stuff I have:
if request.method == 'POST':
if form.is_valid() and formset.is_valid():
[ do all kinds of saving stuff ]
try:
unlock_for_session(workschedule, request.session)
except UnlockForbiddenError:
pass # TODO: should we do anything special? No lock, no big deal, right?
[ redirect to a different view ]
// when not valid, no unlocking just reshowing the form
// when not post, no unlocking just showing the form
So, this works rather well when I open a schedule to edit, and then save it. The lock is removed and someone else can edit the schedule.
However, when I leave the lock to expire (set it to 1 minute) while still in the editing interface and then saving, I get a bunch of errors.
Looks like something tries to get() a specific lock but this fails and is not caught. I think this is where a query is killed and the atomic thing isn't correctly closed.
Anybody got any idea how I should go about creating and deleting locks? How can I allow saving without errors when a lock is already expired?
Traceback:
File "C:\[path to virtualenv]\lib\site-packages\lock_tokens\managers.py" in get_or_create_for_object
17. return (self.get_for_object(obj), False)
File "C:\[path to virtualenv]\lib\site-packages\lock_tokens\managers.py" in get_for_object
13. locked_at__gte=get_oldest_valid_tokens_datetime())
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\manager.py" in manager_method
85. return getattr(self.get_queryset(), name)(*args, **kwargs)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\query.py" in get
380. self.model._meta.object_name
During handling of the above exception (LockToken matching query does not exist.), another exception occurred:
File "C:\[path to virtualenv]\lib\site-packages\django\db\backends\utils.py" in execute
64. return self.cursor.execute(sql, params)
File "C:\[path to virtualenv]\lib\site-packages\django\db\backends\sqlite3\base.py" in execute
328. return Database.Cursor.execute(self, query, params)
The above exception (UNIQUE constraint failed: lock_tokens_locktoken.locked_object_content_type_id, lock_tokens_locktoken.locked_object_id) was the direct cause of the following exception:
File "C:\[path to virtualenv]\lib\site-packages\lock_tokens\models.py" in save
59. return super(LockToken, self).save(*args, **opts)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\base.py" in save
808. force_update=force_update, update_fields=update_fields)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\base.py" in save_base
838. updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\base.py" in _save_table
924. result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\base.py" in _do_insert
963. using=using, raw=raw)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\manager.py" in manager_method
85. return getattr(self.get_queryset(), name)(*args, **kwargs)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\query.py" in _insert
1076. return query.get_compiler(using=using).execute_sql(return_id)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\sql\compiler.py" in execute_sql
1112. cursor.execute(sql, params)
File "C:\[path to virtualenv]\lib\site-packages\django\db\backends\utils.py" in execute
79. return super(CursorDebugWrapper, self).execute(sql, params)
File "C:\[path to virtualenv]\lib\site-packages\django\db\backends\utils.py" in execute
64. return self.cursor.execute(sql, params)
File "C:\[path to virtualenv]\lib\site-packages\django\db\utils.py" in __exit__
94. six.reraise(dj_exc_type, dj_exc_value, traceback)
File "C:\[path to virtualenv]\lib\site-packages\django\utils\six.py" in reraise
685. raise value.with_traceback(tb)
File "C:\[path to virtualenv]\lib\site-packages\django\db\backends\utils.py" in execute
64. return self.cursor.execute(sql, params)
File "C:\[path to virtualenv]\lib\site-packages\django\db\backends\sqlite3\base.py" in execute
328. return Database.Cursor.execute(self, query, params)
During handling of the above exception (UNIQUE constraint failed: lock_tokens_locktoken.locked_object_content_type_id, lock_tokens_locktoken.locked_object_id), another exception occurred:
File "C:\[path to virtualenv]\lib\site-packages\django\core\handlers\exception.py" in inner
41. response = get_response(request)
File "C:\[path to virtualenv]\lib\site-packages\django\core\handlers\base.py" in _get_response
187. response = self.process_exception_by_middleware(e, request)
File "C:\[path to virtualenv]\lib\site-packages\django\core\handlers\base.py" in _get_response
185. response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "C:\[path to virtualenv]\lib\site-packages\django\contrib\admin\options.py" in wrapper
552. return self.admin_site.admin_view(view)(*args, **kwargs)
File "C:\[path to virtualenv]\lib\site-packages\django\utils\decorators.py" in _wrapped_view
149. response = view_func(request, *args, **kwargs)
File "C:\[path to virtualenv]\lib\site-packages\django\views\decorators\cache.py" in _wrapped_view_func
57. response = view_func(request, *args, **kwargs)
File "C:\[path to virtualenv]\lib\site-packages\django\contrib\admin\sites.py" in inner
224. return view(request, *args, **kwargs)
File "C:\[path to virtualenv]\lib\site-packages\reversion\admin.py" in change_view
180. return super(VersionAdmin, self).change_view(request, object_id, form_url, extra_context)
File "C:\[path to virtualenv]\lib\site-packages\django\contrib\admin\options.py" in change_view
1512. return self.changeform_view(request, object_id, form_url, extra_context)
File "C:\_D_\Werk\Real Life Gaming\Community portal\complatform\pe\admin.py" in changeform_view
439. return fill_schedule(request, self, object_id, extra_context)
File "C:\[path to virtualenv]\lib\site-packages\django\contrib\auth\decorators.py" in _wrapped_view
23. return view_func(request, *args, **kwargs)
File "C:\_D_\Werk\Real Life Gaming\Community portal\complatform\pe\adminviews.py" in fill_schedule
1284. lock_for_session(schedule, request.session)
File "C:\[path to virtualenv]\lib\site-packages\lock_tokens\sessions.py" in lock_for_session
14. lock_token = LockableModel.lock(obj, token)
File "C:\[path to virtualenv]\lib\site-packages\lock_tokens\models.py" in lock
99. lock_token = self._lock(token)
File "C:\[path to virtualenv]\lib\site-packages\lock_tokens\models.py" in _lock
78. lock_token, created = LockToken.objects.get_or_create_for_object(self)
File "C:\[path to virtualenv]\lib\site-packages\lock_tokens\managers.py" in get_or_create_for_object
19. return (self.create(locked_object=obj), True)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\manager.py" in manager_method
85. return getattr(self.get_queryset(), name)(*args, **kwargs)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\query.py" in create
394. obj.save(force_insert=True, using=self.db)
File "C:\[path to virtualenv]\lib\site-packages\lock_tokens\models.py" in save
63. locked_object_content_type=self.locked_object_content_type)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\manager.py" in manager_method
85. return getattr(self.get_queryset(), name)(*args, **kwargs)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\query.py" in get
374. num = len(clone)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\query.py" in __len__
232. self._fetch_all()
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\query.py" in _fetch_all
1118. self._result_cache = list(self._iterable_class(self))
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\query.py" in __iter__
53. results = compiler.execute_sql(chunked_fetch=self.chunked_fetch)
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\sql\compiler.py" in execute_sql
899. raise original_exception
File "C:\[path to virtualenv]\lib\site-packages\django\db\models\sql\compiler.py" in execute_sql
889. cursor.execute(sql, params)
File "C:\[path to virtualenv]\lib\site-packages\django\db\backends\utils.py" in execute
79. return super(CursorDebugWrapper, self).execute(sql, params)
File "C:\[path to virtualenv]\lib\site-packages\django\db\backends\utils.py" in execute
59. self.db.validate_no_broken_transaction()
File "C:\[path to virtualenv]\lib\site-packages\django\db\backends\base\base.py" in validate_no_broken_transaction
448. "An error occurred in the current transaction. You can't "
Exception Type: TransactionManagementError at /nl/admin/pe/schedule/42/change/
Exception Value: An error occurred in the current transaction. You can't execute queries until the end of the 'atomic' block.
I'm trying to use LockableModelAdmin
for one of my models (extends LockableModel
) but since I am using a custom change_form.html
template I'm getting some unexpected results. My change_form.html
is basically a complete rebuild of the original admin/change_form.html
.
My whole installment/setup is complete. My modified change_form.html
extends the admin/lock_tokens_change_form.html
and I am passing the extra_context to the page (where the lock_token is added) but when I call {{block.super}}
I also get some stuff from Django's own {{ block content }}
that I do not want or need. Like an extra history button, an extra form tag, etc. When trying to save (getting the save buttons on the required spot is also a bit of a hassle) I get the message "data ManagementForm are missing or have been tampered with". All csrf tokens are in place (and some extra, perhaps that is the issue here...)
This setup works great for a normal model admin without custom templates, but for this case it would be nice if I could just include a template tag to just get all the necessary javascript from django-lock-tokens and do the checking on the presence of lock_token for the submit buttons in my own custom template.
Suggestion / feature request
A simple tag to include the necessary javascript in a custom template.
AFAIU, django-lock-tokens currently exposes the following interface
I wonder if the way expired locks are handled is the best approach here. In particular, consider this case:
In this case, Alice cannot renew its existing lock, since it has effectively disappeared. However, with the current implementation of lock checking, she can still save her changes, since there will be no lock active, which check_and_get_lock_token()
allows, but with a warning
However, now consider the case where, while Alice is gone, Bob takes a lock, makes some edit, saves the object and releases the lock again. When Alice returns, Bob's lock is gone again, so she can still save here pending changes (again, with a warning). However, this time, Alice's changes will overwrite Bob's changes, which I think is precisely what this locking mechanism should prevent.
The cause for this overwriting is that check_lock_token
(and thus also check_for_session
) returns True
when no active lock exists. To really prevent this, it should only return true when the given lock (or the lock from the session) that was taken when the editing started, is still valid and active. If not, some other user might have changed the object.
However, if you would implement this, the "hard" handling of locking (effectively deleting them as soon as they expire) becomes somewhat problematic: If a user lets his lock expire, he can no longer save his changes, even when no other user has made changes in the meanwhile, in which case saving would not be problematic. I would suggest that expired locks are handled a bit more softly.
Here, you can distinguish two cases:
I can imagine an implementation, where:
I believe that this could be implemented with minimal changes to the current code, since expired locks are already kept around in the database.
If the current hard approach is deemed useful as well (or you want to keep more compatibility), you could also consider introducing a soft and hard expiration time. When the soft expiration time is reached, a token remains valid, but can be deleted when another lock is taken. When the hard expiration time is reached, a token becomes invalid immediately. Either or both of these times can be set to None
to disable that expire time.
Similarly, changing the interpretation of no lock from valid (with warning logged) to invalid might not be appropriate for all usecases, but this might be a matter of adding an optional keyword argument (e.g. missing_ok=True
) or global configuration option.
For the first case above (e.g. allow renewing a lock when another user has taken a lock in the meanwhile, but has not actually saved any data), I said that this is out of scope for this package and is more in the area of optimistic locking libraries that save a version number for each object. However, I later realized a possible implementation that could be within scope. You could change the implementatoin to allow (multiple) expired locks to co-exist with one active lock for the same object. Then, instead of deleting expired locks when a new lock is taken, you keep them around but delete them only when saving the object (i.e. when unlocking, you would indicate whether the lock was actually "used" or just released again, and when it was used, you delete previous expired locks). This means that an expired lock stays around, and can be renewed, until a change is made, after which all previous locks become unused. There might be some atomicity issues here (in particular, the current unique constraint cannot be kept) but I suspect those can be solved by using proper transactions. This does mean a bigger change to the current code, though.
I use LockableModelAdmin
without any custom modifications. Right now the admin works as decribed in the readme: when you stay on the same page, the token expires after a certain amount of time (configured in settings).
I would like to change that behavior to:
when you stay on the same page, the token does not expire. This would require some js that keeps renewing the token. When looking at lock_tokens.js
and LockTokens.hold_lock
, this functionality seems already built-in. However, I don't see how I can enable it.
Any help is appreciated!
For a model extending LockableModelAdmin, I load the admin change form page. Then I open the same change form in a second tab. The second tab shows "You cannot edit this object". Now when attempting to save the first tab, you also see "You cannot edit this object". From then on I am fully locked out until the timeout, regardless of what I do.
Given that the lock token is stored on the session which is shared between tabs, probably the second tab should just renew the lock taken out by the first tab, even though this would allow you to overwrite on your own work.
I believe that the issue occurs because lock_for_session()
deletes the reference to the lock out of the session dictionary before it attempts to lock. Locking then fails, leaving us without the original lock token.
Thanks for the useful package! Excellent docs too!
My client asked for adding a functionality to the custom dashboard to lock the edit page for an object while somebody else is already on the page.
I have followed the doc and added the code in function 'view_with_object_edition' it successfully locks the page and if somebody try to access the same page from another session. but if the first user navigate to another page, it does not remove the lock automatically. The lock is not released evenafter the first user is logged out. How the lock can be removed automatically ?
I've tried to add locking to Django Admin as described in README, but it fails:
File "/usr/local/lib/python3.5/dist-packages/lock_tokens/admin.py" in change_view
32. lock_for_session(obj, request.session, force_new=force_new_session_lock)
File "/usr/local/lib/python3.5/dist-packages/lock_tokens/sessions.py" in lock_for_session
17. lock_token = LockableModel.lock(obj, token)
File "/usr/local/lib/python3.5/dist-packages/lock_tokens/models.py" in lock
112. lock_token = self._lock(token)
Exception Type: AttributeError at /admin/shop/order/95/change/
Exception Value: 'Order' object has no attribute '_lock'
I've looked in the code and it looks like proxying is broken. You call lock
as static method:
https://github.com/rparent/django-lock-tokens/blob/master/lock_tokens/sessions.py#L17
but it's not defined as static:
https://github.com/rparent/django-lock-tokens/blob/master/lock_tokens/models.py#L111
I use LockableModelAdmin
to provide basic locking of a change form. The issue is that when I press page reload the object gets locked. (In fact I have custom popup actions that reload the page but simple manual reload has the same effect). Tested in Chrome, can test in Firefox later.
Have you any ideas how to overcome this issue?
Tried to figure out how to use it directly in the Django templates to handle locking on the client side
. The following from the documentation doesn't help too much. Is there any better detailed example on it?
{% load lock_tokens_tags %}
{% lock_tokens_api_client %}
...
<script type="text/javascript">
window.addEventListener('lock_tokens.clientready', function () {
LockTokens.lock(...);
...
LockTokens.unlock(...);
});
</script>
Would it be possible to register the user in the lock token? Sometimes it is unclear who locked the page.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.