Пример #1
0
    def place_hold(self, patron, pin, licensepool, notification_email_address):
        """Place a book on hold.

        :return: A HoldInfo object
        """
        if not notification_email_address:
            notification_email_address = self.default_notification_email_address(
                patron, pin
            )

        overdrive_id = licensepool.identifier.identifier
        headers, document = self.fill_out_form(
            reserveId=overdrive_id, emailAddress=notification_email_address)
        response = self.patron_request(
            patron, pin, self.HOLDS_ENDPOINT, headers, 
            document)
        if response.status_code == 400:
            error = response.json()
            if not error or not 'errorCode' in error:
                raise CannotHold()
            code = error['errorCode']
            if code == 'AlreadyOnWaitList':
                # There's nothing we can do but refresh the queue info.
                hold = self.get_hold(patron, pin, overdrive_id)
                position, start_date = self.extract_data_from_hold_response(
                    hold)
                return HoldInfo(
                    licensepool.collection,
                    licensepool.data_source.name,
                    licensepool.identifier.type,
                    licensepool.identifier.identifier,
                    start_date=start_date, 
                    end_date=None,
                    hold_position=position
                )
            elif code == 'NotWithinRenewalWindow':
                # The patron has this book checked out and cannot yet
                # renew their loan.
                raise CannotRenew()
            elif code == 'PatronExceededHoldLimit':
                raise PatronHoldLimitReached()
            else:
                raise CannotHold(code)
        else:
            # The book was placed on hold.
            data = response.json()
            position, start_date = self.extract_data_from_hold_response(
                data)
            return HoldInfo(
                licensepool.collection,
                licensepool.data_source.name,
                licensepool.identifier.type,
                licensepool.identifier.identifier,
                start_date=start_date,
                end_date=None,
                hold_position=position
            )
Пример #2
0
    def process_one(self, e, ns):

        # Figure out which book we're talking about.
        axis_identifier = self.text_of_subtag(e, "axis:titleId", ns)
        availability = self._xpath1(e, 'axis:availability', ns)
        if availability is None:
            return None
        reserved = self._xpath1_boolean(availability, 'axis:isReserved', ns)
        checked_out = self._xpath1_boolean(availability, 'axis:isCheckedout',
                                           ns)
        on_hold = self._xpath1_boolean(availability, 'axis:isInHoldQueue', ns)

        info = None
        if checked_out:
            start_date = self._xpath1_date(availability,
                                           'axis:checkoutStartDate', ns)
            end_date = self._xpath1_date(availability, 'axis:checkoutEndDate',
                                         ns)
            download_url = self.text_of_optional_subtag(
                availability, 'axis:downloadUrl', ns)
            if download_url:
                fulfillment = FulfillmentInfo(identifier_type=self.id_type,
                                              identifier=axis_identifier,
                                              content_link=download_url,
                                              content_type=None,
                                              content=None,
                                              content_expires=None)
            else:
                fulfillment = None
            info = LoanInfo(identifier_type=self.id_type,
                            identifier=axis_identifier,
                            start_date=start_date,
                            end_date=end_date,
                            fulfillment_info=fulfillment)

        elif reserved:
            end_date = self._xpath1_date(availability, 'axis:reservedEndDate',
                                         ns)
            info = HoldInfo(identifier_type=self.id_type,
                            identifier=axis_identifier,
                            start_date=None,
                            end_date=end_date,
                            hold_position=0)
        elif on_hold:
            position = self.int_of_optional_subtag(availability,
                                                   'axis:holdsQueuePosition',
                                                   ns)
            info = HoldInfo(identifier_type=self.id_type,
                            identifier=axis_identifier,
                            start_date=None,
                            end_date=None,
                            hold_position=position)
        return info
Пример #3
0
    def place_hold(self,
                   patron,
                   pin,
                   licensepool,
                   hold_notification_email=None):
        """Place a hold.

        :return: a HoldInfo object.
        """
        patron_id = patron.authorization_identifier
        item_id = licensepool.identifier.identifier
        args = dict(request_type='PlaceHoldRequest',
                    item_id=item_id,
                    patron_id=patron_id)
        body = self.TEMPLATE % args
        response = self.request('placehold', body, method="PUT")
        if response.status_code in (200, 201):
            start_date = datetime.datetime.utcnow()
            end_date = HoldResponseParser().process_all(response.content)
            return HoldInfo(licensepool.collection,
                            DataSource.BIBLIOTHECA,
                            licensepool.identifier.type,
                            licensepool.identifier.identifier,
                            start_date=start_date,
                            end_date=end_date,
                            hold_position=None)
        else:
            if not response.content:
                raise CannotHold()
            error = ErrorParser().process_all(response.content)
            if isinstance(error, Exception):
                raise error
            else:
                raise CannotHold(error)
