Skip to content

Commit

Permalink
renewals: add renew buttons for patrons checked-out items
Browse files Browse the repository at this point in the history
* Displays renew button when renewal of checked-out item is possible.
* Hides renew button when circulation policies do not permit the renewal action.
* Increases units testing coverage.
* Corrects indentation of the patron_profile Jinja template.

Co-Authored-by: Aly Badr <aly.badr@rero.ch>
  • Loading branch information
Aly Badr committed Nov 13, 2019
1 parent 6d69b22 commit cc39288
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 122 deletions.
39 changes: 27 additions & 12 deletions rero_ils/modules/items/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

from .models import ItemIdentifier, ItemStatus
from ..api import IlsRecord, IlsRecordError, IlsRecordIndexer, IlsRecordsSearch
from ..circ_policies.api import CircPolicy
from ..documents.api import Document, DocumentsSearch
from ..errors import InvalidRecordID
from ..fetchers import id_fetcher
Expand Down Expand Up @@ -547,10 +548,33 @@ def last_location_pid(self):
return loan_location_pid
return self.location_pid

def can_extend(self, loan):
"""Checks if the patron has the rights to renew this item."""
from ..loans.utils import extend_loan_data_is_valid
can_extend = True
patron_pid = loan.get('patron_pid')
patron_type_pid = Patron.get_record_by_pid(
patron_pid).patron_type_pid
circ_policy = CircPolicy.provide_circ_policy(
self.library_pid,
patron_type_pid,
self.item_type_pid
)
extension_count = loan.get('extension_count', 0)
if not (
circ_policy.get('number_renewals') > 0 and
extension_count < circ_policy.get('number_renewals') and
extend_loan_data_is_valid(
loan.get('end_date'),
circ_policy.get('renewal_duration'),
self.library_pid
)
) or self.number_of_requests():
can_extend = False
return can_extend

def action_filter(self, action, loan):
"""Filter actions."""
from ..circ_policies.api import CircPolicy
from ..loans.utils import extend_loan_data_is_valid
patron_pid = loan.get('patron_pid')
patron_type_pid = Patron.get_record_by_pid(
patron_pid).patron_type_pid
Expand All @@ -564,16 +588,7 @@ def action_filter(self, action, loan):
'new_action': None
}
if action == 'extend':
extension_count = loan.get('extension_count', 0)
if not (
circ_policy.get('number_renewals') > 0 and
extension_count < circ_policy.get('number_renewals') and
extend_loan_data_is_valid(
loan.get('end_date'),
circ_policy.get('renewal_duration'),
self.library_pid
)
) or self.number_of_requests():
if not self.can_extend(loan):
data['action_validated'] = False
if action == 'checkout':
if not circ_policy.get('allow_checkout'):
Expand Down
183 changes: 96 additions & 87 deletions rero_ils/modules/patrons/templates/rero_ils/patron_profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,115 +19,124 @@
{%- extends 'rero_ils/page.html' %}

{%- block body %}
{% include('rero_ils/_patron_profile_head.html') %}
{% include('rero_ils/_patron_profile_head.html') %}

