Skip to content

Commit

Permalink
api: add missing transition conditions
Browse files Browse the repository at this point in the history
* Implement checkin, request, validate_request circulation policies
  using states conditions. (closes inveniosoftware#28)

Signed-off-by: Johnny Mariéthoz <Johnny.Mariethoz@rero.ch>
  • Loading branch information
jma committed Jul 19, 2018
1 parent c74bfce commit bb65ae9
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 105 deletions.
171 changes: 111 additions & 60 deletions invenio_circulation/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,56 @@
except ImportError:
DIAGRAM_ENABLED = False

STATES = ['CREATED', 'PENDING', 'ITEM_ON_LOAN', 'ITEM_RETURNED',
'ITEM_IN_TRANSIT', 'ITEM_AT_DESK']

TRANSITIONS = [{
'trigger': 'request',
'source': 'CREATED',
'dest': 'PENDING',
'before': 'set_request_parameters'
}, {
'trigger': 'validate_request',
'source': 'PENDING',
'dest': 'ITEM_IN_TRANSIT',
'before': 'set_parameters',
'unless': 'is_pickup_at_same_library'
}, {
'trigger': 'validate_request',
'source': 'PENDING',
'dest': 'ITEM_AT_DESK',
'before': 'set_parameters',
'conditions': 'is_pickup_at_same_library'
}, {
'trigger': 'checkout',
'source': 'CREATED',
'dest': 'ITEM_ON_LOAN',
'before': 'set_parameters',
'conditions': 'is_checkout_valid'
}, {
'trigger': 'checkin',
'source': 'ITEM_ON_LOAN',
'dest': 'ITEM_RETURNED',
'before': 'set_parameters'
}]
STATES = [
'CREATED',
'PENDING',
'ITEM_ON_LOAN',
'ITEM_RETURNED',
'ITEM_IN_TRANSIT',
'ITEM_AT_DESK',
]

TRANSITIONS = [
{
'trigger': 'request',
'source': 'CREATED',
'dest': 'PENDING',
'before': 'set_request_parameters',
'conditions': 'is_request_valid',
},
{
'trigger': 'validate_request',
'source': 'PENDING',
'dest': 'ITEM_IN_TRANSIT',
'before': 'set_parameters',
'conditions': 'is_validate_request_valid',
'unless': 'is_pickup_at_same_library',
},
{
'trigger': 'validate_request',
'source': 'PENDING',
'dest': 'ITEM_AT_DESK',
'before': 'set_parameters',
'conditions': [
'is_validate_request_valid',
'is_pickup_at_same_library',
],
},
{
'trigger': 'checkout',
'source': 'CREATED',
'dest': 'ITEM_ON_LOAN',
'before': 'set_parameters',
'conditions': 'is_checkout_valid',
},
{
'trigger': 'checkin',
'source': 'ITEM_ON_LOAN',
'dest': 'ITEM_RETURNED',
'before': 'set_parameters',
'conditions': 'is_checkin_valid',
},
]


class Loan(Record):
Expand All @@ -63,10 +81,38 @@ def __init__(self, data, model=None):
"""."""
data.setdefault('state', STATES[0])
super(Loan, self).__init__(data, model)
Machine(model=self, states=STATES, send_event=True,
transitions=TRANSITIONS,
initial=self['state'],
finalize_event='save')
Machine(
model=self,
states=STATES,
send_event=True,
transitions=TRANSITIONS,
initial=self['state'],
finalize_event='save',
)

@property
def policies(self):
"""."""
return current_app.config.get('CIRCULATION_POLICIES')

@classmethod
def export_diagram(cls, output_file):
"""."""
if not DIAGRAM_ENABLED:
warnings.warn(
'dependency not found, please install pygraphviz to '
'export the circulation state diagram.'
)
return False
m = GraphMachine(
states=STATES,
transitions=TRANSITIONS,
initial=STATES[0],
show_conditions=True,
title='Circulation State Diagram',
)
m.get_graph().draw(output_file, prog='dot')
return True

def set_request_parameters(self, event):
"""."""
Expand All @@ -80,21 +126,27 @@ def set_parameters(self, event):
self['patron_pid'] = params.get('patron_pid')
self['item_pid'] = params.get('item_pid')
self['transaction_location_pid'] = params.get(
'transaction_location_pid')
self['transaction_date'] = params.get('transaction_date',
datetime.now().isoformat())
'transaction_location_pid'
)
self['transaction_date'] = params.get(
'transaction_date', datetime.now().isoformat()
)

def save(self, event):
"""."""
if event.error:
raise event.error
else:
self['state'] = self.state
self.commit()

def is_pickup_at_same_library(self, event):
"""."""
item_location_pid = current_app.config.get(
'CIRCULATION_ITEM_LOCATION_RETRIEVER')(self['item_pid'])
'CIRCULATION_ITEM_LOCATION_RETRIEVER'
)(self['item_pid'])
return self['pickup_location_pid'] == item_location_pid

@property
def policies(self):
"""."""
return current_app.config.get('CIRCULATION_POLICIES')

def is_checkout_valid(self, event):
"""."""
dates = self.policies['checkout'](**event.kwargs)
Expand All @@ -103,23 +155,22 @@ def is_checkout_valid(self, event):
self['start_date'], self['end_date'] = dates
return True

def save(self, event):
def is_checkin_valid(self, event):
"""."""
if event.error:
raise Exception(event.error)
else:
self['state'] = self.state
self.commit()
end_date = self.policies['checkin'](**event.kwargs)
if not end_date:
return False
self['end_date'] = end_date
return True

