예제 #1
0
    def checkout(self, patron, pin, licensepool, internal_format):
        """Check out a book on behalf of a patron.

        :param patron: a Patron object for the patron who wants
        to check out the book.

        :param pin: The patron's alleged password.

        :param licensepool: Identifier of the book to be checked out is 
        attached to this licensepool.

        :param internal_format: Represents the patron's desired book format.

        :return: a LoanInfo object.
        """

        identifier = licensepool.identifier
        overdrive_id = identifier.identifier
        headers = {"Content-Type": "application/json"}
        payload = dict(fields=[dict(name="reserveId", value=overdrive_id)])
        payload = json.dumps(payload)

        response = self.patron_request(patron,
                                       pin,
                                       self.CHECKOUTS_ENDPOINT,
                                       extra_headers=headers,
                                       data=payload)
        if response.status_code == 400:
            error = response.json()
            code = error['errorCode']
            if code == 'NoCopiesAvailable':
                # Clearly our info is out of date.
                self.update_licensepool(identifier.identifier)
                raise NoAvailableCopies()
            elif code == 'TitleAlreadyCheckedOut':
                # Client should have used a fulfill link instead, but
                # we can handle it.
                loan = self.get_loan(patron, pin, identifier.identifier)
                expires = self.extract_expiration_date(loan)
                return LoanInfo(licensepool.identifier.type,
                                licensepool.identifier.identifier, None,
                                expires, None)
            elif code == 'PatronHasExceededCheckoutLimit':
                raise PatronLoanLimitReached()
            else:
                raise CannotLoan(code)
        else:
            # Try to extract the expiration date from the response.
            expires = self.extract_expiration_date(response.json())

        # Create the loan info. We don't know the expiration
        loan = LoanInfo(
            licensepool.identifier.type,
            licensepool.identifier.identifier,
            None,
            expires,
            None,
        )
        return loan
예제 #2
0
    def process_one(self, e, namespaces):
        """Either turn the given document into a LoanInfo
        object, or raise an appropriate exception.
        """
        self.raise_exception_on_error(e, namespaces)

        # If we get to this point it's because the checkout succeeded.
        expiration_date = self._xpath1(e, '//axis:expirationDate', namespaces)
        fulfillment_url = self._xpath1(e, '//axis:url', namespaces)
        if fulfillment_url is not None:
            fulfillment_url = fulfillment_url.text

        if expiration_date is not None:
            expiration_date = expiration_date.text
            expiration_date = datetime.strptime(expiration_date,
                                                self.FULL_DATE_FORMAT)

        # WTH??? Why is identifier None? Is this ever used?
        fulfillment = FulfillmentInfo(collection=self.collection,
                                      data_source_name=DataSource.AXIS_360,
                                      identifier_type=self.id_type,
                                      identifier=None,
                                      content_link=fulfillment_url,
                                      content_type=None,
                                      content=None,
                                      content_expires=None)
        loan_start = datetime.utcnow()
        loan = LoanInfo(collection=self.collection,
                        data_source_name=DataSource.AXIS_360,
                        identifier_type=self.id_type,
                        identifier=None,
                        start_date=loan_start,
                        end_date=expiration_date,
                        fulfillment_info=fulfillment)
        return loan
예제 #3
0
    def loan_info_from_odilo_checkout(self, collection, checkout):
        start_date = self.extract_date(checkout, 'startTime')
        end_date = self.extract_date(checkout, 'endTime')

        return LoanInfo(collection, DataSource.ODILO, Identifier.ODILO_ID,
                        checkout['id'], start_date, end_date,
                        checkout['downloadUrl'])
