Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

renewals: add renew buttons for patrons checked-out items #610

Merged
merged 1 commit into from
Nov 20, 2019
Merged
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
renewals: add renew buttons for patrons checked-out items
* 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>
Aly Badr committed Nov 20, 2019

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 5f95c0e02c54fd96ef3d372167f13daedf34b8c3
39 changes: 27 additions & 12 deletions rero_ils/modules/items/api.py
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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'):
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
@@ -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-primary btn-bg">{{_('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
@@ -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
@@ -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(
@@ -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(
@@ -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,
3 changes: 3 additions & 0 deletions tests/ui/holdings/test_holdings_item.py
Original file line number Diff line number Diff line change
@@ -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):
Loading