Coder Social home page Coder Social logo

rparent / django-lock-tokens Goto Github PK

View Code? Open in Web Editor NEW
18.0 3.0 11.0 86 KB

A Django application that provides a locking mechanism to prevent concurrency editing.

License: MIT License

Python 76.22% JavaScript 15.70% HTML 5.33% Makefile 2.75%
concurrency django-application django django-admin locking

django-lock-tokens's Introduction

django-lock-tokens

image

image

image

django-lock-tokens is a Django application that provides a locking mechanism to prevent concurrency editing.

It is not user-based nor session-based, it is just token based. When you lock a resource, you are given a token string with an expiration date, and you will need to provide this token to unlock that resource.

The application provides some useful functions to handle this token mechanism with sessions if you want to, and a REST API (with a javascript client for it) to deal with lock tokens without sessions.

Here is a non exhaustive list of the features coming with this token-based approach, to help you choose django-lock-tokens (or not!) over other concurrent edition preventing solutions:

  • No need to modify your models to use the locking mechanism : you don't "pollute" your datamodel with "non-data" fields. This also means you can use the locking mechanism on third party models that cannot be modified
  • No need to use sessions (but you can still use it if you want to)
  • Ability to check if an object is locked BEFORE trying to modify it
  • Rest API (+ javascript client to use it) out-of-the-box
  • Admin interface integration

Table of Contents

  1. Requirements
  2. Install
  3. TL;DR
  4. How it works
  5. LockableModel proxy
  6. LockableModelAdmin for admin interface
  7. Session-based usage: lock_tokens.sessions module
  8. Session-based usage: lock_tokens.decorators module
  9. REST API
  10. REST API Javascript client
  11. Settings
  12. Tests

Requirements

  • Python (2.7, 3.3, 3.4, 3.5)
  • Django (1.8, 1.9, 1.10, 1.11, 2.0, 2.1)

Install

  1. Run pip install django-lock-tokens
  2. Add lock_tokens to your INSTALLED_APPS setting. As django-lock-tokens uses the contenttypes framework, make sure it is also available in your INSTALLED_APPS setting:
INSTALLED_APPS = [
    ...
    'django.contrib.contenttypes',
    ...
    'lock_tokens.apps.LockTokensConfig',
]
  1. Run python manage.py migrate from the root of your django project to install the lock tokens model.
  2. If you want to use the LockableAdmin and all the session-based functionalities, make sure you have enabled a session middleware in your settings, for example:
MIDDLEWARE_CLASSES = (
    ...
    'django.contrib.sessions.middleware.SessionMiddleware',
    ...
)
  1. If you want to use the REST API, include lock_tokens.urls with the correct namespace in your urls.py like this (it is mandatory if you want to use the LockableModelAdmin):
urlpatterns = [
  ...
  url(r'^lock_tokens/', include('lock_tokens.urls', namespace='lock-tokens')),
  ...
]

TL;DR

After having completed previous steps, using the locking mechanism in your views is as simple as this:

from django.http import HttpResponseForbidden
from lock_tokens.exceptions import AlreadyLockedError, UnlockForbiddenError
from lock_tokens.sessions import check_for_session, lock_for_session, unlock_for_session

from my_app.models import MyModel


def view_with_object_edition(request):
    """This view locks the instance of MyModel that is to be edited."""
    # Get MyModel instance:
    obj = MyModel.objects.get(...)
    try:
        lock_for_session(obj, request.session)
    except AlreadyLockedError:
        return HttpResponseForbidden("This resource is locked, sorry !")
    # ... Do stuff
    return render(...)


def view_that_saves_object(request):
    """This view locks the instance of MyModel that is to be edited."""
    # Get MyModel instance:
    obj = MyModel.objects.get(...)
    if not check_for_session(obj, request.session):
        return HttpResponseForbidden("Cannot modify the object, you don't have the lock.")
    # ... Do stuff
    unlock_for_session(obj, request.session)
    return render(...)

Or use it directly in your Django templates to handle locking on the client side:

{% load lock_tokens_tags %}
{% lock_tokens_api_client %}
...
<script type="text/javascript">
    window.addEventListener('lock_tokens.clientready', function () {
        LockTokens.lock(...);
        ...
        LockTokens.unlock(...);
    });
</script>

How it works

To avoid concurrency editing, django-lock-tokens provides some interfaces to lock and check lock on any model instance before changing it (including third party model instances). This is handled via an internal model (LockToken). There can be only one LockToken instance per model instance.

The lock token lifecycle is the following:

  1. When a lock is created for an object by an entity, it is valid for a certain amount of time. The entity is given a lock token key (a string) that it must hold to perform actions with valid lock required. A new LockToken instance is created in database, after having deleted a potential expired instance in database.
  2. If the entity that holds the lock token key no longer needs the lock on the object, it can unlock this object by providing the lock token key. The LockToken instance is then removed from database.
  3. The entity that holds the lock token key can also renew the lock token by providing the lock token key.
  4. If the lock token is not renewed until the expiration time, it becomes expired, but stays in database until a new lock is created on this instance (or the entity that holds the lock token key deletes it).

So to use this mechanism correctly, you should require a valid lock token key and renew the lock in any method where an object is saved and you want to prevent concurrency editing. Based on the 4 previous points, we can see that there can be 3 cases for a lock token key:

  1. The lock token key has a corresponding lock token in database, and it has not expired.
  2. The lock token key has a corresponding lock token in database, but it has expired.
  3. The lock token key has no correponding lock token in database for the object.

For case 1, it is ok to save the object and then unlock the object by deleting the lock token. The token key is still VALID.

For case 2, the lock has expired but no other entity has created a lock on the object in the meantime. So it is still ok to save the object as it will not overwrite any changes. The token key is still VALID.

In case 3, it means that the lock token created by the entity has expired, and that another entity has taken a lock on the object in the meantime and could have done some changes on it. So it is not ok to save changes. The token key is INVALID.

Here is an example to understand the case 3:

  1. Alice takes a lock on an object and opens up its editing interface. A ``LockToken`` instance ``lt1`` is created in database, and Alice is given a lock token key
  2. Alice walks away from her computer, the lock expires. ``lt1`` is still in database
  3. Bob takes a lock on the same object. ``lt1`` is deleted from database, and a new ``LockToken`` instance ``lt2`` is created
  4. Bob edits the object in the interface, clicks save. The object is modified and the lock is released. ``lt2`` is deleted. The object has no longer any lock in database
  5. Alice returns, clicks save. The lock token key she holds has become invalid, so she gets an error.

This example shows how it is important to require a VALID lock token key to prevent concurrency editing.

LockableModel proxy

To make one of your models lockable, use the LockableModel class. LockableModel is just a Django proxy model, which simply provides additional locking methods to your models.

So you can either make your models inherit from LockableModel:

from lock_tokens.models import LockableModel

class MyModel(LockableModel):
    ...

obj = MyModel.get(...)
token = obj.lock()

or you can simply use it as a proxy on a given model instance:

from lock_tokens.models import LockableModel

from my_app.models import MyModel

obj = MyModel.get(...)
token = LockableModel.lock(obj)

This can be useful if you don't want to expose the locking methods for your models everywhere, or if you want to lock resources that come from a third party application.

Note that as LockableModel is just a proxy model, make your models inherit from it won't change their fields so there will be no additional migrations required.

Additionally, if your model inherits from LockableModel, the objects Manager has a specific method that allows you to get and lock a model like so:

>>>obj, token = MyModel.get_and_lock(...<usual get arguments>)

If you already overrided the default objects manager with a custom one and that you want to get this method available, make your custom manager inherit from lock_tokens.managers.LockableModelManager.

LockableModel.lock(self, token=None)

Locks the given object, or renew existing lock if the token parameter is provided.

Returns a dict containing a token a its expiration date.