예제 #4
0
    def checkout(self, patron, pin, licensepool, internal_format):
        identifier = licensepool.identifier
        enki_id = identifier.identifier
        response = self.loan_request(patron.authorization_identifier, pin,
                                     enki_id)
        if response.status_code != 200:
            raise CannotLoan(response.status_code)
        result = json.loads(response.content)['result']
        if not result['success']:
            message = result['message']
            if "There are no available copies" in message:
                self.log.error("There are no copies of book %s available." %
                               enki_id)
                raise NoAvailableCopies()
            elif "Login unsuccessful" in message:
                self.log.error(
                    "User validation against Enki server with %s / %s was unsuccessful."
                    % (patron.authorization_identifier, pin))
                raise AuthorizationFailedException()
        due_date = result['checkedOutItems'][0]['duedate']
        expires = self.epoch_to_struct(due_date)

        # Create the loan info.
        loan = LoanInfo(
            licensepool.collection,
            licensepool.data_source.name,
            licensepool.identifier.type,
            licensepool.identifier.identifier,
            None,
            expires,
            None,
        )
        return loan
 def checkout(self, patron, pin, licensepool, internal_format):
     return LoanInfo(
         licensepool.collection,
         licensepool.data_source.name,
         licensepool.identifier.type,
         licensepool.identifier.identifier,
         start_date=datetime.datetime.now(),
         end_date=None,
     )
예제 #6
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
예제 #7
0
    def checkout(self, patron, pin, licensepool, internal_format):
        """
        Associate an eBook or eAudio with a patron.

        :param patron: a Patron object for the patron who wants to check out 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.
        :param internal_format: Represents the patron's desired book format.  Ignored for now.

        :return LoanInfo on success, None on failure.
        """
        patron_oneclick_id = self.patron_remote_identifier(patron)
        (item_oneclick_id, item_media) = self.validate_item(licensepool)

        today = datetime.datetime.utcnow()

        library = patron.library

        if item_media == Edition.AUDIO_MEDIUM:
            key = Collection.AUDIOBOOK_LOAN_DURATION_KEY
            _db = Session.object_session(patron)
            days = (ConfigurationSetting.for_library_and_externalintegration(
                _db, key, library,
                self.collection.external_integration).int_value
                    or Collection.STANDARD_DEFAULT_LOAN_PERIOD)
        else:
            days = self.collection.default_loan_period(library)

        resp_dict = self.circulate_item(patron_id=patron_oneclick_id,
                                        item_id=item_oneclick_id,
                                        return_item=False,
                                        days=days)

        if not resp_dict or ('error_code' in resp_dict):
            return None

        self.log.debug(
            "Patron %s/%s checked out item %s with transaction id %s.",
            patron.authorization_identifier, patron_oneclick_id,
            item_oneclick_id, resp_dict['transactionId'])

        expires = today + datetime.timedelta(days=days)
        loan = LoanInfo(
            self.collection,
            DataSource.RB_DIGITAL,
            identifier_type=licensepool.identifier.type,
            identifier=item_oneclick_id,
            start_date=today,
            end_date=expires,
            fulfillment_info=None,
        )
        return loan
예제 #8
0
 def parse_patron_loans(self, checkout_data):
     # We should receive a list of JSON objects
     enki_id = checkout_data['recordId']
     start_date = self.epoch_to_struct(checkout_data['checkoutdate'])
     end_date = self.epoch_to_struct(checkout_data['duedate'])
     return LoanInfo(self.collection,
                     DataSource.ENKI,
                     Identifier.ENKI_ID,
                     enki_id,
                     start_date=start_date,
                     end_date=end_date,
                     fulfillment_info=None)
 def patron_activity(self, patron, pin):
     # Look up 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)
     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) for loan in loans
     ]
