Lock editing page in django

Hi, i have a single page application and a django server with the django rest framework. As an example, I have a resource called House.

User 1 is on the /houses/5/edit page and is currently editing the house with ID 5. Now I want to block editing from the house with ID 5 for other users. Only one user should be able to access the editing page of a particular house at a time.

All other users should see on the detail page of the house with ID 5 (/houses/5/) that the house is already being edited by user 1.

Only when user 1 leaves the page /houses/5/edit, another user should be able to edit the house. I want to have this blocking mechanism because otherwise changes to the house could be overwritten by another user.

So the mechanism should be similar to the blocking mechanism in Wordpress CMS for blocking a page or post.

How can I implement this?

I already use django-channels for push notifications. My idea was to use websockets for blocking. Also, I saw that django-redis offers a blocking mechanism.

However, I still have no idea how to implement my requirements with it and whether these technologies are the best choice. Maybe it is easier to implement this functionality using the normal database?

Thanks for you help!

Which by definition is “stateless”, which means that if you want to apply some type of lock, you’ll want to do it either with a global cache or in the database. Either way, you want the request for a lock to be an atomic operation.

You can implement this using your database, or you could implement it using redis - it doesn’t matter much which one you choose.

One of the factors you will need to address is an “abandoned” connection. You’ll want to decide how long a lock can be held in the absence of something being saved. (You may also want to implement the lock release in the disconnect handler for your websocket consumer.)

One advantage of using redis for this is that you can set an expiration on keys to automatically delete them if they’re still set beyond the allotted time.

You’ll also want to decide how you’re going to handle the second request, the one that gets rejected - do you want that to wait for a short period of time? Or immediately redirect to a different page?

The basic logic for all this is relatively simple. The view to get the form will try to retrieve the lock for the object being edited, and return the form if it was able to do so. When the updates are posted, release the lock.

If you’re using redis, you could use a redis hash type to identify the objects being locked, and use the hsetnx command to set the lock - checking to see if the lock had previously been set.

If you’re using your database, you could use select_for_update to get database locks on those rows being edited along with the rows in your lock table, for the purpose of setting your own “tracking locks” for the duration of the time you will hold the lock.

The “tracking lock” is a model you would create to identify the object(s) being edited. You can’t rely solely upon the database locks, because they are released at the end of the view.

1 Like

Thank you! I think redis is a good option for me.

I now give the retrieve endpoint a get parameter editing=True when I want to open an object in the edit form in the frontend.

In the backend I then check with the function if someone is already editing the object:

def check_editing(object_id, user_id, forename, surname):
    data = {
        "id": user_id,
        "forename": forename,
        "surname": surname
    }

    import redis
    r = Redis.get_connection()
    result = r.set(object_id, pickle.dumps(data), nx=True)
    if result == 1:
        r.expire(object_id, timedelta(hours=2).total_seconds())

    return result

If the function returns 1, then I return the object as expected. If the function returns 0, then someone is already processing the object and I return an HTTP status 409 to the client, which then redirects to the view page.

When the user saves the object and sends a request to the Update View, the object is released for editing again and the user is redirected to the view page:

def delete_editing(object_id):
    import redis
    r = Redis.get_connection()
    r.delete(object_id)

Is this a good way to proceed?
But how do I handle it when the user leaves the edit form in the frontend or closes the browser tab?
The object is released after two hours, but that is a long time.

Side note: If you’re already using the django-redis package, you would use get_redis_connection and not Redis.get_connection.

Yep, you’re stuck. And that’s the problem with preemptive locking in a web-based environment.

You could try to do something with a “heartbeat” feature either through your websocket connection or a short-poll style get, perhaps at 1 - 5 minute intervals with a shorter expiration time.

1 Like

Thank you. How could such a heartbeat feature look like via websockets?

The specifics depends upon how you’re managing your websockets, but basically, you can use something like setInterval in your JavaScript to periodically execute a function, where that function is to send a keep-alive message through the websocket.

Your consumer then would update the expiry time of the redis key every time that message is received.

1 Like

Good idea!
At the moment I don’t use django-redis yet though. I have channels installed though.
Do you know if I can reuse the connection to redis from channels or channels_redis? Or do I need to create a new connection and install django-redis for it?

I don’t know. We’ve never tried anything like that, because we don’t run our Django instances in the same process as our channels code in production.

1 Like

Ok, then I’ll try to use redis-py, because I don’t need a cache and so django-redis is probably a bit too overkill.
Thanks!