From b9710b4d69a658c8ab884ec554681b4051c9763b Mon Sep 17 00:00:00 2001 From: Brett Date: Fri, 6 Sep 2013 16:21:32 -0400 Subject: [PATCH 1/4] add basic structure for locking resources --- bauble/model/lock.py | 19 +++++++++++++++++++ bauble/server/resource.py | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 bauble/model/lock.py diff --git a/bauble/model/lock.py b/bauble/model/lock.py new file mode 100644 index 0000000..c68f76b --- /dev/null +++ b/bauble/model/lock.py @@ -0,0 +1,19 @@ +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.orm.session import object_session +from sqlalchemy.exc import DBAPIError +from sqlalchemy.ext.associationproxy import association_proxy + +import bauble +import bauble.db as db +import bauble.types as types +import bauble.search as search + +class Lock(db.Base): + __tablename__ = 'lock' + + table_name = String(32, nullable=False, index=True) + table_id = Integer(nullable=False, index=True) + lock_created = Column(types.DateTime, nullable=False) + locked_by = String(nullable=False) + locked_released = Column(types.DateTime, nullable=False) diff --git a/bauble/server/resource.py b/bauble/server/resource.py index 5d7b344..2c4c955 100644 --- a/bauble/server/resource.py +++ b/bauble/server/resource.py @@ -65,6 +65,12 @@ def __init__(self): "DELETE": self.delete }) + self.add_route(API_ROOT + self.resource + "//lock>", + {"OPTIONS": self.options_response, + "POST": self.lock, + "DELETE": self.delete_lock, + }) + self.add_route(API_ROOT + self.resource + '/count', {"GET": self.count, "OPTIONS": self.options_response, @@ -80,16 +86,19 @@ def __init__(self): {"OPTIONS": self.options_response, "GET": self.get_schema }) + self.add_route(API_ROOT + self.resource + "//schema", {"OPTIONS": self.options_response, 'GET': self.get_schema }) + self.add_route(API_ROOT + self.resource + "//", {"OPTIONS": self.options_response, "GET": self.get_relation }) + session_events = [] @@ -457,6 +466,18 @@ def save_or_update(self, resource_id=None, depth=1): session.close() + def lock(resource_id): + """ + """ + # 1. Return 423 is resource is already locked + + + def delete_lock(resource_id): + """ + """ + # delete the lock + + @staticmethod def get_ref_id(ref): # assume that if ref is not a str then it is a resource JSON object From 99b329647053965e873b6cadabf9aae2da217f5d Mon Sep 17 00:00:00 2001 From: Brett Date: Sat, 7 Sep 2013 22:31:34 -0400 Subject: [PATCH 2/4] - some of the locking code filled in --- bauble/model/lock.py | 24 +++++++++++++++++++----- bauble/server/resource.py | 30 +++++++++++++++++++++++++++--- test/api/test_lock.py | 23 +++++++++++++++++++++++ 3 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 test/api/test_lock.py diff --git a/bauble/model/lock.py b/bauble/model/lock.py index c68f76b..a4b4f68 100644 --- a/bauble/model/lock.py +++ b/bauble/model/lock.py @@ -12,8 +12,22 @@ class Lock(db.Base): __tablename__ = 'lock' - table_name = String(32, nullable=False, index=True) - table_id = Integer(nullable=False, index=True) - lock_created = Column(types.DateTime, nullable=False) - locked_by = String(nullable=False) - locked_released = Column(types.DateTime, nullable=False) + resource = Column(String(32), nullable=False, index=True) + + date_created = Column(types.DateTime, nullable=False, default=func.now()) + + # a future datetime when this lock automatically expires + date_expires = Column(types.DateTime, nullable=False) + + # a past datetime when this lock was released, either expired or delete + date_released = Column(types.DateTime, nullable=False) + + # this should be a + user_id = Column(Integer, ForeignKey('user.id'), nullable=False) + + def json(self, depth=1): + d = dict(resource=self.resource) + if(depth>0): + d['date_created'] = str(self.date_created) + d['data_released'] = str(self.date_released) if self.date_released else "" + d['user'] = self.user.json(depth=depth - 1) diff --git a/bauble/server/resource.py b/bauble/server/resource.py index 2c4c955..a55ad77 100644 --- a/bauble/server/resource.py +++ b/bauble/server/resource.py @@ -24,6 +24,7 @@ from bauble.model.organization import Organization from bauble.model.user import User from bauble.model.reportdef import ReportDef +from bauble.model.lock import Lock from bauble.server import app, API_ROOT, parse_accept_header, JSON_MIMETYPE, \ TEXT_MIMETYPE, parse_auth_header, accept import bauble.types as types @@ -65,7 +66,7 @@ def __init__(self): "DELETE": self.delete }) - self.add_route(API_ROOT + self.resource + "//lock>", + self.add_route(API_ROOT + self.resource + "//lock", {"OPTIONS": self.options_response, "POST": self.lock, "DELETE": self.delete_lock, @@ -466,10 +467,33 @@ def save_or_update(self, resource_id=None, depth=1): session.close() - def lock(resource_id): + def lock(self, resource_id): """ """ - # 1. Return 423 is resource is already locked + try: + session = self.connect() + username, password = parse_auth_header() + user = session.query(User).filter_by(username=username).one() + + # check if a lock already exists for this resource + resource = request.path[len(API_ROOT):-len("/lock")] + print("search_path: ", session.bind.execute("SHOW search_path").scalar()) + is_locked = session.query(Lock).\ + filter(Lock.resource==resource, Lock.user_id==user.id, + Lock.date_released is not None).count() > 0 + if is_locked: + bottle.abort(423, 'Resource is already locked') + + + + # lock the resource + lock = Lock(resource=resource, user_id=user.id) + session.add(lock) + session.commit() + return lock.json(depth=1) + finally: + if session: + session.close() def delete_lock(resource_id): diff --git a/test/api/test_lock.py b/test/api/test_lock.py new file mode 100644 index 0000000..22d8b94 --- /dev/null +++ b/test/api/test_lock.py @@ -0,0 +1,23 @@ + +import requests + +import test.api as test +import bauble.db as db +from bauble.model.family import Family, FamilySynonym, FamilyNote + +def test_lock(): + # create a family family + family = test.create_resource('/family', {'family': test.get_random_name()}) + url = test.api_root + family['ref'] + "/lock" + response = requests.post(url, auth=(test.default_user, test.default_password)) + assert response.status_code == 201 + + lock = json.loads(response.text) + + # get the lock description + response = requests.get(url, auth=(test.default_user, test.default_password)) + assert response.status_code == 200 + + # don't allow other users to delete the lock + response = requests.delete(url, auth=('test2', 'test2')) + assert response.status_code == 200 From 2ee478ceb8c10b875876ae877934c074e1bce601 Mon Sep 17 00:00:00 2001 From: Brett Date: Sun, 8 Sep 2013 21:52:16 -0400 Subject: [PATCH 3/4] - can now create and delete locks on resources --- bauble/model/lock.py | 13 ++++++++++--- bauble/server/resource.py | 26 ++++++++++++++++++++++---- test/api/test_lock.py | 14 ++++++++------ 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/bauble/model/lock.py b/bauble/model/lock.py index a4b4f68..1fa2888 100644 --- a/bauble/model/lock.py +++ b/bauble/model/lock.py @@ -1,3 +1,5 @@ +from datetime import datetime, timedelta + from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.orm.session import object_session @@ -6,9 +8,13 @@ import bauble import bauble.db as db +from bauble.model.user import User import bauble.types as types import bauble.search as search +def default_expiration(): + return datetime.utcnow() + timedelta(days=90) + class Lock(db.Base): __tablename__ = 'lock' @@ -17,13 +23,14 @@ class Lock(db.Base): date_created = Column(types.DateTime, nullable=False, default=func.now()) # a future datetime when this lock automatically expires - date_expires = Column(types.DateTime, nullable=False) + date_expires = Column(types.DateTime, nullable=False, default=default_expiration) # a past datetime when this lock was released, either expired or delete - date_released = Column(types.DateTime, nullable=False) + date_released = Column(types.DateTime) # this should be a - user_id = Column(Integer, ForeignKey('user.id'), nullable=False) + user_id = Column(Integer, ForeignKey(User.id, schema="public"), nullable=False) + user = relationship(User, backref="locks") def json(self, depth=1): d = dict(resource=self.resource) diff --git a/bauble/server/resource.py b/bauble/server/resource.py index a55ad77..ad7ca65 100644 --- a/bauble/server/resource.py +++ b/bauble/server/resource.py @@ -470,6 +470,7 @@ def save_or_update(self, resource_id=None, depth=1): def lock(self, resource_id): """ """ + session = None try: session = self.connect() username, password = parse_auth_header() @@ -484,22 +485,39 @@ def lock(self, resource_id): if is_locked: bottle.abort(423, 'Resource is already locked') - - # lock the resource lock = Lock(resource=resource, user_id=user.id) session.add(lock) session.commit() + response.status = 201 return lock.json(depth=1) finally: if session: session.close() - def delete_lock(resource_id): + def delete_lock(self, resource_id): """ """ - # delete the lock + session = None + try: + session = self.connect() + username, password = parse_auth_header() + user = session.query(User).filter_by(username=username).one() + + # check if this user has a lock on the resource + resource = request.path[len(API_ROOT):-len("/lock")] + locks = session.query(Lock).filter_by(user_id=user.id, resource=resource) + if locks.count() < 1: + bottle.abort(410, "Resource not locked by user") + + # delete all the locks the user has on this resource + map(session.delete, locks.all()) + session.commit() + finally: + if session: + session.close() + @staticmethod diff --git a/test/api/test_lock.py b/test/api/test_lock.py index 22d8b94..ef30c94 100644 --- a/test/api/test_lock.py +++ b/test/api/test_lock.py @@ -1,4 +1,6 @@ +import json + import requests import test.api as test @@ -12,12 +14,12 @@ def test_lock(): response = requests.post(url, auth=(test.default_user, test.default_password)) assert response.status_code == 201 - lock = json.loads(response.text) + # # get the lock description + # response = requests.get(url, auth=(test.default_user, test.default_password)) + # assert response.status_code == 200 - # get the lock description - response = requests.get(url, auth=(test.default_user, test.default_password)) + # delete the lock + response = requests.delete(url, auth=(test.default_user, test.default_password)) assert response.status_code == 200 - # don't allow other users to delete the lock - response = requests.delete(url, auth=('test2', 'test2')) - assert response.status_code == 200 + # TODO: test that other users can't delete lock From 32e91f9b185fef024d0f268e9b99aa4cb3e41991 Mon Sep 17 00:00:00 2001 From: Brett Date: Sun, 8 Sep 2013 22:17:48 -0400 Subject: [PATCH 4/4] don't allow updating a resource if the resource is locked --- bauble/model/lock.py | 6 ++++++ bauble/server/resource.py | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/bauble/model/lock.py b/bauble/model/lock.py index 1fa2888..0da4c57 100644 --- a/bauble/model/lock.py +++ b/bauble/model/lock.py @@ -15,6 +15,12 @@ def default_expiration(): return datetime.utcnow() + timedelta(days=90) + +def get_lock(resource, session): + lock = session.query(Lock).filter_by(resource).first() + return lock if lock else None + + class Lock(db.Base): __tablename__ = 'lock' diff --git a/bauble/server/resource.py b/bauble/server/resource.py index ad7ca65..0a35cc8 100644 --- a/bauble/server/resource.py +++ b/bauble/server/resource.py @@ -24,7 +24,7 @@ from bauble.model.organization import Organization from bauble.model.user import User from bauble.model.reportdef import ReportDef -from bauble.model.lock import Lock +from bauble.model.lock import Lock, get_lock from bauble.server import app, API_ROOT, parse_accept_header, JSON_MIMETYPE, \ TEXT_MIMETYPE, parse_auth_header, accept import bauble.types as types @@ -441,6 +441,14 @@ def save_or_update(self, resource_id=None, depth=1): # if this is a PUT to a specific ID then get the existing family # else we'll create a new one if request.method == 'PUT' and resource_id is not None: + # first check if the resource is locked + resource = request.path[len(API_ROOT):] + lock = get_lock(resource) + if lock: + response.status_code = 423 # locked + return lock.json(depth=1) + + # create the new instance instance = session.query(self.mapped_class).get(resource_id) for key in data.keys(): setattr(instance, key, data[key]) @@ -478,7 +486,6 @@ def lock(self, resource_id): # check if a lock already exists for this resource resource = request.path[len(API_ROOT):-len("/lock")] - print("search_path: ", session.bind.execute("SHOW search_path").scalar()) is_locked = session.query(Lock).\ filter(Lock.resource==resource, Lock.user_id==user.id, Lock.date_released is not None).count() > 0