Raises a lock_tokens.exceptions.AlreadyLockedError if the resource is already locked, and a lock_tokens.exceptions.InvalidToken if the specified token is invalid.

Example:

def test(myObject):
    try:
        token = myObject.lock()
    except AlreadyLockedError:
        print "This object is already locked"
    return token


>>>token = test(obj)
{"token": "9692ac52a27a40308b82b49b77357c97", "expires": "2016-06-23 09:48:06"}
>>>test(obj)
"This object is already locked"
>>>test(obj, token['token'])
{"token": "9692ac52a27a40308b82b49b77357c97", "expires": "2016-06-23 09:48:26"}

LockableModel.unlock(self, token)

Unlocks the given object if the provided token is correct.

Raises a lock_tokens.exceptions.UnlockForbiddenError

LockableModel.is_locked(self)

Returns a boolean that indicates whether the given object is currently locked or not.

LockableModel.check_lock(self, token)

Returns a boolean that indicates if the given token is valid for this object. Will also return True with a warning if the object is not locked (lock expired or no lock).

LockableModelAdmin for admin interface

If you want to make the admin interface lock-aware, and lock objects that are edited, simply make your ModelAdmin class inherit from LockableModelAdmin:

from lock_tokens.admin import LockableModelAdmin
from django.contrib import admin

from my_app.models import MyModel

class MyModelAdmin(LockableModelAdmin):
    ...

admin.site.register(MyModel, MyModelAdmin)

With this, when accessing a given instance of MyModel from the admin interface, it will check that the instance is not locked. If it is not, it will lock it. If it is, then there will be a warning message displayed to inform that the object cannot be edited, and the saving buttons will not appear. And if despite this, the change form is sent, it will raise a PermissionDenied exception so you will get a HTTP 403 error.

Overrinding change_form_template in LockableModelAdmin

If you want to override the change_form_template, but still make sure the lock will be released when leaving the page without saving, don't forget to add the admin_lock_handler template tag. This template tag needs 4 arguments: the application name of the object, the model name of the object, the object id and the lock token key. So don't forget to add those (especially the lock token) into your template context if you also override the change_view method.

Example to add the template tag to your custom template if you don't override `change_view`:

...
{% load lock_tokens_tags %}
...
{% if lock_token %}
  {% admin_lock_handler opts.app_label opts.model_name original.id lock_token %}
{% endif %}

Session-based usage: lock_tokens.sessions module

In most cases, it will be the easiest way to deal with lock tokens, as you won't need to handle them at all.

lock_for_session(obj, session, force_new=False)

Lock an object in the given session. This function will try to lock the object, and if it succeeds, it will hold the token value in a session variable.

There is a force_new optional parameter that you can set to True if you want to force a new lock generation without using a potentially existing token key stored in session. This is to be used with caution (i.e. exclusively in methods that only read the object, not in methods that save it) as it could lead to a potential overwriting if the session holds an invalid token. To sum up: do not set this parameter to True unless you are sure of what you are doing!

Raises a lock_tokens.exceptions.AlreadyLockedError if the resource is already locked, and a lock_tokens.exceptions.InvalidToken error if the session holds an invalid token.

unlock_for_session(obj, session)

Unlocks an object in the given session.

Raises a lock_tokens.exceptions.UnlockForbiddenError if the session does not hold the lock on the object.

check_for_session(obj, session)

Check if an object has a valid lock in the given session.

Returns True if the session holds a valid lock (even if it has expired), and False if the session holds an invalid lock or no lock.

Session-based usage: lock_tokens.decorators module

This module provides view decorators for common use cases.

locks_object(model, get_object_id_callable)

Locks an object before executing view, and keep lock token in the request session. Does not unlock it when the view returns.

Arguments:

  • model: the concerned django Model
  • get_object_id_callable: a callable that will return the concerned object id based on the view arguments

Example:

from lock_tokens.decorators import locks_object

@locks_object(MyModel, lambda request: request.GET.get('my_model_id'))
def myview(request):
    # In this example the view will lock the MyModel instance with the id
    # provided in the request GET parameter my_model_id
    ...

