Skip to content

Commit

Permalink
views: add loans actions url rules
Browse files Browse the repository at this point in the history
- add view to handle loan actions with REST calls
- closes inveniosoftware#26

Co-authored-by: Zacharias Zacharodimos <zacharias.zacharodimos@cern.ch>
Co-authored-by: Christos Topaloudis <topless@gmail.com>
  • Loading branch information
3 people committed Jul 23, 2018
1 parent 05b7400 commit 6c9367d
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 103 deletions.
109 changes: 29 additions & 80 deletions invenio_circulation/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,84 +8,29 @@

"""Circulation API."""

import config
import warnings
from datetime import datetime

from flask import current_app
from invenio_records.api import Record
from transitions import Machine

DIAGRAM_ENABLED = True
try:
import pygraphviz
from transitions.extensions import GraphMachine
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',
'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):
"""Loan record class."""

def __init__(self, data, model=None):
"""."""
data.setdefault('state', STATES[0])
self.states = current_app.config['CIRCULATION_LOAN_STATES']
self.transitions = current_app.config['CIRCULATION_LOAN_TRANSITIONS']
data.setdefault('state', self.states[0])
super(Loan, self).__init__(data, model)
Machine(
model=self,
states=STATES,
states=self.states,
send_event=True,
transitions=TRANSITIONS,
transitions=self.transitions,
initial=self['state'],
finalize_event='save',
)
Expand All @@ -95,25 +40,6 @@ 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 Down Expand Up @@ -174,3 +100,26 @@ def is_request_valid(self, event):
def is_validate_request_valid(self, event):
"""."""
return self.policies['validate_request'](**event.kwargs)

@classmethod
def export_diagram(cls, output_file):
"""."""
from transitions.extensions import GraphMachine

try:
import pygraphviz
except ImportError:
warnings.warn('dependency not found, please install pygraphviz to '
'export the circulation state diagram.')
return False

# FIXME: replace config with current_app.config when CLI has appcontext
# states = current_app.config['CIRCULATION_LOAN_STATES']
# transitions = current_app.config['CIRCULATION_LOAN_TRANSITIONS']
states = config.CIRCULATION_LOAN_STATES
transitions = config.CIRCULATION_LOAN_TRANSITIONS
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
66 changes: 62 additions & 4 deletions invenio_circulation/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,63 @@

"""Invenio module for the circulation of bibliographic items."""


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

CIRCULATION_LOAN_STATES = [
'CREATED',
'PENDING',
'ITEM_ON_LOAN',
'ITEM_RETURNED',
'ITEM_IN_TRANSIT',
'ITEM_AT_DESK',
]
"""."""

CIRCULATION_LOAN_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',
}
]
"""."""

CIRCULATION_ITEM_LOCATION_RETRIEVER = item_location_retriever
"""."""

Expand All @@ -38,15 +91,20 @@
_CIRCULATION_LOAN_FETCHER = 'circ_loanid'
"""."""

CIRCULATION_LOAN_ITEM_ROUTE = '/circulation/loan/<pid(loanid):pid_value>'
"""."""

