def test_patron_types_subscription(patron_type_youngsters_sion, patron_type_adults_martigny, patron_type_grown_sion, patron_sion): """Test subscription behavior for patron_types.""" assert patron_type_youngsters_sion.is_subscription_required # A patron_type with a subscription amount equal to 0 doesn't # require a subscription patron_type_youngsters_sion['subscription_amount'] = 0 assert not patron_type_youngsters_sion.is_subscription_required del (patron_type_youngsters_sion['subscription_amount']) assert not patron_type_youngsters_sion.is_subscription_required # Test the 'get_yearly_subscription_patron_types' function. assert len(list(PatronType.get_yearly_subscription_patron_types())) == 2 # Test 'get_linked_patrons' functions. assert len(list(patron_type_grown_sion.get_linked_patron())) == 1 assert len(list(patron_type_youngsters_sion.get_linked_patron())) == 0 patron_sion['patron']['type']['$ref'] = get_ref_for_pid( 'ptty', patron_type_youngsters_sion.pid) patron_sion.update(patron_sion, dbcommit=True) patron_sion.reindex() assert len(list(patron_type_grown_sion.get_linked_patron())) == 0 assert len(list(patron_type_youngsters_sion.get_linked_patron())) == 1 patron_sion['patron']['type']['$ref'] = get_ref_for_pid( 'ptty', patron_type_grown_sion.pid) patron_sion.update(patron_sion, dbcommit=True) patron_sion.reindex()
def test_get_ref_for_pid(app): """Test get $ref for pid.""" url = 'https://ils.rero.ch/api/documents/3' assert get_ref_for_pid('documents', '3') == url assert get_ref_for_pid('doc', '3') == url assert get_ref_for_pid(Document, '3') == url assert get_ref_for_pid('test', '3') is None
def create_ill_requests(input_file): """Create ILL request for each organisation.""" locations = get_locations() patron_pids = {} with open(input_file, 'r', encoding='utf-8') as request_file: requests = json.load(request_file) for request_data in requests: for organisation_pid, location_pid in locations.items(): if 'pid' in request_data: del request_data['pid'] if organisation_pid not in patron_pids: patron_pids[organisation_pid] = list( Patron.get_all_pids_for_organisation(organisation_pid)) patron_pid = random.choice(patron_pids[organisation_pid]) request_data['patron'] = { '$ref': get_ref_for_pid('patrons', patron_pid) } request_data['pickup_location'] = { '$ref': get_ref_for_pid('locations', location_pid) } request = ILLRequest.create(request_data, dbcommit=True, reindex=True) click.echo('\tRequest: #{pid} \tfor org#{org_id}'.format( pid=request.pid, org_id=request.organisation_pid))
def test_items_after_holdings_update(holding_lib_martigny_w_patterns, item_type_on_site_martigny, loc_restricted_martigny): """Test location and item type after holdings of type serials changes.""" martigny = holding_lib_martigny_w_patterns assert martigny.get('holdings_type') == HoldingTypes.SERIAL # ensure that all attached items of holdings record has the same location # and item type. item_pids = Item.get_items_pid_by_holding_pid(martigny.pid) for pid in item_pids: item = Item.get_record_by_pid(pid) assert item.location_pid == martigny.location_pid assert item.item_type_pid == martigny.circulation_category_pid assert martigny.location_pid != loc_restricted_martigny.pid assert martigny.circulation_category_pid != item_type_on_site_martigny.pid # change the holdings circulation_category and location. martigny['circulation_category'] = { '$ref': get_ref_for_pid('item_types', item_type_on_site_martigny.pid) } martigny['location'] = { '$ref': get_ref_for_pid('locations', loc_restricted_martigny.pid) } martigny = martigny.update(martigny, dbcommit=True, reindex=True) assert martigny.location_pid == loc_restricted_martigny.pid assert martigny.circulation_category_pid == item_type_on_site_martigny.pid # ensure that all attached items of holdings record has the same location # and item type after holdings changes. for pid in item_pids: item = Item.get_record_by_pid(pid) assert item.location_pid == martigny.location_pid assert item.item_type_pid == martigny.circulation_category_pid
def decorated_view(*args, **kwargs): try: data = flask_request.get_json() description = data.pop('description') except KeyError: # The description parameter is missing. abort(400, str('missing description parameter.')) try: holding_pid = data.pop('holding_pid', None) holding = Holding.get_record_by_pid(holding_pid) if not holding: abort(404, 'Holding not found') # create a provisional item item_metadata = { 'type': 'provisional', 'document': { '$ref': get_ref_for_pid('doc', holding.document_pid) }, 'location': { '$ref': get_ref_for_pid('loc', holding.location_pid) }, 'item_type': { '$ref': get_ref_for_pid('itty', holding.circulation_category_pid) }, 'enumerationAndChronology': description, 'status': ItemStatus.ON_SHELF, 'holding': { '$ref': get_ref_for_pid('hold', holding.pid) } } item = Item.create(item_metadata, dbcommit=True, reindex=True) _, action_applied = func(holding, item, data, *args, **kwargs) return jsonify({'action_applied': action_applied}) except NoCirculationActionIsPermitted: # The circulation specs do not allow updates on some loan states. return jsonify({'status': 'error: Forbidden'}), 403 except MissingRequiredParameterError as error: # Return error 400 when there is a missing required parameter abort(400, str(error)) except CirculationException as error: abort(403, error.description or str(error)) except NotFound as error: raise error except exceptions.RequestError as error: # missing required parameters return jsonify({'status': f'error: {error}'}), 400 except Exception as error: # TODO: need to know what type of exception and document there. # raise error current_app.logger.error(str(error)) return jsonify({'status': f'error: {error}'}), 400
def test_items_availability(item_type_missing_martigny, item_type_standard_martigny, item_lib_martigny_data_tmp, loc_public_martigny, lib_martigny, org_martigny, document): """Test availability for an item.""" # Create a temporary item with correct data for the test item_data = deepcopy(item_lib_martigny_data_tmp) del item_data['pid'] item_data['barcode'] = 'TEST_AVAILABILITY' item_data['temporary_item_type'] = { '$ref': get_ref_for_pid(ItemType, item_type_missing_martigny.pid) } item = Item.create(item_data, dbcommit=True, reindex=True) # test the availability and availability_text assert not item.available assert len(item.availability_text) == \ len(item_type_missing_martigny.get('displayed_status', [])) + 1 del item['temporary_item_type'] item = item.update(item, dbcommit=True, reindex=True) assert item.available assert len(item.availability_text) == 1 # only default value # delete the created item item.delete()
def test_obsolete_temporary_item_types(item_lib_martigny, item_type_on_site_martigny): """Test obsolete temporary_item_types.""" item = item_lib_martigny # First test - No items has temporary_item_type items = Item.get_items_with_obsolete_temporary_item_type() assert len(list(items)) == 0 # Second test - add an infinite temporary_item_type to an item item['temporary_item_type'] = { '$ref': get_ref_for_pid('itty', item_type_on_site_martigny.pid) } item.update(item, dbcommit=True, reindex=True) items = Item.get_items_with_obsolete_temporary_item_type() assert len(list(items)) == 0 # Third test - add an expiration date in the future for the temporary # item_type over_2_days = datetime.now() + timedelta(days=2) item['temporary_item_type']['end_date'] = over_2_days.strftime('%Y-%m-%d') item.update(data=item, dbcommit=True, reindex=True) items = Item.get_items_with_obsolete_temporary_item_type() assert len(list(items)) == 0 # Fourth test - check obsolete with for a specified date in the future over_3_days = datetime.now() + timedelta(days=3) items = Item.get_items_with_obsolete_temporary_item_type( end_date=over_3_days) assert len(list(items)) == 1 # reset the item to original values del item['temporary_item_type'] item.update(data=item, dbcommit=True, reindex=True)
def test_loans_build_refs(item_lib_martigny, patron_martigny, document): """Test functions buildings refs.""" # Create "virtual" Loan (not registered) loan = Loan({ 'item_pid': item_pid_to_object(item_lib_martigny.pid), 'document_pid': document.pid, 'patron_pid': patron_martigny.pid }) assert loan_build_item_ref(None, loan) == \ get_ref_for_pid('items', item_lib_martigny.pid) assert loan_build_document_ref(None, loan) == \ get_ref_for_pid('doc', document.pid) assert loan_build_patron_ref(None, loan) == \ get_ref_for_pid('patrons', patron_martigny.pid)
def test_limits(patron_type_schema, patron_type_tmp): """Test limits fr patron type JSON schema.""" data = patron_type_tmp # checkout limits :: library limit > general limit data['limits'] = { 'checkout_limits': { 'global_limit': 20, 'library_limit': 15 } } validate(data, patron_type_schema) with pytest.raises(ValidationError): data['limits']['checkout_limits']['library_limit'] = 40 validate(data, patron_type_schema) # valid for JSON schema data.validate() # invalid against extented_validation rules data['limits']['checkout_limits']['library_limit'] = 15 with pytest.raises(ValidationError): lib_ref = get_ref_for_pid('lib', 'dummy') data['limits']['checkout_limits']['library_exceptions'] = [ {'library': {'$ref': lib_ref}, 'value': 15} ] validate(data, patron_type_schema) # valid for JSON schema data.validate() # invalid against extented_validation rules with pytest.raises(ValidationError): data['limits']['checkout_limits']['library_exceptions'] = [ {'library': {'$ref': lib_ref}, 'value': 5}, {'library': {'$ref': lib_ref}, 'value': 7} ] validate(data, patron_type_schema) # valid for JSON schema data.validate() # invalid against extented_validation rules
def create_collections(input_file, max_item=10): """Create collections.""" organisation_items = {} with open(input_file, 'r', encoding='utf-8') as request_file: collections = json.load(request_file) for collection_data in collections: organisation_pid = extracted_data_from_ref( collection_data.get('organisation').get('$ref')) if organisation_pid not in organisation_items: organisation_items[organisation_pid] =\ get_items_by_organisation_pid(organisation_pid) items = random.choices( organisation_items[organisation_pid], k=random.randint(1, max_item) ) collection_data['items'] = [] for item_pid in items: ref = get_ref_for_pid('items', item_pid) collection_data['items'].append({'$ref': ref}) request = Collection.create( collection_data, dbcommit=True, reindex=True ) click.echo('\tCollection: #{pid}'.format( pid=request.pid, ))
def test_item_type_circulation_category_pid(item_lib_martigny, item_type_on_site_martigny): """Test item_type circulation category pid.""" assert item_lib_martigny.item_type_pid == \ item_lib_martigny.item_type_circulation_category_pid past_2_days = datetime.now() - timedelta(days=2) over_2_days = datetime.now() + timedelta(days=2) # add an obsolete temporary item_type end_date :: In this case, the # circulation item_type must be the default item_type item_lib_martigny['temporary_item_type'] = { '$ref': get_ref_for_pid('itty', item_type_on_site_martigny.pid), 'end_date': past_2_days.strftime('%Y-%m-%d') } assert item_lib_martigny.item_type_pid == \ item_lib_martigny.item_type_circulation_category_pid # add a valid temporary item_type end_date :: In this case, the # circulation item_type must be the temporary item_type item_lib_martigny['temporary_item_type']['end_date'] = \ over_2_days.strftime('%Y-%m-%d') assert item_type_on_site_martigny.pid == \ item_lib_martigny.item_type_circulation_category_pid # removing any temporary item_type end_date :: In this case, the # circulation item_type must be the temporary item_type del item_lib_martigny['temporary_item_type']['end_date'] assert item_type_on_site_martigny.pid == \ item_lib_martigny.item_type_circulation_category_pid # reset the object with default value del item_lib_martigny['temporary_item_type']
def test_replace_refs(item_lib_martigny, item_type_on_site_martigny): """Test specific replace_refs for items.""" item_lib_martigny['temporary_item_type'] = { '$ref': get_ref_for_pid('itty', item_type_on_site_martigny.pid), 'end_date': '2020-12-31' } assert 'end_date' in item_lib_martigny.replace_refs().\ get('temporary_item_type')
def test_clear_obsolete_temporary_item_type_and_location( item_lib_martigny, item_type_on_site_martigny, loc_restricted_martigny, item2_lib_martigny): """test task test_clear_obsolete_temporary_item_type_and_location""" item = item_lib_martigny end_date = datetime.now() + timedelta(days=2) item['temporary_item_type'] = { '$ref': get_ref_for_pid('itty', item_type_on_site_martigny.pid), 'end_date': end_date.strftime('%Y-%m-%d') } item['temporary_location'] = { '$ref': get_ref_for_pid('loc', loc_restricted_martigny.pid), 'end_date': end_date.strftime('%Y-%m-%d') } item = item.update(item, dbcommit=True, reindex=True) assert item.get('temporary_item_type', {}).get('end_date') assert item.get('temporary_location', {}).get('end_date') end_date = datetime.now() + timedelta(days=25) item2_lib_martigny['temporary_item_type'] = { '$ref': get_ref_for_pid('itty', item_type_on_site_martigny.pid), 'end_date': end_date.strftime('%Y-%m-%d') } item2_lib_martigny['temporary_location'] = { '$ref': get_ref_for_pid('loc', loc_restricted_martigny.pid), 'end_date': end_date.strftime('%Y-%m-%d') } item2_lib_martigny = item2_lib_martigny.update( item2_lib_martigny, dbcommit=True, reindex=True) assert item2_lib_martigny.get('temporary_item_type', {}).get('end_date') assert item2_lib_martigny.get('temporary_location', {}).get('end_date') over_4_days = datetime.now() + timedelta(days=4) with freeze_time(over_4_days.strftime('%Y-%m-%d')): items = Item.get_items_with_obsolete_temporary_item_type_or_location() assert len(list(items)) # run the tasks msg = clean_obsolete_temporary_item_types_and_locations() assert msg['deleted fields'] == 2 # check after task was ran items = Item.get_items_with_obsolete_temporary_item_type_or_location() assert len(list(items)) == 0 item = Item.get_record_by_pid(item.pid) assert not item.get('temporary_item_type') assert not item.get('temporary_location')
def test_patron_payment(client, librarian_martigny, patron_transaction_overdue_event_martigny): """Test patron payment.""" ptre = patron_transaction_overdue_event_martigny transaction = ptre.patron_transaction calculated_amount = sum(event.amount for event in transaction.events) transaction = PatronTransaction.get_record_by_pid(transaction.pid) assert calculated_amount == transaction.total_amount == 2.00 login_user_via_session(client, librarian_martigny.user) post_entrypoint = 'invenio_records_rest.ptre_list' payment = deepcopy(ptre) # STEP#1 :: PARTIAL PAYMENT WITH TOO MUCH DECIMAL # Try to pay a part of the transaction amount, but according to # event amount restriction, only 2 decimals are allowed. del payment['pid'] payment['type'] = 'payment' payment['subtype'] = 'cash' payment['amount'] = 0.545 payment['operator'] = { '$ref': get_ref_for_pid('patrons', librarian_martigny.pid) } res, _ = postdata(client, post_entrypoint, payment) assert res.status_code == 400 # STEP#2 :: PARTIAL PAYMENT WITH GOOD NUMBER OF DECIMALS # Despite if a set a number with 3 decimals, if this number represent # the value of a 2 decimals, it's allowed payment['amount'] = 0.540 res, _ = postdata(client, post_entrypoint, payment) assert res.status_code == 201 transaction = PatronTransaction.get_record_by_pid(transaction.pid) assert transaction.total_amount == 1.46 assert transaction.status == 'open' # STEP#3 :: PAY TOO MUCH MONEY # Try to proceed a payment with too much money, the system must # reject the payment payment['amount'] = 2 res, data = postdata(client, post_entrypoint, payment) assert res.status_code == 400 # STEP#4 :: PAY THE REST # Conclude the transaction by creation of a payment for the rest of the # transaction payment['amount'] = transaction.total_amount res, _ = postdata(client, post_entrypoint, payment) assert res.status_code == 201 transaction = PatronTransaction.get_record_by_pid(transaction.pid) assert transaction.total_amount == 0 assert transaction.status == 'closed'
def test_checkout_temporary_item_type(client, librarian_martigny, lib_martigny, loc_public_martigny, patron_martigny, item_lib_martigny, item_type_on_site_martigny, circ_policy_short_martigny, circ_policy_default_martigny): """Test checkout or item with temporary item_types""" login_user_via_session(client, librarian_martigny.user) item = item_lib_martigny assert item.status == ItemStatus.ON_SHELF # test basic behavior cipo_used = CircPolicy.provide_circ_policy( lib_martigny.organisation_pid, lib_martigny.pid, patron_martigny.patron_type_pid, item.item_type_circulation_category_pid) assert cipo_used == circ_policy_short_martigny # add a temporary_item_type on item # due to this change, the cipo used during circulation operation should # be different from the first cipo found. item['temporary_item_type'] = { '$ref': get_ref_for_pid('itty', item_type_on_site_martigny.pid) } item = item.update(data=item, dbcommit=True, reindex=True) cipo_tmp_used = CircPolicy.provide_circ_policy( lib_martigny.organisation_pid, lib_martigny.pid, patron_martigny.patron_type_pid, item.item_type_circulation_category_pid) assert cipo_tmp_used == circ_policy_default_martigny delta = timedelta(cipo_tmp_used.get('checkout_duration')) expected_date = datetime.now() + delta expected_dates = [expected_date, lib_martigny.next_open(expected_date)] expected_dates = [d.strftime('%Y-%m-%d') for d in expected_dates] # try a checkout and check the transaction end_date is related to the cipo # corresponding to the temporary item_type params = dict(item_pid=item.pid, patron_pid=patron_martigny.pid, transaction_user_pid=librarian_martigny.pid, transaction_location_pid=loc_public_martigny.pid) res, data = postdata(client, 'api_item.checkout', params) assert res.status_code == 200 transaction_end_date = data['action_applied']['checkout']['end_date'] transaction_end_date = ciso8601.parse_datetime(transaction_end_date).date() transaction_end_date = transaction_end_date.strftime('%Y-%m-%d') assert transaction_end_date in expected_dates # reset the item to original value del item['temporary_item_type'] item.update(data=item, dbcommit=True, reindex=True)
def test_order_line_validation_extension(acq_order_line_fiction_martigny_data, acq_account_fiction_martigny, ebook_1): """Test order line validation extension.""" data = deepcopy(acq_order_line_fiction_martigny_data) del data['pid'] # An order line cannot be linked to an harvested document ebook_ref = get_ref_for_pid('doc', ebook_1.pid) test_data = deepcopy(data) test_data['document']['$ref'] = ebook_ref with pytest.raises(ValidationError) as error: AcqOrderLine.create(test_data, delete_pid=True) assert 'Cannot link to an harvested document' in str(error.value)
def test_patron_pending_subscription(client, patron_type_grown_sion, patron_sion_no_email, librarian_sion_no_email, patron_transaction_overdue_event_martigny, lib_sion): """Test get pending subscription for patron.""" # At the beginning, `patron_sion_no_email` should have one pending # subscription. pending_subscription = patron_sion_no_email.get_pending_subscriptions() assert len(pending_subscription) == 1 # Pay this subscription. login_user_via_session(client, librarian_sion_no_email.user) post_entrypoint = 'invenio_records_rest.ptre_list' trans_pid = extracted_data_from_ref( pending_subscription[0]['patron_transaction'], data='pid') transaction = PatronTransaction.get_record_by_pid(trans_pid) payment = deepcopy(patron_transaction_overdue_event_martigny) del payment['pid'] payment['type'] = 'payment' payment['subtype'] = 'cash' payment['amount'] = transaction.total_amount payment['operator'] = { '$ref': get_ref_for_pid('patrons', librarian_sion_no_email.pid) } payment['library'] = {'$ref': get_ref_for_pid('libraries', lib_sion.pid)} payment['parent'] = pending_subscription[0]['patron_transaction'] res, _ = postdata(client, post_entrypoint, payment) assert res.status_code == 201 transaction = PatronTransaction.get_record_by_pid(transaction.pid) assert transaction.status == 'closed' # reload the patron and check the pending subscription. As we paid the # previous subscription, there will be none pending subscription patron_sion_no_email = Patron.get_record_by_pid(patron_sion_no_email.pid) pending_subscription = patron_sion_no_email.get_pending_subscriptions() assert len(pending_subscription) == 0
def test_document_referenced_subject(mock_contributions_mef_get, contribution_person_response_data, contribution_person): """Test referenced document subjects.""" mock_contributions_mef_get.return_value = mock_response( json_data=contribution_person_response_data) # REFERENCED SUBJECTS - SUCCESS data = { '$ref': get_ref_for_pid(Contribution, contribution_person.pid), 'type': DocumentSubjectType.PERSON } subject = SubjectFactory.create_subject(data) assert subject.render(language='ger') == 'Loy, Georg, 1885-19..' assert subject.render(language='dummy') == 'Loy, Georg, 1885-19..' assert subject.render() == 'Loy, Georg, 1885-19..' # REFERENCED SUBJECTS - ERRORS data = { '$dummy_ref': get_ref_for_pid(Contribution, contribution_person.pid), 'type': DocumentSubjectType.PERSON } with pytest.raises(AttributeError): SubjectFactory.create_subject(data).render()
def test_request_notifications_temp_item_type( client, patron_martigny, patron_sion, lib_martigny, lib_fully, item_lib_martigny, librarian_martigny, loc_public_martigny, circulation_policies, loc_public_fully, item_type_missing_martigny, mailbox ): """Test request notifications with item type with negative availability.""" mailbox.clear() login_user_via_session(client, librarian_martigny.user) item_lib_martigny['temporary_item_type'] = { '$ref': get_ref_for_pid('itty', item_type_missing_martigny.pid) } item_lib_martigny.update(item_lib_martigny, dbcommit=True, reindex=True) res, data = postdata( client, 'api_item.librarian_request', dict( item_pid=item_lib_martigny.pid, pickup_location_pid=loc_public_fully.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) assert len(mailbox) == 0 # cancel request res, _ = postdata( client, 'api_item.cancel_item_request', dict( item_pid=item_lib_martigny.pid, pid=request_loan_pid, transaction_user_pid=librarian_martigny.pid, transaction_library_pid=lib_martigny.pid ) ) assert res.status_code == 200 mailbox.clear() del(item_lib_martigny['temporary_item_type']) item_lib_martigny.update(item_lib_martigny, dbcommit=True, reindex=True)
def test_template_replace_refs(templ_doc_public_martigny): """Test template replace_refs method.""" tmpl = templ_doc_public_martigny tmpl.setdefault('data', {})['document'] = { '$ref': get_ref_for_pid('doc', 'dummy_pid') } tmpl = tmpl.update(tmpl, dbcommit=True, reindex=True) assert '$ref' in tmpl['data']['document'] assert '$ref' in tmpl['creator'] replace_data = tmpl.replace_refs() assert '$ref' in replace_data['data']['document'] assert '$ref' not in replace_data['creator'] # reset changes del tmpl['data']['document'] tmpl.update(tmpl, dbcommit=True, reindex=True)
def test_patron_payment( client, librarian_martigny_no_email, librarian_sion_no_email, patron_transaction_overdue_event_martigny): """Test patron payment.""" transaction = \ patron_transaction_overdue_event_martigny.patron_transaction() calculated_amount = sum([event.amount for event in transaction.events]) transaction = PatronTransaction.get_record_by_pid(transaction.pid) assert calculated_amount == transaction.total_amount == 2.00 login_user_via_session(client, librarian_martigny_no_email.user) post_entrypoint = 'invenio_records_rest.ptre_list' payment = deepcopy(patron_transaction_overdue_event_martigny) # partial payment del payment['pid'] payment['type'] = 'payment' payment['subtype'] = 'cash' payment['amount'] = 1.00 payment['operator'] = {'$ref': get_ref_for_pid( 'patrons', librarian_martigny_no_email.pid)} res, _ = postdata( client, post_entrypoint, payment ) assert res.status_code == 201 transaction = PatronTransaction.get_record_by_pid(transaction.pid) assert transaction.total_amount == calculated_amount - 1.00 assert transaction.status == 'open' # full payment payment['type'] = 'payment' payment['subtype'] = 'cash' payment['amount'] = transaction.total_amount res, _ = postdata( client, post_entrypoint, payment ) assert res.status_code == 201 transaction = PatronTransaction.get_record_by_pid(transaction.pid) assert transaction.total_amount == 0.00 assert transaction.status == 'closed'
def test_items_availability(item_type_missing_martigny, item_type_standard_martigny, item_lib_martigny_data_tmp, loc_public_martigny, lib_martigny, org_martigny, document): """Test availability for an item.""" # Create a temporary item with correct data for the test item_data = deepcopy(item_lib_martigny_data_tmp) del item_data['pid'] item_data['barcode'] = 'TEST_AVAILABILITY' item_data['temporary_item_type'] = { '$ref': get_ref_for_pid(ItemType, item_type_missing_martigny.pid) } item = Item.create(item_data, dbcommit=True, reindex=True) # test the availability and availability_text assert not item.available assert len(item.availability_text) == \ len(item_type_missing_martigny.get('displayed_status', [])) + 1 del item['temporary_item_type'] item = item.update(item, dbcommit=True, reindex=True) assert item.available assert len(item.availability_text) == 1 # only default value # test availability and availability_text for an issue item['type'] = TypeOfItem.ISSUE item['enumerationAndChronology'] = 'dummy' item['issue'] = { 'regular': False, 'status': ItemIssueStatus.RECEIVED, 'received_date': '1970-01-01', 'expected_date': '1970-01-01' } item = item.update(item, dbcommit=True, reindex=True) assert item.available assert item.availability_text[0]['label'] == item.status item['issue']['status'] = ItemIssueStatus.LATE item = item.update(item, dbcommit=True, reindex=True) assert not item.available assert item.availability_text[0]['label'] == ItemIssueStatus.LATE # delete the created item item.delete()
def test_temporary_item_type(item_schema, item_lib_martigny): """Test temporary item type for item jsonschemas.""" data = item_lib_martigny # tmp_itty cannot be the same than main_itty with pytest.raises(RecordValidationError): data['temporary_item_type'] = {'$ref': data['item_type']['$ref']} validate(data, item_schema) data.validate() # check extented_validation # tmp_itty_enddate must be older than current date with pytest.raises(RecordValidationError): current_date = datetime.datetime.now().strftime('%Y-%m-%d') data['temporary_item_type'] = { '$ref': get_ref_for_pid('itty', 'sample'), 'end_date': current_date } validate(data, item_schema) data.validate() # check extented_validation
def get_data(self): """Return the form as a valid ILLRequest data structure.""" data = remove_empties_from_dict({ 'document': { 'title': self.document.title.data, 'authors': self.document.authors.data, 'publisher': self.document.publisher.data, 'year': str(self.document.year.data or ''), 'identifier': self.document.identifier.data, 'source': { 'journal_title': self.document.source.journal_title.data, 'volume': self.document.source.volume.data, 'number': self.document.source.number.data, } }, 'pickup_location': { '$ref': get_ref_for_pid('locations', self.pickup_location.data) }, 'pages': self.pages.data, 'found_in': { 'source': self.source.origin.data, 'url': self.source.url.data } }) if self.note.data: data['notes'] = [{ 'type': 'public_note', 'content': self.note.data }] # if we put 'copy' in the dict before the dict cleaning and if 'copy' # is set to 'No', then it will be removed by `remove_empties_from_dict` # So we need to add it after the cleaning data['copy'] = self.request_copy.data == 1 # if user select 'not specified' into the ILL request form, this value # must be removed from the dict. if data.get('document', {}).get('year') == 'n/a': del data['document']['year'] return data
def test_clear_obsolete_temporary_item_type(item_lib_martigny, item_type_on_site_martigny): """test task clear_obsolete_temporary_item_type""" item = item_lib_martigny end_date = datetime.now() + timedelta(days=2) item['temporary_item_type'] = { '$ref': get_ref_for_pid('itty', item_type_on_site_martigny.pid), 'end_date': end_date.strftime('%Y-%m-%d') } item.update(item, dbcommit=True, reindex=True) assert item.item_type_circulation_category_pid == \ item_type_on_site_martigny.pid over_4_days = datetime.now() + timedelta(days=4) with freeze_time(over_4_days.strftime('%Y-%m-%d')): items = Item.get_items_with_obsolete_temporary_item_type() assert len(list(items)) == 1 # run the tasks clean_obsolete_temporary_item_types() # check after task was ran items = Item.get_items_with_obsolete_temporary_item_type() assert len(list(items)) == 0 assert item.item_type_circulation_category_pid == item.item_type_pid
def test_item_can_request(client, document, holding_lib_martigny, item_lib_martigny, librarian_martigny, lib_martigny, patron_martigny, circulation_policies, patron_type_children_martigny, loc_public_martigny_data, system_librarian_martigny, item_lib_martigny_data): """Test item can request API.""" # test no logged user res = client.get( url_for('api_item.can_request', item_pid=item_lib_martigny.pid, library_pid=lib_martigny.pid, patron_barcode=patron_martigny.get('patron', {}).get('barcode')[0])) assert res.status_code == 401 can, _ = can_request(item_lib_martigny) assert not can login_user_via_session(client, librarian_martigny.user) # valid test res = client.get( url_for('api_item.can_request', item_pid=item_lib_martigny.pid, library_pid=lib_martigny.pid, patron_barcode=patron_martigny.get('patron', {}).get('barcode')[0])) assert res.status_code == 200 data = get_json(res) assert data.get('can') # test no valid item res = client.get( url_for('api_item.can_request', item_pid='no_item', library_pid=lib_martigny.pid, patron_barcode=patron_martigny.get('patron', {}).get('barcode')[0])) assert res.status_code == 404 # test no valid library res = client.get( url_for('api_item.can_request', item_pid=item_lib_martigny.pid, library_pid='no_library', patron_barcode=patron_martigny.get('patron', {}).get('barcode')[0])) assert res.status_code == 404 # test no valid patron res = client.get( url_for('api_item.can_request', item_pid=item_lib_martigny.pid, library_pid=lib_martigny.pid, patron_barcode='no_barcode')) assert res.status_code == 404 # test no valid item status item_lib_martigny['status'] = ItemStatus.MISSING item_lib_martigny.update(item_lib_martigny, dbcommit=True, reindex=True) res = client.get( url_for('api_item.can_request', item_pid=item_lib_martigny.pid, library_pid=lib_martigny.pid, patron_barcode=patron_martigny.get('patron', {}).get('barcode')[0])) assert res.status_code == 200 data = get_json(res) assert not data.get('can') item_lib_martigny['status'] = ItemStatus.ON_SHELF item_lib_martigny.update(item_lib_martigny, dbcommit=True, reindex=True) # Location :: allow_request == false # create a new location and set 'allow_request' to false. Assign a new # item to this location. Chek if this item can be requested : it couldn't # with 'Item location doesn't allow request' reason. new_location = deepcopy(loc_public_martigny_data) del new_location['pid'] new_location['allow_request'] = False new_location = Location.create(new_location, dbcommit=True, reindex=True) assert new_location new_item = deepcopy(item_lib_martigny_data) del new_item['pid'] new_item['barcode'] = 'dummy_barcode_allow_request' new_item['location']['$ref'] = get_ref_for_pid(Location, new_location.pid) new_item = Item.create(new_item, dbcommit=True, reindex=True) assert new_item res = client.get(url_for('api_item.can_request', item_pid=new_item.pid)) assert res.status_code == 200 data = get_json(res) assert not data.get('can') # remove created data item_url = url_for('invenio_records_rest.item_item', pid_value=new_item.pid) hold_url = url_for('invenio_records_rest.hold_item', pid_value=new_item.holding_pid) loc_url = url_for('invenio_records_rest.loc_item', pid_value=new_location.pid) client.delete(item_url) client.delete(hold_url) client.delete(loc_url)
def test_cancel_notifications( client, patron_martigny, lib_martigny, item_lib_martigny, librarian_martigny, loc_public_martigny, circulation_policies, mailbox ): """Test cancel notifications.""" login_user_via_session(client, librarian_martigny.user) # CREATE and VALIDATE a request ... res, data = postdata( client, 'api_item.librarian_request', dict( item_pid=item_lib_martigny.pid, pickup_location_pid=loc_public_martigny.pid, patron_pid=patron_martigny.pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 200 request_loan_pid = data.get( 'action_applied')[LoanAction.REQUEST].get('pid') flush_index(NotificationsSearch.Meta.index) res, data = postdata( client, 'api_item.validate_request', dict( pid=request_loan_pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 200 # At this time, an AVAILABILITY notification should be create but not yet # dispatched loan = Loan.get_record_by_pid(request_loan_pid) notification = get_notification(loan, NotificationType.AVAILABILITY) assert notification \ and notification['status'] == NotificationStatus.CREATED # BORROW the requested item res, data = postdata( client, 'api_item.checkout', dict( item_pid=item_lib_martigny.pid, patron_pid=patron_martigny.pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid, ) ) assert res.status_code == 200 loan_pid = data.get( 'action_applied')[LoanAction.CHECKOUT].get('pid') loan = Loan.get_record_by_pid(loan_pid) # Try to dispatch pending availability notifications. # As the item is now checkout, then the availability notification is not # yet relevant. mailbox.clear() process_notifications(NotificationType.AVAILABILITY) notification = get_notification(loan, NotificationType.AVAILABILITY) assert notification and \ notification['status'] == NotificationStatus.CANCELLED assert len(mailbox) == 0 # restore to initial state res, data = postdata( client, 'api_item.checkin', dict( item_pid=item_lib_martigny.pid, # patron_pid=patron_martigny.pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid, ) ) assert res.status_code == 200 mailbox.clear() # Test REMINDERS notifications. # reminders notification check about the end_date. As the loan end_date # is not yet over, the notification could be cancelled. notification = loan.create_notification(_type=NotificationType.DUE_SOON)[0] can_cancel, _ = notification.can_be_cancelled() assert not can_cancel process_notifications(NotificationType.DUE_SOON) notification = Notification.get_record_by_pid(notification.pid) assert notification['status'] == NotificationStatus.DONE flush_index(NotificationsSearch.Meta.index) # try to create a new DUE_SOON notification for the same loan record = { 'creation_date': datetime.now(timezone.utc).isoformat(), 'notification_type': NotificationType.DUE_SOON, 'context': { 'loan': {'$ref': get_ref_for_pid('loans', loan.pid)}, 'reminder_counter': 0 } } notification = Notification.create(record) can_cancel, _ = notification.can_be_cancelled() assert can_cancel Dispatcher.dispatch_notifications([notification.pid]) notification = Notification.get_record_by_pid(notification.pid) assert notification['status'] == NotificationStatus.CANCELLED
def test_notifications_post_put_delete( client, dummy_notification, loan_validated_martigny, json_header): """Test record delete and update.""" record = deepcopy(dummy_notification) del record['pid'] loan_ref = get_ref_for_pid('loans', loan_validated_martigny.get('pid')) record['context']['loan'] = {'$ref': loan_ref} notif = Notification.create( record, dbcommit=True, reindex=True, delete_pid=True ) assert notif == record flush_index(NotificationsSearch.Meta.index) pid = notif.get('pid') item_url = url_for('invenio_records_rest.notif_item', pid_value=pid) list_url = url_for('invenio_records_rest.notif_list', q='pid:pid') new_record = deepcopy(record) # Create record / POST new_record['pid'] = 'x' res, data = postdata( client, 'invenio_records_rest.notif_list', new_record ) assert res.status_code == 201 flush_index(NotificationsSearch.Meta.index) # Check that the returned record matches the given data assert data['metadata'] == new_record res = client.get(item_url) assert res.status_code == 200 data = get_json(res) assert notif == data['metadata'] # Update record/PUT data = data['metadata'] data['notification_type'] = NotificationType.DUE_SOON res = client.put( item_url, data=json.dumps(data), headers=json_header ) assert res.status_code == 200 # Check that the returned record matches the given data data = get_json(res) assert data['metadata']['notification_type'] == NotificationType.DUE_SOON res = client.get(item_url) assert res.status_code == 200 data = get_json(res) assert data['metadata']['notification_type'] == NotificationType.DUE_SOON res = client.get(list_url) assert res.status_code == 200 # Delete record/DELETE res = client.delete(item_url) assert res.status_code == 204 res = client.get(item_url) assert res.status_code == 410 can, reasons = notif.can_delete assert can assert reasons == {} notif.delete(dbcommit=True, delindex=True)
def update_items_locations_and_types(sender, record=None, **kwargs): """This method checks if the items of the parent record needs an update. This method checks the location and item_type of each item attached to the holding record and update the item record accordingly. This method should be connect with 'after_record_update'. :param record: the holding record. """ if not isinstance(record, Holding) and \ record.get('holdings_type') == HoldingTypes.SERIAL: # identify all items records attached to this serials holdings record # with different location and item_type. hold_circ_pid = record.circulation_category_pid hold_loc_pid = record.location_pid search = ItemsSearch().filter('term', holding__pid=record.pid) item_hits = search.\ filter('bool', should=[ Q('bool', must_not=[ Q('match', item_type__pid=hold_circ_pid)]), Q('bool', must_not=[ Q('match', location__pid=hold_loc_pid)])])\ .source(['pid']) items = [hit.meta.id for hit in item_hits.scan()] items_to_index = [] # update these items and make sure they have the same location/category # as the parent holdings record. for id in items: try: item = Item.get_record_by_id(id) if not item: continue items_to_index.append(id) item_temp_loc_pid, item_temp_type_pid = None, None # remove the item temporary_location if it is equal to the # new item location. temporary_location = item.get('temporary_location', {}) if temporary_location: item_temp_loc_pid = extracted_data_from_ref( temporary_location.get('$ref')) if hold_loc_pid != item.location_pid: if item_temp_loc_pid == hold_loc_pid: item.pop('temporary_location', None) item['location'] = { '$ref': get_ref_for_pid('locations', hold_loc_pid) } # remove the item temporary_item_type if it is equal to the # new item item_type. temporary_type = item.get('temporary_item_type', {}) if temporary_type: item_temp_type_pid = extracted_data_from_ref( temporary_type.get('$ref')) if hold_circ_pid != item.item_type_pid: if item_temp_type_pid == hold_circ_pid: item.pop('temporary_item_type', None) item['item_type'] = { '$ref': get_ref_for_pid('item_types', hold_circ_pid) } # update directly in database. db.session.query(item.model_cls).filter_by(id=item.id).update( {item.model_cls.json: item}) except Exception as err: pass if items_to_index: # commit session db.session.commit() # bulk indexing of item records. indexer = ItemsIndexer() indexer.bulk_index(items_to_index) process_bulk_queue.apply_async()
def test_acquisition_order(client, rero_json_header, org_martigny, lib_martigny, budget_2020_martigny, vendor_martigny, librarian_martigny, document): """Scenario to test orders creation.""" login_user_via_session(client, librarian_martigny.user) # STEP 0 :: Create the account tree basic_data = { 'allocated_amount': 1000, 'budget': { '$ref': get_ref_for_pid('budg', budget_2020_martigny.pid) }, 'library': { '$ref': get_ref_for_pid('lib', lib_martigny.pid) } } account_a = dict(name='A', allocated_amount=2000) account_a = {**basic_data, **account_a} account_a = _make_resource(client, 'acac', account_a) account_a_ref = {'$ref': get_ref_for_pid('acac', account_a.pid)} account_b = dict(name='B', allocated_amount=500, parent=account_a_ref) account_b = {**basic_data, **account_b} account_b = _make_resource(client, 'acac', account_b) account_b_ref = {'$ref': get_ref_for_pid('acac', account_b.pid)} # TEST 1 :: Create an order and add some order lines on it. # * The creation of the order will be successful # * We create first order line linked to account B. After this creation, # we can check the encumbrance of this account and its parent account. order_data = { 'vendor': { '$ref': get_ref_for_pid('vndr', vendor_martigny.pid) }, 'library': { '$ref': get_ref_for_pid('lib', lib_martigny.pid) }, 'type': 'monograph', } order = _make_resource(client, 'acor', order_data) assert order['reference'] == f'ORDER-{order.pid}' assert order.get_order_provisional_total_amount() == 0 assert order.status == AcqOrderStatus.PENDING assert order.can_delete basic_data = { 'acq_account': account_b_ref, 'acq_order': { '$ref': get_ref_for_pid('acor', order.pid) }, 'document': { '$ref': get_ref_for_pid('doc', document.pid) }, 'quantity': 4, 'amount': 25 } order_line_1 = _make_resource(client, 'acol', basic_data) assert order_line_1.get('total_amount') == 100 assert account_b.encumbrance_amount[0] == 100 assert account_b.remaining_balance[0] == 400 # 500 - 100 assert account_a.encumbrance_amount == (0, 100) assert account_a.remaining_balance[0] == 1500 assert account_a.expenditure_amount == (0, 0) # TEST 2 :: update the number of received item from the order line. # * The encumbrance amount account should be decrease by quantity # received * amount. # field received_quantity is now dynamically calculated at the receive of # receipt_lines assert order_line_1.received_quantity == 0 # TEST 3 :: add a new cancelled order line. # * As this new order line has CANCELLED status, its amount is not # calculated into the encumbrance_amount basic_data = { 'acq_account': account_b_ref, 'acq_order': { '$ref': get_ref_for_pid('acor', order.pid) }, 'document': { '$ref': get_ref_for_pid('doc', document.pid) }, 'quantity': 2, 'amount': 10, 'is_cancelled': True } order_line_1_1 = _make_resource(client, 'acol', basic_data) assert order_line_1_1.get('total_amount') == 20 assert account_b.encumbrance_amount[0] == 100 assert account_b.remaining_balance[0] == 400 # 500 - 100 assert account_a.encumbrance_amount == (0, 100) assert account_a.remaining_balance[0] == 1500 assert account_a.expenditure_amount == (0, 0) # TEST 4 :: new order line raises the limit of account available money. # * Create a new order line on the same account ; but the total amount # of the line must be larger than account available money --> should # be raise an ValidationError # * Update the first order line to raise the limit and check than the # same validation error occurs. # * Update the first order line to reach the limit without exceeding it order_line_2 = dict(quantity=50) order_line_2 = {**basic_data, **order_line_2} with pytest.raises(Exception) as excinfo: _make_resource(client, 'acol', order_line_2) assert 'Parent account available amount too low' in str(excinfo.value) order_line_1['quantity'] = 50 with pytest.raises(Exception) as excinfo: order_line_1.update(order_line_1, dbcommit=True, reindex=True) assert 'Parent account available amount too low' in str(excinfo.value) order_line_1['quantity'] = 20 order_line_1 = order_line_1.update(order_line_1, dbcommit=True, reindex=True) assert account_b.encumbrance_amount[0] == 500 assert account_b.remaining_balance[0] == 0 assert account_a.encumbrance_amount == (0, 500) assert account_a.remaining_balance[0] == 1500 # TEST 5 :: Update the account encumbrance exceedance and test it. # * At this time, the account B doesn't have any available money to # place any nex order line. Try to add an other item to existing order # line will raise a ValidationError # * Update the account 'encumbrance_exceedance' setting to allow more # encumbrance and try to add an item to order_line. It will be OK order_line_1['quantity'] += 1 with pytest.raises(Exception) as excinfo: order_line_1.update(order_line_1, dbcommit=True, reindex=True) assert 'Parent account available amount too low' in str(excinfo.value) account_b['encumbrance_exceedance'] = 5 # 5% of 500 = 25 account_b = account_b.update(account_b, dbcommit=True, reindex=True) order_line_1 = order_line_1.update(order_line_1, dbcommit=True, reindex=True) assert account_b.encumbrance_amount[0] == 525 assert account_b.remaining_balance[0] == -25 assert account_a.encumbrance_amount == (0, 525) assert account_a.remaining_balance[0] == 1500 # Test cascade deleting of order lines when attempting to delete a # PENDING order. order_line_1 = AcqOrderLine.get_record_by_pid(order_line_1.pid) order_line_1['is_cancelled'] = True order_line_1.update(order_line_1, dbcommit=True, reindex=True) order = AcqOrder.get_record_by_pid(order.pid) assert order.status == AcqOrderStatus.CANCELLED # Delete CANCELLED order is not permitted with pytest.raises(IlsRecordError.NotDeleted): _del_resource(client, 'acor', order.pid) order_line_1['is_cancelled'] = False order_line_1.update(order_line_1, dbcommit=True, reindex=True) order = AcqOrder.get_record_by_pid(order.pid) assert order.status == AcqOrderStatus.PENDING # DELETE created resources _del_resource(client, 'acor', order.pid) # Deleting the parent PENDING order does delete all of its order lines order_line_1 = AcqOrderLine.get_record_by_pid(order_line_1.pid) order_line_1_1 = AcqOrderLine.get_record_by_pid(order_line_1_1.pid) assert not order_line_1 assert not order_line_1_1 _del_resource(client, 'acac', account_b.pid) _del_resource(client, 'acac', account_a.pid)