예제 #10
0
    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'))
        
        usable_formats = []

        # If a format is already locked in, it will be in formats.
        for format in checkout.get('formats', []):
            format_type = format.get('formatType')
            if format_type in cls.FORMATS:
                usable_formats.append(format_type)

        # If a format hasn't been selected yet, available formats are in actions.
        actions = checkout.get('actions', {})
        format_action = actions.get('format', {})
        format_fields = format_action.get('fields', [])
        for field in format_fields:
            if field.get('name', "") == "formatType":
                format_options = field.get("options", [])
                for format_type in format_options:
                    if format_type in cls.FORMATS:
                        usable_formats.append(format_type)

        if not usable_formats:
            # Either this book is not available in any format readable
            # by the default client, or the patron previously chose to
            # fulfill it in a format not readable by the default
            # client. Either way, we cannot fulfill this loan and we
            # shouldn't show it in the list.
            return None

        # TODO: if there is one and only one format (usable or not, do
        # not count overdrive-read), put it into fulfillment_info and
        # let the caller make the decision whether or not to show it.
        return LoanInfo(
            None,
            DataSource.OVERDRIVE,
            Identifier.OVERDRIVE_ID,
            overdrive_identifier,
            start_date=start,
            end_date=end,
            fulfillment_info=None
        )
예제 #11
0
    def checkout(
            self, patron_obj, patron_password, licensepool, 
            delivery_mechanism
    ):

        """Check out a book on behalf of a patron.

        :param patron_obj: a Patron object for the patron who wants
        to check out the book.

        :param patron_password: The patron's alleged password.  Not
        used here since Bibliotheca trusts Simplified to do the check ahead of
        time.

        :param licensepool: LicensePool for the book to be checked out.

        :return: a LoanInfo object
        """
        bibliotheca_id = licensepool.identifier.identifier
        patron_identifier = patron_obj.authorization_identifier
        args = dict(request_type='CheckoutRequest',
                    item_id=bibliotheca_id, patron_id=patron_identifier)
        body = self.TEMPLATE % args 
        response = self.request('checkout', body, method="PUT")
        if response.status_code == 201:
            # New loan
            start_date = datetime.datetime.utcnow()
        elif response.status_code == 200:
            # Old loan -- we don't know the start date
            start_date = None
        else:
            # Error condition.
            error = ErrorParser().process_all(response.content)
            if isinstance(error, AlreadyCheckedOut):
                # It's already checked out. No problem.
                pass
            else:
                raise error

        # At this point we know we have a loan.
        loan_expires = CheckoutResponseParser().process_all(response.content)
        loan = LoanInfo(
            licensepool.collection, DataSource.BIBLIOTHECA,
            licensepool.identifier.type,
            licensepool.identifier.identifier,
            start_date=None,
            end_date=loan_expires,
        )
        return loan
예제 #12
0
    def checkout(self, patron, pin, licensepool, internal_format):
        """
        Associate an ebook or audio with a patron.

        :param patron: a Patron object for the patron who wants to check out 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.
        :param internal_format: Represents the patron's desired book format.  Ignored for now.

        :return LoanInfo on success, None on failure.
        """
        patron_oneclick_id = self.validate_patron(patron)
        (item_oneclick_id, item_media) = self.validate_item(licensepool)

        resp_dict = self.circulate_item(patron_id=patron_oneclick_id,
                                        item_id=item_oneclick_id,
                                        return_item=False)

        if not resp_dict or ('error_code' in resp_dict):
            return None

        self.log.debug(
            "Patron %s/%s checked out item %s with transaction id %s.",
            patron.authorization_identifier, patron_oneclick_id,
            item_oneclick_id, resp_dict['transactionId'])

        today = datetime.datetime.now()
        if item_media == Edition.AUDIO_MEDIUM:
            expires = today + self.eaudio_expiration_default
        else:
            expires = today + self.ebook_expiration_default

        # Create the loan info. We don't know the expiration for sure,
        # but we know the library default.  We do have the option of
        # getting expiration by checking patron's activity, but that
        # would mean another http call and is not currently merited.
        loan = LoanInfo(
            identifier_type=licensepool.identifier.type,
            identifier=item_oneclick_id,
            start_date=today,
            end_date=expires,
            fulfillment_info=None,
        )
        return loan
예제 #13
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
        ]
