def test_delayed_notifications( loan_validated_martigny, item2_lib_martigny, mailbox, patron_martigny, lib_martigny): """Test availability notification created from a loan.""" mailbox.clear() loan = loan_validated_martigny # ensure an availability notification exists (possibly not yet sent) notification = get_notification(loan, NotificationType.AVAILABILITY) assert notification assert notification.loan_pid == loan_validated_martigny.get('pid') assert notification.item_pid == item2_lib_martigny.pid assert notification.patron_pid == patron_martigny.pid # ensure an at_desk notification exists (possibly not yet sent) notification = get_notification(loan, NotificationType.AT_DESK) assert notification assert notification.loan_pid == loan_validated_martigny.get('pid') assert notification.item_pid == item2_lib_martigny.pid flush_index(NotificationsSearch.Meta.index) assert not get_notification(loan, NotificationType.RECALL) for notification_type in NotificationType.ALL_NOTIFICATIONS: process_notifications(notification_type) assert loan.is_notified(notification_type=NotificationType.AVAILABILITY) assert loan.is_notified(notification_type=NotificationType.AT_DESK) # One notification will be sent : AVAILABILITY (sent to patron). # Get the last message from mailbox and check it. availability_msg = mailbox[-1] assert availability_msg.reply_to == lib_martigny.get('email') mailbox.clear()
def test_availability_notification( loan_validated_martigny, item2_lib_martigny, mailbox, patron_martigny, lib_martigny): """Test availability notification created from a loan.""" mailbox.clear() loan = loan_validated_martigny notification = get_notification(loan, NotificationType.AVAILABILITY) assert notification # ensure a notification exists (possibly not yet sent) assert notification.loan_pid == loan_validated_martigny.get('pid') assert notification.item_pid == item2_lib_martigny.pid assert notification.patron_pid == patron_martigny.pid assert not get_notification(loan, NotificationType.RECALL) for notification_type in NotificationType.ALL_NOTIFICATIONS: process_notifications(notification_type) assert len(mailbox) assert loan.is_notified(notification_type=NotificationType.AVAILABILITY) message = mailbox[-1] assert message.reply_to == lib_martigny.get('email') mailbox.clear()
def test_delete_pickup_location( loan2_validated_martigny, loc_restricted_martigny, mailbox): """Test delete pickup location.""" mailbox.clear() loan = loan2_validated_martigny notification = get_notification(loan, NotificationType.AVAILABILITY) assert notification.pickup_location.pid == loc_restricted_martigny.pid # We can not delete location used as transaction or pickup location # # any more. reasons_not_to_delete = loc_restricted_martigny.reasons_not_to_delete() assert reasons_not_to_delete == {'links': {'loans': 1}} with pytest.raises(IlsRecordError.NotDeleted): loc_restricted_martigny.delete(dbcommit=True, delindex=True)
def test_cancel_notifications( client, patron_martigny, lib_martigny, item_lib_martigny, librarian_martigny, loc_public_martigny, circulation_policies, mailbox ): """Test cancel notifications.""" login_user_via_session(client, librarian_martigny.user) # CREATE and VALIDATE a request ... res, data = postdata( client, 'api_item.librarian_request', dict( item_pid=item_lib_martigny.pid, pickup_location_pid=loc_public_martigny.pid, patron_pid=patron_martigny.pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 200 request_loan_pid = data.get( 'action_applied')[LoanAction.REQUEST].get('pid') flush_index(NotificationsSearch.Meta.index) res, data = postdata( client, 'api_item.validate_request', dict( pid=request_loan_pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 200 # At this time, an AVAILABILITY notification should be create but not yet # dispatched loan = Loan.get_record_by_pid(request_loan_pid) notification = get_notification(loan, NotificationType.AVAILABILITY) assert notification \ and notification['status'] == NotificationStatus.CREATED # BORROW the requested item res, data = postdata( client, 'api_item.checkout', dict( item_pid=item_lib_martigny.pid, patron_pid=patron_martigny.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) # Try to dispatch pending availability notifications. # As the item is now checkout, then the availability notification is not # yet relevant. mailbox.clear() process_notifications(NotificationType.AVAILABILITY) notification = get_notification(loan, NotificationType.AVAILABILITY) assert notification and \ notification['status'] == NotificationStatus.CANCELLED assert len(mailbox) == 0 # restore to initial state res, data = postdata( client, 'api_item.checkin', dict( item_pid=item_lib_martigny.pid, # patron_pid=patron_martigny.pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid, ) ) assert res.status_code == 200 mailbox.clear() # Test REMINDERS notifications. # reminders notification check about the end_date. As the loan end_date # is not yet over, the notification could be cancelled. notification = loan.create_notification(_type=NotificationType.DUE_SOON)[0] can_cancel, _ = notification.can_be_cancelled() assert not can_cancel process_notifications(NotificationType.DUE_SOON) notification = Notification.get_record_by_pid(notification.pid) assert notification['status'] == NotificationStatus.DONE flush_index(NotificationsSearch.Meta.index) # try to create a new DUE_SOON notification for the same loan record = { 'creation_date': datetime.now(timezone.utc).isoformat(), 'notification_type': NotificationType.DUE_SOON, 'context': { 'loan': {'$ref': get_ref_for_pid('loans', loan.pid)}, 'reminder_counter': 0 } } notification = Notification.create(record) can_cancel, _ = notification.can_be_cancelled() assert can_cancel Dispatcher.dispatch_notifications([notification.pid]) notification = Notification.get_record_by_pid(notification.pid) assert notification['status'] == NotificationStatus.CANCELLED
def test_recall_notification_with_patron_additional_email_only( client, patron_sion_with_additional_email, lib_martigny, json_header, patron2_martigny, item3_lib_martigny, librarian_martigny, circulation_policies, loc_public_martigny, mailbox): """Test recall notification.""" mailbox.clear() login_user_via_session(client, librarian_martigny.user) res, data = postdata( client, 'api_item.checkout', dict( item_pid=item3_lib_martigny.pid, patron_pid=patron_sion_with_additional_email.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) assert not get_notification(loan, NotificationType.RECALL) # test notification res, data = postdata( client, 'api_item.librarian_request', dict( item_pid=item3_lib_martigny.pid, pickup_location_pid=loc_public_martigny.pid, patron_pid=patron2_martigny.pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 200 flush_index(NotificationsSearch.Meta.index) request_loan_pid = data.get( 'action_applied')[LoanAction.REQUEST].get('pid') for notification_type in NotificationType.ALL_NOTIFICATIONS: process_notifications(notification_type) # one new email for the librarian assert mailbox[0].recipients == \ [patron_sion_with_additional_email[ 'patron']['additional_communication_email']] mailbox.clear() params = { 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid } # cancel request res, _ = postdata( client, 'api_item.cancel_item_request', dict( item_pid=item3_lib_martigny.pid, pid=request_loan_pid, transaction_user_pid=librarian_martigny.pid, transaction_library_pid=lib_martigny.pid ) ) assert res.status_code == 200 _, actions = item3_lib_martigny.checkin(**params)
def test_recall_notification_without_email( client, patron_sion_without_email1, lib_martigny, json_header, patron2_martigny, item3_lib_martigny, librarian_martigny, circulation_policies, loc_public_martigny, mailbox): """Test recall notification.""" mailbox.clear() login_user_via_session(client, librarian_martigny.user) res, data = postdata( client, 'api_item.checkout', dict( item_pid=item3_lib_martigny.pid, patron_pid=patron_sion_without_email1.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) assert not get_notification(loan, NotificationType.RECALL) # test notification res, data = postdata( client, 'api_item.librarian_request', dict( item_pid=item3_lib_martigny.pid, pickup_location_pid=loc_public_martigny.pid, patron_pid=patron2_martigny.pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) request_loan_pid = data.get( 'action_applied')[LoanAction.REQUEST].get('pid') assert res.status_code == 200 flush_index(NotificationsSearch.Meta.index) notification = get_notification(loan, NotificationType.RECALL) assert notification and notification.loan_pid == loan.pid assert not get_notification(loan, NotificationType.AVAILABILITY) for notification_type in NotificationType.ALL_NOTIFICATIONS: process_notifications(notification_type) # one new email for the librarian recipient = lib_martigny.get_email(notification['notification_type']) assert recipient assert mailbox[0].recipients == [recipient] # check the address block assert patron2_martigny.dumps()['street'] in mailbox[0].body mailbox.clear() params = { 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid } # cancel request res, _ = postdata( client, 'api_item.cancel_item_request', dict( item_pid=item3_lib_martigny.pid, pid=request_loan_pid, transaction_user_pid=librarian_martigny.pid, transaction_library_pid=lib_martigny.pid ) ) assert res.status_code == 200 _, actions = item3_lib_martigny.checkin(**params)
def test_recall_notification_with_disabled_config( app, client, librarian_martigny, item3_lib_martigny, patron_sion, loc_public_martigny, patron2_martigny, lib_martigny, circulation_policies, mailbox ): """Test the recall notification if app config disable it.""" initial_config = deepcopy( app.config.get('RERO_ILS_DISABLED_NOTIFICATION_TYPE', [])) app.config.setdefault('RERO_ILS_DISABLED_NOTIFICATION_TYPE', []).append( NotificationType.RECALL) # STEP#0 :: INIT # Create a checkout mailbox.clear() login_user_via_session(client, librarian_martigny.user) res, data = postdata(client, 'api_item.checkout', dict( item_pid=item3_lib_martigny.pid, patron_pid=patron_sion.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) assert not get_notification(loan, NotificationType.RECALL) # STEP#1 :: CREATE A REQUEST ON THIS ITEM # A request on a checkout item should be create a 'recall' notification. # But as 'recall' type is disabled from app config, no notification # must be created/sent. res, data = postdata(client, 'api_item.librarian_request', dict( item_pid=item3_lib_martigny.pid, pickup_location_pid=loc_public_martigny.pid, patron_pid=patron2_martigny.pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny.pid )) request_loan_pid = data.get( 'action_applied')[LoanAction.REQUEST].get('pid') assert res.status_code == 200 flush_index(NotificationsSearch.Meta.index) notification = get_notification(loan, NotificationType.RECALL) assert not notification assert len(mailbox) == 0 # RESET # * Reset application configuration # * Cancel the request, checkin the item app.config['RERO_ILS_DISABLED_NOTIFICATION_TYPE'] = initial_config res, _ = postdata(client, 'api_item.cancel_item_request', dict( item_pid=item3_lib_martigny.pid, pid=request_loan_pid, transaction_user_pid=librarian_martigny.pid, transaction_library_pid=lib_martigny.pid )) assert res.status_code == 200 params = { 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid } _, actions = item3_lib_martigny.checkin(**params)
def test_recall_notification(client, patron_sion, lib_sion, json_header, patron2_martigny, item_lib_sion, librarian_sion, circulation_policies, loc_public_sion, mailbox): """Test recall notification.""" mailbox.clear() login_user_via_session(client, librarian_sion.user) res, data = postdata( client, 'api_item.checkout', dict( item_pid=item_lib_sion.pid, patron_pid=patron_sion.pid, transaction_location_pid=loc_public_sion.pid, transaction_user_pid=librarian_sion.pid, ) ) assert res.status_code == 200 loan_pid = data.get('action_applied')[LoanAction.CHECKOUT].get('pid') loan = Loan.get_record_by_pid(loan_pid) assert not get_notification(loan, NotificationType.RECALL) # test notification permissions res, data = postdata( client, 'api_item.librarian_request', dict( item_pid=item_lib_sion.pid, pickup_location_pid=loc_public_sion.pid, patron_pid=patron2_martigny.pid, transaction_library_pid=lib_sion.pid, transaction_user_pid=librarian_sion.pid ) ) assert res.status_code == 200 request_loan_pid = data.get( 'action_applied')[LoanAction.REQUEST].get('pid') request_loan = Loan.get_record_by_pid(request_loan_pid) flush_index(NotificationsSearch.Meta.index) notification = get_notification(loan, NotificationType.RECALL) assert notification and notification.loan_pid == loan.pid assert not get_notification(loan, NotificationType.AVAILABILITY) assert not get_notification(request_loan, NotificationType.REQUEST) assert not len(mailbox) for notification_type in NotificationType.ALL_NOTIFICATIONS: process_notifications(notification_type) # one new email for the patron assert mailbox[-1].recipients == [patron_sion.dumps()['email']] assert loan.is_notified(notification_type=NotificationType.RECALL) mailbox.clear() # cancel request res, _ = postdata( client, 'api_item.cancel_item_request', dict( item_pid=item_lib_sion.pid, pid=request_loan_pid, transaction_user_pid=librarian_sion.pid, transaction_location_pid=loc_public_sion.pid ) ) assert res.status_code == 200 # no new notification is send for the second time res, _ = postdata( client, 'api_item.librarian_request', dict( item_pid=item_lib_sion.pid, pickup_location_pid=loc_public_sion.pid, patron_pid=patron2_martigny.pid, transaction_library_pid=lib_sion.pid, transaction_user_pid=librarian_sion.pid ) ) assert res.status_code == 200 flush_index(NotificationsSearch.Meta.index) assert not loan.is_notified( notification_type=NotificationType.RECALL, counter=1) assert not loan.is_notified( notification_type=NotificationType.AVAILABILITY) assert not get_notification(loan, NotificationType.AVAILABILITY) assert not get_notification(request_loan, NotificationType.REQUEST) assert not request_loan.is_notified( notification_type=NotificationType.REQUEST) assert len(mailbox) == 0
def test_reminder_notifications_after_extend( item_lib_martigny, patron_martigny, loc_public_martigny, librarian_martigny, circulation_policies, mailbox, client ): """Test any reminder notification could be resend after loan extension.""" # STEP 1 - CREATE BASIC RESOURCES FOR THE TEST # * Create a loan and update it to be considerate as "due soon". # * Run the `notification-creation` task to create a DUE_SOON # notification params = { 'patron_pid': patron_martigny.pid, 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid, 'pickup_location_pid': loc_public_martigny.pid } item, loan = item_record_to_a_specific_loan_state( item=item_lib_martigny, loan_state=LoanState.ITEM_ON_LOAN, params=params, copy_item=True) # get the related cipo and check than an due_soon reminder exists cipo = get_circ_policy(loan) due_soon_reminder = cipo.get_reminder(DUE_SOON_REMINDER_TYPE) assert due_soon_reminder # Update the loan delay = due_soon_reminder.get('days_delay') - 1 due_soon_date = datetime.now() - timedelta(days=delay) end_date = datetime.now() + timedelta(days=1) loan['due_soon_date'] = due_soon_date.astimezone(pytz.utc).isoformat() loan['end_date'] = end_date.astimezone(pytz.utc).isoformat() loan = loan.update(loan, dbcommit=True, reindex=True) assert loan.is_loan_due_soon() # run the create notification task and process notification. mailbox.clear() create_notifications(types=[NotificationType.DUE_SOON]) process_notifications(NotificationType.DUE_SOON) first_notification = get_notification(loan, NotificationType.DUE_SOON) assert first_notification \ and first_notification['status'] == NotificationStatus.DONE assert len(mailbox) == 1 counter = NotificationsSearch()\ .filter('term', context__loan__pid=loan.pid)\ .filter('term', notification_type=NotificationType.DUE_SOON)\ .count() assert counter == 1 # STEP 2 - CHECK NOTIFICATIONS CREATION # Run the `create_notification` task for DUE_SOON notification type. # As a notification already exists, no new DUE_SOON#1 notifications # should be created create_notifications(types=[NotificationType.DUE_SOON]) query = NotificationsSearch() \ .filter('term', context__loan__pid=loan.pid) \ .filter('term', notification_type=NotificationType.DUE_SOON) \ .source('pid').scan() notification_pids = [hit.pid for hit in query] assert len(notification_pids) == 1 assert notification_pids[0] == first_notification.pid # STEP 3 - EXTEND THE LOAN # * User has received the DUE_SOON message and extend the loan. # * Get the new 'due_soon_date' it will be used later to create # notifications login_user_via_session(client, librarian_martigny.user) params = dict( item_pid=item.pid, transaction_user_pid=librarian_martigny.pid, transaction_location_pid=loc_public_martigny.pid ) res, _ = postdata(client, 'api_item.extend_loan', params) assert res.status_code == 200 loan = Loan.get_record_by_pid(loan.pid) due_soon_date = ciso8601.parse_datetime(loan.get('due_soon_date')) # STEP 4 - CHECK NOTIFICATIONS CREATION # Run again the `create_notification` task, again for DUE_SOON # notification type. As the loan is extended, a new DUE_SOON # notification should be created about this loan. # Process the notification, check that this new notification isn't # cancelled and well processed. process_date = due_soon_date + timedelta(days=1) create_notifications( types=[NotificationType.DUE_SOON], tstamp=process_date ) counter = NotificationsSearch() \ .filter('term', context__loan__pid=loan.pid) \ .filter('term', notification_type=NotificationType.DUE_SOON) \ .count() assert counter == 2 process_notifications(NotificationType.DUE_SOON) assert len(mailbox) == 2 second_notification = get_notification(loan, NotificationType.DUE_SOON) assert second_notification \ and second_notification['status'] == NotificationStatus.DONE assert second_notification.pid != first_notification
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
def test_overdue_limit( client, app, librarian_martigny, lib_martigny, item_lib_martigny, item2_lib_martigny, patron_type_children_martigny, item3_lib_martigny, item_lib_martigny_data, item2_lib_martigny_data, item3_lib_martigny_data, loc_public_martigny, patron_martigny, circ_policy_short_martigny, lib_saxon, loc_public_saxon): """Test overdue limit.""" item = item_lib_martigny item_pid = item.pid patron_pid = patron_martigny.pid 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()) # STEP 0 :: Prepare data for test # * Update the patron_type to set a overdue_items_limit rule. # We define than only 1 overdue items are allowed. Trying a second # checkout is disallowed if patron has an overdue item patron_type = patron_type_children_martigny patron_type \ .setdefault('limits', {}) \ .setdefault('overdue_items_limits', {}) \ .setdefault('default_value', 1) patron_type.update(patron_type, dbcommit=True, reindex=True) patron_type = PatronType.get_record_by_pid(patron_type.pid) assert patron_type\ .get('limits', {})\ .get('overdue_items_limits', {}) \ .get('default_value') == 1 # STEP 1 :: Create an checkout with a end_date at the current date # * Create a checkout and set end_date to a fixed_date equals to # current tested date. The loan should not be considered as overdue # and a second checkout should be possible login_user_via_session(client, librarian_martigny.user) 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, end_date=eod.strftime(date_format) ) ) assert res.status_code == 200 loan_pid = data.get('action_applied')[LoanAction.CHECKOUT].get('pid') loan = Loan.get_record_by_pid(loan_pid) assert not loan.is_loan_overdue() tmp_item_data = deepcopy(item_lib_martigny_data) del tmp_item_data['pid'] tmp_item_data['library']['$ref'] = get_ref_for_pid('lib', lib_saxon.pid) tmp_item_data['location']['$ref'] = \ get_ref_for_pid('loc', loc_public_saxon.pid) tmp_item = Item.create(tmp_item_data, dbcommit=True, reindex=True) res, data = postdata( client, 'api_item.checkout', dict( item_pid=tmp_item.pid, patron_pid=patron_pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 200 res, _ = postdata( client, 'api_item.checkin', dict( item_pid=tmp_item.pid, pid=data.get('action_applied')[LoanAction.CHECKOUT].get('pid'), transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid, ) ) assert res.status_code == 200 # STEP 2 :: Set the loan as overdue and test a new checkout # Now there is one loan in overdue, then the limit is reached and a new # checkout shouldn't be possible end_date = eod - timedelta(days=7) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) overdue_loans = list(get_overdue_loans(patron_pid=patron_pid)) assert loan.is_loan_overdue() assert loan.end_date == end_date.isoformat() assert overdue_loans[0].get('pid') == loan_pid assert not get_notification(loan, NotificationType.OVERDUE) notification = loan.create_notification( _type=NotificationType.OVERDUE).pop() Dispatcher.dispatch_notifications([notification.get('pid')]) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert get_notification(loan, NotificationType.OVERDUE) # Try a second checkout - limit should be reached res, data = postdata( client, 'api_item.checkout', dict( item_pid=item2_lib_martigny.pid, patron_pid=patron_pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid, ) ) assert res.status_code == 403 assert 'Checkout denied' in data['message'] # Try a request - limit should be reached res, data = postdata( client, 'api_item.librarian_request', dict( item_pid=item2_lib_martigny.pid, patron_pid=patron_pid, pickup_location_pid=loc_public_martigny.pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 403 assert 'maximal number of overdue items is reached' in data['message'] # Try to extend - limit should be reached res, _ = postdata( client, 'api_item.extend_loan', dict( item_pid=item_pid, transaction_user_pid=librarian_martigny.pid, transaction_location_pid=loc_public_martigny.pid ) ) assert res.status_code == 403 assert 'maximal number of overdue items is reached' in data['message'] # reset the patron_type with default value del patron_type['limits'] # [2] test fee amount limit # Update the patron_type to set a fee_amount_limit rule patron_type \ .setdefault('limits', {}) \ .setdefault('fee_amount_limits', {}) \ .setdefault('default_value', 0.5) patron_type.update(patron_type, dbcommit=True, reindex=True) patron_type = PatronType.get_record_by_pid(patron_type.pid) assert patron_type.get('limits', {}).get('fee_amount_limits', {}) \ .get('default_value') == 0.5 # [2.1] test fee amount limit when we try to checkout a second item res, data = postdata( client, 'api_item.checkout', dict( item_pid=item2_lib_martigny.pid, patron_pid=patron_pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid, ) ) assert res.status_code == 403 assert 'maximal overdue fee amount is reached' in data['message'] # [2.2] test fee amount limit when we try to request another item res, data = postdata( client, 'api_item.librarian_request', dict( item_pid=item2_lib_martigny.pid, patron_pid=patron_pid, pickup_location_pid=loc_public_martigny.pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 403 assert 'maximal overdue fee amount is reached' in data['message'] # [2.3] test fee amount limit when we try to extend loan res, _ = postdata( client, 'api_item.extend_loan', dict( item_pid=item_pid, transaction_user_pid=librarian_martigny.pid, transaction_location_pid=loc_public_martigny.pid ) ) assert res.status_code == 403 assert 'maximal overdue fee amount is reached' in data['message'] # reset the patron_type with default value del patron_type['limits'] patron_type.update(patron_type, dbcommit=True, reindex=True) patron_type = PatronType.get_record_by_pid(patron_type.pid) assert patron_type.get('limits') is None # # 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