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
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
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'])
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, )
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
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
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 ]
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 )
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
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
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 ]
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, )
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, )
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"
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)
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 )
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)