예제 #14
0
    def _make_loan_info(self, item, fulfill=False):
        """Convert one of the items returned by a request to /checkouts into a
        LoanInfo with an RBFulfillmentInfo.
        """

        media_type = item.get('mediaType', 'eBook')
        isbn = item.get('isbn', None)

        # 'expiration' here refers to the expiration date of the loan, not
        # of the fulfillment URL.
        expires = item.get('expiration', None)
        if expires:
            expires = datetime.datetime.strptime(
                expires, self.EXPIRATION_DATE_FORMAT).date()

        identifier, made_new = Identifier.for_foreign_id(
            self._db,
            foreign_identifier_type=Identifier.RB_DIGITAL_ID,
            foreign_id=isbn,
            autocreate=False)
        if not identifier:
            # We have never heard of this book, which means the patron
            # didn't borrow it through us.
            return None

        fulfillment_info = RBFulfillmentInfo(
            self,
            DataSource.RB_DIGITAL,
            identifier,
            item,
        )

        return LoanInfo(
            self.collection,
            DataSource.RB_DIGITAL,
            Identifier.RB_DIGITAL_ID,
            isbn,
            start_date=None,
            end_date=expires,
            fulfillment_info=fulfillment_info,
        )
예제 #15
0
    def checkout(self, patron, pin, licensepool, internal_format):
        """Create a new loan."""
        if licensepool.licenses_owned < 1:
            raise NoLicenses()

        _db = Session.object_session(patron)

        loan = _db.query(Loan).filter(Loan.patron == patron).filter(
            Loan.license_pool_id == licensepool.id)
        if loan.count() > 0:
            raise AlreadyCheckedOut()

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

        hold = get_one(_db,
                       Hold,
                       patron=patron,
                       license_pool_id=licensepool.id)
        if hold:
            self._update_hold_end_date(hold)

        # If there's a holds queue, the patron must be at position 0 and have a
        # non-expired hold to check out the book.
        if ((not hold or hold.position > 0 or
             (hold.end and hold.end < datetime.datetime.utcnow()))
                and licensepool.licenses_available < 1):
            raise NoAvailableCopies()

        # Create a local loan so it's database id can be used to
        # receive notifications from the distributor.
        loan, ignore = get_one_or_create(_db,
                                         Loan,
                                         patron=patron,
                                         license_pool_id=licensepool.id)

        doc = self.get_license_status_document(loan)
        status = doc.get("status")

        if status not in [self.READY_STATUS, self.ACTIVE_STATUS]:
            # Something went wrong with this loan and we don't actually
            # have the book checked out. This should never happen.
            # Remove the loan we created.
            _db.delete(loan)
            raise CannotLoan()

        external_identifier = doc.get("links", {}).get("self", {}).get("href")
        if not external_identifier:
            _db.delete(loan)
            raise CannotLoan()

        start = datetime.datetime.utcnow()
        expires = doc.get("potential_rights", {}).get("end")
        if expires:
            expires = datetime.datetime.strptime(expires, self.TIME_FORMAT)

        # We need to set the start and end dates on our local loan since
        # the code that calls this only sets them when a new loan is created.
        loan.start = start
        loan.end = expires

        # We have successfully borrowed this book.
        if hold:
            _db.delete(hold)
        self.update_hold_queue(licensepool)

        return LoanInfo(
            licensepool.collection,
            licensepool.data_source.name,
            licensepool.identifier.type,
            licensepool.identifier.identifier,
            start,
            expires,
            external_identifier=external_identifier,
        )
