def test_borrow_with_outstanding_fines(self): # This checkout would succeed... now = datetime.now() loaninfo = LoanInfo( self.pool.collection, self.pool.data_source, self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) # ...except the patron has too many fines. old_fines = self.patron.fines self.patron.fines = 1000 setting = ConfigurationSetting.for_library( Configuration.MAX_OUTSTANDING_FINES, self._default_library) setting.value = "$0.50" assert_raises(OutstandingFines, self.borrow) # Test the case where any amount of fines are too much. setting.value = "$0" assert_raises(OutstandingFines, self.borrow) # Remove the fine policy, and borrow succeeds. setting.value = None loan, i1, i2 = self.borrow() assert isinstance(loan, Loan) self.patron.fines = old_fines
def test_borrow_sends_analytics_event(self): now = datetime.utcnow() loaninfo = LoanInfo( self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) now = datetime.utcnow() config = { Configuration.POLICIES: { Configuration.ANALYTICS_POLICY: ["core.mock_analytics_provider"] } } with temp_config(config) as config: provider = MockAnalyticsProvider() analytics = Analytics.initialize(['core.mock_analytics_provider'], config) loan, hold, is_new = self.borrow() # The Loan looks good. eq_(loaninfo.identifier, loan.license_pool.identifier.identifier) eq_(self.patron, loan.patron) eq_(None, hold) eq_(True, is_new) # An analytics event was created. mock = Analytics.instance().providers[0] eq_(1, mock.count) eq_(CirculationEvent.CM_CHECKOUT, mock.event_type) # Try to 'borrow' the same book again. self.remote.queue_checkout(AlreadyCheckedOut()) loan, hold, is_new = self.borrow() eq_(False, is_new) # Since the loan already existed, no new analytics event was # sent. eq_(1, mock.count) # Now try to renew the book. self.remote.queue_checkout(loaninfo) loan, hold, is_new = self.borrow() eq_(False, is_new) # Renewals are counted as loans, since from an accounting # perspective they _are_ loans. eq_(2, mock.count) # Loans of open-access books go through a different code # path, but they count as loans nonetheless. self.pool.open_access = True self.remote.queue_checkout(loaninfo) loan, hold, is_new = self.borrow() eq_(3, mock.count)
def test_borrow_sends_analytics_event(self): now = datetime.utcnow() loaninfo = LoanInfo( self.pool.collection, self.pool.data_source, self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), external_identifier=self._str, ) self.remote.queue_checkout(loaninfo) now = datetime.utcnow() loan, hold, is_new = self.borrow() # The Loan looks good. eq_(loaninfo.identifier, loan.license_pool.identifier.identifier) eq_(self.patron, loan.patron) eq_(None, hold) eq_(True, is_new) eq_(loaninfo.external_identifier, loan.external_identifier) # An analytics event was created. eq_(1, self.analytics.count) eq_(CirculationEvent.CM_CHECKOUT, self.analytics.event_type) # Try to 'borrow' the same book again. self.remote.queue_checkout(AlreadyCheckedOut()) loan, hold, is_new = self.borrow() eq_(False, is_new) eq_(loaninfo.external_identifier, loan.external_identifier) # Since the loan already existed, no new analytics event was # sent. eq_(1, self.analytics.count) # Now try to renew the book. self.remote.queue_checkout(loaninfo) loan, hold, is_new = self.borrow() eq_(False, is_new) # Renewals are counted as loans, since from an accounting # perspective they _are_ loans. eq_(2, self.analytics.count) # Loans of open-access books go through a different code # path, but they count as loans nonetheless. self.pool.open_access = True self.remote.queue_checkout(loaninfo) loan, hold, is_new = self.borrow() eq_(3, self.analytics.count)
def checkout(self, patron, pin, licensepool, internal_format): """Checks out a book on behalf of a patron :param patron: A Patron object for the patron who wants to check out the book :type patron: Patron :param pin: The patron's alleged password :type pin: string :param licensepool: Contains lending info as well as link to parent Identifier :type licensepool: LicensePool :param internal_format: Represents the patron's desired book format. :type internal_format: Any :return: a LoanInfo object :rtype: LoanInfo """ days = self.collection.default_loan_period(patron.library) today = utc_now() expires = today + datetime.timedelta(days=days) loan = get_one( self._db, Loan, patron=patron, license_pool=licensepool, on_multiple="interchangeable", ) if loan: license = self._lcp_server.get_license( self._db, loan.external_identifier, patron ) else: license = self._lcp_server.generate_license( self._db, licensepool.identifier.identifier, patron, today, expires ) loan = LoanInfo( licensepool.collection, licensepool.data_source.name, identifier_type=licensepool.identifier.type, identifier=licensepool.identifier.identifier, start_date=today, end_date=expires, fulfillment_info=None, external_identifier=license["id"], ) return loan
def test_borrow_with_block_fails(self): # This checkout would succeed... now = datetime.now() loaninfo = LoanInfo( self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) # ...except the patron is blocked self.patron.block_reason = "some reason" assert_raises(AuthorizationBlocked, self.borrow) self.patron.block_reason = None
def patron_activity(self, patron, pin): """Returns patron's loans :param patron: A Patron object for the patron who wants to check out the book :type patron: Patron :param pin: The patron's alleged password :type pin: string :return: List of patron's loans :rtype: List[LoanInfo] """ now = datetime.datetime.utcnow() loans = self._db\ .query(Loan)\ .join(LicensePool)\ .join(Collection)\ .filter( Collection.id == self._collection_id, Loan.patron == patron, or_( Loan.start is None, Loan.start <= now ), or_( Loan.end is None, Loan.end > now ) ) loan_info_objects = [] for loan in loans: licensepool = get_one(self._db, LicensePool, id=loan.license_pool_id) loan_info_objects.append( LoanInfo(collection=self.collection, data_source_name=licensepool.data_source.name, identifier_type=licensepool.identifier.type, identifier=licensepool.identifier.identifier, start_date=loan.start, end_date=loan.end, fulfillment_info=None, external_identifier=loan.external_identifier)) return loan_info_objects
def test_borrow_with_expired_card_fails(self): # This checkout would succeed... now = datetime.now() loaninfo = LoanInfo( self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) # ...except the patron's library card has expired. old_expires = self.patron.authorization_expires yesterday = now - timedelta(days=1) self.patron.authorization_expires = yesterday assert_raises(AuthorizationExpired, self.borrow) self.patron.authorization_expires = old_expires
def patron_activity(self, patron, pin): """Return patron's loans. TODO This and code from ODLAPI should be refactored into a generic set of rules for any situation where the CM, not the remote API, is responsible for managing loans and holds. :param patron: A Patron object for the patron who wants to check out the book :type patron: Patron :param pin: The patron's alleged password :type pin: string :return: List of patron's loans :rtype: List[LoanInfo] """ now = utc_now() loans = ( self._db.query(Loan).join(LicensePool).join(Collection).filter( Collection.id == self._collection_id, Loan.patron == patron, or_(Loan.start == None, Loan.start <= now), or_(Loan.end == None, Loan.end > now), )).all() loan_info_objects = [] for loan in loans: licensepool = get_one(self._db, LicensePool, id=loan.license_pool_id) loan_info_objects.append( LoanInfo( collection=self.collection, data_source_name=licensepool.data_source.name, identifier_type=licensepool.identifier.type, identifier=licensepool.identifier.identifier, start_date=loan.start, end_date=loan.end, fulfillment_info=None, external_identifier=None, )) return loan_info_objects
def test_borrow_with_fines_fails(self): # This checkout would succeed... now = datetime.now() loaninfo = LoanInfo( self.pool.collection, self.pool.data_source, self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) # ...except the patron has too many fines. old_fines = self.patron.fines self.patron.fines = 1000 ConfigurationSetting.for_library(Configuration.MAX_OUTSTANDING_FINES, self._default_library).value = "$0.50" assert_raises(OutstandingFines, self.borrow) self.patron.fines = old_fines
def test_borrow_with_fines_fails(self): # This checkout would succeed... now = datetime.now() loaninfo = LoanInfo( self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) # ...except the patron has too many fines. old_fines = self.patron.fines self.patron.fines = 1000 with temp_config() as config: config[Configuration.POLICIES] = { Configuration.MAX_OUTSTANDING_FINES: "$0.50" } assert_raises(OutstandingFines, self.borrow) self.patron.fines = old_fines
def checkout(self, patron, pin, licensepool, internal_format): """Checkout the book. NOTE: This method requires the patron to have either: - an active ProQuest JWT bearer token - or a SAML affiliation ID which will be used to create a new ProQuest JWT bearer token. """ self._logger.info("Started checking out '{0}' for patron {1}".format( internal_format, patron)) try: with self._get_configuration(self._db) as configuration: self._get_or_create_proquest_token(patron, configuration) loan_period = self.collection.default_loan_period( patron.library) start_time = utc_now() end_time = start_time + datetime.timedelta(days=loan_period) loan = LoanInfo( licensepool.collection, licensepool.data_source.name, identifier_type=licensepool.identifier.type, identifier=licensepool.identifier.identifier, start_date=start_time, end_date=end_time, fulfillment_info=None, external_identifier=None, ) self._logger.info( "Finished checking out {0} for patron {1}: {2}".format( internal_format, patron, loan)) return loan except BaseError as exception: self._logger.exception("Failed to check out {0} for patron {1}") raise CannotLoan(str(exception))
def add_remote_loan(self, *args, **kwargs): self.remote_loans.append(LoanInfo(*args, **kwargs))
def test_borrow_loan_limit_reached(self): # The loan limit is 1, and the patron has a previous loan. self.patron.library.setting(Configuration.LOAN_LIMIT).value = 1 previous_loan_pool = self._licensepool(None) previous_loan_pool.open_access = False now = datetime.now() previous_loan, ignore = previous_loan_pool.loan_to(self.patron, end=now + timedelta(days=2)) # If the patron tried to check out when they're at the loan limit, # the API will try to place a hold instead, and catch the error. self.remote.queue_hold(CurrentlyAvailable()) assert_raises(PatronLoanLimitReached, self.borrow) # If we increase the limit, borrow succeeds. self.patron.library.setting(Configuration.LOAN_LIMIT).value = 2 loaninfo = LoanInfo( self.pool.collection, self.pool.data_source, self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) loan, hold, is_new = self.borrow() assert loan != None # An open access book can be borrowed even if the patron's at the limit. open_access_pool = self._licensepool(None, with_open_access_download=True) loan, hold, is_new = self.circulation.borrow(self.patron, '1234', open_access_pool, self.delivery_mechanism) assert loan != None # And that loan doesn't count towards the limit. self.patron.library.setting(Configuration.LOAN_LIMIT).value = 3 pool2 = self._licensepool(None, data_source_name=DataSource.BIBLIOTHECA, collection=self.collection) loaninfo = LoanInfo( pool2.collection, pool2.data_source, pool2.identifier.type, pool2.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) loan, hold, is_new = self.circulation.borrow(self.patron, '1234', pool2, self.delivery_mechanism) assert loan != None # A loan with no end date also doesn't count toward the limit. previous_loan.end = None pool3 = self._licensepool(None, data_source_name=DataSource.BIBLIOTHECA, collection=self.collection) loaninfo = LoanInfo( pool3.collection, pool3.data_source, pool3.identifier.type, pool3.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) loan, hold, is_new = self.circulation.borrow(self.patron, '1234', pool2, self.delivery_mechanism) assert loan != None