Source code for api.util.patron
import datetime
import dateutil
from money import Money
from api.config import Configuration
from api.circulation_exceptions import *
from core.model.patron import Patron
from core.util import MoneyUtility
from core.util.datetime_helpers import utc_now
[docs]class PatronUtility(object):
"""Apply circulation-specific logic to Patron model objects."""
[docs] @classmethod
def needs_external_sync(cls, patron):
"""Could this patron stand to have their metadata synced with the
remote?
By default, all patrons get synced once every twelve
hours. Patrons who lack borrowing privileges can always stand
to be synced, since their privileges may have just been
restored.
"""
if not patron.last_external_sync:
# This patron has never been synced.
return True
now = utc_now()
if cls.has_borrowing_privileges(patron):
# A patron who has borrowing privileges gets synced every twelve
# hours. Their account is unlikely to change rapidly.
check_every = Patron.MAX_SYNC_TIME
else:
# A patron without borrowing privileges might get synced
# every time they make a request. It's likely they are
# taking action to get their account reinstated and we
# don't want to make them wait twelve hours to get access.
check_every = datetime.timedelta(seconds=5)
expired_at = patron.last_external_sync + check_every
if now > expired_at:
return True
return False
[docs] @classmethod
def has_borrowing_privileges(cls, patron):
"""Is the given patron allowed to check out books?
:return: A boolean
"""
try:
cls.assert_borrowing_privileges(patron)
return True
except CannotLoan as e:
return False
[docs] @classmethod
def assert_borrowing_privileges(cls, patron):
"""Raise an exception unless the patron currently has borrowing
privileges.
:raises AuthorizationExpired: If the patron's authorization has expired.
:raises OutstandingFines: If the patron has too many outstanding fines.
"""
if not cls.authorization_is_active(patron):
# The patron's card has expired.
raise AuthorizationExpired()
if cls.has_excess_fines(patron):
raise OutstandingFines()
from api.authenticator import PatronData
if patron.block_reason is not None:
if patron.block_reason is PatronData.EXCESSIVE_FINES:
# The authentication mechanism itself may know that
# the patron has outstanding fines, even if the circulation
# manager is not configured to make that deduction.
raise OutstandingFines()
raise AuthorizationBlocked()
[docs] @classmethod
def has_excess_fines(cls, patron):
"""Does this patron have fines in excess of the maximum fine amount set for their library?
:param a Patron:
:return: A boolean
"""
if not patron.fines:
return False
if isinstance(patron.fines, Money):
patron_fines = patron.fines
else:
patron_fines = MoneyUtility.parse(patron.fines)
actual_fines = patron_fines.amount
max_fines = Configuration.max_outstanding_fines(patron.library)
if max_fines is not None and actual_fines > max_fines.amount:
return True
return False
[docs] @classmethod
def authorization_is_active(cls, patron):
"""Return True unless the patron's authorization has expired."""
# Unlike pretty much every other place in this app, we use
# (server) local time here instead of UTC. This is to make it
# less likely that a patron's authorization will expire before
# they think it should.
now_local = datetime.datetime.now(tz=dateutil.tz.tzlocal())
if (patron.authorization_expires
and cls._to_date(patron.authorization_expires)
< cls._to_date(now_local)):
return False
return True
@classmethod
def _to_date(cls, x):
"""Convert a datetime into a date. Leave a date alone."""
if isinstance(x, datetime.datetime):
return x.date()
return x