Skip to content

Commit

Permalink
circulation: hide buttons if action isn't available.
Browse files Browse the repository at this point in the history
If the patron can't operate an action due to circulation restrictions
(patron blocked, patron type limits, ...), circulation buttons should be
disable. In the patron profile view, adds warning/error messages depending
of patron restrictions.

If the 'renew' button is disable, adds a tooltip to give user the
reasons why.

Closes rero#1357
Closes rero#1356

Co-Authored-by: Renaud Michotte <renaud.michotte@gmail.com>
  • Loading branch information
zannkukai committed Nov 5, 2020
1 parent 273ab44 commit 2afc952
Show file tree
Hide file tree
Showing 17 changed files with 170 additions and 108 deletions.
49 changes: 30 additions & 19 deletions rero_ils/modules/documents/templates/rero_ils/_documents_get.html
Original file line number Diff line number Diff line change
Expand Up @@ -207,27 +207,38 @@
{%- endfor %}

<!-- action button : Should be at the end to be render above and be clickabke -->
{%- set locations = item|item_library_pickup_locations %}
{%- if item|can_request and locations %}
<div class="action-buttons">
<a href="#" type="button" class="btn btn-primary btn-sm" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" id="{{ item.barcode }}-dropdownMenu">
{{ _('Request') }}
<i class="fa fa-caret-down fa-fw"></i>
</a>
<!-- TODO: Style the dropdown header -->
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenu">
<h6 class="dropdown-header">{{ _('Select a Pickup Location') }}</h6>
<div class="dropdown-divider"></div>
{% for location in locations %}
<a class="dropdown-item"
id="{{ location.code }}"
href="{{ url_for('item.patron_request', viewcode=viewcode, item_pid=item.pid, pickup_location_pid=location.pid)}}">
{{ location.pickup_name }}
{%- if item|item_and_patron_in_same_organisation %}
{%- set can_request, reasons = item|can_request %}
{%- set locations = item|item_library_pickup_locations %}
{%- if can_request and locations %}
<div class="action-buttons">
<a href="#" type="button" class="btn btn-primary btn-sm" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" id="{{ item.barcode }}-dropdownMenu">
{{ _('Request') }}
<i class="fa fa-caret-down fa-fw"></i>
</a>
{% endfor %}
<!-- TODO: Style the dropdown header -->
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenu">
<h6 class="dropdown-header">{{ _('Select a Pickup Location') }}</h6>
<div class="dropdown-divider"></div>
{% for location in locations %}
<a class="dropdown-item"
id="{{ location.code }}"
href="{{ url_for('item.patron_request', viewcode=viewcode, item_pid=item.pid, pickup_location_pid=location.pid)}}">
{{ location.pickup_name }}
</a>
{% endfor %}
</div>
</div>
</div>
{%- elif reasons %}
<div class="action-buttons">
<span class="d-inline-block" tabindex="0" data-toggle="tooltip" data-html="true" title="{{ reasons | join('<br/>') }}">
<button type="submit" class="btn btn-primary mt-1" disabled>
{{ _('Request') }}
</button>
</span>
</div>
{%- endif %}
{%- endif %}
</div>
{%- endfor %}
Expand Down
15 changes: 11 additions & 4 deletions rero_ils/modules/documents/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from ..libraries.api import Library
from ..locations.api import Location
from ..organisations.api import Organisation
from ..patrons.api import Patron
from ..patrons.api import Patron, current_patron
from ..persons.api import Person
from ..utils import extracted_data_from_ref
from ...permissions import login_and_librarian
Expand Down Expand Up @@ -116,19 +116,26 @@ def cover(isbn):
)


@blueprint.app_template_filter()
def item_and_patron_in_same_organisation(item):
"""Check if the current user belongs to the same organisation than item."""
return current_patron and current_patron.organisation_pid == \
item.organisation_pid


