diff --git a/modules/weko-search-ui/tests/data/list_records/b4_handle_check_exist_record4.json b/modules/weko-search-ui/tests/data/list_records/b4_handle_check_exist_record4.json new file mode 100644 index 0000000000..c7838c8a48 --- /dev/null +++ b/modules/weko-search-ui/tests/data/list_records/b4_handle_check_exist_record4.json @@ -0,0 +1 @@ +[{"pos_index": ["IndexA"], "publish_status": "public", "feedback_mail": ["wekosoftware@nii.ac.jp"], "edit_mode": "Keep", "metadata": {"pubdate": "2021-08-07", "item_1617186331708": [{"subitem_1551255647225": "ja_conference paperITEM00000001(public_open_access_open_access_simple)", "subitem_1551255648112": "ja"}, {"subitem_1551255647225": "en_conference paperITEM00000001(public_open_access_simple)", "subitem_1551255648112": "en"}], "item_1617186385884": [{"subitem_1551255720400": "Alternative Title", "subitem_1551255721061": "en"}, {"subitem_1551255720400": "Alternative Title", "subitem_1551255721061": "ja"}], "item_1617186419668": [{"creatorAffiliations": [{"affiliationNameIdentifiers": [{"affiliationNameIdentifier": "0000000121691048", "affiliationNameIdentifierScheme": "ISNI", "affiliationNameIdentifierURI": "http://isni.org/isni/0000000121691048"}], "affiliationNames": [{"affiliationName": "University", "affiliationNameLang": "en"}]}], "creatorMails": [{"creatorMail": "wekosoftware@nii.ac.jp"}], "creatorNames": [{"creatorName": "情報, 太郎", "creatorNameLang": "ja"}, {"creatorName": "ジョウホウ, タロウ", "creatorNameLang": "ja-Kana"}, {"creatorName": "Joho, Taro", "creatorNameLang": "en"}], "familyNames": [{"familyName": "情報", "familyNameLang": "ja"}, {"familyName": "ジョウホウ", "familyNameLang": "ja-Kana"}, {"familyName": "Joho", "familyNameLang": "en"}], "givenNames": [{"givenName": "太郎", "givenNameLang": "ja"}, {"givenName": "タロウ", "givenNameLang": "ja-Kana"}, {"givenName": "Taro", "givenNameLang": "en"}], "nameIdentifiers": [{"nameIdentifier": "4", "nameIdentifierScheme": "WEKO"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "ORCID", "nameIdentifierURI": "https://orcid.org/"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "CiNii", "nameIdentifierURI": "https://ci.nii.ac.jp/"}, {"nameIdentifier": "zzzzzzz", "nameIdentifierScheme": "KAKEN2", "nameIdentifierURI": "https://kaken.nii.ac.jp/"}]}, {"creatorMails": [{"creatorMail": "wekosoftware@nii.ac.jp"}], "creatorNames": [{"creatorName": "情報, 太郎", "creatorNameLang": "ja"}, {"creatorName": "ジョウホウ, タロウ", "creatorNameLang": "ja-Kana"}, {"creatorName": "Joho, Taro", "creatorNameLang": "en"}], "familyNames": [{"familyName": "情報", "familyNameLang": "ja"}, {"familyName": "ジョウホウ", "familyNameLang": "ja-Kana"}, {"familyName": "Joho", "familyNameLang": "en"}], "givenNames": [{"givenName": "太郎", "givenNameLang": "ja"}, {"givenName": "タロウ", "givenNameLang": "ja-Kana"}, {"givenName": "Taro", "givenNameLang": "en"}], "nameIdentifiers": [{"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "ORCID", "nameIdentifierURI": "https://orcid.org/"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "CiNii", "nameIdentifierURI": "https://ci.nii.ac.jp/"}, {"nameIdentifier": "zzzzzzz", "nameIdentifierScheme": "KAKEN2", "nameIdentifierURI": "https://kaken.nii.ac.jp/"}]}, {"creatorMails": [{"creatorMail": "wekosoftware@nii.ac.jp"}], "creatorNames": [{"creatorName": "情報, 太郎", "creatorNameLang": "ja"}, {"creatorName": "ジョウホウ, タロウ", "creatorNameLang": "ja-Kana"}, {"creatorName": "Joho, Taro", "creatorNameLang": "en"}], "familyNames": [{"familyName": "情報", "familyNameLang": "ja"}, {"familyName": "ジョウホウ", "familyNameLang": "ja-Kana"}, {"familyName": "Joho", "familyNameLang": "en"}], "givenNames": [{"givenName": "太郎", "givenNameLang": "ja"}, {"givenName": "タロウ", "givenNameLang": "ja-Kana"}, {"givenName": "Taro", "givenNameLang": "en"}], "nameIdentifiers": [{"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "ORCID", "nameIdentifierURI": "https://orcid.org/"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "CiNii", "nameIdentifierURI": "https://ci.nii.ac.jp/"}, {"nameIdentifier": "zzzzzzz", "nameIdentifierScheme": "KAKEN2", "nameIdentifierURI": "https://kaken.nii.ac.jp/"}]}], "item_1617349709064": [{"contributorMails": [{"contributorMail": "wekosoftware@nii.ac.jp"}], "contributorNames": [{"contributorName": "情報, 太郎", "lang": "ja"}, {"contributorName": "ジョウホウ, タロウ", "lang": "ja-Kana"}, {"contributorName": "Joho, Taro", "lang": "en"}], "contributorType": "ContactPerson", "familyNames": [{"familyName": "情報", "familyNameLang": "ja"}, {"familyName": "ジョウホウ", "familyNameLang": "ja-Kana"}, {"familyName": "Joho", "familyNameLang": "en"}], "givenNames": [{"givenName": "太郎", "givenNameLang": "ja"}, {"givenName": "タロウ", "givenNameLang": "ja-Kana"}, {"givenName": "Taro", "givenNameLang": "en"}], "nameIdentifiers": [{"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "ORCID", "nameIdentifierURI": "https://orcid.org/"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "CiNii", "nameIdentifierURI": "https://ci.nii.ac.jp/"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "KAKEN2", "nameIdentifierURI": "https://kaken.nii.ac.jp/"}]}], "item_1617186476635": {"subitem_1522299639480": "open access", "subitem_1600958577026": "http://purl.org/coar/access_right/c_abf2"}, "item_1617351524846": {"subitem_1523260933860": "Unknown"}, "item_1617186499011": [{"subitem_1522650717957": "ja", "subitem_1522650727486": "http://localhost", "subitem_1522651041219": "Rights Information"}], "item_1617610673286": [{"nameIdentifiers": [{"nameIdentifier": "xxxxxx", "nameIdentifierScheme": "ORCID", "nameIdentifierURI": "https://orcid.org/"}], "rightHolderNames": [{"rightHolderLanguage": "ja", "rightHolderName": "Right Holder Name"}]}], "item_1617186609386": [{"subitem_1522299896455": "ja", "subitem_1522300014469": "Other", "subitem_1522300048512": "http://localhost/", "subitem_1523261968819": "Sibject1"}], "item_1617186626617": [{"subitem_description": "Description\nDescription
Description", "subitem_description_language": "en", "subitem_description_type": "Abstract"}, {"subitem_description": "概要\n概要\n概要\n概要", "subitem_description_language": "ja", "subitem_description_type": "Abstract"}], "item_1617186643794": [{"subitem_1522300295150": "en", "subitem_1522300316516": "Publisher"}], "item_1617186660861": [{"subitem_1522300695726": "Available", "subitem_1522300722591": "2021-06-30"}], "item_1617186702042": [{"subitem_1551255818386": "jpn"}], "item_1617258105262": {"resourcetype": "conference paper", "resourceuri": "http://purl.org/coar/resource_type/c_5794"}, "item_1617349808926": {"subitem_1523263171732": "Version"}, "item_1617265215918": {"subitem_1522305645492": "AO", "subitem_1600292170262": "http://purl.org/coar/version/c_b1a7d7d4d402bcce"}, "item_1617186783814": [{"subitem_identifier_type": "URI", "subitem_identifier_uri": "http://localhost"}], "item_1617353299429": [{"subitem_1522306207484": "isVersionOf", "subitem_1522306287251": {"subitem_1522306382014": "arXiv", "subitem_1522306436033": "xxxxx"}, "subitem_1523320863692": [{"subitem_1523320867455": "en", "subitem_1523320909613": "Related Title"}]}], "item_1617186859717": [{"subitem_1522658018441": "en", "subitem_1522658031721": "Temporal"}], "item_1617186882738": [{"subitem_geolocation_place": [{"subitem_geolocation_place_text": "Japan"}]}], "item_1617186901218": [{"subitem_1522399143519": {"subitem_1522399281603": "ISNI", "subitem_1522399333375": "http://xxx"}, "subitem_1522399412622": [{"subitem_1522399416691": "en", "subitem_1522737543681": "Funder Name"}], "subitem_1522399571623": {"subitem_1522399585738": "Award URI", "subitem_1522399628911": "Award Number"}, "subitem_1522399651758": [{"subitem_1522721910626": "en", "subitem_1522721929892": "Award Title"}]}], "item_1617186920753": [{"subitem_1522646500366": "ISSN", "subitem_1522646572813": "xxxx-xxxx-xxxx"}], "item_1617186941041": [{"subitem_1522650068558": "en", "subitem_1522650091861": "Source Title"}], "item_1617186959569": {"subitem_1551256328147": "1"}, "item_1617186981471": {"subitem_1551256294723": "111"}, "item_1617186994930": {"subitem_1551256248092": "12"}, "item_1617187024783": {"subitem_1551256198917": "1"}, "item_1617187045071": {"subitem_1551256185532": "3"}, "item_1617187112279": [{"subitem_1551256126428": "Degree Name", "subitem_1551256129013": "en"}], "item_1617187136212": {"subitem_1551256096004": "2021-06-30"}, "item_1617944105607": [{"subitem_1551256015892": [{"subitem_1551256027296": "xxxxxx", "subitem_1551256029891": "kakenhi"}], "subitem_1551256037922": [{"subitem_1551256042287": "Degree Grantor Name", "subitem_1551256047619": "en"}]}], "item_1617187187528": [{"subitem_1599711633003": [{"subitem_1599711636923": "Conference Name", "subitem_1599711645590": "ja"}], "subitem_1599711655652": "1", "subitem_1599711660052": [{"subitem_1599711680082": "Sponsor", "subitem_1599711686511": "ja"}], "subitem_1599711699392": {"subitem_1599711704251": "2020/12/11", "subitem_1599711712451": "1", "subitem_1599711727603": "12", "subitem_1599711731891": "2000", "subitem_1599711735410": "1", "subitem_1599711739022": "12", "subitem_1599711743722": "2020", "subitem_1599711745532": "ja"}, "subitem_1599711758470": [{"subitem_1599711769260": "Conference Venue", "subitem_1599711775943": "ja"}], "subitem_1599711788485": [{"subitem_1599711798761": "Conference Place", "subitem_1599711803382": "ja"}], "subitem_1599711813532": "JPN"}], "item_1617605131499": [{"accessrole": "open_access", "date": [{"dateType": "Available", "dateValue": "2021-07-12"}], "displaytype": "simple", "filename": "1KB.pdf", "filesize": [{"value": "1 KB"}], "format": "text/plain"}, {"filename": ""}], "item_1617620223087": [{"subitem_1565671149650": "ja", "subitem_1565671169640": "Banner Headline", "subitem_1565671178623": "Subheading"}, {"subitem_1565671149650": "en", "subitem_1565671169640": "Banner Headline", "subitem_1565671178623": "Subheding"}]}, "file_path": ["file00000001/1KB.pdf", ""], "item_type_name": "デフォルトアイテムタイプ(フル)", "item_type_id": 15, "$schema": "https://localhost:8443/items/jsonschema/15", "identifier_key": "item_1617186819068", "errors": null}] \ No newline at end of file diff --git a/modules/weko-search-ui/tests/test_utils.py b/modules/weko-search-ui/tests/test_utils.py index d8209889c5..d00b32afff 100644 --- a/modules/weko-search-ui/tests/test_utils.py +++ b/modules/weko-search-ui/tests/test_utils.py @@ -1192,6 +1192,7 @@ def test_handle_check_duplicate_record(app): # def handle_check_exist_record(list_record) -> list: +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_utils.py::test_handle_check_exist_record -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp def test_handle_check_exist_record(app): case = unittest.TestCase() # case 1 import new items @@ -1213,7 +1214,8 @@ def test_handle_check_exist_record(app): with open(filepath, encoding="utf-8") as f: result = json.load(f) - case.assertCountEqual(handle_check_exist_record(list_record), result) + with app.test_request_context(): + case.assertCountEqual(handle_check_exist_record(list_record), result) # case 2 import items with id filepath = os.path.join( @@ -1287,6 +1289,127 @@ def test_handle_check_exist_record(app): with set_locale("en"): case.assertCountEqual(handle_check_exist_record(list_record), result) +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_utils.py::test_handle_check_exist_record_put_auto_fill -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp +def test_handle_check_exist_record_put_auto_fill(app,db): + class DummyRecord: + def __init__(self, recid="10", deleted=False): + self._recid = recid + self._deleted = deleted + self.pid = self + + def is_deleted(self): + return self._deleted + + def get(self, key): + if key == "recid": + return self._recid + return None + # case 5 no id,uri + with patch("weko_deposit.api.WekoRecord.get_record_by_pid") as mock_get_record: + mock_get_record.return_value = DummyRecord(recid="10", deleted=False) + + filepath = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "data", + "list_records", + "b4_handle_check_exist_record4.json", + ) + with open(filepath, encoding="utf-8") as f: + list_record = json.load(f) + + recid = "10" + with app.test_request_context("/sword/deposit/{}".format(recid), method="PUT"): + request.view_args = {"recid": recid} + result = handle_check_exist_record(list_record) + assert result[0]["id"] == recid + assert result[0]["uri"].endswith("/records/{}".format(recid)) + assert result[0]["status"] == "keep" + +class DummyPid: + def __init__(self, deleted=False): + self._deleted = deleted + def is_deleted(self): + return self._deleted + +class DummyRecord: + def __init__(self, recid="", deleted=False): + self.pid = DummyPid(deleted) + self._recid = recid + def get(self, key): + if key == "recid": + return self._recid + return None + +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_utils.py::test_handle_check_exist_record_branches -vv -s --cov-branch --cov-report=html --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp +def test_handle_check_exist_record_cases(app, db): + from weko_search_ui.utils import handle_check_exist_record + + # 1. item_id is None, recid is set + list_record = [{"id": None, "uri": None}] + with app.test_request_context("/sword/deposit/100", method="PUT"): + request.view_args = {"recid": "100"} + result = handle_check_exist_record(list_record) + assert result[0]["id"] == "100" + assert result[0]["status"] is None + + # 2. uri and system_url are different + list_record = [{"id": "101", "uri": "http://dummy/other/101"}] + with app.test_request_context("/sword/deposit/101", method="PUT"): + request.view_args = {"recid": "101"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] is None + assert any("Specified URI and system URI do not match." in err for err in result[0].get("errors", [])) + + # 3. PIDDoesNotExistError occurs + list_record = [{"id": "102", "uri": None}] + with patch("weko_deposit.api.WekoRecord.get_record_by_pid", side_effect=PIDDoesNotExistError("recid", "102")): + with app.test_request_context("/sword/deposit/102", method="PUT"): + request.view_args = {"recid": "102"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] is None + assert any("Item does not exist in the system." in err for err in result[0].get("errors", [])) + + # 4. item_exist.pid.is_deleted() is True + list_record = [{"id": "103", "uri": None}] + with patch("weko_deposit.api.WekoRecord.get_record_by_pid", return_value=DummyRecord(recid="103", deleted=True)): + with app.test_request_context("/sword/deposit/103", method="PUT"): + request.view_args = {"recid": "103"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] is None + + # 5. edit_mode is None + list_record = [{"id": "104", "uri": None, "edit_mode": None}] + with patch("weko_deposit.api.WekoRecord.get_record_by_pid", return_value=DummyRecord(recid="104", deleted=False)): + with app.test_request_context("/sword/deposit/104", method="PUT"): + request.view_args = {"recid": "104"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] is None + assert any("Please specify either \"Keep\" or \"Upgrade\"." in err for err in result[0].get("errors", [])) + + # 6. edit_mode is invalid value + list_record = [{"id": "105", "uri": None, "edit_mode": "delete"}] + with patch("weko_deposit.api.WekoRecord.get_record_by_pid", return_value=DummyRecord(recid="105", deleted=False)): + with app.test_request_context("/sword/deposit/105", method="PUT"): + request.view_args = {"recid": "105"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] is None + assert any("Please specify either \"Keep\" or \"Upgrade\"." in err for err in result[0].get("errors", [])) + + # 7. edit_mode is correct value + list_record = [{"id": "106", "uri": None, "edit_mode": "keep"}] + with patch("weko_deposit.api.WekoRecord.get_record_by_pid", return_value=DummyRecord(recid="106", deleted=False)): + with app.test_request_context("/sword/deposit/106", method="PUT"): + request.view_args = {"recid": "106"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] == "keep" + + # 8. When item_id is an empty string + list_record = [{"id": "", "uri": None}] + with app.test_request_context("/sword/deposit/", method="PUT"): + request.view_args = {"recid": ""} + result = handle_check_exist_record(list_record) + assert result[0]["id"] is None + assert result[0]["status"] == "new" # def make_file_by_line(lines): def test_make_file_by_line(i18n_app): diff --git a/modules/weko-search-ui/weko_search_ui/utils.py b/modules/weko-search-ui/weko_search_ui/utils.py index 3c02cde998..c9d077f515 100644 --- a/modules/weko-search-ui/weko_search_ui/utils.py +++ b/modules/weko-search-ui/weko_search_ui/utils.py @@ -1589,7 +1589,17 @@ def handle_check_exist_record(list_record) -> list: item = dict(**item, **{"status": "new"}) # current_app.logger.debug("item:{}".format(item)) errors = item.get("errors") or [] + recid = request.view_args.get("recid") item_id = item.get("id") + if item_id is None and recid is not None: + item["id"] = recid + item_id = recid + system_url = ( + request.host_url + "records/" + str(item_id) + if item_id is not None else None + ) + if item.get("uri") is None and system_url is not None: + item["uri"] = system_url # current_app.logger.debug("item_id:{}".format(item_id)) if item_id and item_id is not "": system_url = request.host_url + "records/" + str(item_id) diff --git a/modules/weko-swordserver/tests/data/records/test_items.json b/modules/weko-swordserver/tests/data/records/test_items.json index c1ea3057a0..6e8e8427d3 100644 --- a/modules/weko-swordserver/tests/data/records/test_items.json +++ b/modules/weko-swordserver/tests/data/records/test_items.json @@ -99,4 +99,35 @@ "resourcetype": "conference paper" } } -] \ No newline at end of file +, + { + "id": "filetest", + "pid": { "type": "depid", "value": "filetest", "revision_id": 0 }, + "lang": "ja", + "owner": "99", + "title": "filetest_title", + "owners": [99], + "status": "published", + "$schema": "/items/jsonschema/15", + "pubdate": "2026-02-09", + "created_by": 99, + "owners_ext": { + "email": "filetest@nii.ac.jp", + "username": "fileuser", + "displayname": "File Test User" + }, + "shared_user_ids": [], + "item_1617186331708": [ + { "subitem_1551255647225": "filetest_title", "subitem_1551255648112": "ja" } + ], + "item_file": [ + { + "url": { + "url": "http://TEST_SERVER.localdomain/files/filetest.pdf", + "label": "filetest.pdf" + }, + "mimetype": "application/pdf" + } + ] + } +] diff --git a/modules/weko-swordserver/tests/data/records/test_records.json b/modules/weko-swordserver/tests/data/records/test_records.json index 7a03568086..715786b861 100644 --- a/modules/weko-swordserver/tests/data/records/test_records.json +++ b/modules/weko-swordserver/tests/data/records/test_records.json @@ -202,5 +202,55 @@ ] }, "relation_version_is_last": true + }, + { + "_oai": { "id": "oai:weko3.example.org:filetest", "sets": ["99"] }, + "path": ["99"], + "owner": "99", + "recid": "filetest", + "title": ["filetest_title"], + "pubdate": { "attribute_name": "PubDate", "attribute_value": "2026-02-09" }, + "_buckets": { "deposit": "filetest-bucket-uuid" }, + "_deposit": { + "id": "filetest", + "pid": { "type": "depid", "value": "filetest", "revision_id": 0 }, + "owner": "99", + "owners": [99], + "status": "published", + "created_by": 99, + "owners_ext": { + "email": "filetest@nii.ac.jp", + "username": "fileuser", + "displayname": "File Test User" + } + }, + "item_title": "filetest_title", + "author_link": [], + "item_type_id": "99", + "publish_date": "2026-02-09", + "publish_status": "0", + "weko_shared_ids": [], + "item_1617186331708": { + "attribute_name": "Title", + "attribute_value_mlt": [ + { + "subitem_1551255647225": "filetest_title", + "subitem_1551255648112": "ja" + } + ] + }, + "item_file": { + "attribute_type": "file", + "attribute_value_mlt": [ + { + "url": { + "url": "http://TEST_SERVER.localdomain/files/filetest.pdf", + "label": "filetest.pdf" + }, + "mimetype": "application/pdf" + } + ] + }, + "relation_version_is_last": true } ] diff --git a/modules/weko-swordserver/tests/test_views.py b/modules/weko-swordserver/tests/test_views.py index cd1333289e..c621e19e73 100644 --- a/modules/weko-swordserver/tests/test_views.py +++ b/modules/weko-swordserver/tests/test_views.py @@ -49,8 +49,26 @@ def update_location_size(): loc = db.session.query(Location).filter( Location.id == 1).one() loc.size = 1547 - mocker.patch("weko_swordserver.views._get_status_document", side_effect=lambda id:{"recid": id}) - mocker.patch("weko_swordserver.views._get_status_workflow_document", side_effect=lambda aid, id:{"activity": aid,"recid": id}) + mocker.patch( + "weko_swordserver.views._get_status_document", + side_effect=lambda id: { + "recid": id, + "links": [ + {"@id": f"/records/{id}", "contentType": "text/html"} + ] + } + ) + mocker.patch( + "weko_swordserver.views._get_status_workflow_document", + side_effect=lambda aid, id: { + "activity": aid, + "recid": id, + "links": [ + {"@id": f"/workflow/activity/detail/{aid}", "contentType": "text/html"}, + {"@id": f"/records/{id}", "contentType": "text/html"} + ] + } + ) mocker.patch("weko_search_ui.utils.find_and_update_location_size", side_effect=update_location_size) mocker.patch("weko_swordserver.views.dbsession_clean") @@ -83,7 +101,13 @@ def update_location_size(): result = client.post(url, data={"file": storage}, content_type="multipart/form-data", headers=headers) assert result.status_code == 201 - assert result.json.get("recid") == "2000001" + # Verify not only recid but also the links array + resp = result.json + assert resp.get("recid") == "2000001" + assert "links" in resp + # The links array should contain the HTML link for the recid + html_links = [l for l in resp["links"] if l.get("@id", "").endswith("/records/2000001") and l.get("contentType") == "text/html"] + assert html_links assert not os.path.exists("/var/tmp/test"), os.rmdir("/var/tmp/test") # Workflow registration, duplicate check @@ -113,7 +137,15 @@ def update_location_size(): result = client.post(url, data={"file": storage}, content_type="multipart/form-data", headers=headers) assert result.status_code == 201 - assert result.json.get("recid") == "2000001" + + resp = result.json + assert resp.get("recid") == "2000001" + assert "links" in resp + # The links array should contain both the activity link and the HTML link for the recid + activity_links = [l for l in resp["links"] if "/workflow/activity/detail/" in l.get("@id", "")] + html_links = [l for l in resp["links"] if l.get("@id", "").endswith("/records/2000001") and l.get("contentType") == "text/html"] + assert activity_links + assert html_links # invalid Content-Disposition's filename @@ -352,6 +384,116 @@ def update_location_size(): assert result.status_code == 412 assert result.json.get("error") == "Failed to verify request body and digest." +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_post_service_document_multi_recid -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test_post_service_document_multi_recid(app, client, db, users, make_zip, tokens, mocker): + mocker.patch("invenio_pidstore.resolver.Resolver.resolve", return_value=(MagicMock(), MagicMock())) + url = url_for("weko_swordserver.post_service_document") + token_direct = tokens[0]["token"].access_token + login_user_via_session(client=client, email=users[0]["email"]) + app.config["WEKO_SWORDSERVER_DIGEST_VERIFICATION"] = False + headers = { + "Authorization": f"Bearer {token_direct}", + "Content-Disposition": "attachment; filename=payload.zip", + "Packaging": "http://purl.org/net/sword/3.0/package/SimpleZip", + "On-Behalf-Of": "test_on_behalf_of", + } + zip = make_zip() + storage = FileStorage(filename="payload.zip", stream=zip) + mocker.patch("weko_swordserver.views.check_import_file_format", return_value="TSV/CSV") + mocker.patch("weko_swordserver.views.get_shared_ids_from_on_behalf_of", return_value=[]) + mocker_check_item = mocker.patch("weko_swordserver.views.check_import_items") + mocker_check_item.return_value = { + "data_path": "/var/tmp/test", + "register_type": "Direct", + "list_record": [ + {"status": "new", "metadata": {}, "recid": "2000001"}, + {"status": "new", "metadata": {}, "recid": "2000002"} + ] + } + mocker.patch("weko_swordserver.views.import_items_to_system", return_value={"success": True, "recid": ["2000001", "2000002"]}) + mocker.patch("weko_items_ui.utils.send_mail_direct_registered") + os.makedirs("/var/tmp/test", exist_ok=True) + + mocker.patch("weko_swordserver.views._get_status_multi_document", return_value={ + "@context": "https://swordapp.github.io/swordv3/swordv3.jsonld", + "@type": "Status", + "@id": "/records/2000002", + "links": [ + {"@id": "/records/2000001", "contentType": "text/html"}, + {"@id": "/records/2000002", "contentType": "text/html"} + ] + }) + result = client.post(url, data={"file": storage}, content_type="multipart/form-data", headers=headers) + assert result.status_code == 201 + resp = result.json + assert "links" in resp + html_links = [l for l in resp["links"] if l.get("@id", "").endswith("/records/2000001") or l.get("@id", "").endswith("/records/2000002")] + assert len(html_links) == 2 + assert resp["@id"].endswith("/records/2000002") or resp["@id"].endswith("/sword/deposit/2000002") + assert not os.path.exists("/var/tmp/test"), os.rmdir("/var/tmp/test") + + +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_post_service_document_multi_activity_id -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test_post_service_document_multi_activity_id(app, client, db, users, make_zip, tokens, mocker): + url = url_for("weko_swordserver.post_service_document") + token_workflow = tokens[1]["token"].access_token + login_user_via_session(client=client, email=users[1]["email"]) + app.config["WEKO_SWORDSERVER_DIGEST_VERIFICATION"] = False + zip = make_zip() + storage = FileStorage(filename="payload.zip", stream=zip) + headers = { + "Authorization": f"Bearer {token_workflow}", + "Content-Disposition": "attachment; filename=payload.zip", + "Packaging": "http://purl.org/net/sword/3.0/package/SimpleZip", + "On-Behalf-Of": "test_on_behalf_of", + } + mocker.patch("weko_swordserver.views.check_import_file_format", return_value="TSV/CSV") + mocker.patch("weko_swordserver.views.get_shared_ids_from_on_behalf_of", return_value=[]) + mocker_check_item = mocker.patch("weko_swordserver.views.check_import_items") + mocker_check_item.return_value = { + "data_path": "/var/tmp/test", + "register_type": "Workflow", + "workflow_id": [1001, 1002], + "list_record": [ + {"status": "new", "metadata": {}, "activity_id": "A-TEST-00001"}, + {"status": "new", "metadata": {}, "activity_id": "A-TEST-00002"} + ], + "duplicate_check": True + } + mocker.patch("weko_items_ui.utils.check_duplicate", return_value=(False, [], [])) + def import_items_to_activity_side_effect(*args, **kwargs): + if not hasattr(import_items_to_activity_side_effect, "count"): + import_items_to_activity_side_effect.count = 0 + import_items_to_activity_side_effect.count += 1 + if import_items_to_activity_side_effect.count == 1: + return (url_for("weko_workflow.display_activity", activity_id="A-TEST-00001"), "2000001", "end_action", None) + else: + return (url_for("weko_workflow.display_activity", activity_id="A-TEST-00002"), "2000002", "end_action", None) + mocker.patch("weko_swordserver.views.import_items_to_activity", side_effect=import_items_to_activity_side_effect) + + mocker.patch("weko_swordserver.views._get_status_multi_document", return_value={ + "@context": "https://swordapp.github.io/swordv3/swordv3.jsonld", + "@type": "Status", + "@id": "/records/2000002", + "links": [ + {"@id": "/workflow/activity/detail/A-TEST-00001", "contentType": "text/html"}, + {"@id": "/workflow/activity/detail/A-TEST-00002", "contentType": "text/html"}, + {"@id": "/records/2000001", "contentType": "text/html"}, + {"@id": "/records/2000002", "contentType": "text/html"} + ] + }) + os.makedirs("/var/tmp/test", exist_ok=True) + result = client.post(url, data={"file": storage}, content_type="multipart/form-data", headers=headers) + assert result.status_code == 201 + resp = result.json + assert "links" in resp + activity_links = [l for l in resp["links"] if "/workflow/activity/detail/" in l.get("@id", "")] + assert len(activity_links) == 2 + html_links = [l for l in resp["links"] if l.get("@id", "").endswith("/records/2000001") or l.get("@id", "").endswith("/records/2000002")] + assert len(html_links) == 2 + assert resp["@id"].endswith("/records/2000002") or resp["@id"].endswith("/sword/deposit/2000002") + assert not os.path.exists("/var/tmp/test"), os.rmdir("/var/tmp/test") + # def put_object(recid): # .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_put_object -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp @@ -788,14 +930,13 @@ def test_get_status_document(client, users, tokens): assert res.status_code == 200 assert res.json == {"recid":"test_recid"} - # def _get_status_document(recid): # .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test__get_status_document -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp def test__get_status_document(app,records): recid_doi = records[0][0].pid_value recid_not_doi = records[2][0].pid_value recid_sysdoi = records[3][0].pid_value - + recid_file = records[4][0].pid_value test_doi = { "@context": "https://swordapp.github.io/swordv3/swordv3.jsonld", "@type": "Status", @@ -873,6 +1014,35 @@ def test__get_status_document(app,records): } ] } + test_file = { + "@context": "https://swordapp.github.io/swordv3/swordv3.jsonld", + "@type": "Status", + "@id" : url_for('weko_swordserver.get_status_document', recid=recid_file, _external=True), + "actions" : {"getMetadata" : False,"getFiles" : False,"appendMetadata" : False,"appendFiles" : False,"replaceMetadata" : False,"replaceFiles" : False,"deleteMetadata" : False,"deleteFiles" : False,"deleteObject" : True,}, + "eTag" : str(1), + "fileSet" : {}, + "metadata" : {}, + "service" : url_for('weko_swordserver.get_service_document',_external=False), + "state" : [ + { + "@id" : "http://purl.org/net/sword/3.0/state/ingested", + "description" : "" + } + ], + "links" : [ + { + "@id" : "http://TEST_SERVER.localdomain/records/{}".format(recid_file), + "rel" : ["alternate"], + "contentType" : "text/html" + }, + { + "@id" : "http://TEST_SERVER.localdomain/files/filetest.pdf", + "contentType" : "application/pdf", + "rel" : ["http://purl.org/net/sword/3.0/terms/fileSetFile"], + "derivedFrom" : "http://TEST_SERVER.localdomain/records/{}".format(recid_file) + } + ] + } with app.test_request_context("/test_req"): # exist permalink result = _get_status_document(recid_doi) @@ -886,18 +1056,43 @@ def test__get_status_document(app,records): result = _get_status_document(recid_sysdoi) assert result == test_sysdoi + # exist file information + result = _get_status_document(recid_file) + assert result == test_file + # raise WekoSwordserverException with pytest.raises(WekoSwordserverException) as e: _get_status_document("not_exist_recid") assert e.message == "Item not found. (recid=not_exist_recid)" assert e.errorType == ErrorType.NotFound +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_status_document_files_info_none -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test_status_document_files_info_none(app, mocker): + """ + Test the branch when files_info is None + """ + from weko_swordserver.views import _get_status_document + recid = "dummy_recid" + with app.test_request_context("/test_req"): + # Mock _get_file_info to return None + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + # Mock import_string, Resolver, get_record_permalink minimally + mock_record = type("MockRecord", (), {"revision_id": 1, "get": lambda self, k, d=None: None})() + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda x: mock_record})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, mock_record)})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + result = _get_status_document(recid) + # No file links should be added to links + file_links = [l for l in result["links"] if l.get("rel") and any("file" in r for r in l.get("rel"))] + assert not file_links + # def _get_status_workflow_document(activity, recid): # .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test__get_status_workflow_document -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp def test__get_status_workflow_document(app, records): recid_doi = records[0][0].pid_value recid_not_doi = records[2][0].pid_value + recid_file = records[4][0].pid_value expected_activity_id = "A-20240301-00001" @@ -921,6 +1116,11 @@ def test__get_status_workflow_document(app, records): "rel" : ["alternate"], "contentType" : "text/html" }, + { + "@id" : url_for('weko_swordserver.get_status_document', recid=recid_doi, _external=True), + "rel" : ["alternate"], + "contentType" : "text/html" + } ] } test_doi_no_recid = { @@ -937,11 +1137,49 @@ def test__get_status_workflow_document(app, records): "description" : "" } ], - "links" : [ + "links": [ { "@id" : url_for('weko_workflow.display_activity', activity_id=expected_activity_id, _external=True), "rel" : ["alternate"], "contentType" : "text/html" + }, + { + "@id": url_for('weko_swordserver.get_status_document', recid=recid_not_doi, _external=True), + "rel": ["alternate"], + "contentType": "text/html" + } + ] + } + test_file = { + "@id": url_for('weko_swordserver.get_status_document', recid=recid_file, _external=True), + "@context": "https://swordapp.github.io/swordv3/swordv3.jsonld", + "@type": "ServiceDocument", + "actions": {"getMetadata": False, "getFiles": False, "appendMetadata": False, "appendFiles": False, "replaceMetadata": False, "replaceFiles": False, "deleteMetadata": False, "deleteFiles": False, "deleteObject": True}, + "fileSet": {}, + "metadata": {}, + "service": url_for('weko_swordserver.get_service_document', _external=False), + "state": [ + { + "@id": "http://purl.org/net/sword/3.0/state/inWorkflow", + "description": "" + } + ], + "links": [ + { + "@id": url_for('weko_workflow.display_activity', activity_id=expected_activity_id, _external=True), + "rel": ["alternate"], + "contentType": "text/html" + }, + { + "@id": url_for('weko_swordserver.get_status_document', recid=recid_file, _external=True), + "rel": ["alternate"], + "contentType": "text/html" + }, + { + "@id": "http://TEST_SERVER.localdomain/files/filetest.pdf", + "contentType": "application/pdf", + "rel": ["http://purl.org/net/sword/3.0/terms/fileSetFile"], + "derivedFrom": url_for('weko_swordserver.get_status_document', recid=recid_file, _external=True) } ] } @@ -951,9 +1189,13 @@ def test__get_status_workflow_document(app, records): result = _get_status_workflow_document(expected_activity_id, recid_doi) assert result == test_doi + # exist file + result = _get_status_workflow_document(expected_activity_id, recid_file) + assert result == test_file + # not exist recid - result = _get_status_workflow_document(expected_activity_id, None) - assert result == test_doi_no_recid + with pytest.raises(WekoSwordserverException): + _get_status_workflow_document(expected_activity_id, None) # raise WekoSwordserverException with pytest.raises(WekoSwordserverException) as e: @@ -961,6 +1203,372 @@ def test__get_status_workflow_document(app, records): assert e.message == "Activity created, but not found." assert e.errorType == ErrorType.NotFound + # not exist activity_id + recid_valid = records[0][0].pid_value + with app.test_request_context("/test_req"): + with pytest.raises(WekoSwordserverException) as e: + _get_status_workflow_document(None, recid_valid) + assert e.value.errorType == ErrorType.NotFound + assert "Activity created, but not found" in e.value.message + +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_status_workflow_document_files_info_none -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test_status_workflow_document_files_info_none(app, mocker): + from weko_swordserver.views import _get_status_workflow_document + activity_id = "A-20240301-00001" + recid = "dummy_recid" + with app.test_request_context("/test_req"): + # Mock _get_file_info to return None + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + # Mock import_string, Resolver, get_record_permalink minimally + mock_record = type("MockRecord", (), {"revision_id": 1, "get": lambda self, k, d=None: None})() + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda x: mock_record})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, mock_record)})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + # Mock url_for minimally + mocker.patch("weko_swordserver.views.url_for", side_effect=lambda endpoint, **kwargs: f"/dummy/{endpoint}/{kwargs.get('recid', '') or kwargs.get('activity_id', '')}") + result = _get_status_workflow_document(activity_id, recid) + # No file links should be added to links + file_links = [l for l in result["links"] if l.get("rel") and any("file" in r for r in l.get("rel"))] + assert not file_links + +import os + +from flask import Flask +from weko_swordserver.views import _get_file_info + +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test__get_file_info -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test__get_file_info(app): + # With file attribute + record = { + "file_attr": { + "attribute_type": "file", + "attribute_value_mlt": [ + { + "url": {"url": "http://example.com/files/test.pdf", "label": "test.pdf"}, + "mimetype": "application/pdf", + "format": None + } + ] + } + } + record_url = "http://example.com/records/1" + with app.app_context(): + current_app = app + current_app.config["WEKO_SWORDSERVER_SWORD_VERSION"] = "http://purl.org/net/sword/3.0" + current_app.config["WEKO_SWORDSERVER_FILE_SET_FILE"] = "/terms/fileSetFile" + result = _get_file_info(record, record_url) + expected = { + "test.pdf": { + "@id": "http://example.com/files/test.pdf", + "contentType": "application/pdf", + "rel": ["http://purl.org/net/sword/3.0/terms/fileSetFile"], + "derivedFrom": record_url + } + } + assert result == expected + + # Without file attribute + record_no_file = { + "title": {"attribute_type": "title", "attribute_value_mlt": ["test title"]} + } + with app.app_context(): + result = _get_file_info(record_no_file, record_url) + assert result == {} + + # When url or label does not exist + record = { + "file": { + "attribute_type": "file", + "attribute_value_mlt": [ + {"url": None, "mimetype": "application/pdf"}, + {"url": {"url": None, "label": None}, "mimetype": "application/pdf"}, + {"url": {"url": "", "label": None}, "mimetype": "application/pdf"}, + {"url": {"url": None, "label": ""}, "mimetype": "application/pdf"}, + {"url": {"url": None, "label": "label1"}, "mimetype": "application/pdf"}, + {"url": {"url": "http://example.com/file1.pdf", "label": None}, "mimetype": "application/pdf"}, + ] + } + } + record_url = "http://example.com/records/1" + files_info = _get_file_info(record, record_url) + # None have both url and label, so should be empty + assert files_info == {} + + +from weko_swordserver.views import _sort_links_for_status + +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test__sort_links_for_status -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test__sort_links_for_status(): + links = [ + { + "@id": "http://example.com/records/2", + "rel": ["alternate"], + "contentType": "text/html" + }, + { + "@id": "http://example.com/files/file1.pdf", + "rel": ["http://purl.org/net/sword/3.0/terms/fileSetFile"], + "derivedFrom": "http://example.com/records/1", + "contentType": "application/pdf" + }, + { + "@id": "http://example.com/workflow/activity/detail/A-20260101-00001", + "rel": ["alternate"], + "contentType": "text/html" + }, + { + "@id": "http://example.com/other", + "rel": ["other"], + "contentType": "text/plain" + }, + { + "@id": "http://example.com/records/1", + "rel": ["alternate"], + "contentType": "text/html" + }, + { + "@id": "http://example.com/files/file2.pdf", + "rel": ["http://purl.org/net/sword/3.0/terms/fileSetFile"], + "derivedFrom": "http://example.com/records/2", + "contentType": "application/pdf" + }, + { + "@id": "http://example.com/workflow/activity/detail/A-20260101-00002", + "rel": ["alternate"], + "contentType": "text/html" + } + ] + sorted_links = _sort_links_for_status(links) + assert sorted_links[0]["@id"] == "http://example.com/workflow/activity/detail/A-20260101-00001" + assert sorted_links[1]["@id"] == "http://example.com/workflow/activity/detail/A-20260101-00002" + assert sorted_links[2]["@id"] == "http://example.com/records/1" + assert sorted_links[3]["@id"] == "http://example.com/records/2" + assert sorted_links[4]["@id"] == "http://example.com/files/file1.pdf" + assert sorted_links[5]["@id"] == "http://example.com/files/file2.pdf" + assert sorted_links[6]["@id"] == "http://example.com/other" + + +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test__get_status_multi_document -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test__get_status_multi_document(app, mocker): + from weko_swordserver.views import _get_status_multi_document + + # Common url_for + def url_for_side_effect(endpoint, **kwargs): + if endpoint == "weko_workflow.display_activity": + return f"/workflow/activity/detail/{kwargs.get('activity_id', '')}" + if endpoint == "weko_swordserver.get_status_document": + return f"/dummy/status/{kwargs.get('recid', '')}" + if endpoint == "weko_swordserver.get_service_document": + return "/dummy/service" + return f"/dummy/{endpoint}/{kwargs.get('recid', '') or kwargs.get('activity_id', '')}" + mocker.patch("weko_swordserver.views.url_for", side_effect=url_for_side_effect) + + # 1. With file, with permalink, reverse reference as int type + class MockRecord1: + revision_id = 1 + def get(self, key, default=None): + if key == "system_identifier_doi": + return None + return default + def items(self): + return [ + ("file_attr", { + "attribute_type": "file", + "attribute_value_mlt": [ + {"url": {"url": "http://TEST_SERVER.localdomain/files/test.pdf", "label": "test.pdf"}, "mimetype": "application/pdf", "format": None} + ] + }) + ] + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda recid: MockRecord1()})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, MockRecord1())})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value="http://example.com/permalink") + mocker.patch("weko_swordserver.views._get_file_info", return_value={ + "test.pdf": { + "@id": "http://TEST_SERVER.localdomain/files/test.pdf", + "contentType": "application/pdf", + "rel": ["http://purl.org/net/sword/3.0//terms/fileSetFile"], + "derivedFrom": "/dummy/records/1" + } + }) + MockRef = type("MockRef", (), {"src_item_pid": "1", "reference_type": "cites"}) + mocker.patch("weko_records.models.ItemReference.get_dst_references", return_value=[MockRef()]) + with app.test_request_context("/test_req"): + result = _get_status_multi_document(["1"], [], register_type="Direct") + import json + expected_links = [ + { + "@id": "http://TEST_SERVER.localdomain/records/1", + "contentType": "text/html", + "log": json.dumps([ + {"type": "cites", "url": "http://TEST_SERVER.localdomain/records/1"} + ]), + "rel": ["alternate"] + }, + { + "@id": "http://TEST_SERVER.localdomain/files/test.pdf", + "contentType": "application/pdf", + "rel": ["http://purl.org/net/sword/3.0//terms/fileSetFile"], + "derivedFrom": "/dummy/records/1" + }, + { + "@id": "http://example.com/permalink", + "contentType": "text/html", + "rel": ["alternate"] + } + ] + assert "links" in result + assert len(result["links"]) == len(expected_links) + assert all(link in expected_links for link in result["links"]) + assert all(link in result["links"] for link in expected_links) + + # Pattern where log contains multiple entries + MockRef2 = type("MockRef2", (), {"src_item_pid": "2", "reference_type": "isReferencedBy"}) + MockRef3 = type("MockRef3", (), {"src_item_pid": "3", "reference_type": "isSupplementedBy"}) + MockRef4 = type("MockRef4", (), {"src_item_pid": "4", "reference_type": "otherType"}) + class MockRecordEmpty: + revision_id = 1 + def get(self, key, default=None): return default + def items(self): return [] + def get_record_multi(recid): + return MockRecordEmpty() + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": get_record_multi})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, get_record_multi(recid))})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + mocker.patch("weko_records.models.ItemReference.get_dst_references", return_value=[MockRef(), MockRef2(), MockRef3(), MockRef4()]) + with app.test_request_context("/test_req"): + result_multi = _get_status_multi_document(["1", "2", "3"], [], register_type="Direct") + expected_links = [ + { + "@id": "http://TEST_SERVER.localdomain/records/1", + "contentType": "text/html", + "log": json.dumps([ + {"type": "cites", "url": "http://TEST_SERVER.localdomain/records/1"}, + {"type": "isReferencedBy", "url": "http://TEST_SERVER.localdomain/records/2"}, + {"type": "isSupplementedBy", "url": "http://TEST_SERVER.localdomain/records/3"} + ]), + "rel": ["alternate"] + }, + { + "@id": "http://TEST_SERVER.localdomain/records/2", + "contentType": "text/html", + "log": json.dumps([ + {"type": "cites", "url": "http://TEST_SERVER.localdomain/records/1"}, + {"type": "isReferencedBy", "url": "http://TEST_SERVER.localdomain/records/2"}, + {"type": "isSupplementedBy", "url": "http://TEST_SERVER.localdomain/records/3"} + ]), + "rel": ["alternate"] + }, + { + "@id": "http://TEST_SERVER.localdomain/records/3", + "contentType": "text/html", + "log": json.dumps([ + {"type": "cites", "url": "http://TEST_SERVER.localdomain/records/1"}, + {"type": "isReferencedBy", "url": "http://TEST_SERVER.localdomain/records/2"}, + {"type": "isSupplementedBy", "url": "http://TEST_SERVER.localdomain/records/3"} + ]), + "rel": ["alternate"] + } + ] + assert "links" in result_multi + assert len(result_multi["links"]) == len(expected_links) + assert all(link in expected_links for link in result_multi["links"]) + assert all(link in result_multi["links"] for link in expected_links) + + # 3. No file, no permalink, with system_identifier_doi (permalink supplement) + class MockRecord2: + revision_id = 2 + def get(self, key, default=None): + if key == "system_identifier_doi": + return {"attribute_value_mlt": [{"subitem_systemidt_identifier": "http://example.com/doi_subitem"}]} + return default + def __getitem__(self, key): + if key == "system_identifier_doi": + return {"attribute_value_mlt": [{"subitem_systemidt_identifier": "http://example.com/doi_subitem"}]} + raise KeyError(key) + def items(self): return [] + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda recid: MockRecord2()})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, MockRecord2())})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + mocker.patch("weko_records.models.ItemReference.get_dst_references", return_value=[]) + with app.test_request_context("/test_req"): + result = _get_status_multi_document(["2"], [], register_type="Direct") + expected_links = [ + { + "@id": "http://TEST_SERVER.localdomain/records/2", + "contentType": "text/html", + "rel": ["alternate"] + }, + { + "@id": "http://example.com/doi_subitem", + "contentType": "text/html", + "rel": ["alternate"] + } + ] + assert "links" in result + assert len(result["links"]) == len(expected_links) + assert all(link in expected_links for link in result["links"]) + assert all(link in result["links"] for link in expected_links) + + # 4. Reverse reference as float type (continue branch) + class MockRecord3: + revision_id = 3 + def get(self, key, default=None): return default + def items(self): return [] + MockRefFloat = type("MockRefFloat", (), {"src_item_pid": "10.5", "reference_type": "cites"}) + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda recid: MockRecord3()})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, MockRecord3())})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + mocker.patch("weko_records.models.ItemReference.get_dst_references", return_value=[MockRefFloat()]) + with app.test_request_context("/test_req"): + result = _get_status_multi_document(["3"], [], register_type="Direct") + expected_links = [ + { + "@id": "http://TEST_SERVER.localdomain/records/3", + "contentType": "text/html", + "rel": ["alternate"] + } + ] + assert "links" in result + assert len(result["links"]) == len(expected_links) + assert all(link in expected_links for link in result["links"]) + assert all(link in result["links"] for link in expected_links) + + # 5. Workflow (with activity_ids) + class MockRecord4: + revision_id = 4 + def get(self, key, default=None): return default + def items(self): return [] + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda recid: MockRecord4()})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, MockRecord4())})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + mocker.patch("weko_records.models.ItemReference.get_dst_references", return_value=[]) + with app.test_request_context("/test_req"): + result = _get_status_multi_document(["4"], ["A-0001", "A-0002"], register_type="Workflow") + expected_links = [ + { + "@id": "/workflow/activity/detail/A-0001", + "contentType": "text/html", + "rel": ["alternate"] + }, + { + "@id": "/workflow/activity/detail/A-0002", + "contentType": "text/html", + "rel": ["alternate"] + }, + { + "@id": "http://TEST_SERVER.localdomain/records/4", + "contentType": "text/html", + "rel": ["alternate"] + } + ] + assert "links" in result + assert len(result["links"]) == len(expected_links) + assert all(link in expected_links for link in result["links"]) + assert all(link in result["links"] for link in expected_links) # def delete_item(recid): # .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_delete_item -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp diff --git a/modules/weko-swordserver/weko_swordserver/config.py b/modules/weko-swordserver/weko_swordserver/config.py index 4efff94f9e..35b1638be7 100644 --- a/modules/weko-swordserver/weko_swordserver/config.py +++ b/modules/weko-swordserver/weko_swordserver/config.py @@ -118,3 +118,6 @@ "Contributor" ] """ Roles that can deposit items with token authentication. """ + +WEKO_SWORDSERVER_FILE_SET_FILE = "/terms/fileSetFile" +""" File path of file set file in SWORD server. """ diff --git a/modules/weko-swordserver/weko_swordserver/views.py b/modules/weko-swordserver/weko_swordserver/views.py index e4bce0bbe4..c3f727a9fd 100644 --- a/modules/weko-swordserver/weko_swordserver/views.py +++ b/modules/weko-swordserver/weko_swordserver/views.py @@ -14,6 +14,7 @@ from datetime import datetime, timedelta import sys import traceback +import json from flask import Blueprint, current_app, jsonify, request, url_for, abort, Response from flask_login import current_user @@ -31,6 +32,8 @@ from invenio_oauth2server.decorators import require_oauth_scopes from invenio_oauth2server.ext import verify_oauth_token_and_set_current_user from invenio_oauth2server.provider import oauth2 +from invenio_pidstore.resolver import Resolver +from werkzeug.utils import import_string from weko_accounts.utils import roles_required from weko_admin.api import TempDirInfo @@ -398,12 +401,17 @@ def process_item(item, request_info): warns = [] activity_id = None recid = None + recids = [] + activity_ids = [] action = None # Process and register items for item in check_result["list_record"]: item["root_path"] = os.path.join(data_path, "data") try: activity_id, recid, action, error = process_item(item, request_info) + recids.append(recid) + activity_ids.append(activity_id) + if error: warns.append((activity_id, recid, error)) if file_format == "JSON": @@ -452,11 +460,22 @@ def process_item(item, request_info): .format(request.oauth.client.name, recid) ) if register_type == "Direct": - response = jsonify(_get_status_document(recid)), 201 + if len(recids) > 1: + response = jsonify(_get_status_multi_document(recids, None, register_type)), 201 + else: + recid = recids[0] + response = jsonify(_get_status_document(recid)), 201 else: - response = jsonify( - _get_status_workflow_document(activity_id, recid) - ), 201 if action == "end_action" else 202 + if len(activity_ids) > 1: + response = jsonify( + _get_status_multi_document(recids, activity_ids, register_type) + ), 201 if action == "end_action" else 202 + else: + activity_id = activity_ids[0] + recid = recids[0] + response = jsonify( + _get_status_workflow_document(activity_id, recid) + ), 201 if action == "end_action" else 202 return response @@ -797,8 +816,6 @@ def _get_status_document(recid): """ # Get record - from invenio_pidstore.resolver import Resolver - from werkzeug.utils import import_string record_class = import_string("weko_deposit.api:WekoRecord") try: resolver = Resolver(pid_type="recid", object_type="rec", @@ -819,6 +836,9 @@ def _get_status_document(recid): "attribute_value_mlt"][0][ "subitem_systemidt_identifier"] + # Get file info + files_info = _get_file_info(record, record_uri) + """ Set raw data to StatusDocument @@ -865,6 +885,10 @@ def _get_status_document(recid): }, ] } + if files_info is not None: + for _, file_info in files_info.items(): + raw_data["links"].append(file_info) + if permalink: raw_data["links"].append({ "@id" : permalink, @@ -876,6 +900,138 @@ def _get_status_document(recid): return statusDocument.data +def _get_status_multi_document(recids, activity_ids, register_type="Direct"): + """Generate a Status Document for multiple records. + + Args: + recids (list): List of item identifiers (recid). + activity_ids (list): List of activity identifiers. + register_type (str): Type of registration, either "Direct" or "Workflow". + + Returns: + dict: A Status Document. + """ + from weko_records.models import ItemReference + + record_class = import_string("weko_deposit.api:WekoRecord") + records = [] + for recid in recids: + resolver = Resolver(pid_type="recid", object_type="rec", + getter=record_class.get_record) + pid, record = resolver.resolve(recid) + records.append({recid: record}) + + all_links = [] + last_record = None + last_recid = recids[-1] + for record_val in records: + recid = next(iter(record_val)) + record = record_val[recid] + record_uri = "{}records/{}".format(request.url_root, recid) + + permalink = get_record_permalink(record) + if ( + not permalink + and record.get("system_identifier_doi") + and record.get("system_identifier_doi").get("attribute_value_mlt")[0] + ): + permalink = record["system_identifier_doi"][ + "attribute_value_mlt"][0]["subitem_systemidt_identifier"] + + files_info = _get_file_info(record, record_uri) + + inverse_refs = ItemReference.get_dst_references(recid) + logs = [] + for ref in inverse_refs: + src_pid = ref.src_item_pid + if not float(src_pid).is_integer(): + continue + if str(int(src_pid)) not in [str(int(float(r))) for r in recids]: + continue + src_uri = "{}records/{}".format(request.url_root, src_pid) + ref_type = ref.reference_type + logs.append({"type": ref_type, "url": src_uri}) + + # Add record URI to links + all_links.append({ + "@id": record_uri, + "rel": ["alternate"], + "contentType": "text/html" + }) + + if logs: + all_links[-1]["log"] = json.dumps(logs) + + # Add file links + if files_info is not None: + for _, file_info in files_info.items(): + all_links.append(file_info) + + if permalink: + all_links.append({ + "@id": permalink, + "rel": ["alternate"], + "contentType": "text/html" + }) + + if recid == last_recid: + last_record = record + + if register_type == "Workflow": + for activity_id in activity_ids: + all_links.append({ + "@id": url_for( + "weko_workflow.display_activity", + activity_id=activity_id, + _external=True + ), + "rel": ["alternate"], + "contentType": "text/html" + }) + raw_data = { + "@context": constants.JSON_LD_CONTEXT, + "@type": constants.DocumentType.Status[0], + "@id": url_for( + "weko_swordserver.get_status_document", + recid=last_recid, + _external=True + ), + "actions": { + "getMetadata": False, + "getFiles": False, + "appendMetadata": False, + "appendFiles": False, + "replaceMetadata": False, + "replaceFiles": False, + "deleteMetadata": False, + "deleteFiles": False, + "deleteObject": True, + }, + "fileSet": {}, + "metadata": {}, + "service": url_for("weko_swordserver.get_service_document"), + "links": _sort_links_for_status(all_links) + } + + if register_type == "Workflow": + raw_data["state"] = [ + { + "@id": SwordState.inWorkflow, + "description": "" + } + ] + else: + raw_data["eTag"] = str(last_record.revision_id) + raw_data["state"] = [ + { + "@id": SwordState.ingested, + "description": "" + } + ] + + statusDocument = StatusDocument(raw=raw_data) + return statusDocument.data + def _get_status_workflow_document(activity_id, recid): """ :param recid: Record Identifier. @@ -889,13 +1045,49 @@ def _get_status_workflow_document(activity_id, recid): # "@context" # "@type" """ + record_class = import_string("weko_deposit.api:WekoRecord") + try: + resolver = Resolver(pid_type="recid", object_type="rec", + getter=record_class.get_record) + pid, record = resolver.resolve(recid) + except Exception: + raise WekoSwordserverException("Item not found. (recid={})".format(recid), ErrorType.NotFound) if not activity_id: raise WekoSwordserverException("Activity created, but not found.", ErrorType.NotFound) # Get record uri - record_url = "" - if recid: - record_url = url_for("weko_swordserver.get_status_document", recid=recid, _external=True) + record_url = url_for("weko_swordserver.get_status_document", recid=recid, _external=True) + links_record_url = "{}records/{}".format(request.url_root, recid) + # Get file info + files_info = None + from weko_workflow.models import Activity + activity = Activity.query.filter_by(activity_id=activity_id).first() + if activity and activity.temp_data: + decoded = activity.temp_data.encode().decode('unicode_escape') + temp_data = json.loads(decoded) + files = temp_data.get("files") + files_info = {} + if files: + for file in files: + label = file.get("filename") + host_name = os.environ.get("INVENIO_WEB_HOST_NAME") + url = f"https://{host_name}/record/{recid}/files/{label}" + content_type = file.get("mimetype") + file_rel = ( + current_app.config["WEKO_SWORDSERVER_SWORD_VERSION"] + + current_app.config["WEKO_SWORDSERVER_FILE_SET_FILE"] + ) + if label: + files_info[label] = { + "@id": url, + "contentType": content_type, + "rel": [file_rel], + "derivedFrom": links_record_url + } + if not files_info: + files_info = None + else: + files_info = _get_file_info(record, links_record_url) raw_data = { "@id": record_url, @@ -933,13 +1125,70 @@ def _get_status_workflow_document(activity_id, recid): "rel" : ["alternate"], "contentType" : "text/html" }, + { + "@id" : links_record_url, + "rel" : ["alternate"], + "contentType" : "text/html" + } ] } - + if files_info is not None: + for _, file_info in files_info.items(): + raw_data["links"].append(file_info) statusDocument = StatusDocument(raw=raw_data) return statusDocument.data +def _get_file_info(record, record_url): + files_info = {} + file_rel = ( + current_app.config["WEKO_SWORDSERVER_SWORD_VERSION"] + + current_app.config["WEKO_SWORDSERVER_FILE_SET_FILE"] + ) + for _, attr_val in record.items(): + if isinstance(attr_val, dict) and attr_val.get("attribute_type", None) == "file": + file_mlt = attr_val.get("attribute_value_mlt") + for file in file_mlt: + url_info = file.get("url", None) + url = url_info.get("url") if isinstance(url_info, dict) else None + label = url_info.get("label", None) if isinstance(url_info, dict) else None + content_type = file.get("mimetype") or file.get("format") + if url and label: + files_info[label] = { + "@id": url, + "contentType": content_type, + "rel": [file_rel], + "derivedFrom": record_url + } + return files_info + +def _sort_links_for_status(links): + import re + def link_key(link): + link_id = link.get("@id", "") + rel = link.get("rel", []) + # 1. Workflow activity link + if "/workflow/activity/detail/" in link_id: + group = 0 + m = re.search(r'/workflow/activity/detail/[^-]+-\d+-0*(\d+)', link_id) + order = int(m.group(1)) if m else 0 + # 2. Record HTML link + elif "/records/" in link_id and "alternate" in rel: + group = 1 + m = re.search(r'/records/(\d+)', link_id) + order = int(m.group(1)) if m else 0 + # 3. File link + elif "fileSetFile" in "".join(rel): + group = 2 + derived = link.get("derivedFrom", "") + m = re.search(r'/records/(\d+)', derived) + order = int(m.group(1)) if m else 0 + else: + group = 3 + order = 0 + return (group, order) + return sorted(links, key=link_key) + @blueprint.route("/deposit/", methods=["DELETE"]) @oauth2.require_oauth() @limiter.limit("") diff --git a/scripts/instance.cfg b/scripts/instance.cfg index 90024af4a1..1c3ae1662a 100644 --- a/scripts/instance.cfg +++ b/scripts/instance.cfg @@ -55,7 +55,7 @@ CRAWLER_REDIS_DB = 3 CRAWLER_REDIS_PORT = 6379 CRAWLER_REDIS_TTL = 86400 -GROUP_INFO_REDIS_DB = 4 +GROUP_INFO_REDIS_DB = 4 # Celery CELERY_GET_STATUS_TIMEOUT = 3.0 @@ -96,7 +96,7 @@ WEKO_ITEMS_UI_CRIS_LINKAGE_RESEARCHMAP_MAPPINGS = [ ,{ 'type' : 'lang' , "rm_name" : 'presentation_title', "jpcoar_name" : 'dc:title' , "weko_name" :"title"} ,{ 'type' : 'lang' , "rm_name" : 'work_title', "jpcoar_name" : 'dc:title' , "weko_name" :"title"} ,{ 'type' : 'lang' , "rm_name" : 'other_title', "jpcoar_name" : 'dc:title' , "weko_name" :"title"} - + ,{'type' : 'lang' , "rm_name" : 'description', "jpcoar_name" : 'datacite:description' , "weko_name" :"description"} ,{'type' : 'lang' , "rm_name" : 'publisher', "jpcoar_name" : 'dc:publisher' , "weko_name" :"publisher"} ,{'type' : 'lang' , "rm_name" : 'publication_name', "jpcoar_name" : 'jpcoar:sourceTitle' , "weko_name" :"sourceTitle"} @@ -912,3 +912,6 @@ WEKO_RECORDS_UI_S3_TRANSFER_USE_THREADS = True WEKO_RECORDS_UI_S3_TRANSFER_MAX_CONCURRENCY = 10 """Number of threads for multipart upload/download for S3 compatible service transfer. Default is 10.""" + +WEKO_SWORDSERVER_FILE_SET_FILE = "/terms/fileSetFile" +""" File path of file set file in SWORD server. """