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 )
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 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)
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
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
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, )
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 )
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 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)
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.
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'))
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.
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'))