@blueprint.app_template_filter()
def can_request(item):
"""Check if the current user can request a given item."""
if current_user.is_authenticated:
patron = Patron.get_patron_by_user(current_user)
if patron and patron.is_patron:
can, _ = item.can(
can, reasons = item.can(
ItemCirculationAction.REQUEST,
patron=patron,
library=Library.get_record_by_pid(patron.library_pid)
)
return can
return False
return can, reasons
return False, []


@blueprint.app_template_filter()
Expand Down
2 changes: 2 additions & 0 deletions rero_ils/modules/loans/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,8 @@ def patron_profile(patron):
loan=loan
)
loan['can_renew'] = can
if not can:
loan['can_renew_reasons'] = reasons
loans.append(loan)
elif loan['state'] in [
LoanState.PENDING,
Expand Down
14 changes: 7 additions & 7 deletions rero_ils/modules/patron_types/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ def allow_checkout(cls, item, **kwargs):

# check fee amount limit
if not patron_type.check_fee_amount_limit(patron):
return False, [_('Checkout denied: the maximal fee amount is '
'reached')]
return False, [_('Checkout denied: the maximal overdue fee amount '
'is reached')]

return True, []

Expand All @@ -185,8 +185,8 @@ def allow_request(cls, item, **kwargs):

# check fee amount limit
if not patron_type.check_fee_amount_limit(patron):
return False, [_('Request denied: the maximal fee amount is '
'reached')]
return False, [_('Request denied: the maximal overdue fee amount '
'is reached')]

return True, []

Expand All @@ -213,8 +213,8 @@ def allow_extend(cls, item, **kwargs):

# check fee amount limit
if not patron_type.check_fee_amount_limit(patron):
return False, [_('Renewal denied: the maximal fee amount is '
'reached')]
return False, [_('Renewal denied: the maximal overdue fee amount '
'is reached')]
return True, []

def get_linked_patron(self):
Expand Down Expand Up @@ -302,7 +302,7 @@ def check_checkout_count_limit(self, patron, item=None):
if not global_limit:
return True, None

# [0] get the stats fr this patron by library
# [0] get the stats for this patron by library
patron_library_stats = get_loans_count_by_library_for_patron_pid(
patron.pid, [LoanState.ITEM_ON_LOAN])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@
],
"properties": {
"global_limit": {
"title": "Global checkout limit",
"title": "Number of checkouts in the organisation",
"description": "The checkout is blocked once the specified checkout number is reached by the patron. This is a global checkout limit.",
"type": "number",
"minimum": 1
},
"library_limit": {
"title": "Default checkout limit by library",
"title": "Number of checkouts by library",
"description": "The checkout is blocked once the specified checkout number is reached by the patron for items of the same owning library. This is the same limit for all libraries.",
"type": "number",
"minimum": 1
Expand Down Expand Up @@ -181,8 +181,8 @@
],
"properties": {
"default_value": {
"title": "Default fee amount limit",
"description": "The checkout, renewal and the request are blocked once the specified fee amount is reached by the patron.",
"title": "Fee amount",
"description": "The checkout, renewal and the request are blocked once the specified overdue fee amount is reached by the patron.",
"type": "number",
"minimum": 0
}
Expand All @@ -194,7 +194,7 @@
],
"templateOptions": {
"toggle-switch": {
"label": "Limit by fee amount"
"label": "Limit by overdue fee amount"
}
}
}
Expand All @@ -207,7 +207,7 @@
],
"properties": {
"default_value": {
"title": "Default overdue items limit",
"title": "Number of overdue items",
"description": "The checkout, renewal and the request are blocked once the specified number of overdue items is reached by the patron.",
"type": "number",
"minimum": 1
Expand Down
63 changes: 38 additions & 25 deletions rero_ils/modules/patrons/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ def can_request(cls, item, **kwargs):

# a blocked patron can't request any item
if patron.is_blocked:
return False, [patron.blocked_message]
return False, [patron.get_blocked_message()]

return True, []

Expand All @@ -426,7 +426,7 @@ def can_checkout(cls, item, **kwargs):

# a blocked patron can't request any item
if patron.is_blocked:
return False, [patron.blocked_message]
return False, [patron.get_blocked_message()]

return True, []

Expand Down Expand Up @@ -578,12 +578,16 @@ def is_blocked(self):
"""Shortcut to know if user is blocked."""
return self.patron.get('blocked', False)

@property
def blocked_message(self):
"""Get the message in case of patron is blocked."""
def get_blocked_message(self, public=False):
"""Get the message in case of patron is blocked.
:param public: Is the message is for public interface ?
"""
main = _('Your account is currently blocked.') if public \
else _('This patron is currently blocked.')
if self.is_blocked:
return '{main} {reason_str}: {reason}'.format(
main=_('This patron is currently blocked.'),
main=main,
reason_str=_('Reason'),
reason=self.patron.get('blocked_note')
)
Expand Down Expand Up @@ -688,12 +692,13 @@ def transaction_user_validator(self, user_pid):
"""
return Patron.record_pid_exists(user_pid)

def get_circulation_messages(self):
def get_circulation_messages(self, public=False):
"""Return messages useful for circulation.
* check if the user is blocked ?
* check if the user reaches the maximum loans limit ?
:param public: is messages are for public interface ?
:return an array of messages. Each message is a dictionary with a level
and a content. The level could be used to filters messages if
needed.
Expand All @@ -706,27 +711,35 @@ def get_circulation_messages(self):
if self.is_blocked:
return [{
'type': 'error',
'content': self.blocked_message
'content': self.get_blocked_message(public)
}]

messages = []
# check the patron type define limit
patron_type = PatronType.get_record_by_pid(self.patron_type_pid)
valid, message = patron_type.check_checkout_count_limit(self)
if not valid:
messages.append({
'type': 'error',
'content': message
})
# check fee amount limit
valid = patron_type.check_fee_amount_limit(self)
if not valid:
messages.append({
'type': 'error',
'content': _(
'Transactions denied: the maximal fee amount is reached.')
})

# other messages must be only rendered for the professional interface
if not public:
# check the patron type define limit
patron_type = PatronType.get_record_by_pid(self.patron_type_pid)
valid, message = patron_type.check_checkout_count_limit(self)
if not valid:
messages.append({
'type': 'error',
'content': message
})
# check fee amount limit
if not patron_type.check_fee_amount_limit(self):
messages.append({
'type': 'error',
'content': _(
'Transactions denied: the maximal overdue fee amount '
'is reached.')
})
# check the patron type overdue limit
if not patron_type.check_overdue_items_limit(self):
messages.append({
'type': 'error',
'content': _('Checkout denied: the maximal number of '
'overdue items is reached')
})
return messages


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,23 @@
{% endif %}
</div>
<div class="col-lg-2 text-right">
{% if loan.can_renew %}
{%- with form = can_renew_form %}
{% 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="type" value="renew">
<input type="hidden" name="loan_pid" value="{{ loan.pid }}">
<button type="submit" class="btn btn-primary mt-1">
{{ _('Renew') }}
</button>
</form>
{%- endwith %}
{% endif %}
{%- endwith %}
{%- else -%}
<span class="d-inline-block" tabindex="0" data-toggle="tooltip" data-html="true" title="{{ loan.can_renew_reasons | join('<br/>') }}">
<button type="submit" class="btn btn-primary mt-1" disabled>
{{ _('Renew') }}
</button>
</span>
{%- endif %}
</div>
</div>
<div id="loan-{{ loan.pid }}" class="mt-1 ng-star-inserted d-none">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<div class="col-lg-2{% if loan.state == 'ITEM_AT_DESK' %} text-success{% endif %}">
{% if loan.state == 'ITEM_AT_DESK' %}
<i class="fa fa-check" title="{{ _('item at desk') }}" aria-hidden="true"></i>
{{ _('ready') }}
{{ _('to pick up') }}
{% elif loan.state in ['PENDING', 'ITEM_IN_TRANSIT_FOR_PICKUP'] %}
<i class="fa fa-bullseye" title="{{ _('pending') }}" aria-hidden="true"></i>
<!-- TODO: Add expired date and remove "waiting" text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,13 @@
{% include('rero_ils/_patron_profile_head.html') %}

<section>
{% for key, data in alerts.items() %}
{% for message in data['messages'] %}
<div class="alert alert-{{ data['level'] }}" role="alert">{{ message }}</div>
{% endfor %}
{% for message in messages %}
<div class="alert alert-{{ message['type'] }}" role="alert">{{ message['content'] }}</div>
{% endfor %}
{% set note=record.notes|selectattr('type', '==', 'public_note')|list|last%}
{% if note %}
<div class="alert alert-warning" role="alert">
{{note. content.replace('\n', '<br>')|safe}}
{{note.content.replace('\n', '<br>')|safe}}
</div>
{% endif %}
</section>
Expand Down
2 changes: 1 addition & 1 deletion rero_ils/modules/patrons/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ def get_patron_from_arguments(**kwargs):
return kwargs.get('patron') \
or Patron.get_patron_by_barcode(kwargs.get('patron_barcode')) \
or Patron.get_record_by_pid(kwargs.get('patron_pid')) \
or Patron.get_record_by_pid(kwargs.get('loan').get('patron_id'))
or Patron.get_record_by_pid(kwargs.get('loan').get('patron_pid'))
Loading

0 comments on commit 2afc952

Please sign in to comment.