plone.restapi/unify-expired-and-excluded-items-handling: Merge branch 'master' into unify-expired-and-excluded-items-
GitHub <jenkins-z4DKO/[email protected]> Sat, 29 Jul 2017 16:32:31 -0700 (PDT)
| Newsgroups | gmane.comp.web.zope.plone.cvs |
|---|---|
| Message-ID | <[email protected]> |
Repository: plone.restapi Branch: refs/heads/unify-expired-and-excluded-items-handling Date: 2017-07-30T01:31:17+02:00 Author: Timo Stollenwerk (tisto) <[email protected]> Commit: https://github.com/plone/plone.restapi/commit/c85659f5dac7f9f7f4a6e9fd5e962d55968cd160 Merge branch 'master' into unify-expired-and-excluded-items-handling Files changed: A docs/source/_json/lock.req A docs/source/_json/lock.resp A docs/source/_json/lock_get.req A docs/source/_json/lock_get.resp A docs/source/_json/lock_nonstealable_timeout.req A docs/source/_json/lock_nonstealable_timeout.resp A docs/source/_json/lock_update.req A docs/source/_json/lock_update.resp A docs/source/_json/refresh_lock.req A docs/source/_json/refresh_lock.resp A docs/source/_json/unlock.req A docs/source/_json/unlock.resp A docs/source/locking.rst A src/plone/restapi/services/locking/__init__.py A src/plone/restapi/services/locking/configure.zcml A src/plone/restapi/services/locking/locking.py A src/plone/restapi/tests/test_locking.py M CHANGES.rst M docs/source/_json/workflow_post.resp M docs/source/index.rst M src/plone/restapi/services/configure.zcml M src/plone/restapi/services/content/update.py M src/plone/restapi/services/workflow/info.py M src/plone/restapi/services/workflow/transition.py M src/plone/restapi/tests/test_documentation.py diff --git a/CHANGES.rst b/CHANGES.rst index 87a7d6e4..b07fdd3d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,9 @@ New Features: - Add support for expandable elements. [buchi] +- Translate titles in @workflow. + [csenger] + - Add skipped tests from @breadcrumbs and @navigation now that the expansion is in place [sneridagh] @@ -16,6 +19,9 @@ New Features: it on @search via the show_inactive parameter [sneridagh] +- Add endpoints for locking/unlocking. + [buchi] + 1.0a20 (2017-07-24) ------------------- diff --git a/docs/source/_json/lock.req b/docs/source/_json/lock.req new file mode 100644 index 00000000..0cba19bb --- /dev/null +++ b/docs/source/_json/lock.req @@ -0,0 +1,3 @@ +POST /plone/front-page/@lock HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/docs/source/_json/lock.resp b/docs/source/_json/lock.resp new file mode 100644 index 00000000..06da01d4 --- /dev/null +++ b/docs/source/_json/lock.resp @@ -0,0 +1,12 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "creator": "admin", + "locked": true, + "name": "plone.locking.stealable", + "stealable": true, + "time": 1477076400.0, + "timeout": 600, + "token": "0.684672730996-0.25195226375-00105A989226:1477076400.000" +} \ No newline at end of file diff --git a/docs/source/_json/lock_get.req b/docs/source/_json/lock_get.req new file mode 100644 index 00000000..43cd7c0e --- /dev/null +++ b/docs/source/_json/lock_get.req @@ -0,0 +1,3 @@ +GET /plone/front-page/@lock HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/docs/source/_json/lock_get.resp b/docs/source/_json/lock_get.resp new file mode 100644 index 00000000..52852385 --- /dev/null +++ b/docs/source/_json/lock_get.resp @@ -0,0 +1,7 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "locked": false, + "stealable": true +} \ No newline at end of file diff --git a/docs/source/_json/lock_nonstealable_timeout.req b/docs/source/_json/lock_nonstealable_timeout.req new file mode 100644 index 00000000..40383b76 --- /dev/null +++ b/docs/source/_json/lock_nonstealable_timeout.req @@ -0,0 +1,9 @@ +POST /plone/front-page/@lock HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 +Content-Type: application/json + +{ + "stealable": false, + "timeout": 3600 +} \ No newline at end of file diff --git a/docs/source/_json/lock_nonstealable_timeout.resp b/docs/source/_json/lock_nonstealable_timeout.resp new file mode 100644 index 00000000..e3927240 --- /dev/null +++ b/docs/source/_json/lock_nonstealable_timeout.resp @@ -0,0 +1,12 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "creator": "admin", + "locked": true, + "name": "plone.locking.stealable", + "stealable": true, + "time": 1477076400.0, + "timeout": 3600, + "token": "0.684672730996-0.25195226375-00105A989226:1477076400.000" +} \ No newline at end of file diff --git a/docs/source/_json/lock_update.req b/docs/source/_json/lock_update.req new file mode 100644 index 00000000..b9e39d84 --- /dev/null +++ b/docs/source/_json/lock_update.req @@ -0,0 +1,9 @@ +PATCH /plone/front-page HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 +Lock-Token: 0.684672730996-0.25195226375-00105A989226:1477076400.000 +Content-Type: application/json + +{ + "title": "New Title" +} \ No newline at end of file diff --git a/docs/source/_json/lock_update.resp b/docs/source/_json/lock_update.resp new file mode 100644 index 00000000..0074ded3 --- /dev/null +++ b/docs/source/_json/lock_update.resp @@ -0,0 +1,2 @@ +HTTP/1.1 204 No Content + diff --git a/docs/source/_json/refresh_lock.req b/docs/source/_json/refresh_lock.req new file mode 100644 index 00000000..f1268de1 --- /dev/null +++ b/docs/source/_json/refresh_lock.req @@ -0,0 +1,3 @@ +POST /plone/front-page/@refresh-lock HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/docs/source/_json/refresh_lock.resp b/docs/source/_json/refresh_lock.resp new file mode 100644 index 00000000..06da01d4 --- /dev/null +++ b/docs/source/_json/refresh_lock.resp @@ -0,0 +1,12 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "creator": "admin", + "locked": true, + "name": "plone.locking.stealable", + "stealable": true, + "time": 1477076400.0, + "timeout": 600, + "token": "0.684672730996-0.25195226375-00105A989226:1477076400.000" +} \ No newline at end of file diff --git a/docs/source/_json/unlock.req b/docs/source/_json/unlock.req new file mode 100644 index 00000000..8ea9689a --- /dev/null +++ b/docs/source/_json/unlock.req @@ -0,0 +1,3 @@ +POST /plone/front-page/@unlock HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/docs/source/_json/unlock.resp b/docs/source/_json/unlock.resp new file mode 100644 index 00000000..52852385 --- /dev/null +++ b/docs/source/_json/unlock.resp @@ -0,0 +1,7 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "locked": false, + "stealable": true +} \ No newline at end of file diff --git a/docs/source/_json/workflow_post.resp b/docs/source/_json/workflow_post.resp index 96325403..a6ad4a79 100644 --- a/docs/source/_json/workflow_post.resp +++ b/docs/source/_json/workflow_post.resp @@ -6,5 +6,6 @@ Content-Type: application/json "actor": "admin", "comments": "", "review_state": "published", - "time": "2016-10-21T19:05:00+00:00" + "time": "2016-10-21T19:05:00+00:00", + "title": "Published" } \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index ea4278fd..a0d8af72 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -28,6 +28,7 @@ Contents copymove expansion workflow + locking sharing registry types diff --git a/docs/source/locking.rst b/docs/source/locking.rst new file mode 100644 index 00000000..56097e06 --- /dev/null +++ b/docs/source/locking.rst @@ -0,0 +1,92 @@ +Locking +======= + +Locking is a mechanism to prevent users from accidentially overriding each others changes. + +When a user edits a content object in Plone, the object is locked until the user hits the save or cancel button. +If a second user tries to edit the object at the same time, she will see a message that this object is locked. + + +Locking an object +----------------- + +To lock an object send a POST request to the ``/@lock`` endpoint that is available on any content object in Plone: + +.. http:example:: curl httpie python-requests + :request: _json/lock.req + +If the lock operation succeeds, the server will respond with status :term:`200 OK` and return various information +about the lock including the lock token. The token is needed in later requests to update the locked object. + +.. literalinclude:: _json/lock.resp + :language: http + + +By default, locks are stealable. That means that another user can unlock the object. +If you want to create a non-stealable lock, pass ``"stealable": false`` in the request body. + +To create a lock with a non-default timeout, you can pass the the timeout value in seconds in the request body. + +The following example creates a non-stealable lock with a timeout of 1h. + +.. http:example:: curl httpie python-requests + :request: _json/lock_nonstealable_timeout.req + + +The server responds with status :term:`200 OK` and returns the lock information. + +.. literalinclude:: _json/lock_nonstealable_timeout.resp + :language: http + + +Unlocking an object +------------------- + +To unlock an object send a POST request to the ``/@unlock`` endpoint. + +.. http:example:: curl httpie python-requests + :request: _json/unlock.req + +The server responds with status :term:`200 OK` and returns the lock information. + +.. literalinclude:: _json/unlock.resp + :language: http + + +Refreshing a lock +----------------- + +An existing lock can be refreshed by sending a POST request to the ``@refresh-lock`` endpoint. + +.. http:example:: curl httpie python-requests + :request: _json/refresh_lock.req + +The server responds with status :term:`200 OK` and returns the lock information containing the updated creation time. + +.. literalinclude:: _json/refresh_lock.resp + :language: http + + +Getting lock information +------------------------ + +To find out if an object is locked or to get information about the current lock +you can send a GET request to the ``@lock`` endpoint. + +.. http:example:: curl httpie python-requests + :request: _json/lock_get.req + +The server responds with status :term:`200 OK` and returns the information about the lock. + +.. literalinclude:: _json/lock_get.resp + :language: http + + +Updating a locked object +------------------------ + +To update a locked object with a PATCH request, you have to provide the lock +token with the ``Lock-Token`` header. + +.. http:example:: curl httpie python-requests + :request: _json/lock_update.req diff --git a/src/plone/restapi/services/configure.zcml b/src/plone/restapi/services/configure.zcml index 800995bb..cdce2f5f 100644 --- a/src/plone/restapi/services/configure.zcml +++ b/src/plone/restapi/services/configure.zcml @@ -10,6 +10,7 @@ <include package=".groups"/> <include package=".navigation"/> <include package=".history"/> + <include package=".locking" /> <include package=".principals"/> <include package=".registry"/> <include package=".roles"/> diff --git a/src/plone/restapi/services/content/update.py b/src/plone/restapi/services/content/update.py index d394bd4c..19d2eb31 100644 --- a/src/plone/restapi/services/content/update.py +++ b/src/plone/restapi/services/content/update.py @@ -2,6 +2,7 @@ from plone.restapi.exceptions import DeserializationError from plone.restapi.interfaces import IDeserializeFromJson from plone.restapi.services import Service +from plone.restapi.services.locking.locking import is_locked from zope.component import queryMultiAdapter @@ -10,6 +11,12 @@ class ContentPatch(Service): """ def reply(self): + + if is_locked(self.context, self.request): + self.request.response.setStatus(403) + return dict(error=dict( + type='Forbidden', message='Resource is locked.')) + deserializer = queryMultiAdapter((self.context, self.request), IDeserializeFromJson) if deserializer is None: diff --git a/src/plone/restapi/services/locking/__init__.py b/src/plone/restapi/services/locking/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/plone/restapi/services/locking/configure.zcml b/src/plone/restapi/services/locking/configure.zcml new file mode 100644 index 00000000..d3eb4c94 --- /dev/null +++ b/src/plone/restapi/services/locking/configure.zcml @@ -0,0 +1,38 @@ +<configure + xmlns="http://namespaces.zope.org/zope" + xmlns:plone="http://namespaces.plone.org/plone" + xmlns:zcml="http://namespaces.zope.org/zcml"> + + <plone:service + method="POST" + name="@lock" + for="Products.CMFCore.interfaces.IContentish" + factory=".locking.Lock" + permission="cmf.ModifyPortalContent" + /> + + <plone:service + method="POST" + name="@unlock" + for="Products.CMFCore.interfaces.IContentish" + factory=".locking.Unlock" + permission="cmf.ModifyPortalContent" + /> + + <plone:service + method="POST" + name="@refresh-lock" + for="Products.CMFCore.interfaces.IContentish" + factory=".locking.RefreshLock" + permission="cmf.ModifyPortalContent" + /> + + <plone:service + method="GET" + name="@lock" + for="Products.CMFCore.interfaces.IContentish" + factory=".locking.LockInfo" + permission="zope2.View" + /> + +</configure> diff --git a/src/plone/restapi/services/locking/locking.py b/src/plone/restapi/services/locking/locking.py new file mode 100644 index 00000000..9037879e --- /dev/null +++ b/src/plone/restapi/services/locking/locking.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +from plone.locking.interfaces import ILockable +from plone.locking.interfaces import INonStealableLock +from plone.locking.interfaces import IRefreshableLockable +from plone.restapi.deserializer import json_body +from plone.restapi.services import Service +from zope.interface import alsoProvides +from zope.interface import noLongerProvides + +import plone.protect.interfaces + + +class Lock(Service): + """Lock an object""" + + def reply(self): + data = json_body(self.request) + + lockable = IRefreshableLockable(self.context, None) + if lockable is not None: + lockable.lock() + + if 'stealable' in data and not data['stealable']: + alsoProvides(self.context, INonStealableLock) + + if 'timeout' in data: + lock_item = webdav_lock(self.context) + lock_item.setTimeout("Second-%s" % data['timeout']) + + # Disable CSRF protection + if 'IDisableCSRFProtection' in dir(plone.protect.interfaces): + alsoProvides(self.request, + plone.protect.interfaces.IDisableCSRFProtection) + + return lock_info(self.context) + + +class Unlock(Service): + """Unlock an object""" + + def reply(self): + lockable = ILockable(self.context) + if lockable.can_safely_unlock(): + lockable.unlock() + + if INonStealableLock.providedBy(self.context): + noLongerProvides(self.context, INonStealableLock) + + # Disable CSRF protection + if 'IDisableCSRFProtection' in dir(plone.protect.interfaces): + alsoProvides(self.request, + plone.protect.interfaces.IDisableCSRFProtection) + + return lock_info(self.context) + + +class RefreshLock(Service): + """Refresh the lock of an object""" + + def reply(self): + lockable = IRefreshableLockable(self.context, None) + if lockable is not None: + lockable.refresh_lock() + + # Disable CSRF protection + if 'IDisableCSRFProtection' in dir(plone.protect.interfaces): + alsoProvides(self.request, + plone.protect.interfaces.IDisableCSRFProtection) + + return lock_info(self.context) + + +class LockInfo(Service): + """Lock information about the current lock""" + + def reply(self): + return lock_info(self.context) + + +def lock_info(obj): + """Returns lock information about the given object.""" + lockable = ILockable(obj, None) + if lockable is not None: + info = { + 'locked': lockable.locked(), + 'stealable': lockable.stealable(), + } + lock_info = lockable.lock_info() + if len(lock_info) > 0: + info['creator'] = lock_info[0]['creator'] + info['time'] = lock_info[0]['time'] + info['token'] = lock_info[0]['token'] + lock_type = lock_info[0]['type'] + if lock_type: + info['name'] = lock_info[0]['type'].__name__ + lock_item = webdav_lock(obj) + if lock_item: + info['timeout'] = lock_item.getTimeout() + return info + + +def webdav_lock(obj): + """Returns the WebDAV LockItem""" + lockable = ILockable(obj, None) + if lockable is None: + return + + lock_info = lockable.lock_info() + if len(lock_info) > 0: + token = lock_info[0]['token'] + return obj.wl_getLock(token) + + +def is_locked(obj, request): + """Returns true if the object is locked and the request doesn't contain + the lock token. + """ + lockable = ILockable(obj, None) + if lockable is None: + return False + if lockable.locked(): + token = request.getHeader('Lock-Token', '') + lock_info = lockable.lock_info() + if len(lock_info) > 0 and lock_info[0]['token'] == token: + return False + return True + return False diff --git a/src/plone/restapi/services/workflow/info.py b/src/plone/restapi/services/workflow/info.py index a8fb73f4..4b8e2063 100644 --- a/src/plone/restapi/services/workflow/info.py +++ b/src/plone/restapi/services/workflow/info.py @@ -38,13 +38,14 @@ def __call__(self, expand=False): transitions.append({ '@id': '{}/@workflow/{}'.format( self.context.absolute_url(), action['id']), - 'title': action['title'], + 'title': self.context.translate(action['title']), }) for item, action in enumerate(history): - history[item]['title'] = wftool.getTitleForStateOnType( - action['review_state'], - self.context.portal_type) + history[item]['title'] = self.context.translate( + wftool.getTitleForStateOnType( + action['review_state'], + self.context.portal_type)) result['workflow'].update({ 'history': json_compatible(history), diff --git a/src/plone/restapi/services/workflow/transition.py b/src/plone/restapi/services/workflow/transition.py index 8f4fc940..fa8d1910 100644 --- a/src/plone/restapi/services/workflow/transition.py +++ b/src/plone/restapi/services/workflow/transition.py @@ -59,4 +59,10 @@ def reply(self): message=translate(e.message, context=self.request))) history = wftool.getInfoFor(self.context, "review_history") - return json_compatible(history[-1]) + action = history[-1] + action['title'] = self.context.translate( + wftool.getTitleForStateOnType( + action['review_state'], + self.context.portal_type)) + + return json_compatible(action) diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index cdbc8c3d..bb28114b 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -15,6 +15,7 @@ from plone.app.testing import pushGlobalRegistry from plone.app.testing import setRoles from plone.app.textfield.value import RichTextValue +from plone.locking.interfaces import ITTWLockable from plone.namedfile.file import NamedBlobFile from plone.namedfile.file import NamedBlobImage from plone.registry.interfaces import IRegistry @@ -24,6 +25,7 @@ from plone.testing.z2 import Browser from zope.component import createObject from zope.component import getUtility +from zope.interface import alsoProvides from zope.site.hooks import getSite import collections @@ -48,6 +50,7 @@ REQUEST_HEADER_KEYS = [ 'accept', 'authorization', + 'lock-token', ] + TUS_HEADERS RESPONSE_HEADER_KEYS = [ @@ -142,6 +145,7 @@ def setUp(self): setRoles(self.portal, TEST_USER_ID, ['Manager']) self.document = self.create_document() + alsoProvides(self.document, ITTWLockable) transaction.commit() self.browser = Browser(self.app) @@ -978,6 +982,68 @@ def test_tusreplace_post_patch(self): upload_url.split('/')[:-1] + ['4e465958b24a46ec8657e6f3be720991']) save_request_and_response_for_docs('tusreplace_patch', response) + def test_locking_lock(self): + url = '{}/@lock'.format(self.document.absolute_url()) + response = self.api_session.post(url) + # Replace dynamic lock token with a static one + response._content = re.sub( + r'"token": "[^"]+"', + '"token": "0.684672730996-0.25195226375-00105A989226:1477076400.000"', # noqa + response.content) + save_request_and_response_for_docs('lock', response) + + def test_locking_lock_nonstealable_and_timeout(self): + url = '{}/@lock'.format(self.document.absolute_url()) + response = self.api_session.post( + url, + json={ + 'stealable': False, + 'timeout': 3600, + }, + ) + # Replace dynamic lock token with a static one + response._content = re.sub( + r'"token": "[^"]+"', + '"token": "0.684672730996-0.25195226375-00105A989226:1477076400.000"', # noqa + response.content) + save_request_and_response_for_docs( + 'lock_nonstealable_timeout', response) + + def test_locking_unlock(self): + url = '{}/@lock'.format(self.document.absolute_url()) + response = self.api_session.post(url) + url = '{}/@unlock'.format(self.document.absolute_url()) + response = self.api_session.post(url) + save_request_and_response_for_docs('unlock', response) + + def test_locking_refresh_lock(self): + url = '{}/@lock'.format(self.document.absolute_url()) + response = self.api_session.post(url) + url = '{}/@refresh-lock'.format(self.document.absolute_url()) + response = self.api_session.post(url) + # Replace dynamic lock token with a static one + response._content = re.sub( + r'"token": "[^"]+"', + '"token": "0.684672730996-0.25195226375-00105A989226:1477076400.000"', # noqa + response.content) + save_request_and_response_for_docs('refresh_lock', response) + + def test_locking_lockinfo(self): + url = '{}/@lock'.format(self.document.absolute_url()) + response = self.api_session.get(url) + save_request_and_response_for_docs('lock_get', response) + + def test_update_with_lock(self): + url = '{}/@lock'.format(self.document.absolute_url()) + response = self.api_session.post(url) + token = response.json()['token'] + response = self.api_session.patch( + self.document.absolute_url(), + headers={'Lock-Token': token}, + json={'title': 'New Title'}) + response.request.headers['Lock-Token'] = u"0.684672730996-0.25195226375-00105A989226:1477076400.000" # noqa + save_request_and_response_for_docs('lock_update', response) + class TestCommenting(unittest.TestCase): diff --git a/src/plone/restapi/tests/test_locking.py b/src/plone/restapi/tests/test_locking.py new file mode 100644 index 00000000..0e7b49fb --- /dev/null +++ b/src/plone/restapi/tests/test_locking.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import login +from plone.locking.interfaces import ITTWLockable +from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING +from plone.restapi.testing import RelativeSession +from zope.interface import alsoProvides +from plone.locking.interfaces import ILockable +from plone.locking.interfaces import INonStealableLock + + +import unittest +import transaction + + +class TestLocking(unittest.TestCase): + + layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING + + def setUp(self): + self.portal = self.layer['portal'] + self.request = self.layer['request'] + login(self.portal, SITE_OWNER_NAME) + self.doc = self.portal[self.portal.invokeFactory( + 'Document', id='doc1', title='My Document')] + alsoProvides(self.doc, ITTWLockable) + + self.api_session = RelativeSession(self.doc.absolute_url()) + self.api_session.headers.update({'Accept': 'application/json'}) + self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + + transaction.commit() + + def test_lock_object(self): + response = self.api_session.post('/@lock',) + transaction.commit() + + self.assertEqual(response.status_code, 200) + self.assertTrue(ILockable(self.doc).locked()) + + def test_lock_object_non_stealable(self): + response = self.api_session.post('/@lock', json={'stealable': False}) + transaction.commit() + + self.assertEqual(response.status_code, 200) + self.assertTrue(INonStealableLock.providedBy(self.doc)) + + def test_lock_object_with_custom_timeout(self): + response = self.api_session.post('/@lock', json={'timeout': 86400}) + transaction.commit() + + self.assertEqual(response.status_code, 200) + self.assertEqual(self.doc.wl_lockValues()[0].getTimeout(), 86400) + + def test_unlock_object(self): + lockable = ILockable(self.doc) + lockable.lock() + transaction.commit() + response = self.api_session.post('/@unlock') + transaction.commit() + + self.assertEqual(response.status_code, 200) + self.assertFalse(lockable.locked()) + + def test_refresh_lock(self): + lockable = ILockable(self.doc) + lockable.lock() + modified = self.doc.wl_lockValues()[0].getModifiedTime() + transaction.commit() + response = self.api_session.post('/@refresh-lock') + transaction.commit() + + self.assertEqual(response.status_code, 200) + self.assertTrue( + self.doc.wl_lockValues()[0].getModifiedTime() > modified) + + def test_lock_info_for_locked_object(self): + lockable = ILockable(self.doc) + lockable.lock() + transaction.commit() + response = self.api_session.get('/@lock') + + self.assertEqual(response.status_code, 200) + self.assertTrue(response.json()['locked']) + + def test_lock_info_for_unlocked_object(self): + response = self.api_session.get('/@lock') + + self.assertEqual(response.status_code, 200) + self.assertFalse(response.json()['locked']) + + def test_update_locked_object_without_token_fails(self): + lockable = ILockable(self.doc) + lockable.lock() + transaction.commit() + response = self.api_session.patch('/', json={'title': 'New Title'}) + transaction.commit() + self.assertEqual(response.status_code, 403) + self.assertEqual(self.doc.Title(), 'My Document') + + def test_update_locked_object_with_token_succeeds(self): + lockable = ILockable(self.doc) + lockable.lock() + transaction.commit() + response = self.api_session.patch( + '/', + headers={'Lock-Token': lockable.lock_info()[0]['token']}, + json={'title': 'New Title'}) + transaction.commit() + self.assertEqual(response.status_code, 204) + self.assertEqual(self.doc.Title(), 'New Title') ------------------------------------------------------------------------------ Check out the vibrant tech community on one of the world's most engaging tech sites, Slashdot.org! http://sdm.link/slashdot