Esempio n. 1
0
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()
Esempio n. 2
0
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()
Esempio n. 3
0
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)
Esempio n. 4
0
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
Esempio n. 5
0
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)
Esempio n. 6
0
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)
Esempio n. 7
0
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)
Esempio n. 8
0
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
Esempio n. 9
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
Esempio n. 10
0
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
Esempio n. 11
0
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