==============
Using Locking
==============

Substance D allows you to lock content resources programmatically.  When a
resource is locked, its UI can change to indicate that it cannot be edited by
someone other than the user holding the lock.

Locking a resource *only* locks the resource, not its children.  The locking
system is not recursive at this time.

Locking a Resource
==================

To lock a resource:

.. code-block:: python

   from substanced.locking import lock_resource

   if request.has_permission('sdi.lock', someresource):
       lock_resource(someresource, request.user, timeout=3600)

If the resource is already locked by the owner supplied as ``owner_or_ownerid``
(the parameter filled by ``request.user`` above), calling this function will
refresh the lock.  If the resource is not already locked by another user,
calling this function will create a new lock.  If the resource is already
locked by a different user, a :class:`substanced.locking.LockError` will be
raised.

Using the :func:`substanced.locking.lock_resource` function has the side effect
of creating a "Lock Service" (named ``locks``) in the Substance D root if one
does not already exist.

.. warning::

   Callers should assert that the owner has the ``sdi.lock`` permission against
   the resource before calling :func:`~substanced.locking.lock_resource` to
   ensure that a user can't lock a resource he is not permitted to.

Unlocking a Resource
====================

To unlock a resource:

.. code-block:: python

   from substanced.locking import unlock_resource

   if request.has_permission('sdi.lock', someresource):
       unlock_resource(someresource, request.user)

If the resource is already locked by a user other than the owner supplied as
``owner_or_ownerid`` (the parameter filled by ``request.user`` above) or the
resource isn't already locked with this lock type, calling this function will
raise a :exc:`substanced.locking.UnlockError` exception.  Otherwise the lock
will be removed.

Using the :func:`substanced.locking.unlock_resource` function has the side
effect of creating a "Lock Service" (named ``locks``) in the Substance D root
if one does not already exist.

.. warning::

   Callers should assert that the owner has the ``sdi.lock`` permission against
   the resource before calling :func:`~substanced.locking.unlock_resource` to
   ensure that a user can't lock a resource he is not permitted to.

To unlock a resource using an explicit lock token:

.. code-block:: python

   from substanced.locking import unlock_token

   if request.has_permission('sdi.lock', someresource):
       unlock_token(someresource, token, request.user)

If the lock identified by ``token`` belongs to a user other than the owner
supplied as ``owner_or_ownerid`` (the parameter filled by ``request.user``
above) or if no lock exists under ``token`` , calling this function will
raise a :exc:`substanced.locking.LockError` exception.  Otherwise the lock
will be removed.

Using the :func:`substanced.locking.unlock_token` function has the side
effect of creating a "Lock Service" (named ``locks``) in the Substance D root
if one does not already exist.

.. warning::

   Callers should assert that the owner has the ``sdi.lock`` permission against
   the resource before calling :func:`~substanced.locking.unlock_token` to
   ensure that a user can't lock a resource he is not permitted to.

Discovering Existing Locks
==========================

To discover any existing locks for a resource:

.. code-block:: python

   from substanced.locking import discover_resource_locks

   locks = discover_resource_locks(someresource)
   # "locks" will be a sequence

The :func:`substanced.locking.discover_resource_locks` function will return a
sequence of :class:`substanced.locking.Lock` objects related to the resource
for the lock type provided to the function.  By default, only valid locks are
returned.  Invalid locks for the resource may exist, but they are not returned
unless the ``include_invalid`` argument passed to
::func:`~substanced.locking.discover_resource_locks` is ``True``.

Under normal circumstances, the length of the sequence returned will be either
0 (if there are no locks) or 1 (if there is any lock).  In some special
circumstances, however, when the :func:`substanced.locking.lock_resource` API
is not used to create locks, there may be more than one lock related to a
resource of the same type.

By default, the ``discover_resource_locks`` API returns locks for the
provided object, plus locks on any object in its :term:`lineage`.  To suppress
this default, pass ``include_lineage=False``, e.g.:

.. code-block:: python

   locks = discover_resource_locks(someresource)
   # "locks" will be only those set on 'someresource'

In some applications, the important thing is to ensure that a particular
user *could* lock a resource before updating it (e.g., from a browser view
on a property sheet).  The ::func:`~substanced.locking.could_lock_resource`
API is designed for these cases:  if the supplied userid could not lock the
resource, it raises a :exc:`substanced.locking.LockError` exception:

.. code-block:: python

   from substanced.locking import could_lock_resource, LockError

   try:
       could_lock_resource(someresource, request.user)
   except LockError as e:
       raise FormError('locked by "%s"' % e.lock.owner.__name__)

Viewing The Lock Service
========================

Once some locks have been created, a *lock service* will have been created.
The lock service is an object named ``locks`` in the Substance D root.

You can use the SDI UI of this locks service to delete and edit existing locks.
It's a good idea to periodically use the "Delete Expired" button in this UI to
clear out any existing expired locks that were orphaned by buggy or interrupted
clients.
