def test_cancel_pending_on_item_in_transit_to_house( client, item3_in_transit_martigny_patron_and_loan_to_house, loc_public_martigny, librarian_martigny_no_email, patron2_martigny_no_email): """Test cancel pending loan on an in_transit to_house item.""" item, patron, loan = item3_in_transit_martigny_patron_and_loan_to_house # the following tests the circulation action CANCEL_REQUEST_5_2 # an item in_transit to house with pending loans. when a # librarian wants to cancel the pending loan. action is permitted. # the loan will be cancelled. and in_transit loan remains in transit. 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 } item, requested_loan = item_record_to_a_specific_loan_state( item=item, loan_state=LoanState.PENDING, params=params, copy_item=False) assert requested_loan['state'] == LoanState.PENDING params = { 'pid': requested_loan.pid, 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny_no_email.pid } item.cancel_item_request(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) requested_loan = Loan.get_record_by_pid(requested_loan.pid) assert item.status == ItemStatus.IN_TRANSIT assert loan['state'] == LoanState.ITEM_IN_TRANSIT_TO_HOUSE assert requested_loan['state'] == LoanState.CANCELLED
def test_validate_on_item_at_desk( item_at_desk_martigny_patron_and_loan_at_desk, loc_public_martigny, librarian_martigny, circulation_policies, patron2_martigny): """Test validate a request on an item at_desk.""" # the following tests the circulation action VALIDATE_2 # on at_desk item, the validation is not possible item, patron, loan = item_at_desk_martigny_patron_and_loan_at_desk params = { 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid, 'pid': loan.pid } with pytest.raises(NoValidTransitionAvailableError): item, actions = item.validate_request(**params) assert item.status == ItemStatus.AT_DESK loan = Loan.get_record_by_pid(loan.pid) assert loan['state'] == LoanState.ITEM_AT_DESK # will not be able to validate any requestes for this item params = { '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 } item, requested_loan = item_record_to_a_specific_loan_state( item=item, loan_state=LoanState.PENDING, params=params, copy_item=False) params['pid'] = requested_loan.pid with pytest.raises(NoValidTransitionAvailableError): item, actions = item.validate_request(**params) requested_loan = Loan.get_record_by_pid(requested_loan.pid) assert requested_loan['state'] == LoanState.PENDING loan = Loan.get_record_by_pid(loan.pid) assert loan['state'] == LoanState.ITEM_AT_DESK
def test_cancel_request_on_item_at_desk_with_requests_externally( client, item3_at_desk_martigny_patron_and_loan_at_desk, loc_public_martigny, librarian_martigny_no_email, patron2_martigny_no_email, loc_public_fully): """Test cancel requests on an at_desk item with requests at externally.""" item, patron, loan = item3_at_desk_martigny_patron_and_loan_at_desk # the following tests the circulation action CANCEL_REQUEST_2_1_2_1 # an item at_desk with other pending loans. # pickup location of 1st pending loan != pickup location of current loan # cancel the current loan and item is: in_transit, automatic validation of # firt pending loan params = { 'patron_pid': patron2_martigny_no_email.pid, 'transaction_location_pid': loc_public_fully.pid, 'transaction_user_pid': librarian_martigny_no_email.pid, 'pickup_location_pid': loc_public_fully.pid } item, requested_loan = item_record_to_a_specific_loan_state( item=item, loan_state=LoanState.PENDING, params=params, copy_item=False) assert requested_loan['state'] == LoanState.PENDING params = { 'pid': loan.pid, 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny_no_email.pid } item.cancel_item_request(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) requested_loan = Loan.get_record_by_pid(requested_loan.pid) assert item.status == ItemStatus.IN_TRANSIT assert loan['state'] == LoanState.CANCELLED assert requested_loan['state'] == LoanState.ITEM_IN_TRANSIT_FOR_PICKUP
def wrapper(item, *args, **kwargs): """Executed before loan action.""" checkin_loan = None if function.__name__ == 'validate_request': # checks if the given loan pid can be validated item.prior_validate_actions(**kwargs) elif function.__name__ == 'checkin': # the smart checkin requires extra checks/actions before a checkin loan, kwargs = item.prior_checkin_actions(**kwargs) checkin_loan = loan # CHECKOUT: Case where no loan PID elif function.__name__ == 'checkout' and not kwargs.get('pid'): patron_pid = kwargs['patron_pid'] item_pid = item.pid request = get_request_by_item_pid_by_patron_pid( item_pid=item_pid, patron_pid=patron_pid) if request: kwargs['pid'] = request.pid elif function.__name__ == 'extend_loan': loan, kwargs = item.prior_extend_loan_actions(**kwargs) checkin_loan = loan loan, kwargs = item.complete_action_missing_params( item=item, checkin_loan=checkin_loan, **kwargs) Loan.check_required_params(loan, function.__name__, **kwargs) item, action_applied = function(item, loan, *args, **kwargs) item.change_status_commit_and_reindex_item(item) return item, action_applied
def test_cancel_pending_loan_on_item_in_transit_for_pickup_with_requests( client, item3_in_transit_martigny_patron_and_loan_for_pickup, loc_public_martigny, librarian_martigny, patron2_martigny): """Test cancel pending loan on an in_transit for pickup item.""" item, patron, loan = item3_in_transit_martigny_patron_and_loan_for_pickup # the following tests the circulation action CANCEL_REQUEST_4_2 # an item in_transit for pickup with other pending loans. when a # librarian wants to cancel the pending loan. action is permitted. # item remains in_transit params = { '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 } item, requested_loan = item_record_to_a_specific_loan_state( item=item, loan_state=LoanState.PENDING, params=params, copy_item=False) assert requested_loan['state'] == LoanState.PENDING params = { 'pid': requested_loan.pid, 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid } item.cancel_item_request(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) requested_loan = Loan.get_record_by_pid(requested_loan.pid) assert item.status == ItemStatus.IN_TRANSIT assert loan['state'] == LoanState.ITEM_IN_TRANSIT_FOR_PICKUP assert requested_loan['state'] == LoanState.CANCELLED
def test_cancel_request_on_item_in_transit_to_house_with_requests( client, item2_in_transit_martigny_patron_and_loan_to_house, loc_public_martigny, librarian_martigny, patron2_martigny): """Test cancel request on an in_transit to_house item with requests.""" item, patron, loan = item2_in_transit_martigny_patron_and_loan_to_house # the following tests the circulation action CANCEL_REQUEST_5_1_2 # an item in_transit to house with pending loans. when a # librarian wants to cancel the in_transit loan. action is permitted. # the loan will be cancelled. and first pending loan will be validated. params = { '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 } item, requested_loan = item_record_to_a_specific_loan_state( item=item, loan_state=LoanState.PENDING, params=params, copy_item=False) assert requested_loan['state'] == LoanState.PENDING params = { 'pid': loan.pid, 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid } item.cancel_item_request(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) requested_loan = Loan.get_record_by_pid(requested_loan.pid) assert item.status == ItemStatus.AT_DESK assert loan['state'] == LoanState.CANCELLED assert requested_loan['state'] == LoanState.ITEM_AT_DESK
def test_checkin_on_item_on_shelf_with_requests( item_on_shelf_martigny_patron_and_loan_pending, loc_public_martigny, librarian_martigny, patron2_martigny, loc_public_fully, lib_martigny): """Test checkin on an on_shelf item with requests.""" item, patron, loan = item_on_shelf_martigny_patron_and_loan_pending # the following tests the circulation action CHECKIN_1_2_1 # for an item on_shelf with pending loans, the pickup library of the first # pending loan equal to the transaction library, the first pending loan # is validated and item assigned the at_desk # validate_request circulation action will be performed. # create a second pending loan on same item params = { 'patron_pid': patron2_martigny.pid, 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid, 'pickup_location_pid': loc_public_fully.pid } item, requested_loan = item_record_to_a_specific_loan_state( item=item, loan_state=LoanState.PENDING, params=params, copy_item=False) assert requested_loan['state'] == LoanState.PENDING params = { 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid } item, actions = item.checkin(**params) assert item.status == ItemStatus.AT_DESK assert Loan.get_record_by_pid(loan.pid)['state'] == LoanState.ITEM_AT_DESK assert Loan.get_record_by_pid(requested_loan.pid)['state'] == \ LoanState.PENDING
def test_cancel_pending_request_on_item_at_desk( client, item5_at_desk_martigny_patron_and_loan_at_desk, loc_public_martigny, librarian_martigny_no_email, patron2_martigny_no_email): """Test cancel requests on an at_desk item with requests at home.""" item, patron, loan = item5_at_desk_martigny_patron_and_loan_at_desk # the following tests the circulation action CANCEL_REQUEST_2_2 # an item at_desk with other pending loans. when a librarian wants to # cancel one of the pending loans. the item remains at_desk 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 } item, requested_loan = item_record_to_a_specific_loan_state( item=item, loan_state=LoanState.PENDING, params=params, copy_item=False) assert requested_loan['state'] == LoanState.PENDING params = { 'pid': requested_loan.pid, 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny_no_email.pid } item.cancel_item_request(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) requested_loan = Loan.get_record_by_pid(requested_loan.pid) assert item.status == ItemStatus.AT_DESK assert requested_loan['state'] == LoanState.CANCELLED assert loan['state'] == LoanState.ITEM_AT_DESK
def test_items_extend_end_date(client, librarian_martigny, patron_martigny, loc_public_martigny, item_type_standard_martigny, item_lib_martigny, json_header, circ_policy_short_martigny): """Test correct renewal due date for items.""" login_user_via_session(client, librarian_martigny.user) item = item_lib_martigny item_pid = item.pid patron_pid = patron_martigny.pid # checkout res, data = postdata( client, 'api_item.checkout', dict(item_pid=item_pid, patron_pid=patron_pid, transaction_user_pid=librarian_martigny.pid, transaction_location_pid=loc_public_martigny.pid)) assert res.status_code == 200 actions = data.get('action_applied') loan_pid = actions[LoanAction.CHECKOUT].get('pid') loan = Loan.get_record_by_pid(loan_pid) assert not item.get_extension_count() max_count = get_extension_params(loan=loan, parameter_name='max_count') renewal_duration_policy = circ_policy_short_martigny['renewal_duration'] renewal_duration = get_extension_params(loan=loan, parameter_name='duration_default') assert renewal_duration_policy <= renewal_duration.days # extend loan res, data = postdata( client, 'api_item.extend_loan', dict(item_pid=item_pid, pid=loan_pid, transaction_user_pid=librarian_martigny.pid, transaction_location_pid=loc_public_martigny.pid)) assert res.status_code == 200 # Compare expected loan date with processed one # first get loan UTC date actions = data.get('action_applied') loan_pid = actions[LoanAction.EXTEND].get('pid') loan = Loan.get_record_by_pid(loan_pid) loan_date = loan.get('end_date') # then process a date with current UTC date + renewal current_date = datetime.now(timezone.utc) calc_date = current_date + renewal_duration # finally the comparison should give the same date (in UTC)! assert (calc_date.strftime('%Y-%m-%d') == ciso8601.parse_datetime( loan_date).astimezone(timezone.utc).strftime('%Y-%m-%d')) # checkin res, _ = postdata( client, 'api_item.checkin', dict(item_pid=item_pid, pid=loan_pid, transaction_user_pid=librarian_martigny.pid, transaction_location_pid=loc_public_martigny.pid)) assert res.status_code == 200
def test_checkin_on_item_on_loan_with_requests( item3_on_loan_martigny_patron_and_loan_on_loan, loc_public_martigny, librarian_martigny_no_email, patron2_martigny_no_email): """Test checkin on an on_loan item with requests at local library.""" item, patron, loan = item3_on_loan_martigny_patron_and_loan_on_loan # the following tests the circulation action CHECKIN_3_2_1 # for an item on_loan, with pending requests. when the pickup library of # the first pending request equal to the transaction library, # checkin the item and item becomes at_desk. # the on_loan is returned and validating the first pending loan request. 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 } item, requested_loan = item_record_to_a_specific_loan_state( item=item, loan_state=LoanState.PENDING, params=params, copy_item=False) item, actions = item.checkin(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) requested_loan = Loan.get_record_by_pid(requested_loan.pid) assert item.status == ItemStatus.AT_DESK assert loan['state'] == LoanState.ITEM_RETURNED assert requested_loan['state'] == LoanState.ITEM_AT_DESK
def item_record_to_a_specific_loan_state(item=None, loan_state=None, params=None, copy_item=True): """Put an item into a specific circulation loan state. :param item: the item record :param loan_state: the desired loan state and attached to the given item :param params: the required parameters to perform the circ transactions :param copy_item: an option to perform transaction on a copy of the item :return: the item and its loan """ if copy_item: item = create_new_item_from_existing_item(item=item) # complete missing parameters params.setdefault('transaction_date', datetime.now(timezone.utc).isoformat()) params.setdefault('document_pid', item.document_pid) # a parameter to allow in_transit returns checkin_transaction_location_pid = \ params.pop('checkin_transaction_location_pid', None) patron = Patron.get_record_by_pid(params.get('patron_pid')) # perform circulation actions if loan_state in [ LoanState.PENDING, LoanState.ITEM_AT_DESK, LoanState.ITEM_ON_LOAN, LoanState.ITEM_IN_TRANSIT_FOR_PICKUP, LoanState.ITEM_IN_TRANSIT_TO_HOUSE ]: item, actions = item.request(**params) loan = Loan.get_record_by_pid(actions[LoanAction.REQUEST].get('pid')) assert item.number_of_requests() >= 1 assert item.is_requested_by_patron( patron.get('patron', {}).get('barcode')) if loan_state in [ LoanState.ITEM_AT_DESK, LoanState.ITEM_IN_TRANSIT_FOR_PICKUP, LoanState.ITEM_IN_TRANSIT_TO_HOUSE ]: item, actions = item.validate_request(**params, pid=loan.pid) loan = Loan.get_record_by_pid(actions[LoanAction.VALIDATE].get('pid')) if loan_state in [ LoanState.ITEM_ON_LOAN, LoanState.ITEM_IN_TRANSIT_TO_HOUSE ]: item, actions = item.checkout(**params, pid=loan.pid) loan = Loan.get_record_by_pid(actions[LoanAction.CHECKOUT].get('pid')) if loan_state == LoanState.ITEM_IN_TRANSIT_TO_HOUSE: if checkin_transaction_location_pid: params['transaction_location_pid'] = \ checkin_transaction_location_pid item, actions = item.checkin(**params, pid=loan.pid) loan = Loan.get_record_by_pid(actions[LoanAction.CHECKIN].get('pid')) flush_index(ItemsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert loan['state'] == loan_state return item, loan
def test_checkin_on_item_on_loan_with_requests_externally( item4_on_loan_martigny_patron_and_loan_on_loan, item5_on_loan_martigny_patron_and_loan_on_loan, loc_public_martigny, librarian_martigny, patron2_martigny, loc_public_fully, loc_public_saxon): """Test checkin on an on_loan item with requests at an external library.""" item, patron, loan = item4_on_loan_martigny_patron_and_loan_on_loan # the following tests the circulation action CHECKIN_3_2_2_1 # for an item on_loan, with pending requests. when the pickup library of # the first pending request does not equal to the transaction library, # checkin the item and the loan on_loan is cancelled. # if the pickup location of the first pending equal the item library, # the pending loan becomes ITEM_IN_TRANSIT_FOR_PICKUP params = { 'patron_pid': patron2_martigny.pid, 'transaction_location_pid': loc_public_fully.pid, 'transaction_user_pid': librarian_martigny.pid, 'pickup_location_pid': loc_public_martigny.pid } item, requested_loan = item_record_to_a_specific_loan_state( item=item, loan_state=LoanState.PENDING, params=params, copy_item=False) item, actions = item.checkin(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) requested_loan = Loan.get_record_by_pid(requested_loan.pid) assert item.status == ItemStatus.IN_TRANSIT assert loan['state'] == LoanState.CANCELLED assert requested_loan['state'] == LoanState.ITEM_IN_TRANSIT_FOR_PICKUP item, patron, loan = item5_on_loan_martigny_patron_and_loan_on_loan # the following tests the circulation action CHECKIN_3_2_2_2 # for an item on_loan, with pending requests. when the pickup library of # the first pending request does not equal to the transaction library, # checkin the item and the loan on_loan is cancelled. # if the pickup location of the first pending does not equal the item # library, the pending loan becomes ITEM_IN_TRANSIT_FOR_PICKUP params = { 'patron_pid': patron2_martigny.pid, 'transaction_location_pid': loc_public_saxon.pid, 'transaction_user_pid': librarian_martigny.pid, 'pickup_location_pid': loc_public_fully.pid } item, requested_loan = item_record_to_a_specific_loan_state( item=item, loan_state=LoanState.PENDING, params=params, copy_item=False) item, actions = item.checkin(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) requested_loan = Loan.get_record_by_pid(requested_loan.pid) assert item.status == ItemStatus.IN_TRANSIT assert loan['state'] == LoanState.CANCELLED assert requested_loan['state'] == LoanState.ITEM_IN_TRANSIT_FOR_PICKUP
def test_loan_es_mapping(es_clear, db, loan_data_tmp, item_on_loan, location, library): """.""" search = current_circulation.loan_search mapping = get_mapping(search.Meta.index) assert mapping Loan.create(loan_data_tmp, dbcommit=True, reindex=True, delete_pid=True) assert mapping == get_mapping(search.Meta.index)
def test_item_information(client, librarian_martigny, selfcheck_patron_martigny, loc_public_martigny, item_lib_martigny, circulation_policies): """Test item information.""" login_user_via_session(client, librarian_martigny.user) # checkout res, data = postdata( client, 'api_item.checkout', dict(item_pid=item_lib_martigny.pid, patron_pid=selfcheck_patron_martigny.pid, transaction_user_pid=librarian_martigny.pid, transaction_location_pid=loc_public_martigny.pid)) assert res.status_code == 200 actions = data.get('action_applied') loan_pid = actions[LoanAction.CHECKOUT].get('pid') loan = Loan.get_record_by_pid(loan_pid) assert not loan.is_loan_overdue() # set loan on overdue end_date = datetime.now(timezone.utc) - timedelta(days=7) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) loan = Loan.get_record_by_pid(loan_pid) assert loan['state'] == LoanState.ITEM_ON_LOAN assert loan.is_loan_overdue() loan.create_notification( notification_type=Notification.OVERDUE_NOTIFICATION_TYPE) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert number_of_reminders_sent(loan) == 1 patron_barcode = selfcheck_patron_martigny\ .get('patron', {}).get('barcode')[0] item_pid = item_lib_martigny.pid # get item information response = item_information(patron_barcode=patron_barcode, item_pid=item_pid) assert response # check required fields in response assert all(key in response for key in ( 'item_id', 'title_id', 'circulation_status', 'fee_type', 'security_marker', )) assert response['due_date'] assert response['fee_amount'] # checkin res, _ = postdata( client, 'api_item.checkin', dict(item_pid=item_lib_martigny.pid, pid=loan_pid, transaction_user_pid=librarian_martigny.pid, transaction_location_pid=loc_public_martigny.pid)) assert res.status_code == 200
def test_checkout_item_transit(client, item2_lib_martigny, librarian_martigny_no_email, librarian_saxon_no_email, patron_martigny_no_email, loc_public_saxon, circulation_policies): """Test checkout of an item in transit.""" assert item2_lib_martigny.available # request login_user_via_session(client, librarian_martigny_no_email.user) res, data = postdata( client, 'api_item.librarian_request', dict(item_pid=item2_lib_martigny.pid, pickup_location_pid=loc_public_saxon.pid, patron_pid=patron_martigny_no_email.pid)) assert res.status_code == 200 actions = data.get('action_applied') loan_pid = actions[LoanAction.REQUEST].get('pid') assert not item2_lib_martigny.available loan = Loan.get_record_by_pid(loan_pid) assert loan.get('state') == 'PENDING' # validate request res, _ = postdata(client, 'api_item.validate_request', dict(item_pid=item2_lib_martigny.pid, pid=loan_pid)) assert res.status_code == 200 assert not item2_lib_martigny.available item = Item.get_record_by_pid(item2_lib_martigny.pid) assert not item.available loan = Loan.get_record_by_pid(loan_pid) assert loan.get('state') == 'ITEM_IN_TRANSIT_FOR_PICKUP' login_user_via_session(client, librarian_saxon_no_email.user) # receive res, _ = postdata(client, 'api_item.receive', dict(item_pid=item2_lib_martigny.pid, pid=loan_pid)) assert res.status_code == 200 assert not item2_lib_martigny.available item = Item.get_record_by_pid(item2_lib_martigny.pid) assert not item.available loan_before_checkout = get_loan_for_item(item.pid) assert loan_before_checkout.get('state') == 'ITEM_AT_DESK' # checkout res, _ = postdata( client, 'api_item.checkout', dict(item_pid=item2_lib_martigny.pid, patron_pid=patron_martigny_no_email.pid)) assert res.status_code == 200 item = Item.get_record_by_pid(item2_lib_martigny.pid) loan_after_checkout = get_loan_for_item(item.pid) assert loan_after_checkout.get('state') == 'ITEM_ON_LOAN' assert loan_before_checkout.get('pid') == loan_after_checkout.get('pid')
def test_multiple_loans_on_item_error( client, patron_martigny_no_email, patron2_martigny_no_email, loc_public_martigny, item_type_standard_martigny, item_lib_martigny, json_header, circulation_policies, loc_public_fully, librarian_martigny_no_email): """Test MultipleLoansOnItemError.""" login_user_via_session(client, librarian_martigny_no_email.user) item = item_lib_martigny checked_patron = patron2_martigny_no_email.pid requested_patron = patron_martigny_no_email.pid location = loc_public_martigny # checkout to checked_patron res, data = postdata( client, 'api_item.checkout', dict(item_pid=item.pid, patron_pid=checked_patron, transaction_location_pid=location.pid)) assert res.status_code == 200 assert Item.get_record_by_pid(item.pid).get('status') == ItemStatus.ON_LOAN item_data = data.get('metadata') actions = data.get('action_applied') assert item_data.get('status') == ItemStatus.ON_LOAN assert actions.get(LoanAction.CHECKOUT) loan_pid = actions[LoanAction.CHECKOUT].get('pid') item = Item.get_record_by_pid(item.pid) # request by requested patron to pick at another location res, data = postdata( client, 'api_item.librarian_request', dict(item_pid=item.pid, pickup_location_pid=loc_public_fully.pid, patron_pid=requested_patron)) 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.REQUEST) req_loan_pid = actions[LoanAction.REQUEST].get('pid') item = Item.get_record_by_pid(item.pid) # checkin at the request location res, data = postdata( client, 'api_item.checkin', dict(item_pid=item.pid, pid=loan_pid, transaction_location_pid=loc_public_fully.pid)) assert res.status_code == 200 assert Loan.get_record_by_pid(loan_pid).get('state') == 'CANCELLED' assert Loan.get_record_by_pid(req_loan_pid).get( 'state') == 'ITEM_IN_TRANSIT_FOR_PICKUP' assert Item.get_record_by_pid(item.pid).get('status') == 'in_transit' # cancel request res, _ = postdata(client, 'api_item.cancel_loan', dict(item_pid=item.pid, pid=req_loan_pid)) assert res.status_code == 200
def test_items_extend_end_date(client, librarian_martigny_no_email, patron_martigny_no_email, loc_public_martigny, item_type_standard_martigny, item_lib_martigny, json_header, circ_policy_short_martigny): """Test correct renewal due date for items.""" 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 # checkout res = client.post( url_for('api_item.checkout'), data=json.dumps(dict(item_pid=item_pid, patron_pid=patron_pid)), content_type='application/json', ) assert res.status_code == 200 data = get_json(res) actions = data.get('action_applied') loan_pid = actions[LoanAction.CHECKOUT].get('pid') loan = Loan.get_record_by_pid(loan_pid) assert not item.get_extension_count() max_count = get_extension_params(loan=loan, parameter_name='max_count') renewal_duration_policy = circ_policy_short_martigny['renewal_duration'] renewal_duration = get_extension_params(loan=loan, parameter_name='duration_default') assert renewal_duration_policy <= renewal_duration.days # extend loan res = client.post( url_for('api_item.extend_loan'), data=json.dumps(dict(item_pid=item_pid, pid=loan_pid)), content_type='application/json', ) assert res.status_code == 200 data = get_json(res) actions = data.get('action_applied') loan_pid = actions[LoanAction.EXTEND].get('pid') loan = Loan.get_record_by_pid(loan_pid) end_date = loan.get('end_date') current_date = datetime.now() calc_date = current_date + renewal_duration assert (calc_date.strftime('%Y-%m-%d') == ciso8601.parse_datetime_as_naive( end_date).strftime('%Y-%m-%d')) # checkin res = client.post( url_for('api_item.checkin'), data=json.dumps(dict(item_pid=item_pid, pid=loan_pid)), content_type='application/json', ) assert res.status_code == 200
def test_checkin_on_item_on_loan_with_requests( item3_on_loan_martigny_patron_and_loan_on_loan, loc_public_martigny, librarian_martigny, patron2_martigny): """Test checkin on an on_loan item with requests at local library.""" # the following tests the circulation action CHECKIN_3_2_1 # for an item on_loan, with pending requests. when the pickup library of # the first pending request equal to the transaction library, # checkin the item and item becomes at_desk. # the on_loan is returned and validating the first pending loan request. # # In this test, we will also ensure that the request expiration date of the # automatic validated request is correct item, patron, loan = item3_on_loan_martigny_patron_and_loan_on_loan # create a request on the same item one day after the first loan tomorrow = ciso8601.parse_datetime(loan['start_date']) + timedelta(days=10) with freeze_time(tomorrow.isoformat()): item, actions = item.request( pickup_location_pid=loc_public_martigny.pid, patron_pid=patron2_martigny.pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid ) requested_loan_pid = actions[LoanAction.REQUEST].get('pid') requested_loan = Loan.get_record_by_pid(requested_loan_pid) # Check-in the item # * reload item, loan and requested_loan # * ensure the item is still AT_DESK (because the first pending request # has been automatically validate and pickup location is the same than # previous loan location) # * ensure first loan is concluded # * ensure the requested loan is now "AT_DESK" with a valid request # expiration date next_day = tomorrow + timedelta(days=10) with freeze_time(next_day.isoformat()): item, actions = item.checkin( 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 ) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) requested_loan = Loan.get_record_by_pid(requested_loan.pid) assert item.status == ItemStatus.AT_DESK assert loan['state'] == LoanState.ITEM_RETURNED assert requested_loan['state'] == LoanState.ITEM_AT_DESK trans_date = ciso8601.parse_datetime(requested_loan['transaction_date']) assert trans_date.strftime('%Y%m%d') == next_day.strftime('%Y%m%d')
def test_loans_create(db, loan_data_tmp): """Test loananisation creation.""" loan = Loan.create(loan_data_tmp, delete_pid=True) assert loan == loan_data_tmp assert loan.get('loan_pid') == '1' assert loan.get('state') == 'ITEM_ON_LOAN' loan = Loan.get_record_by_pid('1') assert loan == loan_data_tmp fetched_pid = loan_pid_fetcher(loan.id, loan) assert fetched_pid.pid_value == '1'
def test_loan_keep_and_to_anonymize( item_on_loan_martigny_patron_and_loan_on_loan, item2_on_loan_martigny_patron_and_loan_on_loan, librarian_martigny_no_email, loc_public_martigny): """Test anonymize and keep loan based on open transactions.""" item, patron, loan = item_on_loan_martigny_patron_and_loan_on_loan assert not loan.concluded(loan) assert not loan.can_anonymize(loan_data=loan) params = { 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny_no_email.pid } item, actions = item.checkin(**params) loan = Loan.get_record_by_pid(loan.pid) # item checkedin and has no open events assert loan.concluded(loan) assert not loan.can_anonymize(loan_data=loan) patron['patron']['keep_history'] = False patron.update(patron, dbcommit=True, reindex=True) # when the patron asks to anonymise history the can_anonymize is true loan = Loan.get_record_by_pid(loan.pid) assert loan.concluded(loan) assert loan.can_anonymize(loan_data=loan) loan = loan.update(loan, dbcommit=True, reindex=True) # test loans with fees item, patron, loan = item2_on_loan_martigny_patron_and_loan_on_loan assert not loan.concluded(loan) assert not loan.can_anonymize(loan_data=loan) end_date = datetime.now(timezone.utc) - timedelta(days=7) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) create_over_and_due_soon_notifications() flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) overdue_loans = list(get_overdue_loans()) params = { 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny_no_email.pid } item, actions = item.checkin(**params) loan = Loan.get_record_by_pid(loan.pid) assert not loan.concluded(loan) assert not loan.can_anonymize(loan_data=loan)
def test_patron_information(client, librarian_martigny, selfcheck_patron_martigny, loc_public_martigny, item_lib_martigny, item2_lib_martigny, circulation_policies, lib_martigny): """Test patron information.""" login_user_via_session(client, librarian_martigny.user) # checkout res, data = postdata( client, 'api_item.checkout', dict(item_pid=item_lib_martigny.pid, patron_pid=selfcheck_patron_martigny.pid, transaction_user_pid=librarian_martigny.pid, transaction_location_pid=loc_public_martigny.pid)) assert res.status_code == 200 actions = data.get('action_applied') loan_pid = actions[LoanAction.CHECKOUT].get('pid') loan = Loan.get_record_by_pid(loan_pid) assert not loan.is_loan_overdue() # set loan on overdue end_date = datetime.now(timezone.utc) - timedelta(days=7) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) loan = Loan.get_record_by_pid(loan_pid) assert loan.is_loan_overdue() loan.create_notification( notification_type=Notification.OVERDUE_NOTIFICATION_TYPE) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) assert number_of_reminders_sent(loan) == 1 # create request res, data = postdata( client, 'api_item.librarian_request', dict(item_pid=item2_lib_martigny.pid, patron_pid=selfcheck_patron_martigny.pid, pickup_location_pid=loc_public_martigny.pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny.pid)) assert res.status_code == 200 # get patron information response = patron_information( selfcheck_patron_martigny.get('patron', {}).get('barcode')[0]) assert response # checkin res, _ = postdata( client, 'api_item.checkin', dict(item_pid=item_lib_martigny.pid, pid=loan_pid, transaction_user_pid=librarian_martigny.pid, transaction_location_pid=loc_public_martigny.pid)) assert res.status_code == 200
def test_loan_keep_and_to_anonymize( item_on_loan_martigny_patron_and_loan_on_loan, item2_on_loan_martigny_patron_and_loan_on_loan, librarian_martigny, loc_public_martigny): """Test anonymize and keep loan based on open transactions.""" item, patron, loan = item_on_loan_martigny_patron_and_loan_on_loan assert not loan.concluded(loan) assert not loan.can_anonymize(loan_data=loan) params = { 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid } item.checkin(**params) loan = Loan.get_record_by_pid(loan.pid) # item checkedin and has no open events assert loan.concluded(loan) assert not loan.can_anonymize(loan_data=loan) patron.user.profile.keep_history = False # when the patron asks to anonymise history the can_anonymize is true loan = Loan.get_record_by_pid(loan.pid) assert loan.concluded(loan) assert loan.can_anonymize(loan_data=loan) loan.update(loan, dbcommit=True, reindex=True) # test loans with fees item, patron, loan = item2_on_loan_martigny_patron_and_loan_on_loan assert not loan.concluded(loan) assert not loan.can_anonymize(loan_data=loan) end_date = datetime.now(timezone.utc) - timedelta(days=7) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) create_notifications(types=[ Notification.DUE_SOON_NOTIFICATION_TYPE, Notification.OVERDUE_NOTIFICATION_TYPE ]) flush_index(NotificationsSearch.Meta.index) flush_index(LoansSearch.Meta.index) params = { 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid } item.checkin(**params) loan = Loan.get_record_by_pid(loan.pid) assert not loan.concluded(loan) assert not loan.can_anonymize(loan_data=loan)
def test_cancel_item_request_on_item_on_loan( client, item_on_loan_martigny_patron_and_loan_on_loan, loc_public_martigny, librarian_martigny_no_email, patron2_martigny_no_email): """Test cancel requests on an on_loan item.""" item, patron, loan = item_on_loan_martigny_patron_and_loan_on_loan # the following tests the circulation action CANCEL_REQUEST_3_1 # an item on_loan with no other pending loans. when a librarian wants to # cancel the on_loan loan. action is not permitted and item remain on_loan. params = { 'pid': loan.pid, 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny_no_email.pid } with pytest.raises(NoCirculationAction): item.cancel_item_request(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) assert item.status == ItemStatus.ON_LOAN assert loan['state'] == LoanState.ITEM_ON_LOAN # the following tests the circulation action CANCEL_REQUEST_3_2 # an item on_loan with other pending loans. when a librarian wants to # cancel the pending loan. action is permitted and item remains on_loan. 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 } item, requested_loan = item_record_to_a_specific_loan_state( item=item, loan_state=LoanState.PENDING, params=params, copy_item=False) assert requested_loan['state'] == LoanState.PENDING params = { 'pid': requested_loan.pid, 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny_no_email.pid } item.cancel_item_request(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) requested_loan = Loan.get_record_by_pid(requested_loan.pid) assert item.status == ItemStatus.ON_LOAN assert loan['state'] == LoanState.ITEM_ON_LOAN assert requested_loan['state'] == LoanState.CANCELLED
def test_checkin_on_item_on_loan( item_on_loan_martigny_patron_and_loan_on_loan, item2_on_loan_martigny_patron_and_loan_on_loan, item_on_loan_fully_patron_and_loan_on_loan, loc_public_fully, loc_public_martigny, librarian_martigny): """Test checkin on an on_loan item.""" item, patron, loan = item_on_loan_martigny_patron_and_loan_on_loan # the following tests the circulation action CHECKIN_3_1_1 # for an item on_loan, the item library equal the transaction library, # checkin the item and item becomes on_shelf # case when the loan pid is given as a parameter params = { 'transaction_location_pid': loc_public_martigny.pid, 'transaction_user_pid': librarian_martigny.pid, 'pid': loan.pid } item, actions = item.checkin(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) assert item.status == ItemStatus.ON_SHELF assert loan['state'] == LoanState.ITEM_RETURNED # case when the loan pid is not given as a parameter item, patron, loan = item_on_loan_fully_patron_and_loan_on_loan params = { 'transaction_location_pid': loc_public_fully.pid, 'transaction_user_pid': librarian_martigny.pid } item, actions = item.checkin(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) assert item.status == ItemStatus.ON_SHELF assert loan['state'] == LoanState.ITEM_RETURNED # the following tests the circulation action CHECKIN_3_1_2 # for an item on_loan, the item library does not equal the transaction # library, checkin the item and item becomes in_transit item, patron, loan = item2_on_loan_martigny_patron_and_loan_on_loan params = { 'transaction_location_pid': loc_public_fully.pid, 'transaction_user_pid': librarian_martigny.pid, 'pid': loan.pid } item, actions = item.checkin(**params) item = Item.get_record_by_pid(item.pid) loan = Loan.get_record_by_pid(loan.pid) assert item.status == ItemStatus.IN_TRANSIT assert loan['state'] == LoanState.ITEM_IN_TRANSIT_TO_HOUSE
def test_checkout_on_item_in_transit_to_house_for_another_patron( item2_in_transit_martigny_patron_and_loan_to_house, patron2_martigny, librarian_martigny, loc_public_martigny, loc_public_saxon): """Test CHECKOUT on an IN_TRANSIT (to house) item.""" # Create a new item in IN_TRANSIT_TO_HOUSE intransit_item, patron, loan = \ item2_in_transit_martigny_patron_and_loan_to_house assert intransit_item.number_of_requests() == 0 # the following tests the circulation action CHECKOUT_5_1 # an IN_TRANSIT (to house) item # WITHOUT pending loan # CAN be CHECKOUT params = { 'patron_pid': patron2_martigny.pid, 'transaction_location_pid': loc_public_saxon.pid, 'transaction_user_pid': librarian_martigny.pid, 'pickup_location_pid': loc_public_martigny.pid, } # Checkout it! asked_item, actions = intransit_item.checkout(**params, pid=loan.pid) checkout_loan = Loan.get_record_by_pid( actions[LoanAction.CHECKOUT].get('pid')) # Check loan is ITEM_ON_LOAN and item is ON_LOAN assert intransit_item.status == ItemStatus.ON_LOAN assert checkout_loan['state'] == LoanState.ITEM_ON_LOAN assert intransit_item.number_of_requests() == 0
def test_recall_notification(client, patron_martigny_no_email, json_header, patron2_martigny_no_email, item_lib_martigny, librarian_martigny_no_email, circulation_policies, loc_public_martigny): """Test recall notification.""" login_user_via_session(client, librarian_martigny_no_email.user) res, data = postdata( client, 'api_item.checkout', dict(item_pid=item_lib_martigny.pid, patron_pid=patron_martigny_no_email.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 loan.is_notified(notification_type='recall') # test notification permissions res, _ = postdata( client, 'api_item.librarian_request', dict(item_pid=item_lib_martigny.pid, pickup_location_pid=loc_public_martigny.pid, patron_pid=patron2_martigny_no_email.pid)) assert res.status_code == 200 flush_index(NotificationsSearch.Meta.index) assert loan.is_notified(notification_type='recall') notification = get_recall_notification(loan) assert notification.loan_pid == loan.pid assert not loan.is_notified(notification_type='availability') assert not get_availability_notification(loan)
def upgrade(): """Update loans records.""" query = current_circulation.loan_search_cls() \ .filter('term', state=LoanState.ITEM_AT_DESK) \ .filter('bool', must_not=[Q('exists', field='request_expire_date')]) \ .source('pid') loan_pids = [hit.pid for hit in query.scan()] ids = [] for pid in loan_pids: loan = Loan.get_record_by_pid(pid) trans_date = ciso8601.parse_datetime(loan.transaction_date) expire_date = trans_date + timedelta(days=10) expire_date = expire_date.replace(hour=23, minute=59, second=00, microsecond=000, tzinfo=None) expire_date = pytz.timezone('Europe/Zurich').localize(expire_date) loan['request_expire_date'] = expire_date.isoformat() loan['request_start_date'] = datetime.now().isoformat() loan.update(loan, dbcommit=True, reindex=False) LOGGER.info(f' * Updated loan#{loan.pid}') ids.append(loan.id) if len(ids): LOGGER.info(f'Indexing {len(ids)} records ....') indexer = LoansIndexer() indexer.bulk_index(ids) count = indexer.process_bulk_queue() LOGGER.info(f'{count} records indexed.') LOGGER.info(f'TOTAL :: {len(ids)}')
def loan_overdue_saxon( app, document, item2_lib_saxon, loc_public_martigny, item_type_standard_martigny, librarian_martigny_no_email, patron_martigny_no_email, circulation_policies): """Checkout an item to a patron. item2_lib_saxon is overdue. """ transaction_date = datetime.now(timezone.utc).isoformat() item2_lib_saxon.checkout( patron_pid=patron_martigny_no_email.pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny_no_email.pid, transaction_date=transaction_date, document_pid=item2_lib_saxon.replace_refs()['document']['pid'] ) flush_index(ItemsSearch.Meta.index) flush_index(LoansSearch.Meta.index) loan = Loan.get_record_by_pid( item2_lib_saxon.get_loan_pid_with_item_on_loan( item2_lib_saxon.pid)) end_date = datetime.now(timezone.utc) - timedelta(days=25) loan['end_date'] = end_date.isoformat() loan = loan.update(loan, dbcommit=True, reindex=True) return loan
def read(cls, user, record): """Read permission check. :param user: Logged user. :param record: Record to check. :return: True is action can be done. """ if current_patron \ and current_organisation.pid == Loan(record).organisation_pid: # staff member (lib, sys_lib) can always read loans if current_patron.is_librarian: return True # patron can only read their own loans if current_patron.is_patron: return Loan(record).patron_pid == current_patron.pid return False
def test_blocked_patron_cannot_request(client, librarian_martigny_no_email, item_lib_martigny, lib_martigny, patron_martigny_no_email, patron3_martigny_blocked_no_email, circulation_policies): login_user_via_session(client, librarian_martigny_no_email.user) res = client.get( url_for('api_item.can_request', item_pid=item_lib_martigny.pid, library_pid=lib_martigny.pid, patron_barcode=patron3_martigny_blocked_no_email.get( 'patron', {}).get('barcode'))) assert res.status_code == 200 data = get_json(res) assert not data['can'] # Check with valid patron res = client.get( url_for('api_item.can_request', item_pid=item_lib_martigny.pid, library_pid=lib_martigny.pid, patron_barcode=patron_martigny_no_email.get( 'patron', {}).get('barcode'))) assert res.status_code == 200 data = get_json(res) assert data['can'] # Create "virtual" Loan (not registered) loan = Loan({ 'item_pid': item_pid_to_object(item_lib_martigny.pid), 'library_pid': lib_martigny.pid, 'patron_pid': patron3_martigny_blocked_no_email.pid }) assert not can_be_requested(loan)