def get_notification_context(cls, notifications=None): """Get the context to render the notification template.""" context = {} notifications = notifications or [] if not notifications: return context context['loans'] = [] doc_dumper = DocumentGenericDumper() item_dumper = ItemNotificationDumper() patron_dumper = PatronNotificationDumper() for notification in notifications: loan = notification.loan creation_date = format_date_filter( notification.get('creation_date'), date_format='medium', locale=language_iso639_2to1( notification.get_language_to_use())) request_expire_date = format_date_filter( loan.get('request_expire_date'), date_format='medium', locale=language_iso639_2to1( notification.get_language_to_use())) # merge doc and item metadata preserving document key item_data = notification.item.dumps(dumper=item_dumper) doc_data = notification.document.dumps(dumper=doc_dumper) doc_data = {**item_data, **doc_data} # pickup location name --> !! pickup is on notif.request_loan, not # on notif.loan request_loan = notification.request_loan pickup_location = Location.get_record_by_pid( request_loan.get('pickup_location_pid')) if not pickup_location: pickup_location = Location.get_record_by_pid( request_loan.get('transaction_location_pid')) # request_patron request_patron = Patron.get_record_by_pid( request_loan.get('patron_pid')) loan_context = { 'creation_date': creation_date, 'document': doc_data, 'pickup_name': pickup_location.get('pickup_name', pickup_location.get('name')), 'request_expire_date': request_expire_date, 'patron': request_patron.dumps(dumper=patron_dumper) } context['loans'].append(loan_context) return context
def post_process_serialize_search(self, results, pid_fetcher): """Post process the search results. :param results: Elasticsearch search result. :param pid_fetcher: Persistent identifier fetcher. """ records = results.get('hits', {}).get('hits', {}) for record in records: metadata = record.get('metadata', {}) document = search_document_by_pid( metadata.get('document').get('pid')) metadata['ui_title_text'] = title_format_text_head( document['title'], with_subtitle=True) # Add library name for lib_term in results.get('aggregations', {}).get('library', {}).get('buckets', []): lib = Library.get_record_by_pid(lib_term.get('key')) lib_term['name'] = lib.get('name') # Add location name for loc_term in results.get('aggregations', {}).get('location', {}).get('buckets', []): loc = Location.get_record_by_pid(loc_term.get('key')) loc_term['name'] = loc.get('name') # Add library name for item_type_term in results.get('aggregations', {}).get('item_type', {}).get('buckets', []): item_type = ItemType.get_record_by_pid(item_type_term.get('key')) item_type_term['name'] = item_type.get('name') return super(ItemsJSONSerializer, self).post_process_serialize_search(results, pid_fetcher)
def get_pickup_location_options(): """Get all pickup location for all patron accounts.""" for ptrn_pid in [ptrn.pid for ptrn in current_patrons]: for pid in Location.get_pickup_location_pids(ptrn_pid): location = Location.get_record_by_pid(pid) location_name = location.get('pickup_name', location.get('name')) yield (location.pid, location_name)
def test_location_cannot_delete(item_lib_martigny): """Test cannot delete.""" location_pid = item_lib_martigny.location_pid location = Location.get_record_by_pid(location_pid) can, reasons = location.can_delete assert not can assert reasons['links']['holdings'] == 1 assert reasons['links']['items'] == 1
def get_locations(): """Get one pickup_location for each organisation. :return: A dict of locations pids by organisation """ location_data = {} for pid in Location.get_pickup_location_pids(): record = Location.get_record_by_pid(pid) if record.organisation_pid not in location_data: location_data[record.organisation_pid] = pid return location_data
def test_location_create(db, es_clear, loc_public_martigny_data): """Test location creation.""" loc = Location.create(loc_public_martigny_data, delete_pid=True) assert loc == loc_public_martigny_data assert loc.get('pid') == '1' loc = Location.get_record_by_pid('1') assert loc == loc_public_martigny_data fetched_pid = fetcher(loc.id, loc) assert fetched_pid.pid_value == '1' assert fetched_pid.pid_type == 'loc'
def _process_loan_pending_at_desk_in_transit_for_pickup( self, metadata, item_pid): """Process for PENDING, ITEM_AT_DESK, ITEM_IN_TRANSIT_FOR_PICKUP.""" pickup_loc = Location.get_record_by_pid( metadata['pickup_location_pid']) metadata['pickup_name'] = \ pickup_loc.get('pickup_name', pickup_loc.get('name')) if metadata['state'] == LoanState.ITEM_AT_DESK: metadata['rank'] = 0 if metadata['state'] in [ LoanState.PENDING, LoanState.ITEM_IN_TRANSIT_FOR_PICKUP ]: patron = Patron.get_record_by_pid(metadata['patron_pid']) item = Item.get_record_by_pid(item_pid) metadata['rank'] = item.patron_request_rank(patron)
def _build_notification_email_context(loan, item, location): """Build the context used by the send_notification_to_location function. :param loan : the loan for which build context :param item : the item for which build context :param location : the item location """ document_pid = Item.get_document_pid_by_item_pid(loan.item_pid) document = Document.get_record_by_pid(document_pid) pickup_location = Location.get_record_by_pid( loan.get('pickup_location_pid')) patron = Patron.get_record_by_pid(loan.patron_pid) # inherit holdings call number when possible issue_call_number = item.issue_inherited_first_call_number if issue_call_number: item['call_number'] = issue_call_number ctx = { 'loan': loan.replace_refs().dumps(), 'item': item.replace_refs().dumps(), 'document': document.replace_refs().dumps(), 'pickup_location': pickup_location, 'item_location': location.dumps(), 'patron': patron } library = pickup_location.get_library() ctx['pickup_location']['library'] = library ctx['item']['item_type'] = \ ItemType.get_record_by_pid(item.item_type_circulation_category_pid) titles = [ title for title in ctx['document'].get('title', []) if title['type'] == 'bf:Title' ] ctx['document']['title_text'] = \ next(iter(titles or []), {}).get('_text') responsibility_statement = create_title_responsibilites( document.get('responsibilityStatement', [])) ctx['document']['responsibility_statement'] = \ next(iter(responsibility_statement or []), '') trans_date = ciso8601.parse_datetime(loan.get('transaction_date')) trans_date = trans_date\ .replace(tzinfo=timezone.utc)\ .astimezone(tz=library.get_timezone()) ctx['loan']['transaction_date'] = \ trans_date.strftime("%d.%m.%Y - %H:%M:%S") return ctx
def test_location_create(db, es_clear, loc_public_martigny_data, lib_martigny, loc_online_martigny): """Test location creation.""" loc_public_martigny_data['is_online'] = True with pytest.raises(RecordValidationError): loc = Location.create(loc_public_martigny_data, delete_pid=True) del loc_public_martigny_data['is_online'] loc = Location.create(loc_public_martigny_data, delete_pid=True) assert loc == loc_public_martigny_data assert loc.get('pid') == '2' loc = Location.get_record_by_pid('2') assert loc == loc_public_martigny_data fetched_pid = fetcher(loc.id, loc) assert fetched_pid.pid_value == '2' assert fetched_pid.pid_type == 'loc'
def test_location_create(db, es, loc_public_martigny_data, lib_martigny, loc_online_martigny): """Test location creation.""" loc_public_martigny_data['is_online'] = True with pytest.raises(ValidationError): Location.create(loc_public_martigny_data, delete_pid=True) db.session.rollback() next_pid = Location.provider.identifier.next() del loc_public_martigny_data['is_online'] loc = Location.create(loc_public_martigny_data, delete_pid=True) next_pid += 1 assert loc == loc_public_martigny_data assert loc.get('pid') == str(next_pid) loc = Location.get_record_by_pid(loc.pid) assert loc == loc_public_martigny_data fetched_pid = fetcher(loc.id, loc) assert fetched_pid.pid_value == str(next_pid) assert fetched_pid.pid_type == 'loc'
def calculate_notification_amount(notification): """Return amount due for a notification. :param notification: the notification for which to compute the amount. At this time, this is not yet a `Notification`, only a dict of structured data. :return the amount due for this notification. 0 if no amount could be compute. """ # Find the reminder type to use based on the notification that we would # sent. If no reminder type is found, then no amount could be calculated # and we can't return '0' notif_type = notification.get('notification_type') reminder_type_mapping = { NotificationType.DUE_SOON: DUE_SOON_REMINDER_TYPE, NotificationType.OVERDUE: OVERDUE_REMINDER_TYPE } reminder_type = reminder_type_mapping.get(notif_type) if not notif_type or not reminder_type: return 0 # to find the notification due amount, we firstly need to get the # circulation policy linked to the parent loan. location_pid = notification.transaction_location_pid location = Location.get_record_by_pid(location_pid) cipo = CircPolicy.provide_circ_policy( location.organisation_pid, location.library_pid, notification.patron.patron_type_pid, notification.item.holding_circulation_category_pid) # now we get the circulation policy, search the correct reminder depending # of the reminder_counter from the notification context. reminder = cipo.get_reminder(reminder_type=reminder_type, idx=notification.get('context', {}).get( 'reminder_counter', 0)) return reminder.get('fee_amount', 0) if reminder else 0
def transaction_location(self): """Shortcut for transaction location of the notification.""" return Location.get_record_by_pid(self.transaction_location_pid)
def get_pickup_location_options(): """Get all pickup location.""" for pid in Location.get_pickup_location_pids(current_patron.pid): location = Location.get_record_by_pid(pid) location_name = location.get('pickup_name', location.get('name')) yield tuple([location.pid, location_name])
def post_process_serialize_search(self, results, pid_fetcher): """Post process the search results. :param results: Elasticsearch search result. :param pid_fetcher: Persistent identifier fetcher. """ records = results.get('hits', {}).get('hits', {}) orgs = {} libs = {} locs = {} for record in records: metadata = record.get('metadata', {}) document = search_document_by_pid( metadata.get('document').get('pid') ) metadata['ui_title_text'] = title_format_text_head( document['title'], with_subtitle=True ) item = Item.get_record_by_pid(metadata.get('pid')) metadata['availability'] = { 'available': item.available, 'status': metadata['status'], 'display_text': item.availability_text, 'request': item.number_of_requests() } if not metadata['available']: if metadata['status'] == ItemStatus.ON_LOAN: metadata['availability']['due_date'] =\ item.get_item_end_date(format='long', language='en') # Item in collection collection = item.in_collection() if collection: metadata['in_collection'] = collection # Organisation organisation = metadata['organisation'] if organisation['pid'] not in orgs: orgs[organisation['pid']] = Organisation \ .get_record_by_pid(organisation['pid']) organisation['viewcode'] = orgs[organisation['pid']].get('code') # Library library = metadata['library'] if library['pid'] not in libs: libs[library['pid']] = Library \ .get_record_by_pid(library['pid']) library['name'] = libs[library['pid']].get('name') # Location location = metadata['location'] if location['pid'] not in locs: locs[location['pid']] = Location \ .get_record_by_pid(location['pid']) location['name'] = locs[location['pid']].get('name') # Add library name for lib_term in results.get('aggregations', {}).get( 'library', {}).get('buckets', []): lib = Library.get_record_by_pid(lib_term.get('key')) lib_term['name'] = lib.get('name') # Add location name for loc_term in results.get('aggregations', {}).get( 'location', {}).get('buckets', []): loc = Location.get_record_by_pid(loc_term.get('key')) loc_term['name'] = loc.get('name') # Add item type name for item_type_term in results.get('aggregations', {}).get( 'item_type', {}).get('buckets', []): item_type = ItemType.get_record_by_pid(item_type_term.get('key')) item_type_term['name'] = item_type.get('name') # Add vendor name for vendor_term in results.get('aggregations', {}).get( 'vendor', {}).get('buckets', []): vendor = Vendor.get_record_by_pid(vendor_term.get('key')) vendor_term['name'] = vendor.get('name') # Correct document type buckets buckets = results['aggregations']['document_type']['buckets'] results['aggregations']['document_type']['buckets'] = \ filter_document_type_buckets(buckets) return super().post_process_serialize_search(results, pid_fetcher) # Correct document type buckets buckets = results['aggregations']['document_type']['buckets'] results['aggregations']['document_type']['buckets'] = \ filter_document_type_buckets(buckets)
def pickup_location(self): """Shortcut for pickup location of the notification.""" return Location.get_record_by_pid(self.pickup_location_pid)
def location(self): """Shortcut for item location of the notification.""" return Location.get_record_by_pid(self.location_pid)
def test_requesting_item_from_non_circulating_library( client, librarian_martigny, lib_martigny, lib_martigny_bourg, loc_restricted_martigny_bourg, patron_martigny, loc_public_martigny, loc_public_martigny_bourg, item_lib_martigny_bourg, circulation_policies, librarian_martigny_bourg): """Test requests on items of a non-circulating library.""" login_user_via_session(client, librarian_martigny_bourg.user) # Test a checkout of an item at a library with open-hours and no pickup # locations defined is possible. opening_hours = [ { "day": "monday", "is_open": True, "times": [ { "start_time": "07:00", "end_time": "19:00" } ] } ] lib_martigny_bourg['opening_hours'] = opening_hours lib_martigny_bourg.update(lib_martigny_bourg, dbcommit=True, reindex=True) params = dict( item_pid=item_lib_martigny_bourg.pid, patron_pid=patron_martigny.pid, transaction_user_pid=librarian_martigny_bourg.pid, transaction_library_pid=lib_martigny_bourg.pid, ) res, data = postdata( client, 'api_item.checkout', params ) assert res.status_code == 200 loan_pid = data.get('action_applied').get('checkout').get('pid') loan = Loan.get_record_by_pid(loan_pid) transaction_loc = Location.get_record_by_pid(loan.transaction_location_pid) assert transaction_loc.library_pid == lib_martigny_bourg.pid pickup_lib_pid = Location.get_record_by_pid( loan.pickup_location_pid).library_pid assert pickup_lib_pid == lib_martigny_bourg.pid assert loan.get('state') == LoanState.ITEM_ON_LOAN res, data = postdata( client, 'api_item.checkin', dict( item_pid=item_lib_martigny_bourg.pid, transaction_library_pid=lib_martigny_bourg.pid, transaction_user_pid=librarian_martigny_bourg.pid, ) ) assert res.status_code == 200 item = Item.get_record_by_pid(item_lib_martigny_bourg.pid) assert item.status == ItemStatus.ON_SHELF # TEST: a librarian from an external library can request and item from a # non-circulating library to be picked-up at his own library. lib_martigny_bourg.pop('opening_hours', None) lib_martigny_bourg.update(lib_martigny_bourg, dbcommit=True, reindex=True) login_user_via_session(client, librarian_martigny.user) res, data = postdata( client, 'api_item.librarian_request', dict( item_pid=item_lib_martigny_bourg.pid, patron_pid=patron_martigny.pid, pickup_location_pid=loc_public_martigny.pid, transaction_library_pid=lib_martigny_bourg.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 200 loan = Loan(data.get('metadata').get('pending_loans')[0]) transaction_loc = Location.get_record_by_pid( loan.transaction_location_pid) assert transaction_loc.library_pid == lib_martigny_bourg.pid pickup_lib_pid = Location.get_record_by_pid( loan.pickup_location_pid).library_pid assert pickup_lib_pid == lib_martigny.pid # non-circulating library send items to requesting library login_user_via_session(client, librarian_martigny_bourg.user) res, data = postdata( client, 'api_item.validate_request', dict( pid=loan.get('pid'), transaction_library_pid=lib_martigny_bourg.pid, transaction_user_pid=librarian_martigny_bourg.pid ) ) assert res.status_code == 200 loan = Loan(data.get('metadata').get('pending_loans')[0]) transaction_loc = Location.get_record_by_pid( loan.transaction_location_pid) assert transaction_loc.library_pid == lib_martigny_bourg.pid pickup_lib_pid = Location.get_record_by_pid( loan.pickup_location_pid).library_pid assert pickup_lib_pid == lib_martigny.pid assert loan.get('state') == LoanState.ITEM_IN_TRANSIT_FOR_PICKUP # requesting library receives an item from non-circulating library. login_user_via_session(client, librarian_martigny.user) res, data = postdata( client, 'api_item.receive', dict( pid=loan.get('pid'), transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 200 loan = Loan(data.get('metadata').get('pending_loans')[0]) transaction_loc = Location.get_record_by_pid( loan.transaction_location_pid) assert transaction_loc.library_pid == lib_martigny.pid pickup_lib_pid = \ Location.get_record_by_pid(loan.pickup_location_pid).library_pid assert pickup_lib_pid == lib_martigny.pid assert loan.get('state') == LoanState.ITEM_AT_DESK # checkout item to requested patron login_user_via_session(client, librarian_martigny.user) date_format = '%Y/%m/%dT%H:%M:%S.000Z' today = datetime.utcnow() eod = today.replace(hour=23, minute=59, second=0, microsecond=0, tzinfo=lib_martigny.get_timezone()) params = dict( item_pid=item_lib_martigny_bourg.pid, patron_pid=patron_martigny.pid, transaction_user_pid=librarian_martigny.pid, transaction_library_pid=lib_martigny.pid, end_date=eod.strftime(date_format) ) res, data = postdata( client, 'api_item.checkout', params ) assert res.status_code == 200 loan_pid = data.get('action_applied').get('checkout').get('pid') loan = Loan.get_record_by_pid(loan_pid) transaction_loc = Location.get_record_by_pid( loan.transaction_location_pid) assert transaction_loc.library_pid == lib_martigny.pid pickup_lib_pid = Location.get_record_by_pid( loan.pickup_location_pid).library_pid assert pickup_lib_pid == lib_martigny.pid assert loan.get('state') == LoanState.ITEM_ON_LOAN