<article class="mt-4">
<header>
<nav>
<ul class="nav nav-tabs" id="nav-tab" role="tablist">
<article class="mt-4">
<header>
<nav>
<ul class="nav nav-tabs" id="nav-tab" role="tablist">
<li class="nav-item">
<a class="nav-link active" href="#checkouts" data-toggle="tab"
id="checkouts-tab" title="{{ _('Checkouts') }}" role="tab"
aria-controls="checkouts" aria-selected="true">
<a class="nav-link active" href="#checkouts" data-toggle="tab" id="checkouts-tab" title="{{ _('Checkouts') }}"
role="tab" aria-controls="checkouts" aria-selected="true">
{{ _('Checkouts') }} ({{ checkouts|length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#pending" data-toggle="tab"
id="pending-tab" title="{{ _('Pending') }}" role="tab"
aria-controls="pending" aria-selected="false">
<a class="nav-link" href="#pending" data-toggle="tab" id="pending-tab" title="{{ _('Pending') }}" role="tab"
aria-controls="pending" aria-selected="false">
{{ _('Pending') }} ({{ pendings|length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#personal-data" data-toggle="tab"
id="personal-data-tab" title="{{ _('Personal data') }}" role="tab"
aria-controls="personal-data" aria-selected="false">
<a class="nav-link" href="#personal-data" data-toggle="tab" id="personal-data-tab"
title="{{ _('Personal data') }}" role="tab" aria-controls="personal-data" aria-selected="false">
{{ _('Personal data') }}
</a>
</li>
</ul>
</nav>
</header>
<article class="tab-content">
<section class="tab-pane show active py-2" id="checkouts" role="tabpanel" aria-labelledby="checkouts-tab">
{% if checkouts|length > 0 %}
{{ build_table('checkouts', checkouts) }}
{% else %}
<p>{{ _('No loan') }}</p>
{% endif %}
</section>
<section class="tab-pane py-2" id="pending" role="tabpanel" aria-labelledby="pending-tab">
{% if pendings|length > 0 %}
{{ build_table('requests', pendings) }}
{% else %}
<p>{{ _('No pending') }}</p>
{% endif %}
</section>
<section class="tab-pane py-2" id="personal-data" role="tabpanel" aria-labelledby="personal-data-tab">
{% include('rero_ils/_patron_profile_personal.html') %}
</section>
</article>
</ul>
</nav>
</header>
<article class="tab-content">
<section class="tab-pane show active py-2" id="checkouts" role="tabpanel" aria-labelledby="checkouts-tab">
{% if checkouts|length > 0 %}
{{ build_table('checkouts', checkouts) }}
{% else %}
<p>{{ _('No loan') }}</p>
{% endif %}
</section>
<section class="tab-pane py-2" id="pending" role="tabpanel" aria-labelledby="pending-tab">
{% if pendings|length > 0 %}
{{ build_table('requests', pendings) }}
{% else %}
<p>{{ _('No pending') }}</p>
{% endif %}
</section>
<section class="tab-pane py-2" id="personal-data" role="tabpanel" aria-labelledby="personal-data-tab">
{% include('rero_ils/_patron_profile_personal.html') %}
</section>
</article>
{%- endblock body %}
</article>


{%- endblock body %}
{% macro build_table(type, loans) %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th class="col-md-6 border-top-0" scope="col">{{ _('Title') }}</th>
<th class="col-md-2 border-top-0" scope="col">{{ _('Call Number') }}</th>
{% if type != 'checkouts' %}
<th class="col-md-2 border-top-0" scope="col">
{{ _('Pickup library') }}
</th>
{% endif %}
<th class="col-md-2 border-top-0" scope="col">
{% if type == 'checkouts' %}
{{ _('Due date') }}
{% else %}
{{ _('Reservation date') }}
{% endif %}
</th>
{% if type == 'checkouts' %}
<th class="col-md-1 border-top-0" scope="col">
{{ _('Renewals') }}
</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for loan in loans %}
<tr>
<td>{{ loan.document_title }}</td>
<td>{{ loan.item_call_number }}</td>
{% if type != 'checkouts' %}
<td>
{{ loan.pickup_library_name }}
</td>
{% endif %}
<td>
{% if type == 'checkouts' %}
{{ loan.end_date|format_date(
<table class="table table-striped table-sm">
<thead>
<tr>
<th class="col-md-6 border-top-0" scope="col">{{ _('Title') }}</th>
<th class="col-md-2 border-top-0" scope="col">{{ _('Call Number') }}</th>
{% if type != 'checkouts' %}
<th class="col-md-2 border-top-0" scope="col">
{{ _('Pickup library') }}
</th>
{% endif %}
<th class="col-md-2 border-top-0" scope="col">
{% if type == 'checkouts' %}
{{ _('Due date') }}
{% else %}
{{ _('Reservation date') }}
{% endif %}
</th>
{% if type == 'checkouts' %}
<th class="col-md-1 border-top-0" scope="col">
{{ _('Renewals') }}
</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for loan in loans %}
<tr>
<td>{{ loan.document_title }}</td>
<td>{{ loan.item_call_number }}</td>
{% if type != 'checkouts' %}
<td>
{{ loan.pickup_library_name }}
</td>
{% endif %}
<td>
{% if type == 'checkouts' %}
{{ loan.end_date|format_date(
format='short_date',
locale=current_i18n.locale.language
)}}
{% else %}
{{ loan.transaction_date|format_date(
{% else %}
{{ loan.transaction_date|format_date(
format='short_date',
locale=current_i18n.locale.language
)}}
{% endif %}
</td>
{% if type == 'checkouts' %}
<td>
{{ loan.extension_count }}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</td>
{% if type == 'checkouts' %}
<td>
{{ loan.extension_count }}
</td>
<td>
{% if loan.can_renew %}
{%- with form = can_renew_form %}
<form action="{{ url_for('patrons.profile') }}" method="POST" name="can_renew_form">
<input type="hidden" name="loan_pid" value="{{ loan.pid }}">
<button type="submit" class="btn btn btn-outline-primary btn-sm">{{_('Renew')}}</button>
</form>
{%- endwith %}
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endmacro %}

63 changes: 41 additions & 22 deletions rero_ils/modules/patrons/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@

from __future__ import absolute_import, print_function

from flask import Blueprint, current_app, jsonify, render_template, request
from flask import Blueprint, current_app, flash, jsonify, render_template, \
request
from flask_babelex import gettext as _
from flask_login import current_user, login_required
from flask_menu import register_menu
Expand All @@ -31,7 +32,7 @@
from ..documents.api import Document
from ..items.api import Item
from ..libraries.api import Library
from ..loans.api import get_loans_by_patron_pid
from ..loans.api import get_loans_by_patron_pid, Loan
from ..locations.api import Location

api_blueprint = Blueprint(
Expand Down Expand Up @@ -88,7 +89,8 @@ def logged_user():
return jsonify(data)


@blueprint.route('/global/patrons/profile', defaults={'viewcode': 'global'})
@blueprint.route('/global/patrons/profile', defaults={'viewcode': 'global'},
methods=['GET', 'POST'])
@blueprint.route('/<string:viewcode>/patrons/profile')
@login_required
@register_menu(
Expand All @@ -102,28 +104,45 @@ def profile(viewcode):
patron = Patron.get_patron_by_user(current_user)
if patron is None:
raise NotFound()
if request.method == 'POST':
loan = Loan.get_record_by_pid(request.values.get('loan_pid'))
item = Item.get_record_by_pid(loan.get('item_pid'))
data = {
'item_pid': item.pid,
'pid': request.values.get('loan_pid'),
'transaction_location_pid': item.location_pid
}
try:
item.extend_loan(**data)
flash(_('The item %(item_id)s has been renewed.',
item_id=item.pid), 'success')
except Exception:
flash(_('Error during the renewal of the item %(item_id)s.',
item_id=item.pid), 'danger')

loans = get_loans_by_patron_pid(patron.pid)
checkouts = []
requests = []
if loans:
for loan in loans:
item_pid = loan.get('item_pid')
item = Item.get_record_by_pid(item_pid).replace_refs()
document = Document.get_record_by_pid(item['document']['pid'])
loan['document_title'] = document['title']
loan['item_call_number'] = item['call_number']
if loan['state'] == 'ITEM_ON_LOAN':
checkouts.append(loan)
elif loan['state'] in (
'PENDING',
'ITEM_AT_DESK',
'ITEM_IN_TRANSIT_FOR_PICKUP'
):
pickup_loc = Location.get_record_by_pid(
loan['pickup_location_pid'])
loan['pickup_library_name'] = \
pickup_loc.get_library().get('name')
requests.append(loan)
for loan in loans:
item_pid = loan.get('item_pid')
item = Item.get_record_by_pid(item_pid)
document = Document.get_record_by_pid(
item.replace_refs()['document']['pid'])
loan['document_title'] = document['title']
loan['item_call_number'] = item['call_number']
if loan['state'] == 'ITEM_ON_LOAN':
loan['can_renew'] = item.can_extend(loan)
checkouts.append(loan)
elif loan['state'] in (
'PENDING',
'ITEM_AT_DESK',
'ITEM_IN_TRANSIT_FOR_PICKUP'
):
pickup_loc = Location.get_record_by_pid(
loan['pickup_location_pid'])
loan['pickup_library_name'] = \
pickup_loc.get_library().get('name')
requests.append(loan)
return render_template(
'rero_ils/patron_profile.html',
record=patron,
Expand Down
3 changes: 3 additions & 0 deletions tests/ui/holdings/test_holdings_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ def test_holding_item_links(client, holding_lib_martigny, item_lib_martigny,
assert not holding_lib_martigny.delete_from_index()
holding_lib_martigny.dbcommit(forceindex=True)

# test item count by holdings pid
assert holding_lib_martigny.get_items_count_by_holding_pid == 2


def test_holding_delete_after_item_deletion(
client, holding_lib_martigny, item_lib_martigny):
Expand Down
Loading

0 comments on commit cc39288

Please sign in to comment.