@locks_object(MyModel, lambda request, object_id: object_id)
def anotherview(request, object_id):
    # In this example the view will lock the MyModel instance with the id
    # provided as the second view argument
    ...

holds_lock_on_object(model, get_object_id_callable)

Locks an object before executing view, and keep lock token in the request session. Hold lock until the view is finished executing, then release it.

Arguments:

  • model: the concerned django Model
  • get_object_id_callable: a callable that will return the concerned object id based on the view arguments

See examples for locks_object.

REST API

If you want to use locking mechanism from outside your views, there is a simple HTTP API to handle tokens. It does not use sessions at all, so you need to handle the tokens yourself in this case.

Here are the different entry points, where <app_label> is the name of the application of the concerned model, <model> is the name of the model, <object_id> is the id of the cmodel instance, and <token> is the lock token value.

POST /lock_tokens/<app_label>/<model>/<object_id>/

Locks object. Returns a JSON response with "token" and "expires" keys.

Returns a 404 HTTP error if the object could not be found.

Returns a 403 HTTP error if the object is already locked.

GET /lock_tokens/<app_label>/<model>/<object_id>/<token>/

Returns a JSON response with "token" and "expires" keys.

Returns a 404 HTTP error if the object could not be found.

Returns a 403 HTTP error if the token is incorrect.

PATCH /lock_tokens/<app_label>/<model>/<object_id>/<token>/

Renews the lock on the object. Returns a JSON response with "token" and "expires" keys.

Returns a 404 HTTP error if the object could not be found.

Returns a 403 HTTP error if the token is incorrect.

DELETE /lock_tokens/<app_label>/<model>/<object_id>/<token>/

Unlocks object.

Returns a 404 HTTP error if the object could not be found.

Returns a 403 HTTP error if the token is incorrect.

REST API Javascript client

The application includes a javascript client to interact with the API. To enable it, simply add the following lines to your template, somewhere in the <body> section :

{% load lock_tokens_tags %}
{% lock_tokens_api_client %}

Don't forget to include the REST API urls with the correct namespace as described in section 1, otherwise it won't work.

Adding those lines in your template will create a variable named LockTokens, and emit a lock_tokens.clientready event when it is available in the javascript scope. This object has the following methods (parameters are self-describing):

LockTokens.lock(app_label, model, object_id, callback)

Locks the corresponding object. When the call to the API is completed, calls the callback method with a lock_tokens.Token instance as an argument, or null if the API request failed.

NB: The LockTokens handles the tokens for you, so you don't need to read API responses and/or store tokens yourself.

LockTokens.register_existing_lock_token(app_label, model, object_id, token_string, callback)

Add an existing token to the LockTokens registry. This method is useful for example when you want to handle on client side a lock that has been set on the server side. You must provide the token string in addition to other parameters, the client will make a call to the API to ensure the token is valid and get its expiration date. Calls the callback method with a lock_tokens.Token instance as an argument, or null if the registration failed.

LockTokens.unlock(app_label, model, object_id, callback)

Locks the corresponding object. When the call to the API is completed, calls the callback method with a boolean that indicates whether the API request has succeeded. Note that this method can be called only on an object that has been locked or registered as locked by the LockTokens object.

LockTokens.hold_lock(app_label, model, object_id)

Holds a lock on the corresponding object. It is like the lock method, except it renews the token each time it is about to expire. A call to unlock will stop the lock holding.

LockTokens.clear_all_locks(callback)

Unlocks all registered objects. Calls callback with no arguments when unlocking of every objects is done.

Settings

You can override lock_token default settings by adding a dict named LOCK_TOKENS to your settings.py like so:

LOCK_TOKENS = {
    'API_CSRF_EXEMPT': True,
    'DATEFORMAT': "%Y%m%d%H%M%S",
    'TIMEOUT': 60,
}

TIMEOUT

The validity duration for a lock token in seconds. Defaults to 3600 (one hour).