Пример #4
0
    def process_one(self, e, namespaces):
        """Either turn the given document into a HoldInfo
        object, or raise an appropriate exception.
        """
        self.raise_exception_on_error(e, namespaces, {3109: AlreadyOnHold})

        # If we get to this point it's because the hold place succeeded.
        queue_position = self._xpath1(e, '//axis:holdsQueuePosition',
                                      namespaces)
        if queue_position is None:
            queue_position = None
        else:
            try:
                queue_position = int(queue_position.text)
            except ValueError:
                print "Invalid queue position: %s" % queue_position
                queue_position = None

        hold_start = datetime.utcnow()
        # NOTE: The caller needs to fill in Collection -- we have no idea
        # what collection this is.
        hold = HoldInfo(collection=self.collection,
                        data_source_name=DataSource.AXIS_360,
                        identifier_type=self.id_type,
                        identifier=None,
                        start_date=hold_start,
                        end_date=None,
                        hold_position=queue_position)
        return hold
Пример #5
0
    def process_one(self, e, namespaces):
        """Either turn the given document into a HoldInfo
        object, or raise an appropriate exception.
        """
        self.raise_exception_on_error(e, namespaces, {3109: AlreadyOnHold})

        # If we get to this point it's because the hold place succeeded.
        queue_position = self._xpath1(e, '//axis:holdsQueuePosition',
                                      namespaces)
        if queue_position is None:
            queue_position = None
        else:
            try:
                queue_position = int(queue_position.text)
            except ValueError:
                print "Invalid queue position: %s" % queue_position
                queue_position = None

        hold_start = datetime.utcnow()
        hold = HoldInfo(identifier_type=self.id_type,
                        identifier=None,
                        start_date=hold_start,
                        end_date=None,
                        hold_position=queue_position)
        return hold
Пример #6
0
    def place_hold(self, patron, pin, licensepool, notification_email_address):
        """Create a new hold."""
        _db = Session.object_session(patron)

        # Make sure pool info is updated.
        self.update_hold_queue(licensepool)

        if licensepool.licenses_available > 0:
            raise CurrentlyAvailable()

        # Create local hold.
        hold, is_new = get_one_or_create(
            _db,
            Hold,
            license_pool=licensepool,
            patron=patron,
            create_method_kwargs=dict(start=datetime.datetime.utcnow()),
        )

        if not is_new:
            raise AlreadyOnHold()

        licensepool.patrons_in_hold_queue += 1
        self._update_hold_end_date(hold)

        return HoldInfo(
            licensepool.collection,
            licensepool.data_source.name,
            licensepool.identifier.type,
            licensepool.identifier.identifier,
            start_date=hold.start,
            end_date=hold.end,
            hold_position=hold.position,
        )
Пример #7
0
 def make_holdinfo(hold_response):
     # Create a HoldInfo object by combining data passed into
     # the enclosing method with the data from a hold response
     # (either creating a new hold or fetching an existing
     # one).
     position, start_date = self.extract_data_from_hold_response(
         hold_response
     )
     return HoldInfo(
         licensepool.collection,
         licensepool.data_source.name,
         licensepool.identifier.type,
         licensepool.identifier.identifier,
         start_date=start_date,
         end_date=None,
         hold_position=position
     )
Пример #8
0
    def patron_activity(self, patron, pin):
        """Look up non-expired loans for this collection in the database."""
        _db = Session.object_session(patron)
        loans = _db.query(Loan).join(Loan.license_pool).filter(
            LicensePool.collection_id == self.collection_id).filter(
                Loan.patron == patron).filter(
                    Loan.end >= datetime.datetime.utcnow())

        # Get the patron's holds. If there are any expired holds, delete them.
        # Update the end date and position for the remaining holds.
        holds = _db.query(Hold).join(Hold.license_pool).filter(
            LicensePool.collection_id == self.collection_id).filter(
                Hold.patron == patron)
        remaining_holds = []
        for hold in holds:
            if hold.end and hold.end < datetime.datetime.utcnow():
                _db.delete(hold)
                self.update_hold_queue(hold.license_pool)
            else:
                self._update_hold_end_date(hold)
                remaining_holds.append(hold)

        return [
            LoanInfo(
                loan.license_pool.collection,
                loan.license_pool.data_source.name,
                loan.license_pool.identifier.type,
                loan.license_pool.identifier.identifier,
                loan.start,
                loan.end,
                external_identifier=loan.external_identifier,
            ) for loan in loans
        ] + [
            HoldInfo(
                hold.license_pool.collection,
                hold.license_pool.data_source.name,
                hold.license_pool.identifier.type,
                hold.license_pool.identifier.identifier,
                start_date=hold.start,
                end_date=hold.end,
                hold_position=hold.position,
            ) for hold in remaining_holds
        ]
