Skip to content
This repository was archived by the owner on Mar 22, 2018. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions bauble/model/lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from datetime import datetime, timedelta

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
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)


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'

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, default=default_expiration)

# a past datetime when this lock was released, either expired or delete
date_released = Column(types.DateTime)

# this should be a
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)
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)
70 changes: 70 additions & 0 deletions bauble/server/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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
Expand Down Expand Up @@ -65,6 +66,12 @@ def __init__(self):
"DELETE": self.delete
})

self.add_route(API_ROOT + self.resource + "/<resource_id>/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,
Expand All @@ -80,16 +87,19 @@ def __init__(self):
{"OPTIONS": self.options_response,
"GET": self.get_schema
})

self.add_route(API_ROOT + self.resource + "/<relation:path>/schema",
{"OPTIONS": self.options_response,
'GET': self.get_schema
})

self.add_route(API_ROOT +
self.resource + "/<resource_id>/<relation:path>",
{"OPTIONS": self.options_response,
"GET": self.get_relation
})


session_events = []


Expand Down Expand Up @@ -431,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])
Expand All @@ -457,6 +475,58 @@ def save_or_update(self, resource_id=None, depth=1):
session.close()


def lock(self, resource_id):
"""
"""
session = None
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")]
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()
response.status = 201
return lock.json(depth=1)
finally:
if session:
session.close()


def delete_lock(self, resource_id):
"""
"""
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
def get_ref_id(ref):
# assume that if ref is not a str then it is a resource JSON object
Expand Down
25 changes: 25 additions & 0 deletions test/api/test_lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

import json

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

# # get the lock description
# response = requests.get(url, auth=(test.default_user, test.default_password))
# assert response.status_code == 200

# delete the lock
response = requests.delete(url, auth=(test.default_user, test.default_password))
assert response.status_code == 200

# TODO: test that other users can't delete lock