예제 #16
0
                acs_resource_id = file.get('acsResourceId', None)

            # TODO: For audio books, the downloads are done by parts, and there are
            # multiple download urls.  Need to have a mechanism for putting lists of
            # parts into fulfillment objects.
            fulfillment_info = FulfillmentInfo(Identifier.ONECLICK_ID,
                                               identifier,
                                               content_link=download_url,
                                               content_type=file_format,
                                               content=None,
                                               content_expires=None)

            loan = LoanInfo(
                Identifier.ONECLICK_ID,
                isbn,
                start_date=None,
                end_date=expires,
                fulfillment_info=fulfillment_info,
            )

            loans.append(loan)

        return loans

    def get_patron_holds(self, patron_id):
        """
        :param patron_id OneClick internal id for the patron.
        """
        url = "%s/libraries/%s/patrons/%s/holds/" % (
            self.base_url, str(self.library_id), patron_id)
        action = "patron_holds"
예제 #17
0
            fulfillment_info = FulfillmentInfo(
                self.collection,
                DataSource.RB_DIGITAL,
                Identifier.RB_DIGITAL_ID, 
                identifier, 
                content_link = content_link, 
                content_type = content_type, 
                content = None, 
                content_expires = None
            )

            loan = LoanInfo(
                self.collection,
                DataSource.RB_DIGITAL,
                Identifier.RB_DIGITAL_ID,
                isbn,
                start_date=None,
                end_date=expires,
                fulfillment_info=fulfillment_info,
            )

            loans.append(loan)

        return loans


    def get_patron_holds(self, patron_id):
        """
        :param patron_id OneClick internal id for the patron.
        """
        url = "%s/libraries/%s/patrons/%s/holds/" % (self.base_url, str(self.library_id), patron_id)
예제 #18
0
    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'))
        end = cls._pd(checkout.get('expires'))

        usable_formats = []

        # If a format is already locked in, it will be in formats.
        for format in checkout.get('formats', []):
            format_type = format.get('formatType')
            if format_type in cls.FORMATS:
                usable_formats.append(format_type)


        # If a format hasn't been selected yet, available formats are in actions.
        actions = checkout.get('actions', {})
        format_action = actions.get('format', {})
        format_fields = format_action.get('fields', [])
        for field in format_fields:
            if field.get('name', "") == "formatType":
                format_options = field.get("options", [])
                for format_type in format_options:
                    if format_type in cls.FORMATS:
                        usable_formats.append(format_type)

        if not usable_formats:
            # Either this book is not available in any format readable
            # by the default client, or the patron previously chose to
            # fulfill it in a format not readable by the default
            # client. Either way, we cannot fulfill this loan and we
            # shouldn't show it in the list.
            return None

        locked_to = None
        if len(usable_formats) == 1:
            # Either the book has been locked into a specific format,
            # or only one usable format is available. We don't know
            # which case we're looking at, but for our purposes the
            # book is locked.
            [format] = usable_formats
            media_type, drm_scheme = (
                OverdriveRepresentationExtractor.format_data_for_overdrive_format.get(
                    format, (None, None)
                )
            )
            if media_type:
                # Make it clear that Overdrive will only deliver the content
                # in one specific media type.
                locked_to = DeliveryMechanismInfo(
                    content_type=media_type,
                    drm_scheme=drm_scheme
                )

        return LoanInfo(
            collection,
            DataSource.OVERDRIVE,
            Identifier.OVERDRIVE_ID,
            overdrive_identifier,
            start_date=start,
            end_date=end,
            locked_to=locked_to
        )
예제 #19
0
            fulfillment_info = FulfillmentInfo(
                self.collection,
                DataSource.ONECLICK,
                Identifier.ONECLICK_ID, 
                identifier, 
                content_link = download_url, 
                content_type = file_format, 
                content = None, 
                content_expires = None
            )

            loan = LoanInfo(
                self.collection,
                DataSource.ONECLICK,
                Identifier.ONECLICK_ID,
                isbn,
                start_date=None,
                end_date=expires,
                fulfillment_info=fulfillment_info,
            )

            loans.append(loan)

        return loans


    def get_patron_holds(self, patron_id):
        """
        :param patron_id OneClick internal id for the patron.
        """
        url = "%s/libraries/%s/patrons/%s/holds/" % (self.base_url, str(self.library_id), patron_id)