Пример #9
0
    def hold_from_odilo_hold(self, collection, hold):
        start = self.extract_date(hold, 'startTime')
        # end_date: The estimated date the title will be available for the patron to borrow.
        end = self.extract_date(hold, 'notifiedTime')
        position = hold.get('holdQueuePosition')

        if position is not None:
            position = int(position)

        # Patron already notified to borrow the title
        if 'informed' == hold['status']:
            position = 0

        return HoldInfo(collection,
                        DataSource.ODILO,
                        Identifier.ODILO_ID,
                        hold['id'],
                        start_date=start,
                        end_date=end,
                        hold_position=position)
Пример #10
0
        try:
            transaction_id = int(resp_obj)
        except Exception, e:
            self.log.error("Item hold request failed: %r", e, exc_info=e)
            raise CannotHold(e.message)

        self.log.debug("Patron %s/%s reserved item %s with transaction id %s.",
                       patron.authorization_identifier, patron_oneclick_id,
                       item_oneclick_id, resp_obj)

        today = datetime.datetime.now()

        hold = HoldInfo(
            identifier_type=licensepool.identifier.type,
            identifier=item_oneclick_id,
            start_date=today,
            # OneClick sets hold expirations to 2050-12-31, as a "forever"
            end_date=None,
            hold_position=None,
        )

        return hold

    def release_hold(self, patron, pin, licensepool):
        """Release a patron's hold on a book.

        :param patron: a Patron object for the patron who wants to return the book.
        :param pin: The patron's password (not used).
        :param licensepool: The Identifier of the book to be checked out is 
        attached to this licensepool.

        :return True on success, raises circulation exceptions on failure.
Пример #11
0
            overdrive_identifier = hold['reserveId'].lower()
            start = self._pd(hold.get('holdPlacedDate'))
            end = self._pd(hold.get('holdExpires'))
            position = hold.get('holdListPosition')
            if position is not None:
                position = int(position)
            if 'checkout' in hold.get('actions', {}):
                # This patron needs to decide whether to check the
                # book out. By our reckoning, the patron's position is
                # 0, not whatever position Overdrive had for them.
                position = 0
            yield HoldInfo(
                self.collection,
                DataSource.OVERDRIVE,
                Identifier.OVERDRIVE_ID,
                overdrive_identifier,
                start_date=start,
                end_date=end,
                hold_position=position
            )

    @classmethod
    def process_checkout_data(cls, checkout, collection):
        """Convert one checkout from Overdrive's list of checkouts
        into a LoanInfo object.

        :return: A LoanInfo object if the book can be fulfilled
            by the default Library Simplified client, and None otherwise.
        """
        overdrive_identifier = checkout['reserveId'].lower()
        start = cls._pd(checkout.get('checkoutDate'))
Пример #12
0
            transaction_id = int(resp_obj)
        except Exception, e:
            self.log.error("Item hold request failed: %r", e, exc_info=e)
            raise CannotHold(e.message)

        self.log.debug("Patron %s/%s reserved item %s with transaction id %s.",
                       patron.authorization_identifier, patron_oneclick_id,
                       item_oneclick_id, resp_obj)

        today = datetime.datetime.now()

        hold = HoldInfo(
            self.collection,
            DataSource.RB_DIGITAL,
            identifier_type=licensepool.identifier.type,
            identifier=item_oneclick_id,
            start_date=today,
            # OneClick sets hold expirations to 2050-12-31, as a "forever"
            end_date=None,
            hold_position=None,
        )

        return hold

    def release_hold(self, patron, pin, licensepool):
        """Release a patron's hold on a book.

        :param patron: a Patron object for the patron who wants to return the book.
        :param pin: The patron's password (not used).
        :param licensepool: The Identifier of the book to be checked out is 
        attached to this licensepool.
Пример #13
0
        for hold in holds.get('holds', []):
            overdrive_identifier = hold['reserveId'].lower()
            start = self._pd(hold.get('holdPlacedDate'))
            end = self._pd(hold.get('holdExpires'))
            position = hold.get('holdListPosition')
            if position is not None:
                position = int(position)
            if 'checkout' in hold.get('actions', {}):
                # This patron needs to decide whether to check the
                # book out. By our reckoning, the patron's position is
                # 0, not whatever position Overdrive had for them.
                position = 0
            yield HoldInfo(Identifier.OVERDRIVE_ID,
                           overdrive_identifier,
                           start_date=start,
                           end_date=end,
                           hold_position=position)

    @classmethod
    def process_checkout_data(cls, checkout):
        """Convert one checkout from Overdrive's list of checkouts
        into a LoanInfo object.

        :return: A LoanInfo object if the book can be fulfilled
        by the default Library Simplified client, and None otherwise.
        """
        overdrive_identifier = checkout['reserveId'].lower()
        start = cls._pd(checkout.get('checkoutDate'))
        end = cls._pd(checkout.get('expires'))