def test_library_es_mapping(es_clear, db, library_data, organisation): """.""" search = LibrariesSearch() mapping = get_mapping(search.Meta.index) assert mapping Library.create(library_data, dbcommit=True, reindex=True, delete_pid=True) assert mapping == get_mapping(search.Meta.index)
def test_library_es_mapping(es_clear, db, lib_martigny_data, org_martigny): """Test library elasticsearch mapping.""" search = LibrariesSearch() mapping = get_mapping(search.Meta.index) assert mapping Library.create( lib_martigny_data, dbcommit=True, reindex=True, delete_pid=True) assert mapping == get_mapping(search.Meta.index)
def test_library_create(db, library_data): """Test libanisation creation.""" lib = Library.create(library_data, delete_pid=True) assert lib == library_data assert lib.get('pid') == '1' lib = Library.get_record_by_pid('1') assert lib == library_data fetched_pid = fetcher(lib.id, lib) assert fetched_pid.pid_value == '1' assert fetched_pid.pid_type == 'lib'
def test_item_loans_elements(loan_pending_martigny, item_lib_fully, circ_policy_default_martigny): """Test loan elements.""" assert loan_pending_martigny.item_pid == item_lib_fully.pid loan = list(get_loans_by_patron_pid(loan_pending_martigny.patron_pid))[0] assert loan.pid == loan_pending_martigny.get('pid') new_loan = deepcopy(loan_pending_martigny) del new_loan['transaction_location_pid'] assert get_default_loan_duration(new_loan, None) == \ get_default_loan_duration(loan_pending_martigny, None) assert item_lib_fully.last_location_pid == item_lib_fully.location_pid del circ_policy_default_martigny['checkout_duration'] circ_policy_default_martigny.update(circ_policy_default_martigny, dbcommit=True, reindex=True) today = datetime.now() library = Library.get_record_by_pid(item_lib_fully.library_pid) eve_end_date = today \ + get_default_loan_duration(new_loan, None) \ - timedelta(days=1) end_date = library.next_open(eve_end_date) assert today.strftime('%Y-%m-%d') == end_date.strftime('%Y-%m-%d')
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 library(app, organisation, library_data): """.""" lib = Library.create(data=library_data, delete_pid=False, dbcommit=True, reindex=True) flush_index(LibrariesSearch.Meta.index) return lib
def lib_martigny(app, org_martigny, lib_martigny_data): """Martigny-ville library.""" lib = Library.create(data=lib_martigny_data, delete_pid=False, dbcommit=True, reindex=True) flush_index(LibrariesSearch.Meta.index) return lib
def lib_aproz(app, org_sion, lib_aproz_data): """Aproz library.""" lib = Library.create(data=lib_aproz_data, delete_pid=False, dbcommit=True, reindex=True) flush_index(LibrariesSearch.Meta.index) return lib
def test_less_than_one_day_checkout( client, circ_policy_less_than_one_day_martigny, patron_martigny, patron2_martigny, item_lib_martigny, loc_public_martigny, librarian_martigny, item_on_shelf_martigny_patron_and_loan_pending): """Test checkout on an ON_SHELF item with 'less than one day' cipo.""" # Create a new item in ON_SHELF (without Loan) data = deepcopy(item_lib_martigny) data.pop('barcode') data.setdefault('status', ItemStatus.ON_SHELF) created_item = Item.create( data=data, dbcommit=True, reindex=True, delete_pid=True) # Check item is ON_SHELF and NO PENDING loan exist! assert created_item.number_of_requests() == 0 assert created_item.status == ItemStatus.ON_SHELF assert not created_item.is_requested_by_patron( patron2_martigny.get('patron', {}).get('barcode')[0]) # Ensure than the transaction date used will be an open_day. owner_lib = Library.get_record_by_pid(created_item.library_pid) transaction_date = owner_lib.next_open(ensure=True) with freeze_time(transaction_date): # the following tests the circulation action CHECKOUT_1_1 # an ON_SHELF item # WITHOUT pending loan # CAN be CHECKOUT for less than one day login_user_via_session(client, librarian_martigny.user) res, data = postdata( client, 'api_item.checkout', dict( item_pid=created_item.pid, patron_pid=patron2_martigny.pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid, pickup_location_pid=loc_public_martigny.pid ) ) assert res.status_code == 200 actions = data['action_applied'] onloan_item = Item.get_record_by_pid(data['metadata']['pid']) loan = Loan.get_record_by_pid(actions[LoanAction.CHECKOUT].get('pid')) # Check loan is ITEM_ON_LOAN and item is ON_LOAN assert onloan_item.number_of_requests() == 0 assert onloan_item.status == ItemStatus.ON_LOAN assert loan['state'] == LoanState.ITEM_ON_LOAN loan_end_date = ciso8601.parse_datetime(loan.get('end_date')) loan_end_date_formatted = loan_end_date.strftime('%Y-%m-%d') transaction_date_formatted = transaction_date.strftime('%Y-%m-%d') assert loan_end_date_formatted == transaction_date_formatted
def test_less_than_one_day_checkout( circ_policy_less_than_one_day_martigny, patron_martigny_no_email, patron2_martigny_no_email, item_lib_martigny, loc_public_martigny, librarian_martigny_no_email, item_on_shelf_martigny_patron_and_loan_pending): """Test checkout on an ON_SHELF item with 'less than one day' cipo.""" # Create a new item in ON_SHELF (without Loan) data = deepcopy(item_lib_martigny) data.pop('barcode') data.setdefault('status', ItemStatus.ON_SHELF) created_item = Item.create(data=data, dbcommit=True, reindex=True, delete_pid=True) # Check item is ON_SHELF and NO PENDING loan exist! assert created_item.number_of_requests() == 0 assert created_item.status == ItemStatus.ON_SHELF assert not created_item.is_requested_by_patron( patron2_martigny_no_email.get('patron', {}).get('barcode')) # the following tests the circulation action CHECKOUT_1_1 # an ON_SHELF item # WITHOUT pending loan # CAN be CHECKOUT for less than one day params = { 'patron_pid': patron2_martigny_no_email.pid, 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny_no_email.pid, 'pickup_location_pid': loc_public_martigny.pid } onloan_item, actions = created_item.checkout(**params) loan = Loan.get_record_by_pid(actions[LoanAction.CHECKOUT].get('pid')) # Check loan is ITEM_ON_LOAN and item is ON_LOAN assert onloan_item.number_of_requests() == 0 assert onloan_item.status == ItemStatus.ON_LOAN assert loan['state'] == LoanState.ITEM_ON_LOAN # Check due date loan_end_date = loan.get('end_date') lib = Library.get_record_by_pid(onloan_item.library_pid) today = datetime.now(pytz.utc) # Get next open day next_open_day = lib.next_open(today) if lib.is_open(today): next_open_day = today # Loan date should be in UTC. loan_datetime = ciso8601.parse_datetime(loan_end_date) # Compare year, month and date fail_msg = "Check timezone for Loan and Library. \ It should be the same date, even if timezone changed." assert loan_datetime.year == next_open_day.year, fail_msg assert loan_datetime.month == next_open_day.month, fail_msg assert loan_datetime.day == next_open_day.day, fail_msg
def test_items_extend(client, librarian_martigny_no_email, patron_martigny_no_email, loc_public_martigny, item_type_standard_martigny, item_lib_martigny, json_header, circulation_policies): """Test item renewal.""" login_user_via_session(client, librarian_martigny_no_email.user) item = item_lib_martigny item_pid = item.pid patron_pid = patron_martigny_no_email.pid location = loc_public_martigny # checkout res, data = postdata(client, 'api_item.checkout', dict(item_pid=item_pid, patron_pid=patron_pid)) assert res.status_code == 200 actions = data.get('action_applied') loan_pid = actions[LoanAction.CHECKOUT].get('pid') assert not item.get_extension_count() # extend loan res, data = postdata(client, 'api_item.extend_loan', dict(item_pid=item_pid, pid=loan_pid)) assert res.status_code == 200 item_data = data.get('metadata') actions = data.get('action_applied') assert item_data.get('status') == ItemStatus.ON_LOAN assert actions.get(LoanAction.EXTEND) assert item.get_extension_count() == 1 # Get library timezone lib = Library.get_record_by_pid(item.library_pid) lib_tz = lib.get_timezone() # test renewal due date hour extended_loan = Loan.get_record_by_pid(loan_pid) end_date = ciso8601.parse_datetime(extended_loan.get('end_date')) check_timezone_date(lib_tz, end_date) # second extenion res, _ = postdata(client, 'api_item.extend_loan', dict(item_pid=item_pid, pid=loan_pid)) assert res.status_code == 403 # checkin res, _ = postdata(client, 'api_item.checkin', dict(item_pid=item_pid, pid=loan_pid)) assert res.status_code == 200
def list_closed_dates(library_pid): """HTTP GET request to get the closed dates for a given library pid. USAGE : /api/libraries/<pid>/closed_dates?from=<from>&until=<until> optional parameters: * from: the lower interval date limit as 'YYYY-MM-DD' format. default value are sysdate - 1 month * until: the upper interval date limit as 'YYYY-MM-DD' format. default value are sysdate + 1 year :param library_pid: the library pid to search. """ library = Library.get_record_by_pid(library_pid) if not library: abort(404) # get start date from 'from' parameter from query string request start_date = request.args.get('from', datetime.now() - timedelta(days=31)) if isinstance(start_date, str): start_date = date_string_to_utc(start_date) start_date = start_date.replace(tzinfo=library.get_timezone()) # get end date from 'until' parameter from query string request end_date = request.args.get('until', add_years(datetime.now(), 1)) if isinstance(end_date, str): end_date = date_string_to_utc(end_date) end_date = end_date.replace(tzinfo=library.get_timezone()) delta = end_date - start_date # compute closed date closed_date = [] for i in range(delta.days + 1): tmp_date = start_date + timedelta(days=i) if not library.is_open(date=tmp_date, day_only=True): closed_date.append(tmp_date.strftime('%Y-%m-%d')) return jsonify({ 'params': { 'from': start_date.strftime('%Y-%m-%d'), 'until': end_date.strftime('%Y-%m-%d') }, 'closed_dates': closed_date })
def can_request(holding_pid): """HTTP request to check if an holding can be requested. Depending of query string argument, check if either configuration allows the request of the holding or if a librarian can request an holding for a patron. `api/holding/<holding_pid>/can_request` : --> only check config `api/holding/<holding_pid>/can_request?library_pid=<library_pid>&patron_barcode=<barcode>`: --> check if the patron can request an holding (check the cipo) """ kwargs = {} holding = Holding.get_record_by_pid(holding_pid) if not holding: abort(404, 'Holding not found') patron_barcode = flask_request.args.get('patron_barcode') if patron_barcode: kwargs['patron'] = Patron.get_patron_by_barcode( patron_barcode, holding.organisation_pid) if not kwargs['patron']: abort(404, 'Patron not found') library_pid = flask_request.args.get('library_pid') if library_pid: kwargs['library'] = Library.get_record_by_pid(library_pid) if not kwargs['library']: abort(404, 'Library not found') can, reasons = holding.can(HoldingCirculationAction.REQUEST, **kwargs) # check the `reasons_not_request` array. If it's empty, the request is # allowed, otherwise the request is not allowed and we need to return the # reasons why response = {'can': can} if reasons: response['reasons'] = {'others': {reason: True for reason in reasons}} return jsonify(response)
def can_request(item_pid): """HTTP request to check if an item can be requested. Depending of query string argument, either only check if configuration allows the request of this item ; either if a librarian can request an item for a patron. `api/item/<item_pid>/can_request` : --> only check config `api/item/<item_pid>/can_request?library_pid=<library_pid>&patron_barcode=<barcode>`: --> check if the patron can request this item (check the cipo) """ kwargs = {} item = Item.get_record_by_pid(item_pid) if not item: abort(404, 'Item not found') patron_barcode = flask_request.args.get('patron_barcode') if patron_barcode: kwargs['patron'] = Patron.get_patron_by_barcode( patron_barcode, item.organisation_pid) if not kwargs['patron']: abort(404, 'Patron not found') library_pid = flask_request.args.get('library_pid') if library_pid: kwargs['library'] = Library.get_record_by_pid(library_pid) if not kwargs['library']: abort(404, 'Library not found') # ask to item if the request is possible with these data. can, reasons = item.can(ItemCirculationAction.REQUEST, **kwargs) # check the `reasons_not_request` array. If it's empty, the request is # allowed ; if not the request is disallow and we need to return the # reasons why response = {'can': can} if reasons: response['reasons'] = {'others': {reason: True for reason in reasons}} return jsonify(response)
def test_loan_get_overdue_fees(item_on_loan_martigny_patron_and_loan_on_loan): """Test the overdue fees computation.""" def get_end_date(delta=0): end = date.today() - timedelta(days=delta) end = datetime(end.year, end.month, end.day, tzinfo=timezone.utc) return end - timedelta(microseconds=1) _, _, loan = item_on_loan_martigny_patron_and_loan_on_loan cipo = get_circ_policy(loan) library = Library.get_record_by_pid(loan.library_pid) # CASE#1 :: classic settings. # * 3 intervals with no gap into each one. # * no limit on last interval # * no maximum overdue cipo['overdue_fees'] = { 'intervals': [ { 'from': 1, 'to': 1, 'fee_amount': 0.10 }, { 'from': 2, 'to': 2, 'fee_amount': 0.20 }, { 'from': 3, 'fee_amount': 0.50 }, ] } cipo.update(data=cipo, dbcommit=True, reindex=True) expected_due_amount = [0.1, 0.3, 0.8, 1.3, 1.8, 2.3, 2.8, 3.3, 3.8, 4.3] for delta in range(0, len(expected_due_amount)): end = get_end_date(delta) loan['end_date'] = end.isoformat() loan = loan.update(loan, dbcommit=True, reindex=True) count_open = library.count_open(start_date=end + timedelta(days=1)) if count_open == 0: continue assert sum_for_fees(loan.get_overdue_fees) == \ expected_due_amount[count_open - 1] # CASE#2 :: no more overdue after 3 days. # * same definition than before, but add a upper limit to the last # interval cipo['overdue_fees'] = { 'intervals': [ { 'from': 1, 'to': 1, 'fee_amount': 0.10 }, { 'from': 2, 'to': 2, 'fee_amount': 0.20 }, { 'from': 3, 'to': 3, 'fee_amount': 0.50 }, ] } cipo.update(data=cipo, dbcommit=True, reindex=True) expected_due_amount = [0.1, 0.3, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8] for delta in range(0, len(expected_due_amount)): end = get_end_date(delta) loan['end_date'] = end.isoformat() loan = loan.update(loan, dbcommit=True, reindex=True) count_open = library.count_open(start_date=end + timedelta(days=1)) if count_open == 0: continue assert sum_for_fees(loan.get_overdue_fees) == \ expected_due_amount[count_open - 1] # CASE#3 :: classic setting + maximum overdue. # * 3 intervals with no gap into each one. # * no limit on last interval # * maximum overdue = 2 cipo['overdue_fees'] = { 'intervals': [ { 'from': 1, 'to': 1, 'fee_amount': 0.10 }, { 'from': 2, 'to': 2, 'fee_amount': 0.20 }, { 'from': 3, 'fee_amount': 0.50 }, ], 'maximum_total_amount': 2 } cipo.update(data=cipo, dbcommit=True, reindex=True) expected_due_amount = [0.1, 0.3, 0.8, 1.3, 1.8, 2.0, 2.0, 2.0, 2.0, 2.0] for delta in range(0, len(expected_due_amount)): end = get_end_date(delta) loan['end_date'] = end.isoformat() loan = loan.update(loan, dbcommit=True, reindex=True) count_open = library.count_open(start_date=end + timedelta(days=1)) if count_open == 0: continue assert sum_for_fees(loan.get_overdue_fees) == \ expected_due_amount[count_open - 1] # CASE#4 :: intervals with gaps # * define 2 intervals with gaps between # * grace period for first overdue day # * maximum overdue to 2.5 (not a normal step) cipo['overdue_fees'] = { 'intervals': [{ 'from': 2, 'to': 3, 'fee_amount': 0.10 }, { 'from': 5, 'fee_amount': 0.50 }], 'maximum_total_amount': 1.1 } cipo.update(data=cipo, dbcommit=True, reindex=True) expected_due_amount = [0, 0.1, 0.2, 0.2, 0.7, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1] for delta in range(0, len(expected_due_amount)): end = get_end_date(delta) loan['end_date'] = end.isoformat() loan = loan.update(loan, dbcommit=True, reindex=True) count_open = library.count_open(start_date=end + timedelta(days=1)) if count_open == 0: continue assert sum_for_fees(loan.get_overdue_fees) == \ expected_due_amount[count_open-1] # RESET THE CIPO del cipo['overdue_fees'] cipo.update(data=cipo, dbcommit=True, reindex=True)
def library(self): """Shortcut for item library of the notification.""" return Library.get_record_by_pid(self.library_pid)
def test_timezone_due_date(client, librarian_martigny_no_email, patron_martigny_no_email, loc_public_martigny, item_type_standard_martigny, item3_lib_martigny, circ_policy_short_martigny, lib_martigny): """Test that timezone affects due date regarding library location.""" # Login to perform action login_user_via_session(client, librarian_martigny_no_email.user) # Close the library all days. Except Monday. del lib_martigny['opening_hours'] del lib_martigny['exception_dates'] lib_martigny['opening_hours'] = [ { "day": "monday", "is_open": True, "times": [ { "start_time": "07:00", "end_time": "19:00" } ] }, { "day": "tuesday", "is_open": False, "times": [] }, { "day": "wednesday", "is_open": False, "times": [] }, { "day": "thursday", "is_open": False, "times": [] }, { "day": "friday", "is_open": False, "times": [] }, { "day": "saturday", "is_open": False, "times": [] }, { "day": "sunday", "is_open": False, "times": [] } ] lib_martigny.update(lib_martigny, dbcommit=True, reindex=True) # Change circulation policy checkout_duration = 3 item = item3_lib_martigny item_pid = item.pid patron_pid = patron_martigny_no_email.pid from rero_ils.modules.circ_policies.api import CircPolicy circ_policy = CircPolicy.provide_circ_policy( item.library_pid, patron_martigny_no_email.patron_type_pid, item.item_type_pid ) circ_policy['number_of_days_before_due_date'] = 7 circ_policy['checkout_duration'] = checkout_duration circ_policy.update( circ_policy, dbcommit=True, reindex=True ) # Checkout the item res, data = postdata( client, 'api_item.checkout', dict( item_pid=item_pid, patron_pid=patron_pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny_no_email.pid, ) ) assert res.status_code == 200 # Get Loan date (should be in UTC) loan_pid = data.get('action_applied')[LoanAction.CHECKOUT].get('pid') loan = Loan.get_record_by_pid(loan_pid) loan_end_date = loan.get('end_date') # Get next library open date (should be next monday after X-1 days) where # X is checkout_duration soon = datetime.now(pytz.utc) + timedelta(days=(checkout_duration-1)) lib = Library.get_record_by_pid(item.library_pid) lib_datetime = lib.next_open(soon) # Loan date should be in UTC (as lib_datetime). loan_datetime = ciso8601.parse_datetime(loan_end_date) # Compare year, month and date for Loan due date: should be the same! fail_msg = "Check timezone for Loan and Library. \ It should be the same date, even if timezone changed." assert loan_datetime.year == lib_datetime.year, fail_msg assert loan_datetime.month == lib_datetime.month, fail_msg assert loan_datetime.day == lib_datetime.day, fail_msg # Loan date differs regarding timezone, and day of the year (GMT+1/2). check_timezone_date(pytz.utc, loan_datetime, [21, 22])
def transaction_library(self): """Shortcut to get notification transaction library.""" return Library.get_record_by_pid(self.transaction_library_pid)
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 test_notifications_task( client, librarian_martigny, patron_martigny, item_lib_martigny, circ_policy_short_martigny, loc_public_martigny, lib_martigny): """Test overdue and due_soon loans.""" login_user_via_session(client, librarian_martigny.user) item = item_lib_martigny item_pid = item.pid patron_pid = patron_martigny.pid # First we need to create a checkout res, data = postdata( client, 'api_item.checkout', dict( item_pid=item_pid, patron_pid=patron_pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid, ) ) assert res.status_code == 200 loan_pid = data.get('action_applied')[LoanAction.CHECKOUT].get('pid') loan = Loan.get_record_by_pid(loan_pid) # test due_soon notification # update the loan end_date to reflect the due_soon date. So when we run # the task to create notification this loan should be considerate as # due_soon and a notification should be created. end_date = datetime.now(timezone.utc) + timedelta(days=5) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) flush_index(LoansSearch.Meta.index) due_soon_loans = list(get_due_soon_loans()) assert due_soon_loans[0].get('pid') == loan_pid create_notifications(types=[ NotificationType.DUE_SOON, NotificationType.OVERDUE ]) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert loan.is_notified(NotificationType.DUE_SOON) notif = get_notification(loan, NotificationType.DUE_SOON) notif_date = ciso8601.parse_datetime(notif.get('creation_date')) assert notif_date.date() == datetime.today().date() # -- test overdue notification -- # For this test, we simulate an overdue on Friday and the library is closed # during the weekend. No notification should be generated. # Friday end_date = datetime(year=2021, month=1, day=22, tzinfo=timezone.utc) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) # Process the notification during the weekend (Saturday) process_date = datetime(year=2021, month=1, day=23, tzinfo=timezone.utc) overdue_loans = list(get_overdue_loans(tstamp=process_date)) assert overdue_loans[0].get('pid') == loan_pid create_notifications(types=[ NotificationType.OVERDUE ], tstamp=process_date) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) # Should not be created assert not loan.is_notified(NotificationType.OVERDUE, 1) # Should not be sent assert number_of_notifications_sent( loan, notification_type=NotificationType.OVERDUE) == 0 # For this test, we will update the loan to simulate an overdue of 12 # days. With this delay, regarding the cipo configuration, only the first # overdue reminder should be sent. # NOTE : the cipo define the first overdue reminder after 5 days. But we # use an overdue of 12 days because the overdue is based on # loan->item->library open days. Using 12 (5 days + 1 week) we # ensure than the overdue notification will be sent. loan_lib = Library.get_record_by_pid(loan.library_pid) add_days = 12 open_days = [] while len(open_days) < 12: end_date = datetime.now(timezone.utc) - timedelta(days=add_days) open_days = loan_lib.get_open_days(end_date) add_days += 1 loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) overdue_loans = list(get_overdue_loans()) assert overdue_loans[0].get('pid') == loan_pid create_notifications(types=[ NotificationType.DUE_SOON, NotificationType.OVERDUE ]) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert loan.is_notified(NotificationType.OVERDUE, 0) assert number_of_notifications_sent( loan, notification_type=NotificationType.OVERDUE) == 1 # test overdue notification#2 # Now simulate than the previous call crashed. So call the task with a # fixed date. In our test, no new notifications should be sent create_notifications(types=[ NotificationType.DUE_SOON, NotificationType.OVERDUE ], tstamp=datetime.now(timezone.utc)) assert number_of_notifications_sent( loan, notification_type=NotificationType.OVERDUE) == 1 # test overdue notification#3 # For this test, we will update the loan to simulate an overdue of 40 # days. With this delay, regarding the cipo configuration, the second # (and last) overdue reminder should be sent. end_date = datetime.now(timezone.utc) - timedelta(days=40) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) overdue_loans = list(get_overdue_loans()) assert overdue_loans[0].get('pid') == loan_pid create_notifications(types=[ NotificationType.DUE_SOON, NotificationType.OVERDUE ]) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert loan.is_notified(NotificationType.OVERDUE, 1) assert number_of_notifications_sent( loan, notification_type=NotificationType.OVERDUE) == 2 # checkin the item to put it back to it's original state res, _ = postdata( client, 'api_item.checkin', dict( item_pid=item_pid, pid=loan_pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 200