diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 08cea835a0b23ef8b2b8d03c0ccb97f58d39d074..e339dbc8eb6b3e2a038f31ead56641068e4ad872 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -176,7 +176,7 @@ code-style: - job: build-testenv optional: true script: - - autopep8 -r --diff --exit-code . + - autopep8 -r --diff --exit-code --max-line-length=100 . allow_failure: true pylint: @@ -188,7 +188,7 @@ pylint: optional: true allow_failure: true script: - - pylint --unsafe-load-any-extension=y -d all -e E,F src/linkahead_python_package_template + - pylint --unsafe-load-any-extension=y -d all -e E,F loanpy/src/loan # Build the sphinx documentation and make it ready for deployment by Gitlab Pages # Special job for serving a static website. See https://docs.gitlab.com/ee/ci/yaml/README.html#pages diff --git a/loanpy/CHANGELOG.md b/loanpy/CHANGELOG.md index dd4609543d8988a0a2bf0f35846a1cc80beb55f1..b85d8ecd53abcbef85ed12bec0db3752c79aeaa4 100644 --- a/loanpy/CHANGELOG.md +++ b/loanpy/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## ### Added ### +* Support for borrowing multiple items ### Changed ### diff --git a/loanpy/README.md b/loanpy/README.md index 4e457007fa2dc3a3b3d241183ef7d2e7e5b69e79..c99c2e2d7abdc5e497ae4022e3d3f33a7b60ccb5 100644 --- a/loanpy/README.md +++ b/loanpy/README.md @@ -22,6 +22,18 @@ If you want to publish the result on PyPi, there is a release.sh helper script. Make sure that your `pyproject.toml` contains the correct and complete metadata and check the `RELEASE_GUIDELINES.md` +## Configuration +You should supply the following configuration settings in you `.pylinkahead.ini` file which is used +by loanpy: +``` +[Misc] +sendmail=/usr/local/bin/sendmail_to_file +entity_loan.curator_mail_from=admin@example.com +entity_loan.curator_mail_to=admin@example.com +[sss_helper] +external_uri='example.com' +``` + ### Unit Tests Run `tox` or alternatively `pytest unittests/`. diff --git a/loanpy/integrationtests/basic_test.py b/loanpy/integrationtests/basic_test.py index 56080918fb59173cbcd67bb390f6922bb6396430..448f0ef02d1952071de3d82fd99e8693002a1030 100644 --- a/loanpy/integrationtests/basic_test.py +++ b/loanpy/integrationtests/basic_test.py @@ -19,31 +19,42 @@ """ basic integration test of the loan workflow """ - -from pytest import fixture import json -import linkahead as db import re +from functools import partial from tempfile import NamedTemporaryFile -from linkahead.utils.server_side_scripting import run_server_side_script -from loan.box_loan import (BORROWER, BOX, COMMENT, DESTINATION, EXHAUST_CONTENTS, LENT, - F_CURRENT_LOCATION, - EXPECTED_RETURN, F_BOX, F_COMMENT, F_DESTINATION,PERSON, LOAN_ACCEPTED, - F_EMAIL, F_EXHAUST_CONTENTS, F_EXPECTED_RETURN_DATE, EMAIL, F_LOAN, - F_FIRST_NAME, F_LAST_NAME, FIRST_NAME, LAST_NAME, LOAN, LOCATION, RETURNED, - RETURN_ACCEPTED, - LOAN_REQUESTED, assert_date_in_future, CONTENT, RETURNLOCATION, - assert_key_in_data, get_box, insert_or_update_person, main, RETURN_REQUESTED, - send_loan_request_mail) +from unittest.mock import patch +import linkahead as db +from caosadvancedtools.serverside.helper import init_data_model +from linkahead.cached import cached_get_entity_by +from linkahead.utils.get_entity import get_entity_by_id, get_entity_by_name +from linkahead.utils.server_side_scripting import run_server_side_script +from loan.accept_loan_request import accept_loan_request +from loan.accept_return_request import accept_return_request +from loan.box_loan import (BORROWER, BOX, BOX_BORROWED, BOX_RETURNED, COMMENT, CONTENT, DESTINATION, + EMAIL, EXHAUST_CONTENTS, EXPECTED_RETURN, F_BOX, F_COMMENT, + F_CURRENT_LOCATION, F_DESTINATION, F_EMAIL, F_EXHAUST_CONTENTS, + F_EXPECTED_RETURN_DATE, F_FIRST_NAME, F_LAST_NAME, F_LOAN, FIRST_NAME, + LAST_NAME, LENT, LOAN, LOAN_ACCEPTED, LOAN_REQUESTED, LOCATION, PERSON, + PROPERTIES, RECORD_TYPES, RETURN_ACCEPTED, RETURN_REQUESTED, RETURNED, + RETURNLOCATION, assert_date_in_future, assert_key_in_data, + insert_or_update_person, main, send_loan_request_mail) +from loan.confirm_loan import confirm_loan +from loan.manual_return import manual_return +from loan.request_loan import issue_loan_request +from loan.request_return import issue_return_request +from pytest import fixture TESTLOANCOMMENT = 'This is a test' TESTRETURNCOMMENT = 'This is a return test' +TESTPROP = 'TESTPROP' TESTBOXNUMBER = '0123 TEST' +TESTBOXNUMBER2 = '0124 TEST' TESTDESTINATION = 'TEST DEST' TESTCURRENTDESTINATION = 'TEST CUR DEST' TESTBORROWEREMAIL = 'test@example.com' -TESTRETURNDATE="2025-03-04" +TESTRETURNDATE = "2025-03-04" TESTFN = "Alice" TESTLN = "Wunderland" @@ -55,50 +66,90 @@ def save_dict_to_jsonfile(data): json.dump(data, fi) return tmp.name + def delete_stuff(): to_be_deleted = db.Container() to_be_deleted.extend(db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'")) to_be_deleted.extend(db.execute_query(f"FIND '{TESTDESTINATION}'")) to_be_deleted.extend(db.execute_query(f"FIND '{TESTCURRENTDESTINATION}'")) to_be_deleted.extend(db.execute_query(f"FIND '{TESTBOXNUMBER}'")) + to_be_deleted.extend(db.execute_query(f"FIND '{TESTBOXNUMBER2}'")) to_be_deleted.extend(db.execute_query(f"FIND person with {EMAIL.name}='{TESTBORROWEREMAIL}'")) - if len(to_be_deleted)>0: + to_be_deleted.extend(db.execute_query(f"FIND Property '{TESTPROP}'")) + if len(to_be_deleted) > 0: to_be_deleted.delete() + @fixture(autouse=True) def prepare(): delete_stuff() db.Record(name=TESTBOXNUMBER).add_parent(BOX.name).insert() + db.Record(name=TESTBOXNUMBER2).add_parent(BOX.name).insert() db.Record(name=TESTDESTINATION).add_parent(LOCATION.name).insert() db.Record(name=TESTCURRENTDESTINATION).add_parent(LOCATION.name).insert() (db.Record().add_parent(PERSON.name).add_property(EMAIL.name, TESTBORROWEREMAIL) .add_property(FIRST_NAME.name, TESTFN) .add_property(LAST_NAME.name, TESTLN).insert()) + db.Property(name=TESTPROP, datatype=db.TEXT).insert() yield delete_stuff() -def test_request_loan(): - - ##### request loan ##### +@fixture() +def base_loan_form_data(): data = {} data[F_EXHAUST_CONTENTS] = False - data[F_EXPECTED_RETURN_DATE]= "2042-02-02" - data[F_BOX]=db.utils.get_entity.get_entity_by_name(TESTBOXNUMBER).id - data[F_COMMENT]=TESTLOANCOMMENT - data[F_DESTINATION]=db.utils.get_entity.get_entity_by_name(TESTDESTINATION).id - data[F_EMAIL]=TESTBORROWEREMAIL - data[F_LAST_NAME]=TESTLN - data[F_FIRST_NAME]=TESTFN - - response = run_server_side_script("loan_management/request_loan.py", - #"pos0", - #option1="val1", - files={"-p0": save_dict_to_jsonfile(data)}, - **{"auth-token":db.get_connection()._authenticator.auth_token}) - assert response.stderr is None + data[F_EXPECTED_RETURN_DATE] = "2042-02-02" + data[F_COMMENT] = TESTLOANCOMMENT + data[F_DESTINATION] = get_entity_by_name(TESTDESTINATION).id + data[F_EMAIL] = TESTBORROWEREMAIL + data[F_LAST_NAME] = TESTLN + data[F_FIRST_NAME] = TESTFN + return data + + +@fixture() +def loan_form_data_single(base_loan_form_data): + base_loan_form_data[F_BOX] = get_entity_by_name(TESTBOXNUMBER).id + return base_loan_form_data + + +@fixture() +def loan_form_data_multi(base_loan_form_data): + base_loan_form_data[F_BOX] = [ + str(get_entity_by_name(TESTBOXNUMBER).id), # strings are allowed + get_entity_by_name(TESTBOXNUMBER2).id] + return base_loan_form_data + + +def get_return_request_data(loanid): + data = {} + data[F_LOAN] = loanid + # TODO: this is writen in the content field. Change name? + data[F_COMMENT] = TESTRETURNCOMMENT + # TODO: this is written in RETURNLOCATION. Change name? + data[F_CURRENT_LOCATION] = get_entity_by_name(TESTCURRENTDESTINATION).id + data[F_EXPECTED_RETURN_DATE] = TESTRETURNDATE + data[F_EMAIL] = TESTBORROWEREMAIL + data[F_LAST_NAME] = TESTLN + data[F_FIRST_NAME] = TESTFN + return data + + +def test_request_loan(loan_form_data_single): + data = loan_form_data_single + + # #### request loan ##### # + response = run_server_side_script( + "loan_management/request_loan.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(data)}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) + assert response.stderr is None, response.stderr+response.stdout assert response.code == 0 loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) + loanid = loan.id assert loan.get_property(f"{BOX.name}").value == data[F_BOX] assert loan.get_property(f"{LOAN_REQUESTED.name}").value.startswith("20") assert loan.get_property(f"{BORROWER.name}").value == db.execute_query( @@ -109,57 +160,152 @@ def test_request_loan(): assert loan.get_property(f"{LOAN_ACCEPTED.name}") is None assert loan.get_property(f"{LENT.name}") is None - ##### accept loan ##### + # #### accept loan ##### # data = {} - data[F_LOAN] = loan.id - response = run_server_side_script("loan_management/accept_loan_request.py", - #"pos0", - #option1="val1", - files={"-p0": save_dict_to_jsonfile(data)}, - **{"auth-token":db.get_connection()._authenticator.auth_token}) + data[F_LOAN] = loanid + response = run_server_side_script( + "loan_management/accept_loan_request.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(data)}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) assert response.stderr is None, response.stderr assert response.code == 0 - loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) + loan = get_entity_by_id(loanid) assert loan.get_property(f"{LOAN_ACCEPTED.name}").value.startswith("20") assert loan.get_property(f"{LENT.name}") is None assert loan.get_property(f"{RETURN_REQUESTED.name}") is None - ##### confirm loan ##### + # #### confirm loan #### # data = {} - data[F_LOAN] = loan.id - response = run_server_side_script("loan_management/confirm_loan.py", - #"pos0", - #option1="val1", - files={"-p0": save_dict_to_jsonfile(data)}, - **{"auth-token":db.get_connection()._authenticator.auth_token}) + data[F_LOAN] = loanid + response = run_server_side_script( + "loan_management/confirm_loan.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(data)}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) + assert response.stderr is None, response.stderr + assert response.code == 0 + loan = get_entity_by_id(loanid) + assert loan.get_property(f"{LENT.name}").value.startswith("20") + + # #### request return #### # + response = run_server_side_script( + "loan_management/request_return.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(get_return_request_data(loan.id))}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) + assert response.stderr is None, response.stderr + assert response.code == 0 + loan = get_entity_by_id(loanid) + assert loan.get_property(f"{LOAN_ACCEPTED.name}").value.startswith("20") + assert loan.get_property(f"{CONTENT.name}").value == TESTRETURNCOMMENT + assert loan.get_property(f"{EXPECTED_RETURN.name}").value == TESTRETURNDATE + assert loan.get_property( + f"{RETURNLOCATION.name}").value == get_entity_by_name(TESTCURRENTDESTINATION).id + assert loan.get_property(f"{RETURN_REQUESTED.name}").value.startswith("20") + assert loan.get_property(f"{RETURN_ACCEPTED.name}") is None + # TODO test change of borrower + + # accept return + data = {} + data[F_LOAN] = loanid + response = run_server_side_script( + "loan_management/accept_return_request.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(data)}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) + assert response.stderr is None, response.stderr + assert response.code == 0 + loan = get_entity_by_id(loanid) + assert loan.get_property(f"{RETURN_ACCEPTED.name}").value.startswith("20") + assert loan.get_property(f"{RETURNED.name}") is None + + # manual return + data = {} + data[F_LOAN] = loanid + response = run_server_side_script( + "loan_management/manual_return.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(data)}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) assert response.stderr is None assert response.code == 0 + loan = get_entity_by_id(loanid) + assert loan.get_property(f"{RETURNED.name}").value.startswith("20") + + +def test_request_loan_multiple_items(loan_form_data_multi): + + # #### request loan #### # + data = loan_form_data_multi + box_ids = loan_form_data_multi[F_BOX] + box_versionids = [get_entity_by_id(eid=bid).get_versionid() for bid in box_ids] + + response = run_server_side_script( + "loan_management/request_loan.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(data)}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) + assert response.stderr is None, response.stderr + assert response.code == 0 loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) - assert loan.get_property(f"{LENT.name}").value.startswith("20") + loanid = loan.id + # We need to cast data[F_BOX] to int, since we may have strings in + # the form. + assert loan.get_property(f"{BOX.name}").value == [int(bid) for bid in data[F_BOX]] - ##### request return ##### + # #### accept loan #### # data = {} data[F_LOAN] = loan.id - # TODO: this is writen in the content field. Change name? - data[F_COMMENT] = TESTRETURNCOMMENT - # TODO: this is written in RETURNLOCATION. Change name? - data[F_CURRENT_LOCATION]=db.utils.get_entity.get_entity_by_name(TESTCURRENTDESTINATION).id - data[F_EXPECTED_RETURN_DATE] = TESTRETURNDATE - data[F_EMAIL]=TESTBORROWEREMAIL - data[F_LAST_NAME]=TESTLN - data[F_FIRST_NAME]=TESTFN - response = run_server_side_script("loan_management/request_return.py", - #"pos0", - #option1="val1", - files={"-p0": save_dict_to_jsonfile(data)}, - **{"auth-token":db.get_connection()._authenticator.auth_token}) + response = run_server_side_script( + "loan_management/accept_loan_request.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(data)}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) assert response.stderr is None, response.stderr assert response.code == 0 - loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) + loan = get_entity_by_id(loanid) assert loan.get_property(f"{LOAN_ACCEPTED.name}").value.startswith("20") - assert loan.get_property(f"{CONTENT.name}").value == TESTRETURNCOMMENT - assert loan.get_property(f"{EXPECTED_RETURN.name}").value == TESTRETURNDATE - assert loan.get_property(f"{RETURNLOCATION.name}").value == db.utils.get_entity.get_entity_by_name(TESTCURRENTDESTINATION).id + + # #### confirm loan #### # + data = {} + data[F_LOAN] = loan.id + response = run_server_side_script( + "loan_management/confirm_loan.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(data)}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) + assert response.stderr is None, response.stderr + assert response.code == 0 + loan = get_entity_by_id(loanid) + assert loan.get_property(BOX_BORROWED).value == box_versionids + assert loan.get_property(f"{LENT.name}").value.startswith("20") + box1 = get_entity_by_name(TESTBOXNUMBER) + box2 = get_entity_by_name(TESTBOXNUMBER2) + assert box1.get_property(f"{LOCATION.name}").value == loan.get_property( + f"{DESTINATION.name}").value + assert box2.get_property(f"{LOCATION.name}").value == loan.get_property( + f"{DESTINATION.name}").value + + # #### request return #### # + data = get_return_request_data(loan.id) + response = run_server_side_script( + "loan_management/request_return.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(data)}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) + assert response.stderr is None, response.stderr+response.stdout + assert response.code == 0 + loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) assert loan.get_property(f"{RETURN_REQUESTED.name}").value.startswith("20") assert loan.get_property(f"{RETURN_ACCEPTED.name}") is None # TODO test change of borrower @@ -167,26 +313,170 @@ def test_request_loan(): # accept return data = {} data[F_LOAN] = loan.id - response = run_server_side_script("loan_management/accept_return_request.py", - #"pos0", - #option1="val1", - files={"-p0": save_dict_to_jsonfile(data)}, - **{"auth-token":db.get_connection()._authenticator.auth_token}) + response = run_server_side_script( + "loan_management/accept_return_request.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(data)}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) assert response.stderr is None, response.stderr assert response.code == 0 loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) assert loan.get_property(f"{RETURN_ACCEPTED.name}").value.startswith("20") assert loan.get_property(f"{RETURNED.name}") is None + box1 = get_entity_by_name(TESTBOXNUMBER) + box2 = get_entity_by_name(TESTBOXNUMBER2) + assert box1.get_property(f"{LOCATION.name}").value == loan.get_property( + f"{RETURNLOCATION.name}").value + assert box2.get_property(f"{LOCATION.name}").value == loan.get_property( + f"{RETURNLOCATION.name}").value # manual return data = {} data[F_LOAN] = loan.id - response = run_server_side_script("loan_management/manual_return.py", - #"pos0", - #option1="val1", - files={"-p0": save_dict_to_jsonfile(data)}, - **{"auth-token":db.get_connection()._authenticator.auth_token}) - assert response.stderr is None + response = run_server_side_script( + "loan_management/manual_return.py", + # "pos0", + # option1="val1", + files={"-p0": save_dict_to_jsonfile(data)}, + **{"auth-token": db.get_connection()._authenticator.auth_token}) + assert response.stderr is None, response.stderr assert response.code == 0 loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) assert loan.get_property(f"{RETURNED.name}").value.startswith("20") + + +def test_direct_call(loan_form_data_multi): + init_data_model(RECORD_TYPES + PROPERTIES) + data = loan_form_data_multi + test_prop = cached_get_entity_by(name=TESTPROP) + box_ids = loan_form_data_multi[F_BOX] + box_initial_versionids = [get_entity_by_id(eid=bid).get_versionid() for bid in box_ids] + issue_loan_request(data) + loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) + loanid = loan.id + # We need to cast data[F_BOX] to int, since we may have strings in + # the form. + assert loan.get_property(f"{BOX.name}").value == [int(bid) for bid in data[F_BOX]] + + loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) + data = {} + data[F_LOAN] = loan.id + accept_loan_request(data) + loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) + assert loan.get_property(f"{LOAN_ACCEPTED.name}").value.startswith("20") + + # make changes to the boxes after the first request and before loan is confirmed + for bid in box_ids: + box = get_entity_by_id(eid=bid) + box.add_property(id=test_prop.id, value='a') + box.update() + box_before_confirm_versionids = [get_entity_by_id(eid=bid).get_versionid() for bid in box_ids] + + data = {} + data[F_LOAN] = loan.id + confirm_loan(data) + loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) + assert loan.get_property(BOX_BORROWED).value == box_before_confirm_versionids + assert loan.get_property(f"{LENT.name}").value.startswith("20") + box1 = get_entity_by_name(TESTBOXNUMBER) + box2 = get_entity_by_name(TESTBOXNUMBER2) + assert box1.get_property(f"{LOCATION.name}").value == loan.get_property( + f"{DESTINATION.name}").value + assert box2.get_property(f"{LOCATION.name}").value == loan.get_property( + f"{DESTINATION.name}").value + + data = get_return_request_data(loan.id) + issue_return_request(data) + loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) + assert loan.get_property(f"{RETURN_REQUESTED.name}").value.startswith("20") + assert loan.get_property(f"{RETURN_ACCEPTED.name}") is None + + data = {} + data[F_LOAN] = loan.id + accept_return_request(data) + loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) + assert loan.get_property(f"{RETURN_ACCEPTED.name}").value.startswith("20") + assert loan.get_property(f"{RETURNED.name}") is None + box1 = get_entity_by_name(TESTBOXNUMBER) + box2 = get_entity_by_name(TESTBOXNUMBER2) + assert box1.get_property(f"{LOCATION.name}").value == loan.get_property( + f"{RETURNLOCATION.name}").value + assert box2.get_property(f"{LOCATION.name}").value == loan.get_property( + f"{RETURNLOCATION.name}").value + + # make changes to the boxes before manual return + for bid in box_ids: + box = get_entity_by_id(eid=bid) + box.add_property(id=test_prop.id, value='a') + box.update() + box_before_return_versionids = [get_entity_by_id(eid=bid).get_versionid() for bid in box_ids] + + data = {} + data[F_LOAN] = loan.id + manual_return(data) + loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) + assert loan.get_property(f"{RETURNED.name}").value.startswith("20") + assert loan.get_property(f"{RETURNED.name}").value.startswith("20") + assert loan.get_property(BOX_BORROWED).value == box_before_confirm_versionids + assert loan.get_property(BOX_RETURNED).value == box_before_return_versionids + + +def modded_config(config): + config = dict(config) + print("changing original config") + config["Misc"]["entity_loan.no_location_updates"] = "True" + return config + + +@patch('linkahead.get_config', new=partial(modded_config, db.get_config())) +def test_no_location_setting(loan_form_data_multi): + init_data_model(RECORD_TYPES + PROPERTIES) + data = loan_form_data_multi + box_ids = loan_form_data_multi[F_BOX] + box_versionids = [get_entity_by_id(eid=bid).get_versionid() for bid in box_ids] + + issue_loan_request(data) + loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True) + loanid = loan.id + + data = {} + data[F_LOAN] = loanid + accept_loan_request(data) + + data = {} + data[F_LOAN] = loanid + confirm_loan(data) + loan = get_entity_by_id(loanid) + assert loan.get_property(BOX_BORROWED).value == box_versionids + assert loan.get_property(f"{LENT.name}").value.startswith("20") + box1 = get_entity_by_name(TESTBOXNUMBER) + box2 = get_entity_by_name(TESTBOXNUMBER2) + assert box1.get_property(f"{LOCATION.name}") is None + assert box2.get_property(f"{LOCATION.name}") is None + + data = get_return_request_data(loan.id) + issue_return_request(data) + loan = get_entity_by_id(loanid) + assert loan.get_property(f"{RETURN_REQUESTED.name}").value.startswith("20") + assert loan.get_property(f"{RETURN_ACCEPTED.name}") is None + + data = {} + data[F_LOAN] = loan.id + accept_return_request(data) + loan = get_entity_by_id(loanid) + assert loan.get_property(f"{RETURN_ACCEPTED.name}").value.startswith("20") + box1 = get_entity_by_name(TESTBOXNUMBER) + box2 = get_entity_by_name(TESTBOXNUMBER2) + assert box1.get_property(f"{LOCATION.name}") is None + assert box2.get_property(f"{LOCATION.name}") is None + + data = {} + data[F_LOAN] = loan.id + manual_return(data) + loan = get_entity_by_id(loanid) + assert loan.get_property(f"{RETURNED.name}").value.startswith("20") + box1 = get_entity_by_name(TESTBOXNUMBER) + box2 = get_entity_by_name(TESTBOXNUMBER2) + assert box1.get_property(f"{LOCATION.name}") is None + assert box2.get_property(f"{LOCATION.name}") is None diff --git a/loanpy/pyproject.toml b/loanpy/pyproject.toml index 5379cb78bc17bd8bae3ca0af9546a701d4ca12e4..0e4f0696632bcc6f833eeeec8b05cf4db47c09c2 100644 --- a/loanpy/pyproject.toml +++ b/loanpy/pyproject.toml @@ -25,7 +25,7 @@ requires-python = ">= 3.8" dependencies = [ "caosadvancedtools", "py3-validate-email", - "linkahead" + "linkahead@git+https://gitlab.indiscale.com/caosdb/src/caosdb-pylib.git@dev" ] [project.urls] diff --git a/loanpy/src/loan/accept_loan_request.py b/loanpy/src/loan/accept_loan_request.py index b1f521a0dcbedf14528a9209c67fc7fc93a2de75..79feca34b93f4c9e57b9fe85cf73094ca1b1d3a5 100755 --- a/loanpy/src/loan/accept_loan_request.py +++ b/loanpy/src/loan/accept_loan_request.py @@ -21,11 +21,12 @@ Accept a loan request. """ from __future__ import absolute_import + import caosdb as db -from caosadvancedtools.serverside.helper import print_success, get_timestamp -from .box_loan import (main, get_loan, F_LOAN,update_loan_state, - LOAN_ACCEPTED, S_LOAN_ACCEPTED, S_LOAN_REQUESTED, - get_borrower_names, set_property) +from caosadvancedtools.serverside.helper import get_timestamp, print_success + +from .box_loan import (F_LOAN, LOAN_ACCEPTED, S_LOAN_ACCEPTED, S_LOAN_REQUESTED, get_borrower_names, + get_loan, main, set_property, update_loan_state) def _accept_loan_request(loan): diff --git a/loanpy/src/loan/accept_return_request.py b/loanpy/src/loan/accept_return_request.py index 5a2ea7acce2d731016256c8cc3f5e108a8cc023b..d12516cf2044071315d0a045725e94b45a64e76e 100755 --- a/loanpy/src/loan/accept_return_request.py +++ b/loanpy/src/loan/accept_return_request.py @@ -22,30 +22,33 @@ Accept a return request. """ # @review Timm Fitschen 2022-03-16 from __future__ import absolute_import + import caosdb as db -from caosadvancedtools.serverside.helper import print_success, get_timestamp -from .box_loan import (BOX_BORROWED, CONTENT, main, get_loan, set_property, - F_LOAN, RETURN_ACCEPTED,update_loan_state, - RETURNLOCATION, S_RETURN_ACCEPTED, S_RETURN_REQUESTED, - get_borrower_names, set_location_of_borrowed_items) +from caosadvancedtools.serverside.helper import get_timestamp, print_success + +from .box_loan import (BOX, CONTENT, F_LOAN, RETURN_ACCEPTED, RETURNLOCATION, + S_RETURN_ACCEPTED, S_RETURN_REQUESTED, get_borrower_names, get_loan, main, + set_location_of_borrowed_items, set_property, update_loan_state) -def _accept_return_request(loan): +def _accept_return_request(loan, location_updates=True, content_updates=True): """Update a loan Record and add the `accepted` Property.""" # This changes the state from "return_requested" to "return_accepted". update_loan_state(loan, S_RETURN_ACCEPTED) - items = set_location_of_borrowed_items(loan, RETURNLOCATION) - if loan.get_property(CONTENT) is not None and loan.get_property(CONTENT).value: - for item in items: - if item.get_property(CONTENT) is not None: - item.get_property(CONTENT).value = loan.get_property(CONTENT).value - else: - item.add_property(id=CONTENT.retrieve().id, - value=loan.get_property(CONTENT).value) + if location_updates: + items = set_location_of_borrowed_items(loan, RETURNLOCATION) + if (content_updates and loan.get_property(CONTENT) is not None and + loan.get_property(CONTENT).value): + for item in items: + if item.get_property(CONTENT) is not None: + item.get_property(CONTENT).value = loan.get_property(CONTENT).value + else: + item.add_property(id=CONTENT.id, + value=loan.get_property(CONTENT).value) - items.append(loan) - items.update() + items.update() + loan.update() def accept_return_request(data): @@ -54,9 +57,18 @@ def accept_return_request(data): I.e.add the `returnAccepted` Property to the `Loan` Record. """ loan = get_loan(data[F_LOAN]) - _accept_return_request(loan) + + config = db.get_config() + location_updates = (not ("Misc" in config + and "entity_loan.no_location_updates" in config["Misc"] + and config["Misc"]["entity_loan.no_location_updates"])) + content_updates = (not ("Misc" in config and "entity_loan.no_content_updates" in config["Misc"] + and config["Misc"]["entity_loan.no_content_updates"])) + _accept_return_request(loan, location_updates=location_updates) fn, ln = get_borrower_names(loan) - box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] + box_id = loan.get_property(BOX).value + if not isinstance(box_id, list): + box_id = [box_id] print_success('Thank you for accepting the return request by {fn} {ln}.<br>' 'If necessary, location and content of the box have been ' @@ -64,7 +76,9 @@ def accept_return_request(data): '<a href="/Entity/{bid}" title="Reload this page.">Reload</a> ' 'to view the new Location.<br>' 'See <a href="{loan}" title="Go to the accepted loan ' - 'request.">here</a>'.format(fn=fn, ln=ln, loan=loan.id, bid=box_id)) + 'request.">here</a>'.format( + fn=fn, ln=ln, loan=loan.id, + bid="&".join([str(el) for el in box_id]))) return 0 diff --git a/loanpy/src/loan/box_loan.py b/loanpy/src/loan/box_loan.py index fea71720cb5e801d90270da1c8a2520ba6dd9eae..f810c495c98bad87497831cf7228779f1bbb355e 100644 --- a/loanpy/src/loan/box_loan.py +++ b/loanpy/src/loan/box_loan.py @@ -27,19 +27,22 @@ import sys import traceback from datetime import datetime -import caosdb as db -from caosadvancedtools.serverside.helper import (DataModelError, get_data, - init_data_model,get_timestamp, - parse_arguments, print_error, - print_info, print_warning, - send_mail) +import linkahead as db +from caosadvancedtools.serverside.helper import (DataModelError, get_data, get_timestamp, + init_data_model, parse_arguments, print_error, + print_info, print_warning, send_mail) from caosadvancedtools.serverside.logging import configure_server_side_logging +from linkahead.cached import cached_get_entity_by +from linkahead.common.models import get_id_from_versionid, value_matches_versionid from linkahead.exceptions import EmptyUniqueQueryError +from linkahead.utils.get_entity import get_entity_by_id from validate_email import validate_email + from .conf import * LOGGER_NAME = "box_loan" LOGGER = logging.getLogger(LOGGER_NAME) +LOGGER.addHandler(logging.StreamHandler(stream=sys.stdout)) # Form names F_BOX = "box" @@ -147,8 +150,8 @@ class StateError(RuntimeError): "This transition of the loan state is not possible. The allowed target " "state is {one}'{expected}' " "while the actual target state is '{actual}'.".format(one=one, - actual=actual, - expected=_expected)) + actual=actual, + expected=_expected)) class EmailPatternError(RuntimeError): @@ -192,52 +195,38 @@ def assert_date_in_future(actual, msg=None): raise DataError(_msg) -def get_record_by_id(parent, entity_id): - """ Retrieve a record with a particular parent by id. """ - - return db.execute_query("FIND RECORD {} WITH ID = {}".format(parent, - entity_id), - unique=True) - - -def get_box(box): - """ Retrieve a box record by id. """ - - return get_record_by_id(BOX.name, box) - - def get_loan(loan_id): """ Retrieve a loan record by id. """ - return get_record_by_id(LOAN.name, loan_id) + return get_entity_by_id(loan_id) def update_loan_state(loan, new_state): old_state = get_loan_state(loan) - if old_state==S_LOAN_REQUESTED: - if new_state==S_LOAN_ACCEPTED: + if old_state == S_LOAN_REQUESTED: + if new_state == S_LOAN_ACCEPTED: loan.add_property(LOAN_ACCEPTED, get_timestamp()) else: raise StateError(new_state, [S_LOAN_ACCEPTED]) - elif old_state==S_LOAN_ACCEPTED: - if new_state==S_LENT: + elif old_state == S_LOAN_ACCEPTED: + if new_state == S_LENT: loan.add_property(LENT, get_timestamp()) else: raise StateError(new_state, [S_LENT]) - elif old_state==S_LENT: - if new_state==S_RETURN_REQUESTED: + elif old_state == S_LENT: + if new_state == S_RETURN_REQUESTED: loan.add_property(RETURN_REQUESTED, get_timestamp()) else: raise StateError(new_state, [S_RETURN_REQUESTED]) - elif old_state==S_RETURN_REQUESTED: - if new_state==S_RETURN_ACCEPTED: + elif old_state == S_RETURN_REQUESTED: + if new_state == S_RETURN_ACCEPTED: loan.add_property(RETURN_ACCEPTED, get_timestamp()) - elif new_state==S_LENT: + elif new_state == S_LENT: loan.remove_property(RETURN_REQUESTED) else: raise StateError(new_state, [S_RETURN_ACCEPTED, S_LENT]) - elif old_state==S_RETURN_ACCEPTED: - if new_state==S_RETURNED: + elif old_state == S_RETURN_ACCEPTED: + if new_state == S_RETURNED: loan.add_property(RETURNED, get_timestamp()) else: raise StateError(new_state, [S_RETURNED]) @@ -297,7 +286,7 @@ def get_borrower_names(loan): fn = "an" ln = "unknown borrower" else: - borrower = db.Record(id=borrower_property.value).retrieve() + borrower: db.Record = db.Record(id=borrower_property.value).retrieve() fn = borrower.get_property(FIRST_NAME.name).value ln = borrower.get_property(LAST_NAME.name).value @@ -360,27 +349,39 @@ def get_external_server_uri(): raise RuntimeError("Could not determine the external server uri") +def _create_items_description_list(item_ids): + description = "" + for iid in item_ids: + item = get_entity_by_id(iid) + description += (f"{item.parents[0].name}: " + f"{get_property_value(item, BOX_NUMBER, 'UNKNOWN')}" + f" - {iid}\n") + + return description + + def send_loan_request_mail(data, borrower, loan): try: - boxid = str(loan.get_property(BOX).value).split("@")[0] - box = get_box(boxid) - boxnumber = get_property_value(box, BOX_NUMBER, "UNKNOWN") - - link = "{uri}Entity/{boxid}&{loanid}".format( + item_ids = loan.get_property(BOX).value + if not isinstance(item_ids, list): + item_ids = [item_ids] + description = _create_items_description_list(item_ids) + link = "{uri}Entity/{item_ids}&{loanid}".format( uri=get_external_server_uri(), - boxid=boxid, + item_ids="&".join([str(i) for i in item_ids]), loanid=loan.id) body = """Dear Curator, - a new loan has been request by {borrower} for box number {boxnumber}. + a new loan has been request by {borrower} for the following items: + {items}. Loan request: {data} View box and loan records: {link} - """.format(boxnumber=boxnumber, borrower=get_requester_string(borrower), + """.format(items=description, borrower=get_requester_string(borrower), data=data, link=link) send_mail( @@ -389,30 +390,34 @@ def send_loan_request_mail(data, borrower, loan): subject="loan request", body=body) print_info("An email has been sent to the responsible box curator.") - except BaseException as e: + except Exception as e: print_error("""Sending an email to the responsible box curator failed for an unknown reason. Please inform the box curator by yourself about your loan request and that the email sending failed. Thank you very much for your -help.""") +help."""+str(e)) LOGGER.error(e) def send_return_request_mail(data, returner, loan): try: - boxid = str(loan.get_property(BOX).value).split("@")[0] - box = get_box(boxid) - boxnumber = get_property_value(box, BOX_NUMBER, "UNKNOWN") + item_ids = loan.get_property(BOX).value + if not isinstance(item_ids, list): + item_ids = [item_ids] + description = _create_items_description_list(item_ids) current_location = data[F_CURRENT_LOCATION] - link = "{uri}Entity/{boxid}&{loanid}".format( + link = "{uri}Entity/{item_ids}&{loanid}".format( uri=get_external_server_uri(), - boxid=boxid, + item_ids="&".join([str(i) for i in item_ids]), loanid=loan.id) body = """Dear Curator, - a new return request by {returner} for box number {boxnumber} is pending. + a new return request by {returner} for the following items is pending: + {items} + + The current location of the box according to the return request is "{current_location}". @@ -421,7 +426,7 @@ def send_return_request_mail(data, returner, loan): View box and loan records: {link} - """.format(boxnumber=boxnumber, returner=get_requester_string(returner), + """.format(items=description, returner=get_requester_string(returner), data=data, link=link, current_location=current_location) send_mail( @@ -430,13 +435,14 @@ def send_return_request_mail(data, returner, loan): subject="return request", body=body) print_info("An email has been sent to the responsible box curator.") - except BaseException as e: + except Exception as e: + tb = traceback.format_exc() print_error("""Sending an email to the responsible box curator failed for an unknown reason. Please inform the box curator by yourself about this return request and that the email sending failed. Thank you very much for your -help.""") +help."""+str(e)+str(tb)) LOGGER.error(e) @@ -459,15 +465,13 @@ def query_person(firstname, lastname, email): 2. or the first name and the last name """ - query=(f'FIND RECORD {PERSON.name} ' + query = (f'FIND RECORD {PERSON.name} ' f'WITH {EMAIL.name} = "{email}" ' f'OR ({FIRST_NAME.name} = "{firstname}" AND {LAST_NAME.name} = "{lastname}") ' ) return db.execute_query(query, unique=True) - - def _update_person(person, firstname, lastname, email): """Update the persons names and email address. @@ -548,7 +552,8 @@ def _caller(func, args): return func(data) -def get_borrowed_items_from(loan)-> db.Container: + +def get_borrowed_items_from(loan) -> db.Container: """Retrieves and returns the Records of items that were borrowed Paramters @@ -569,10 +574,12 @@ def get_borrowed_items_from(loan)-> db.Container: borrowed = [borrowed.value] else: borrowed = borrowed.value - recs = db.Container().extend([db.Record(id=rec_id.split("@")[0]) for rec_id in borrowed]) + recs = db.Container().extend([ + db.Record(id=get_id_from_versionid(rec_id)) for rec_id in borrowed]) return recs.retrieve() -def set_location_of_borrowed_items(loan, kind)-> list(db.Record): + +def set_location_of_borrowed_items(loan, kind) -> list(db.Record): """Retrieve Records of borrowed items, sets the location and returns the container Parameters @@ -581,12 +588,38 @@ def set_location_of_borrowed_items(loan, kind)-> list(db.Record): """ if loan.get_property(kind) is None: raise RuntimeError( - f"Cannot set {kind} because the loan does not have one. Loan ID: {laon.id}") + f"Cannot set {kind} because the loan does not have one. Loan ID: {loan.id}") borrowed = get_borrowed_items_from(loan) for item in borrowed: set_property(item, LOCATION, loan.get_property(kind).value) return borrowed + +def set_references_to_current_version(value): + """ + creates a reference value from a given value by replacing all references with such that point + to the current version of the respecive entity. + + If the value is a list, then this is done for all references in the list and thus a list is + returned. + + Returns + ------- + Union[list, str]: the new value + """ + islist = isinstance(value, list) + if not islist: + value = [value] + + b_items = [] + for item in value: + bid = get_id_from_versionid(item) if value_matches_versionid(item) else item + b_items.append(str(bid)+"@HEAD") + if not islist: + b_items = b_items[0] + return b_items + + def main(func, args=None): # configure_server_side_logging(LOGGER_NAME) try: @@ -601,11 +634,11 @@ def main(func, args=None): print_error(e.args[0]) code = 4 LOGGER.debug(e) - except BaseException as e: # pylint: disable=W0703 + except Exception as e: # pylint: disable=W0703 tb = traceback.format_exc() print_error("An unknown error occured." "<br>{}<code>{}</code>".format(str(e), tb)) code = 1 - LOGGER.debug("Unknown error!!!", e) + LOGGER.debug("Unknown error!!!") LOGGER.info("exiting with code {}".format(code)) sys.exit(code) diff --git a/loanpy/src/loan/conf.py b/loanpy/src/loan/conf.py index a2eeb2ec78ed6ea64ede3a9a7d5d27dd466e1288..16d4bc7b8a30c1711f23eb02bf2ef888286c7490 100644 --- a/loanpy/src/loan/conf.py +++ b/loanpy/src/loan/conf.py @@ -1,4 +1,5 @@ import linkahead as db + # RecordTypes BOX = db.RecordType(name="Box") PERSON = db.RecordType(name="Person") diff --git a/loanpy/src/loan/confirm_loan.py b/loanpy/src/loan/confirm_loan.py index d2c69b05fa9d22090f13e97a5439f81ae8f352ab..9e5b9825e6de5cdf3f92a7740e18bd9e6e0646b8 100755 --- a/loanpy/src/loan/confirm_loan.py +++ b/loanpy/src/loan/confirm_loan.py @@ -22,29 +22,30 @@ Confirm a loan. """ from __future__ import absolute_import -import caosdb as db +import linkahead as db from caosadvancedtools.serverside.helper import get_timestamp, print_success -from .box_loan import (BOX, BOX_BORROWED, DESTINATION, F_LOAN, LENT, S_LENT, - S_LOAN_ACCEPTED, get_borrower_names,update_loan_state, - get_loan, main, set_location_of_borrowed_items, set_property) +from .box_loan import (BOX, BOX_BORROWED, DESTINATION, F_LOAN, LENT, S_LENT, S_LOAN_ACCEPTED, + get_borrower_names, get_loan, main, set_location_of_borrowed_items, + set_property, set_references_to_current_version, update_loan_state) def _set_lent_box(loan): - """Set the box version to HEAD. - - This stores the version of the box when it was delivered to the borrower. - """ - # TODO: Adapt datamodel and remove name override - box_prop = loan.get_property(BOX) - box_prop.name = BOX_BORROWED - box_prop.value = str(box_prop.value) + "@HEAD" + """ Store the exact versions of borrowed items when it was delivered to the borrower. """ + references = set_references_to_current_version(loan.get_property(BOX).value) + loan.add_property(id=BOX.id, name=BOX_BORROWED, value=references, + datatype=db.LIST(BOX.name) if isinstance(references, list) else BOX.name) + loan.update() def set_loan_location(loan): - """Set the location of the box to the return location of the loan. """ - items = set_location_of_borrowed_items(loan, DESTINATION) - items.update() + """Set the location of the box to the return location of the loan if configured.""" + + config = db.get_config() + if not ("Misc" in config and "entity_loan.no_location_updates" in config["Misc"] + and config["Misc"]["entity_loan.no_location_updates"]): + items = set_location_of_borrowed_items(loan, DESTINATION) + items.update() def _confirm_loan(data): @@ -58,10 +59,6 @@ def _confirm_loan(data): # updates the box location set_loan_location(loan) - db.Container().extend([ - loan - ]).update() - return loan @@ -73,7 +70,10 @@ def confirm_loan(data): loan = _confirm_loan(data) fn, ln = get_borrower_names(loan) loan = get_loan(data[F_LOAN]) - box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] + box_id = loan.get_property(BOX).value + + if not isinstance(box_id, list): + box_id = [box_id] print_success('Thank you for confirming the loan request by {fn} {ln}.<br>' 'The Location of the Box was updated. ' '<a href="/Entity/{bid}" title="Reload this page.">Reload</a> ' @@ -81,7 +81,7 @@ def confirm_loan(data): 'You can also checkout the new loan state ' '<a href="{loan}" title="Go to the confirmed loan ' 'record.">here</a>'.format(fn=fn, ln=ln, loan=loan.id, - bid=box_id)) + bid="&".join([str(el) for el in box_id]))) return 0 diff --git a/loanpy/src/loan/manual_return.py b/loanpy/src/loan/manual_return.py index 1fe79b19a655d9dc37e429e435509c1661f52e58..a2acab7bcb46a71c5a5419c66b1b604ad90e9047 100755 --- a/loanpy/src/loan/manual_return.py +++ b/loanpy/src/loan/manual_return.py @@ -25,10 +25,10 @@ from __future__ import absolute_import import caosdb as db from caosadvancedtools.serverside.helper import get_timestamp, print_success -from .box_loan import (BOX, BOX_BORROWED, BOX_RETURNED, CONTENT, F_LOAN, - RETURNED, RETURNLOCATION, S_RETURN_ACCEPTED, S_RETURNED, - get_borrower_names, get_loan, main,update_loan_state, - set_location_of_borrowed_items, set_property) +from .box_loan import (BOX, BOX_BORROWED, BOX_RETURNED, CONTENT, F_LOAN, RETURNED, RETURNLOCATION, + S_RETURN_ACCEPTED, S_RETURNED, get_borrower_names, get_loan, main, + set_location_of_borrowed_items, set_property, + set_references_to_current_version, update_loan_state) def _set_returned_box(loan): @@ -36,25 +36,34 @@ def _set_returned_box(loan): This stores the version of the box that was returned by a borrower. """ - box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] - # TODO: Adapt datamodel and remove name override - loan.add_property(property=BOX, - name=BOX_RETURNED, - value=box_id + "@HEAD") + references = set_references_to_current_version(loan.get_property(BOX_BORROWED).value) + loan.add_property(id=BOX.id, name=BOX_RETURNED, value=references, + datatype=db.LIST(BOX.name) if isinstance(references, list) else BOX.name) def set_return_location(loan): """Set the location of the box to the return location of the loan. """ - items = set_location_of_borrowed_items(loan, RETURNLOCATION) - items.update() + config = db.get_config() + if (not ("Misc" in config and "entity_loan.no_location_updates" in config["Misc"] + and config["Misc"]["entity_loan.no_location_updates"])): + items = set_location_of_borrowed_items(loan, RETURNLOCATION) + items.update() def set_content(loan): """Set the content to the box to the one given in the loan. """ - box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] - box = db.Record(id=box_id).retrieve() - set_property(box, CONTENT, loan.get_property(CONTENT).value) - box.update() + config = db.get_config() + if ("Misc" in config + and "entity_loan.no_content_updates" in config["Misc"] + and config["Misc"]["entity_loan.no_content_updates"]): + return + items = loan.get_property(BOX_BORROWED).value + if not isinstance(items, list): + items = [items] + for item in items: + box = db.Record(id=item).retrieve() + set_property(box, CONTENT, loan.get_property(CONTENT).value) + box.update() def _manual_return(data): @@ -87,12 +96,14 @@ def manual_return(data): loan = _manual_return(data) fn, ln = get_borrower_names(loan) loan = get_loan(data[F_LOAN]) - box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] + box_id = loan.get_property(BOX).value + if not isinstance(box_id, list): + box_id = [box_id] print_success('The box borrowed by {fn} {ln} '.format(fn=fn, ln=ln) + 'has been returned and the Box Location has been updated.' '<a href="/Entity/{bid}" title="Reload this page.">Reload</a> ' - 'to view the new Location.<br>'.format(bid=box_id)) + 'to view the new Location.<br>'.format(bid="&".join([str(el) for el in box_id]))) return 0 diff --git a/loanpy/src/loan/reject_return_request.py b/loanpy/src/loan/reject_return_request.py index c381f18b3b5cb2818353ec9afc2a6433c12c7802..fcbc365b8e858eaa4a24328d314169f433305670 100755 --- a/loanpy/src/loan/reject_return_request.py +++ b/loanpy/src/loan/reject_return_request.py @@ -21,11 +21,12 @@ Reject a return request. """ from __future__ import absolute_import + import caosdb as db from caosadvancedtools.serverside.helper import print_success -from .box_loan import (main, get_loan, F_LOAN,update_loan_state, - S_RETURN_REQUESTED, RETURN_REQUESTED, S_LENT, - get_borrower_names) + +from .box_loan import (F_LOAN, RETURN_REQUESTED, S_LENT, S_RETURN_REQUESTED, get_borrower_names, + get_loan, main, update_loan_state) def _reject_return_request(loan): diff --git a/loanpy/src/loan/request_loan.py b/loanpy/src/loan/request_loan.py index 723f569c784f4af9b54ea3dce131ba3f09b58971..16eb783153a1cf292d81262d5c221a37efe27790 100755 --- a/loanpy/src/loan/request_loan.py +++ b/loanpy/src/loan/request_loan.py @@ -24,23 +24,26 @@ from __future__ import absolute_import import linkahead as db from caosadvancedtools.serverside.helper import get_timestamp, print_success +from linkahead.common.models import get_id_from_versionid, value_matches_versionid -from .box_loan import (BORROWER, BOX, COMMENT, DESTINATION, EXHAUST_CONTENTS, - EXPECTED_RETURN, F_BOX, F_COMMENT, F_DESTINATION, - F_EMAIL, F_EXHAUST_CONTENTS, F_EXPECTED_RETURN_DATE, - F_FIRST_NAME, F_LAST_NAME, FIRST_NAME, LAST_NAME, LOAN, - LOAN_REQUESTED, assert_date_in_future, - assert_key_in_data, get_box, insert_or_update_person, main, - send_loan_request_mail) +from .box_loan import (BORROWER, BOX, COMMENT, DESTINATION, EXHAUST_CONTENTS, EXPECTED_RETURN, + F_BOX, F_COMMENT, F_DESTINATION, F_EMAIL, F_EXHAUST_CONTENTS, + F_EXPECTED_RETURN_DATE, F_FIRST_NAME, F_LAST_NAME, FIRST_NAME, LAST_NAME, + LOAN, LOAN_REQUESTED, assert_date_in_future, assert_key_in_data, + insert_or_update_person, main, send_loan_request_mail) -def create_loan(box, borrower, expected_return, exhaust_contents, comment, - destination): +def create_loan(box, borrower, expected_return, exhaust_contents, comment, destination): """ Create a new loan record. """ + if isinstance(box, list): + ids = [get_id_from_versionid(val) if value_matches_versionid(val) else val for val in box] + ids = [str(val) for val in ids] + if len(ids) != len(set(ids)): + raise ValueError("The loan must not reference the same item twice") loan = db.Record().add_parent(LOAN) - - loan.add_property(BOX, box) + loan.add_property(BOX, box, datatype=( + db.LIST(BOX.name) if isinstance(box, list) else BOX.name)) loan.add_property(BORROWER, borrower) loan.add_property(EXPECTED_RETURN, expected_return) loan.add_property(EXHAUST_CONTENTS, exhaust_contents) @@ -62,27 +65,40 @@ _OBLIGATORY = [ ] -def _check_data(data): +def _set_defaults(data): if F_EXHAUST_CONTENTS not in data: data[F_EXHAUST_CONTENTS] = False + return data + +def _check_data(data): for field in _OBLIGATORY: assert_key_in_data(data, field) + items = data[F_BOX] + if not isinstance(items, list): + items = [items] + bad_references = [] + for item in items: + if not (isinstance(item, int) or item.isnumeric()): + bad_references.append(item) + if bad_references: + raise ValueError( + f"The following value(s) of {F_BOX} has/have the wrong type. It should be " + f"integers or string representations thereof.\n{bad_references}") assert_date_in_future( data[F_EXPECTED_RETURN_DATE], ("The expected return date needs to be in the future." " You submitted '{}'.").format( data[F_EXPECTED_RETURN_DATE])) - return data - def _issue_loan_request(data): """ Insert a loan record a insert/update a person record. """ - data = _check_data(data) + data = _set_defaults(data) + _check_data(data) borrower = insert_or_update_person(firstname=data[F_FIRST_NAME], - lastname=data[F_LAST_NAME], - email=data[F_EMAIL]) + lastname=data[F_LAST_NAME], + email=data[F_EMAIL]) loan = create_loan(box=data[F_BOX], borrower=borrower, expected_return=data[F_EXPECTED_RETURN_DATE], @@ -117,7 +133,7 @@ def issue_loan_request(data): '<a href="{loan}" title="Go to the newly created loan ' 'request.">here</a>'.format(fn=fn, ln=ln, loan=loan.id)) - send_loan_request_mail(data, borrower, loan) + # send_loan_request_mail(data, borrower, loan) return 0 diff --git a/loanpy/src/loan/request_return.py b/loanpy/src/loan/request_return.py index 8cd851660c0809fb508b256bffbc065f2ffdcfc7..4142c435efd5b6d88d2f420ecfce00545eecb3d2 100755 --- a/loanpy/src/loan/request_return.py +++ b/loanpy/src/loan/request_return.py @@ -29,12 +29,13 @@ from __future__ import absolute_import from caosadvancedtools.serverside.helper import get_timestamp, print_success from .box_loan import (BORROWER, COMMENT, CONTENT, EXPECTED_RETURN, F_COMMENT, - F_CURRENT_LOCATION, F_EMAIL, F_EXPECTED_RETURN_DATE, - F_FIRST_NAME, F_LAST_NAME, F_LOAN, FIRST_NAME, LAST_NAME,update_loan_state, - RETURN_REQUESTED, RETURNLOCATION, S_LENT, S_RETURN_REQUESTED, - assert_date_in_future, assert_key_in_data, - get_loan, insert_or_update_person, main, - send_return_request_mail, set_property) + F_CURRENT_LOCATION, F_EMAIL, F_EXPECTED_RETURN_DATE, + F_FIRST_NAME, F_LAST_NAME, F_LOAN, FIRST_NAME, + LAST_NAME, RETURN_REQUESTED, RETURNLOCATION, S_LENT, + S_RETURN_REQUESTED, assert_date_in_future, + assert_key_in_data, get_loan, insert_or_update_person, + main, send_return_request_mail, set_property, + update_loan_state) def _check_data(data): diff --git a/loanpy/unittests/request_loan_form.json b/loanpy/unittests/request_loan_form.json index 3db881151c2bf48b006a98cfcd90609fcf7d7c8b..4c473759bff0fc4988b93cbc9fdeeaaac8f3f253 100644 --- a/loanpy/unittests/request_loan_form.json +++ b/loanpy/unittests/request_loan_form.json @@ -1,6 +1,6 @@ { - "loan": 12345, - "box": 2345, + "loan": "12345", + "box": "2345", "first_name": "Anna", "last_name": "Lytik", "email": "a.lytik@example.com", diff --git a/loanpy/unittests/test_box_loan.py b/loanpy/unittests/test_box_loan.py index ebeccb7ac904a6e4803f2e6fb299a62d303bda10..78820aef23a6af0da4cc5c9fac5086daa410a7ee 100644 --- a/loanpy/unittests/test_box_loan.py +++ b/loanpy/unittests/test_box_loan.py @@ -17,20 +17,20 @@ # along with this program. If not, see <https://www.gnu.org/licenses/>. from os.path import abspath, dirname, join -from pytest import raises -from linkahead import get_connection, configure_connection -from linkahead.connection.mockup import (MockUpServerConnection, MockUpResponse) + +from linkahead import configure_connection, get_connection +from linkahead.connection.mockup import MockUpResponse, MockUpServerConnection from loan.box_loan import * from loan.box_loan import _caller +from pytest import raises from utils import get_form_data_example - def test_return_value_of_caller(): tval = 1337 - assert _caller(lambda d: tval, [get_form_data_example()] -) == tval + assert _caller(lambda d: tval, [get_form_data_example()]) == tval + def test_create_person(): p = create_person("anna", "lytik", "a@b.com") @@ -39,6 +39,7 @@ def test_create_person(): assert p.get_property(LAST_NAME.name).value == "lytik" assert p.get_property(EMAIL.name).value == "a@b.com" + def test_email_validation(): with raises(EmailPatternError): assert_email_pattern("@asdf") @@ -53,6 +54,7 @@ def test_email_validation(): assert_email_pattern("a@b.da") assert_email_pattern("\"#\"@ö.de") + def test_assert_date_in_future(): # I can't wait for 2050 :-) assert assert_date_in_future("2050-01-01") is None diff --git a/loanpy/unittests/test_request_loan.py b/loanpy/unittests/test_request_loan.py index 63c937579048001ac944988be11058659f08baef..2b6bcc2748f33f5de6ac1e6b1e556b6127aeccd7 100644 --- a/loanpy/unittests/test_request_loan.py +++ b/loanpy/unittests/test_request_loan.py @@ -1,13 +1,14 @@ from os.path import abspath, dirname, join -from pytest import raises -from linkahead import get_connection, configure_connection, Record -from linkahead.connection.mockup import (MockUpServerConnection, MockUpResponse) + from caosadvancedtools.serverside.helper import get_data -from loan.box_loan import (PERSON, FIRST_NAME, EMAIL, BOX, BORROWER, - EXPECTED_RETURN, EXHAUST_CONTENTS, COMMENT, - DESTINATION, F_FIRST_NAME, F_EMAIL, - F_EXPECTED_RETURN_DATE, DataError, BOX_NUMBER) -from loan.request_loan import (create_loan, _issue_loan_request) +from linkahead import Record, configure_connection, get_connection +from linkahead.connection.mockup import MockUpResponse, MockUpServerConnection +from loan.box_loan import (BORROWER, BOX, BOX_NUMBER, COMMENT, DESTINATION, EMAIL, EXHAUST_CONTENTS, + EXPECTED_RETURN, F_EMAIL, F_EXPECTED_RETURN_DATE, F_FIRST_NAME, + FIRST_NAME, PERSON, DataError) +from loan.request_loan import _issue_loan_request, create_loan +from pytest import raises + from utils import get_form_data_example @@ -20,15 +21,10 @@ def test_issue_loan_request_with_wrong_return_date(): def test_create_loan(): borrower = Record(name="Person1") - l = create_loan(1234, - borrower, - "2020-03-23", - False, - "blablabla", - "my office") - assert l.get_property(BOX.name).value == 1234 - assert l.get_property(BORROWER.name).value == borrower - assert l.get_property(EXPECTED_RETURN.name).value == "2020-03-23" - assert l.get_property(EXHAUST_CONTENTS.name).value == False - assert l.get_property(COMMENT.name).value == "blablabla" - assert l.get_property(DESTINATION.name).value == "my office" + loan = create_loan(1234, borrower, "2020-03-23", False, "blablabla", "my office") + assert loan.get_property(BOX.name).value == 1234 + assert loan.get_property(BORROWER.name).value == borrower + assert loan.get_property(EXPECTED_RETURN.name).value == "2020-03-23" + assert loan.get_property(EXHAUST_CONTENTS.name).value is False + assert loan.get_property(COMMENT.name).value == "blablabla" + assert loan.get_property(DESTINATION.name).value == "my office" diff --git a/loanpy/unittests/test_request_return.py b/loanpy/unittests/test_request_return.py index 8f6bd3b00d8cf02707eb0e688ff92e871f7f42f6..1480596b0116d0143608f667a54f9422a8c03c53 100644 --- a/loanpy/unittests/test_request_return.py +++ b/loanpy/unittests/test_request_return.py @@ -1,14 +1,14 @@ from os.path import abspath, dirname, join -from pytest import raises -from linkahead import get_connection, configure_connection -from linkahead.connection.mockup import (MockUpServerConnection, MockUpResponse) + from caosadvancedtools.serverside.helper import get_data -from loan.box_loan import (FIRST_NAME, EMAIL, LAST_NAME, F_FIRST_NAME, - F_LAST_NAME, F_EMAIL, EXPECTED_RETURN, LENT, - RETURN_REQUESTED, PERSON, BORROWER, BOX, - LOAN_REQUESTED, StateError, F_EXPECTED_RETURN_DATE, - DataError, LOAN) -from loan.request_return import get_loan, _issue_return_request +from linkahead import configure_connection, get_connection +from linkahead.connection.mockup import MockUpResponse, MockUpServerConnection +from loan.box_loan import (BORROWER, BOX, EMAIL, EXPECTED_RETURN, F_EMAIL, F_EXPECTED_RETURN_DATE, + F_FIRST_NAME, F_LAST_NAME, FIRST_NAME, LAST_NAME, LENT, LOAN, + LOAN_REQUESTED, PERSON, RETURN_REQUESTED, DataError, StateError) +from loan.request_return import _issue_return_request, get_loan +from pytest import raises + from utils import get_form_data_example diff --git a/loanpy/unittests/utils.py b/loanpy/unittests/utils.py index c56975795276d029c644b06b1d0a2bc3aba7add8..bebb1921db10a59c73ef00ce47ef7e4626f2eba6 100644 --- a/loanpy/unittests/utils.py +++ b/loanpy/unittests/utils.py @@ -22,5 +22,6 @@ some utility functions from os.path import abspath, dirname, join + def get_form_data_example(): return abspath(join(dirname(__file__), "request_loan_form.json")) diff --git a/test-profile/custom/caosdb-server/scripting/home/.pylinkahead.ini b/test-profile/custom/caosdb-server/scripting/home/.pylinkahead.ini index c9099248f250c1e53796f352d85ccf4a37d0097e..0ede554a30ba59a06393d179ea0c7a41078c75d1 100644 --- a/test-profile/custom/caosdb-server/scripting/home/.pylinkahead.ini +++ b/test-profile/custom/caosdb-server/scripting/home/.pylinkahead.ini @@ -8,5 +8,6 @@ timeout = 5000 sendmail=/usr/local/bin/sendmail_to_file entity_loan.curator_mail_from=admin@indiscale.com entity_loan.curator_mail_to=admin@indiscale.com +entity_loan.update_locations=True [sss_helper] external_uri = https://example.com:443 diff --git a/test-profile/custom/other/restore/caosdb.2024-10-24T12:10:03.770053767+00:00.dump.sql b/test-profile/custom/other/restore/caosdb.2024-10-24T12:10:03.770053767+00:00.dump.sql index f4a6848ab78afd610d6d231aa3100fd7a9ea7e1a..7c547b0a9108f6f7285fa4fbf0557fd578ce0577 100644 --- a/test-profile/custom/other/restore/caosdb.2024-10-24T12:10:03.770053767+00:00.dump.sql +++ b/test-profile/custom/other/restore/caosdb.2024-10-24T12:10:03.770053767+00:00.dump.sql @@ -1758,7 +1758,7 @@ DELIMITER ; /*!50003 SET collation_connection = utf8_general_ci */ ; DELIMITER ;; CREATE DEFINER=`root`@`%` FUNCTION `get_head_relative`(EntityID VARCHAR(255), - Offset INT UNSIGNED) RETURNS varbinary(255) + HeadOffset INT UNSIGNED) RETURNS varbinary(255) READS SQL DATA BEGIN DECLARE InternalEntityID INT UNSIGNED DEFAULT NULL; @@ -1774,7 +1774,7 @@ BEGIN FROM entity_version AS e WHERE e.entity_id = InternalEntityID ORDER BY e._iversion DESC - LIMIT 1 OFFSET Offset + LIMIT 1 OFFSET HeadOffset ); END ;; DELIMITER ; diff --git a/test-profile/profile.yml b/test-profile/profile.yml index 9120f1aa485ed23ab21284ebdf591cef567ebaea..2d96f7f37ba6001fa340a7ddd8f86e0ac9af8337 100644 --- a/test-profile/profile.yml +++ b/test-profile/profile.yml @@ -203,4 +203,6 @@ default: mode: "copy" path: "../loanpy" package: "loanpy" - + linkahead: + mode: "pip" + package: "git+https://gitlab.indiscale.com/caosdb/src/caosdb-pylib.git@dev"