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 ee3b632
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 54 deletions.
72 changes: 49 additions & 23 deletions invenio_circulation/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,24 @@
'trigger': 'request',
'source': 'CREATED',
'dest': 'PENDING',
'before': 'set_request_parameters'
'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_pickup_at_same_library'
'conditions': [
'is_validate_request_valid',
'is_pickup_at_same_library'
]
}, {
'trigger': 'checkout',
'source': 'CREATED',
Expand All @@ -52,7 +57,8 @@
'trigger': 'checkin',
'source': 'ITEM_ON_LOAN',
'dest': 'ITEM_RETURNED',
'before': 'set_parameters'
'before': 'set_parameters',
'conditions': 'is_checkin_valid'
}]


Expand All @@ -68,6 +74,24 @@ def __init__(self, data, model=None):
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):
"""."""
self.set_parameters(event)
Expand All @@ -84,17 +108,20 @@ def set_parameters(self, event):
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'])
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 +130,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)
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
)
60 changes: 56 additions & 4 deletions invenio_circulation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

from datetime import datetime, timedelta

from flask import current_app


def item_location_retriever(item_pid):
"""."""
Expand All @@ -18,14 +20,64 @@ def item_location_retriever(item_pid):

def is_checkout_valid(transaction_user_pid,
patron_pid,
item_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
7 changes: 7 additions & 0 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,6 +36,12 @@ def instance_path():
shutil.rmtree(path)


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


@pytest.fixture()
def params():
"""."""
Expand Down
88 changes: 63 additions & 25 deletions tests/test_loan.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

"""Tests for loan states."""

import pytest

from invenio_circulation.api import Loan


Expand All @@ -23,18 +25,8 @@ def test_loan_creation(db, params):
assert loan.state == 'ITEM_RETURNED'


def test_conditional_checkout(app, db, params):
"""Test loan checkout and checkin."""
loan = Loan.create({})
loan.checkout(**params)
assert loan.state == 'ITEM_ON_LOAN'
assert loan['start_date'] == params.get('transaction_date')


def test_storing_loan_parameters(db, params):
def test_storing_loan_parameters(loan, db, params):
"""."""
loan = Loan.create({})

loan.checkout(**params)

params['state'] = 'ITEM_ON_LOAN'
Expand All @@ -43,39 +35,85 @@ def test_storing_loan_parameters(db, params):
assert loan == params


def test_persist_loan(db, params):
def test_persist_loan(loan, db, params):
"""."""
loan = Loan.create({})
loan.checkout(**params)
db.session.commit()

loan = Loan.get_record(loan.id)
assert loan.state == 'ITEM_ON_LOAN'


def test_validate_item_in_transit(app, db, params):
def test_validate_item_in_transit(loan, app, db, params):
"""."""
loan = Loan.create({})
params['pickup_location_pid'] = 'pickup_location_pid'

loan.request(**params)
loan.request(**params, pickup_location_pid='pickup_location_pid')
assert loan.state == 'PENDING'

app.config['CIRCULATION_ITEM_LOCATION_RETRIEVER'] =\
lambda x: 'external_location_pid'
loan.validate_request()
loan.validate_request(**params)
assert loan.state == 'ITEM_IN_TRANSIT'


def test_validate_item_at_desk(app, db, params):
def test_validate_item_at_desk(loan, app, db, params):
"""."""
loan = Loan.create({})
params['pickup_location_pid'] = 'pickup_location_pid'

loan.request(**params)
loan.request(**params, pickup_location_pid='pickup_location_pid')
assert loan.state == 'PENDING'

app.config['CIRCULATION_ITEM_LOCATION_RETRIEVER'] =\
lambda x: 'pickup_location_pid'
loan.validate_request()
loan.validate_request(**params)
assert loan.state == 'ITEM_AT_DESK'


def test_conditional_checkout(loan, app, db, params):
"""Test checkout with some conditions."""
loan.checkout(**params)
assert loan.state == 'ITEM_ON_LOAN'
assert loan['start_date'] == params.get('transaction_date')

loan = Loan.create({})
loan.checkout(**params, start_date='2020-01-01', end_date='2020-02-01')
assert loan.state == 'ITEM_ON_LOAN'
assert loan['start_date'] == '2020-01-01'
assert loan['end_date'] == '2020-02-01'

loan = Loan.create({})
with pytest.raises(ValueError):
loan.checkout(**params, start_date='2020-01-xx')
with pytest.raises(ValueError):
loan.checkout(**params, end_date='2020-01-xx')


def test_conditional_checkin(loan, app, db, params):
"""Test checkin with some conditions."""
loan.checkout(**params)
loan.checkin(**params)
assert loan.state == 'ITEM_RETURNED'

loan = Loan.create({})
loan.checkout(**params)
loan.checkin(**params, end_date='2020-02-01')
assert loan.state == 'ITEM_RETURNED'
assert loan['end_date'] == '2020-02-01'

loan = Loan.create({})
loan.checkout(**params)
with pytest.raises(ValueError):
loan.checkin(**params, end_date='2020-01-xx')


def test_conditional_request(loan, app, db, params):
"""Test request with some conditions."""
loan.request(**params, pickup_location_pid='my_pickup_location_pid',
request_expire_date='2020-02-01')
assert loan.state == 'PENDING'
loan['request_expire_date'] == '2020-02-01'
loan['pickup_location_pid'] == 'my_pickup_location_pid'

loan = Loan.create({})
app.config['CIRCULATION_ITEM_LOCATION_RETRIEVER'] =\
lambda x: 'custom_pickup_location_pid'
loan.request(**params)
assert loan.state == 'PENDING'
loan['pickup_location_pid'] == 'custom_pickup_location_pid'

0 comments on commit ee3b632

Please sign in to comment.