_Loan_PID = 'pid(loanid,record_class="invenio_circulation.api:Loan")'
CIRCULATION_REST_ENDPOINTS = dict(
loanid=dict(
pid_type=_CIRCULATION_LOAN_PID_TYPE,
pid_minter=_CIRCULATION_LOAN_MINTER,
pid_fetcher=_CIRCULATION_LOAN_FETCHER,
# search_class=RecordsSearch,
# indexer_class=RecordIndexer,
search_index=None,
search_type=None,
# search_index=None,
# search_type=None,
record_class=Loan,
record_serializers={
'application/json': ('invenio_records_rest.serializers'
':json_v1_response'),
Expand All @@ -56,7 +114,7 @@
':json_v1_search'),
},
list_route='/circulation/loan/',
item_route='/circulation/loan/<pid(loanid):pid_value>',
item_route='/circulation/loan/<{0}:pid_value>'.format(_Loan_PID),
default_media_type='application/json',
max_result_window=10000,
error_handlers=dict(),
Expand Down
13 changes: 13 additions & 0 deletions invenio_circulation/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 CERN.
# Copyright (C) 2018 RERO.
#
# Invenio-Circulation is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.

"""Circulation exceptions."""


class LoanActionError(Exception):
"""Loan action error."""
4 changes: 4 additions & 0 deletions invenio_circulation/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from __future__ import absolute_import, print_function

from . import config
from .views import build_blueprint_with_loan_actions


class InvenioCirculation(object):
Expand All @@ -27,6 +28,9 @@ def init_app(self, app):
app.config.setdefault('RECORDS_REST_ENDPOINTS', {})
app.config['RECORDS_REST_ENDPOINTS'].update(
app.config['CIRCULATION_REST_ENDPOINTS'])

blueprint = build_blueprint_with_loan_actions(app)
app.register_blueprint(blueprint)
app.extensions['invenio-circulation'] = self

def init_config(self, app):
Expand Down
126 changes: 126 additions & 0 deletions invenio_circulation/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 CERN.
# Copyright (C) 2018 RERO.
#
# Invenio-Circulation is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.

"""Circulation views."""

from copy import deepcopy

from flask import Blueprint, current_app, request
from invenio_db import db
from invenio_records_rest.views import \
create_error_handlers as records_rest_error_handlers
from invenio_records_rest.views import pass_record
from invenio_records_rest.utils import obj_or_import_string
from invenio_rest import ContentNegotiatedMethodView
from invenio_rest.views import create_api_errorhandler
from transitions import MachineError

from invenio_circulation.errors import LoanActionError

HTTP_CODES = {
'method_not_allowed': 405,
'accepted': 202
}


def create_error_handlers(blueprint):
"""Create error handlers on blueprint."""
blueprint.errorhandler(LoanActionError)(create_api_errorhandler(
status=HTTP_CODES['method_not_allowed'], message='Invalid loan action'
))
records_rest_error_handlers(blueprint)


def build_blueprint_with_loan_actions(app):
"""."""
blueprint = Blueprint(
'invenio_circulation',
__name__,
url_prefix='',
)
create_error_handlers(blueprint)

endpoints = app.config.get('CIRCULATION_REST_ENDPOINTS', [])
transitions = app.config.get('CIRCULATION_LOAN_TRANSITIONS', [])

for endpoint, options in (endpoints or {}).items():
options = deepcopy(options)

if 'record_serializers' in options:
serializers = options.get('record_serializers')
serializers = {mime: obj_or_import_string(func)
for mime, func in serializers.items()}
else:
serializers = {}

ctx = dict(
read_permission_factory=obj_or_import_string(
options.get('read_permission_factory_imp')
),
create_permission_factory=obj_or_import_string(
options.get('create_permission_factory_imp')
),
update_permission_factory=obj_or_import_string(
options.get('update_permission_factory_imp')
),
delete_permission_factory=obj_or_import_string(
options.get('delete_permission_factory_imp')
),
)

loan_actions = LoanActionResource.as_view(
LoanActionResource.view_name.format(endpoint),
serializers=serializers,
ctx=ctx,
)

distinct_actions = (transition['trigger'] for transition in
transitions)
url = '{0}/<any({1}):action>'.format(
options['item_route'],
','.join(distinct_actions),
)
blueprint.add_url_rule(
url,
view_func=loan_actions,
methods=['POST'],
)

return blueprint


class LoanActionResource(ContentNegotiatedMethodView):
"""Loan action resource."""

view_name = '{0}_actions'

def __init__(self, serializers, ctx, *args, **kwargs):
"""Constructor."""
super(LoanActionResource, self).__init__(
serializers,
default_media_type=ctx.get('default_media_type'),
*args,
**kwargs
)
for key, value in ctx.items():
setattr(self, key, value)

@pass_record
def post(self, pid, record, action, **kwargs):
"""Handle loan action."""
params = request.get_json()

try:
# perform action on the current loan
getattr(record, action)(**params)
db.session.commit()
except MachineError as ex:
current_app.logger.exception(ex)
raise LoanActionError(ex)

return self.make_response(pid, record, HTTP_CODES['accepted'])
Loading

0 comments on commit 6c9367d

Please sign in to comment.