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. """