Esempio n. 1
0
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)
Esempio n. 3
0
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'
Esempio n. 4
0
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')
Esempio n. 5
0
    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)
Esempio n. 6
0
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
Esempio n. 7
0
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
Esempio n. 8
0
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
Esempio n. 9
0
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
Esempio n. 10
0
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
Esempio n. 11
0
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
Esempio n. 12
0
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
    })
Esempio n. 13
0
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)
Esempio n. 14
0
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)
Esempio n. 15
0
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)
Esempio n. 16
0
 def library(self):
     """Shortcut for item library of the notification."""
     return Library.get_record_by_pid(self.library_pid)
Esempio n. 17
0
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])
Esempio n. 18
0
 def transaction_library(self):
     """Shortcut to get notification transaction library."""
     return Library.get_record_by_pid(self.transaction_library_pid)
Esempio n. 19
0
    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)
Esempio n. 20
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