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
Esempio n. 2
0
    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)
Esempio n. 4
0
    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
Esempio n. 5
0
    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
Esempio n. 6
0
    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
Esempio n. 7
0
    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
Esempio n. 8
0
    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
Esempio n. 10
0
    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
Esempio n. 11
0
    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))
Esempio n. 12
0
 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