@classmethod
def export_diagram(cls, output_file):
def is_request_valid(self, event):
"""."""
if not DIAGRAM_ENABLED:
warnings.warn('dependency not found, please install pygraphviz to '
'export the circulation state diagram.')
extra_params = self.policies['request'](**event.kwargs)
if not extra_params:
return False
m = GraphMachine(states=STATES, transitions=TRANSITIONS,
initial=STATES[0], show_conditions=True,
title='Circulation State Diagram')
m.get_graph().draw(output_file, prog='dot')
self['pickup_location_pid'], self['request_expire_date'] = extra_params
return True

def is_validate_request_valid(self, event):
"""."""
return self.policies['validate_request'](**event.kwargs)
1 change: 1 addition & 0 deletions invenio_circulation/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ def circulation():
def diagram(output_file_name):
"""Save the circulation state diagram to a png file."""
from .api import Loan

if not Loan.export_diagram(output_file_name):
raise click.Abort()
10 changes: 8 additions & 2 deletions invenio_circulation/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
# TODO: This is an example file. Remove it if your package does not use any
# extra configuration variables.

from .utils import is_checkout_valid, item_location_retriever
from .utils import is_checkin_valid, is_checkout_valid, is_request_valid, \
is_request_validate_valid, item_location_retriever

CIRCULATION_DEFAULT_VALUE = 'foobar'
"""Default value for the application."""
Expand All @@ -21,4 +22,9 @@

CIRCULATION_ITEM_LOCATION_RETRIEVER = item_location_retriever

CIRCULATION_POLICIES = dict(checkout=is_checkout_valid)
CIRCULATION_POLICIES = dict(
checkout=is_checkout_valid,
checkin=is_checkin_valid,
request=is_request_valid,
validate_request=is_request_validate_valid,
)
84 changes: 74 additions & 10 deletions invenio_circulation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,86 @@

from datetime import datetime, timedelta

from flask import current_app


def item_location_retriever(item_pid):
"""."""
pass


def is_checkout_valid(transaction_user_pid,
patron_pid,
item_pid,
transaction_location_pid,
transaction_date,
start_date=None,
end_date=None):
def is_checkout_valid(
transaction_user_pid,
patron_pid,
transaction_location_pid,
transaction_date,
item_pid,
start_date=None,
end_date=None,
):
"""."""
if not start_date:
start_date = datetime.strptime(transaction_date, '%Y-%m-%d')
start_date = transaction_date
# 30 days by default
if not end_date:
end_date = datetime.strptime(start_date, '%Y-%m-%d') + timedelta(
days=30
)
end_date = end_date.strftime('%Y-%m-%d')
assert datetime.strptime(start_date, '%Y-%m-%d')
assert datetime.strptime(end_date, '%Y-%m-%d')
return (start_date, end_date)


def is_checkin_valid(
transaction_user_pid,
patron_pid,
transaction_location_pid,
transaction_date,
item_pid,
end_date=None,
):
"""."""
# 30 days by default
if not end_date:
end_date = start_date + timedelta(days=30)
return (start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d'))
end_date = datetime.strptime(transaction_date, '%Y-%m-%d') + timedelta(
days=30
)
end_date = end_date.strftime('%Y-%m-%d')
assert datetime.strptime(end_date, '%Y-%m-%d')
return end_date


def is_request_valid(
transaction_user_pid,
patron_pid,
transaction_location_pid,
transaction_date,
item_pid,
pickup_location_pid=None,
request_expire_date=None,
):
"""."""
# item location by default
if not pickup_location_pid:
pickup_location_pid = current_app.config.get(
'CIRCULATION_ITEM_LOCATION_RETRIEVER'
)(item_pid)
# 30 days by default
if not request_expire_date:
request_expire_date = datetime.strptime(
transaction_date, '%Y-%m-%d'
) + timedelta(days=30)
request_expire_date = request_expire_date.strftime('%Y-%m-%d')
return (pickup_location_pid, request_expire_date)


def is_request_validate_valid(
transaction_user_pid,
patron_pid,
item_pid,
transaction_location_pid,
transaction_date,
):
"""."""
return True
22 changes: 17 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from invenio_records.ext import InvenioRecords
from sqlalchemy_utils.functions import create_database, database_exists

from invenio_circulation.api import Loan
from invenio_circulation.ext import InvenioCirculation


Expand All @@ -35,13 +36,23 @@ def instance_path():
shutil.rmtree(path)


@pytest.yield_fixture()
def loan(db):
"""Minimal Loan object."""
yield Loan.create({})


@pytest.fixture()
def params():
"""."""
now = datetime.now().strftime('%Y-%m-%d')
return dict(transaction_user_pid='user_pid', patron_pid='patron_pid',
item_pid='item_pid', transaction_location_pid='loc_pid',
transaction_date=now)
return dict(
transaction_user_pid='user_pid',
patron_pid='patron_pid',
item_pid='item_pid',
transaction_location_pid='loc_pid',
transaction_date=now,
)


@pytest.fixture
Expand Down Expand Up @@ -73,8 +84,9 @@ def base_app(instance_path, tmp_db_path):
app_ = Flask('testapp', instance_path=instance_path)
app_.config.update(
SECRET_KEY='SECRET_KEY',
SQLALCHEMY_DATABASE_URI=os.environ.get('SQLALCHEMY_DATABASE_URI',
tmp_db_path),
SQLALCHEMY_DATABASE_URI=os.environ.get(
'SQLALCHEMY_DATABASE_URI', tmp_db_path
),
SQLALCHEMY_TRACK_MODIFICATIONS=True,
TESTING=True,
)
Expand Down
Loading

0 comments on commit bb65ae9

Please sign in to comment.