DATEFORMAT

The format of the expiration date returned in the token dict. Defaults to "%Y-%m-%d %H:%M:%S %Z"

API_CSRF_EXEMPT

A boolean that indicates whether to deactivate CSRF checks on the API views or not. Defaults to False.

Tests

To run tests simply run from the root of the repository:

source <YOURVIRTUALENV>/bin/activate
(myenv) $ pip install tox
(myenv) $ tox

Credits

Tools used in rendering this package:

django-lock-tokens's People

Contributors

brendah avatar juliomrqz avatar rparent avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

django-lock-tokens's Issues

Getting it to work with custom change_form.html template

  • django-lock-tokens version: 0.1.4
  • Django version: 1.11.15
  • Python version: 3.5
  • Operating System: Windows 10

Description & What I Did

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.

Opening a duplicate tab of LockableModelAdmin leaves me completely locked out

  • django-lock-tokens version: 0.2.5
  • Django version: 2.2.10
  • Python version: 3.7.4
  • Operating System: Guix System

Description

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!

Is there any better example on how to use it directly on the client side?

  • django-lock-tokens version: 0.2.5
  • Django version: 1.11
  • Python version: 3.6
  • Operating System: linux

Description

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>

LockableModel does not work as proxy

  • django-lock-tokens version: 0.2.1
  • Django version: 1.11
  • Python version: 3.5
  • Operating System: Debian

Description

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

Softer handling of expired locks

AFAIU, django-lock-tokens currently exposes the following interface

  1. You can obtain a lock on any object, provided that no other lock is active
  2. When a lock is not renewed for some time, it expires and effectively disappears (note that it remains in the database to be deleted when a new lock is taken, but it is already hidden from most operations, so things behave as if it was deleted already).
  3. When you want to change an object, no active lock by someone else must be active (so either you have an active lock, or no lock is active).

I wonder if the way expired locks are handled is the best approach here. In particular, consider this case:

  1. Alice takes a lock on an object and opens up its editing interface.
  2. Alice walks away from her computer, the lock expires.
  3. Alice returns, clicks save. She now no longer has a valid lock.

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:

  • While the lock was expired, (no) changes were made to the object. If no changes were made since the lock was opened (rather, since the editing interface was loaded), it is safe to allow committing unsaved changes without risk of overwriting other changes. However, detecting whether an object changed is not currently within scope of the django-lock-tokens package (it is more in the area of optimistic locking), so this case is hard to handle inside the package.
  • While the lock was expired (no) locks were taken on the object. If no lock was taken, you can be sure that no changes were made and thus committing pending changes is safe. When a lock was taken, changes might have been made, so the safe approach is to deny committing changes.

I can imagine an implementation, where:

  • An expired lock remains valid and can be renewed and used when saving normally.
  • An expired lock becomes invalid (and is deleted) as soon as another lock is taken on the same object.
  • To save an object, a valid lock is always needed (so no lock is no longer acceptable).

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.

Lock not removed automatically even after user navigated to another page.

  • django-lock-tokens version: 0.2.5
  • Django version:2.2.4
  • Python version:3.5.2
  • Operating System:Ubuntu 16.04

Description

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.

What I Did

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 ?

Infinite RecursionError with inherited models

  • django-lock-tokens version: 0.2.4
  • Django version: 1.11.16
  • Python version: 3.5
  • Operating System: Windows 10

Description

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.

Possible fix

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 )

Page reload locks object in admin

  • django-lock-tokens version: 0.2.3
  • Django version: 1.11
  • Python version: 3.5
  • Operating System: Debian

Description

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?

Atomic error on saving after expired lock

  • django-lock-tokens version: 0.1.4
  • Django version: 1.11.15
  • Python version: 3.5
  • Operating System: Windows 10

Description & What I Did

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.

How do I enable token renewal for the LockableAdmin?

  • django-lock-tokens version: 0.2.5
  • Django version: 1.11.23
  • Python version: 3.6.7
  • Operating System: linux

Description

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!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.