From f5098f7c292f11fe18a9e6240ea610f5a808ed11 Mon Sep 17 00:00:00 2001 From: "kenji.shiokawa" Date: Tue, 10 Feb 2026 01:22:43 +0900 Subject: [PATCH 01/10] SWORDv3 Links Information Revision --- .../weko-search-ui/weko_search_ui/utils.py | 7 + .../weko_swordserver/config.py | 3 + .../weko_swordserver/views.py | 223 +++++++++++++++++- 3 files changed, 226 insertions(+), 7 deletions(-) diff --git a/modules/weko-search-ui/weko_search_ui/utils.py b/modules/weko-search-ui/weko_search_ui/utils.py index 3c02cde998..bd9ce82118 100644 --- a/modules/weko-search-ui/weko_search_ui/utils.py +++ b/modules/weko-search-ui/weko_search_ui/utils.py @@ -1589,7 +1589,14 @@ 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: + item["id"] = recid + item_id = recid + system_url = request.host_url + "records/" + str(item_id) + if item.get("uri") is 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/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..26fcd77aad 100644 --- a/modules/weko-swordserver/weko_swordserver/views.py +++ b/modules/weko-swordserver/weko_swordserver/views.py @@ -31,6 +31,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 +400,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 +459,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 +815,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 +835,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 +884,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 +899,129 @@ 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 + 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"] = 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,6 +1035,13 @@ 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) @@ -896,6 +1049,8 @@ def _get_status_workflow_document(activity_id, recid): record_url = "" if recid: record_url = url_for("weko_swordserver.get_status_document", recid=recid, _external=True) + # Get file info + files_info = _get_file_info(record, record_url) raw_data = { "@id": record_url, @@ -933,13 +1088,67 @@ def _get_status_workflow_document(activity_id, recid): "rel" : ["alternate"], "contentType" : "text/html" }, + { + "@id" : 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) + 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. ワークフローactivityリンク + 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. レコードHTMLリンク + 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. ファイルリンク + 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("") From a37e8172e9c704c7f8c92ce67b0c196398c1b58b Mon Sep 17 00:00:00 2001 From: "kenji.shiokawa" Date: Tue, 10 Feb 2026 02:02:18 +0900 Subject: [PATCH 02/10] Revert "SWORDv3 Links Information Revision" This reverts commit f6af8dc36e928269ef8227dea3be9c216e6b9367. --- .../weko-search-ui/weko_search_ui/utils.py | 7 - .../weko_swordserver/config.py | 3 - .../weko_swordserver/views.py | 223 +----------------- 3 files changed, 7 insertions(+), 226 deletions(-) diff --git a/modules/weko-search-ui/weko_search_ui/utils.py b/modules/weko-search-ui/weko_search_ui/utils.py index bd9ce82118..3c02cde998 100644 --- a/modules/weko-search-ui/weko_search_ui/utils.py +++ b/modules/weko-search-ui/weko_search_ui/utils.py @@ -1589,14 +1589,7 @@ 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: - item["id"] = recid - item_id = recid - system_url = request.host_url + "records/" + str(item_id) - if item.get("uri") is 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/weko_swordserver/config.py b/modules/weko-swordserver/weko_swordserver/config.py index 35b1638be7..4efff94f9e 100644 --- a/modules/weko-swordserver/weko_swordserver/config.py +++ b/modules/weko-swordserver/weko_swordserver/config.py @@ -118,6 +118,3 @@ "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 26fcd77aad..e4bce0bbe4 100644 --- a/modules/weko-swordserver/weko_swordserver/views.py +++ b/modules/weko-swordserver/weko_swordserver/views.py @@ -31,8 +31,6 @@ 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 @@ -400,17 +398,12 @@ 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": @@ -459,22 +452,11 @@ def process_item(item, request_info): .format(request.oauth.client.name, recid) ) if register_type == "Direct": - 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 + response = jsonify(_get_status_document(recid)), 201 else: - 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 + response = jsonify( + _get_status_workflow_document(activity_id, recid) + ), 201 if action == "end_action" else 202 return response @@ -815,6 +797,8 @@ 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", @@ -835,9 +819,6 @@ 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 @@ -884,10 +865,6 @@ 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, @@ -899,129 +876,6 @@ 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 - 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"] = 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. @@ -1035,13 +889,6 @@ 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) @@ -1049,8 +896,6 @@ def _get_status_workflow_document(activity_id, recid): record_url = "" if recid: record_url = url_for("weko_swordserver.get_status_document", recid=recid, _external=True) - # Get file info - files_info = _get_file_info(record, record_url) raw_data = { "@id": record_url, @@ -1088,67 +933,13 @@ def _get_status_workflow_document(activity_id, recid): "rel" : ["alternate"], "contentType" : "text/html" }, - { - "@id" : 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) - 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. ワークフローactivityリンク - 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. レコードHTMLリンク - 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. ファイルリンク - 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("") From 9d49f30261a4e548b32622e74b5651bdf979f481 Mon Sep 17 00:00:00 2001 From: "kenji.shiokawa" Date: Wed, 11 Feb 2026 08:17:41 +0900 Subject: [PATCH 03/10] SWORDv3 Links Information Revision --- .../weko-search-ui/weko_search_ui/utils.py | 7 + .../weko_swordserver/config.py | 3 + .../weko_swordserver/views.py | 228 +++++++++++++++++- scripts/instance.cfg | 7 +- 4 files changed, 233 insertions(+), 12 deletions(-) diff --git a/modules/weko-search-ui/weko_search_ui/utils.py b/modules/weko-search-ui/weko_search_ui/utils.py index 3c02cde998..68154be4ff 100644 --- a/modules/weko-search-ui/weko_search_ui/utils.py +++ b/modules/weko-search-ui/weko_search_ui/utils.py @@ -1589,7 +1589,14 @@ 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/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..3bc8446e4c 100644 --- a/modules/weko-swordserver/weko_swordserver/views.py +++ b/modules/weko-swordserver/weko_swordserver/views.py @@ -31,6 +31,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 +400,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 +459,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 +815,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 +835,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 +884,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 +899,129 @@ 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 + 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"] = 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 +1035,20 @@ 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) + # Get file info + files_info = _get_file_info(record, record_url) raw_data = { "@id": record_url, @@ -933,13 +1086,68 @@ def _get_status_workflow_document(activity_id, recid): "rel" : ["alternate"], "contentType" : "text/html" }, + { + "@id" : 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) + print("url_info:", url_info) + 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. ワークフローactivityリンク + 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. レコードHTMLリンク + 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. ファイルリンク + 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. """ From 60b4afd32f77d2159675caf93c0f00ea2062b5b5 Mon Sep 17 00:00:00 2001 From: "kenji.shiokawa" Date: Thu, 12 Feb 2026 03:09:49 +0900 Subject: [PATCH 04/10] SWORDv3 Links Information Unit Test --- .../b4_handle_check_exist_record4.json | 1 + modules/weko-search-ui/tests/test_utils.py | 125 +++- .../tests/data/records/test_items.json | 33 +- .../tests/data/records/test_records.json | 50 ++ modules/weko-swordserver/tests/test_views.py | 559 +++++++++++++++++- 5 files changed, 757 insertions(+), 11 deletions(-) create mode 100644 modules/weko-search-ui/tests/data/list_records/b4_handle_check_exist_record4.json 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-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..658f1afe81 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,305 @@ 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 = [ + # group=2 file link + { + "@id": "http://example.com/files/file1.pdf", + "rel": ["http://purl.org/net/sword/3.0/terms/fileSetFile"], + "derivedFrom": "http://example.com/records/2", + "contentType": "application/pdf" + }, + # group=1 record HTML link recid=1 + { + "@id": "http://example.com/records/1", + "rel": ["alternate"], + "contentType": "text/html" + }, + # group=0 workflow activity link + { + "@id": "http://example.com/workflow/activity/detail/A-TEST-00001-001-00001", + "rel": ["alternate"], + "contentType": "text/html" + }, + # group=1 record HTML link recid=2 + { + "@id": "http://example.com/records/2", + "rel": ["alternate"], + "contentType": "text/html" + }, + # group=3 other + { + "@id": "http://example.com/other", + "rel": ["other"], + "contentType": "text/plain" + } + ] + sorted_links = _sort_links_for_status(links) + # Order: activity link → records/1 → records/2 → file1.pdf → other + assert sorted_links[0]["@id"].startswith("http://example.com/workflow/activity/detail/") + assert sorted_links[1]["@id"] == "http://example.com/records/1" + assert sorted_links[2]["@id"] == "http://example.com/records/2" + assert sorted_links[3]["@id"] == "http://example.com/files/file1.pdf" + assert sorted_links[4]["@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', '')}" + elif endpoint == "weko_swordserver.get_status_document": + return f"/dummy/status/{kwargs.get('recid', '')}" + elif 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. where recid==last_recid is False (recids=["1", "2"]) + class MockRecordA: + revision_id = 1 + def get(self, key, default=None): + return default + def items(self): + return [] + class MockRecordB: + revision_id = 2 + def get(self, key, default=None): + return default + def items(self): + return [] + def get_record_multi(recid): + if recid == "1": + return MockRecordA() + return MockRecordB() + 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=[]) + with app.test_request_context("/test_req"): + result = _get_status_multi_document(["1", "2"], [], register_type="Direct") + assert isinstance(result, dict) + assert "@context" in result + assert len(result["links"]) >= 2 + + # 2. 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://example.com/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://example.com/files/test.pdf", + "contentType": "application/pdf", + "rel": ["http://purl.org/net/sword/3.0//terms/fileSetFile"], + "derivedFrom": "/dummy/records/1" + } + }) + class MockRef: + src_item_pid = "10" + 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") + assert isinstance(result, dict) + assert "@context" in result + assert "links" in result + assert any(link["@id"] == "http://example.com/files/test.pdf" for link in result["links"]) + assert any(link["@id"] == "http://example.com/permalink" for link in result["links"]) + assert any( + link.get("log") and "10" in str(link["log"]) + for link in result["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") + assert isinstance(result, dict) + assert "@context" in result + assert "links" in result + assert any(link["@id"] == "http://example.com/doi_subitem" for link in result["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 [] + class 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") + assert isinstance(result, dict) + assert "@context" in result + assert "links" in result + assert all( + not link.get("log") + for link in result["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") + assert isinstance(result, dict) + assert "@context" in result + assert "links" in result + assert any(link["@id"] == "/workflow/activity/detail/A-0001" for link in result["links"]) + assert any(link["@id"] == "/workflow/activity/detail/A-0002" for link in result["links"]) + assert any( + s.get("@id") == "http://purl.org/net/sword/3.0/state/inWorkflow" + for s in result.get("state", []) + ) + assert "eTag" not in result # 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 From d659fbb5cbeae408c146a1f4622e36219abb6063 Mon Sep 17 00:00:00 2001 From: "kenji.shiokawa" Date: Thu, 12 Feb 2026 18:29:43 +0900 Subject: [PATCH 05/10] Correction of Issues Raised --- .../weko-search-ui/weko_search_ui/utils.py | 5 +- modules/weko-swordserver/tests/test_views.py | 118 ++++++++++++++---- .../weko_swordserver/views.py | 27 ++-- 3 files changed, 114 insertions(+), 36 deletions(-) diff --git a/modules/weko-search-ui/weko_search_ui/utils.py b/modules/weko-search-ui/weko_search_ui/utils.py index 68154be4ff..c9d077f515 100644 --- a/modules/weko-search-ui/weko_search_ui/utils.py +++ b/modules/weko-search-ui/weko_search_ui/utils.py @@ -1594,7 +1594,10 @@ def handle_check_exist_record(list_record) -> list: 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 + 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)) diff --git a/modules/weko-swordserver/tests/test_views.py b/modules/weko-swordserver/tests/test_views.py index 658f1afe81..158e8217e5 100644 --- a/modules/weko-swordserver/tests/test_views.py +++ b/modules/weko-swordserver/tests/test_views.py @@ -1254,14 +1254,14 @@ def test__get_file_info(app): 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_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"], + "rel": ["http://purl.org/net/sword/3.0/terms/fileSetFile"], "derivedFrom": record_url } } @@ -1300,45 +1300,52 @@ def test__get_file_info(app): # .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 = [ - # group=2 file link + { + "@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/2", + "derivedFrom": "http://example.com/records/1", "contentType": "application/pdf" }, - # group=1 record HTML link recid=1 { - "@id": "http://example.com/records/1", + "@id": "http://example.com/workflow/activity/detail/A-20260101-00001", "rel": ["alternate"], "contentType": "text/html" }, - # group=0 workflow activity link { - "@id": "http://example.com/workflow/activity/detail/A-TEST-00001-001-00001", - "rel": ["alternate"], - "contentType": "text/html" + "@id": "http://example.com/other", + "rel": ["other"], + "contentType": "text/plain" }, - # group=1 record HTML link recid=2 { - "@id": "http://example.com/records/2", + "@id": "http://example.com/records/1", "rel": ["alternate"], "contentType": "text/html" }, - # group=3 other { - "@id": "http://example.com/other", - "rel": ["other"], - "contentType": "text/plain" + "@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) - # Order: activity link → records/1 → records/2 → file1.pdf → other - assert sorted_links[0]["@id"].startswith("http://example.com/workflow/activity/detail/") - assert sorted_links[1]["@id"] == "http://example.com/records/1" - assert sorted_links[2]["@id"] == "http://example.com/records/2" - assert sorted_links[3]["@id"] == "http://example.com/files/file1.pdf" - assert sorted_links[4]["@id"] == "http://example.com/other" + 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 @@ -1413,7 +1420,7 @@ def items(self): } }) class MockRef: - src_item_pid = "10" + 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"): @@ -1423,10 +1430,67 @@ class MockRef: assert "links" in result assert any(link["@id"] == "http://example.com/files/test.pdf" for link in result["links"]) assert any(link["@id"] == "http://example.com/permalink" for link in result["links"]) - assert any( - link.get("log") and "10" in str(link["log"]) - for link in result["links"] - ) + expected_log = [{"type": "cites", "url": "http://TEST_SERVER.localdomain/records/1"}] + log_links = [link for link in result["links"] if link.get("log")] + assert len(log_links) == 1 + import ast + log_value = log_links[0]["log"] + if isinstance(log_value, str): + log_value = ast.literal_eval(log_value) + assert log_value == expected_log + + # Pattern where log contains multiple entries + class MockRef2: + src_item_pid = "2" + reference_type = "isReferencedBy" + class MockRef3: + src_item_pid = "3" + reference_type = "isSupplementedBy" + class MockRef4: + src_item_pid = "4" + reference_type = "otherType" + class MockRecord1: + revision_id = 1 + def get(self, key, default=None): + return default + def items(self): + return [] + class MockRecord2: + revision_id = 2 + def get(self, key, default=None): + return default + def items(self): + return [] + class MockRecord3: + revision_id = 3 + def get(self, key, default=None): + return default + def items(self): + return [] + def get_record_multi(recid): + if recid == "1": + return MockRecord1() + elif recid == "2": + return MockRecord2() + return MockRecord3() + 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()]) + expected_multi_log = [ + {"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"} + ] + with app.test_request_context("/test_req"): + result_multi = _get_status_multi_document(["1", "2", "3"], [], register_type="Direct") + log_links_multi = [link for link in result_multi["links"] if link.get("log")] + assert len(log_links_multi) == 3 + log_raw = log_links_multi[0]["log"] + import ast + log_value = ast.literal_eval(log_raw) + assert log_value == expected_multi_log # 3. No file, no permalink, with system_identifier_doi (permalink supplement) class MockRecord2: diff --git a/modules/weko-swordserver/weko_swordserver/views.py b/modules/weko-swordserver/weko_swordserver/views.py index 3bc8446e4c..8085ba34ef 100644 --- a/modules/weko-swordserver/weko_swordserver/views.py +++ b/modules/weko-swordserver/weko_swordserver/views.py @@ -945,6 +945,8 @@ def _get_status_multi_document(recids, activity_ids, register_type="Direct"): 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}) @@ -977,15 +979,22 @@ def _get_status_multi_document(recids, activity_ids, register_type="Direct"): 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), + "@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), + "@id": url_for( + "weko_swordserver.get_status_document", + recid=last_recid, + _external=True + ), "actions": { "getMetadata": False, "getFiles": False, @@ -1102,13 +1111,15 @@ def _get_status_workflow_document(activity_id, recid): 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"] + 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) - print("url_info:", url_info) 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") @@ -1126,17 +1137,17 @@ def _sort_links_for_status(links): def link_key(link): link_id = link.get("@id", "") rel = link.get("rel", []) - # 1. ワークフローactivityリンク + # 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. レコードHTMLリンク + # 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. ファイルリンク + # 3. File link elif "fileSetFile" in "".join(rel): group = 2 derived = link.get("derivedFrom", "") From 194354e7c1461f3288b2091dfaa429affbf41fd5 Mon Sep 17 00:00:00 2001 From: "kenji.shiokawa" Date: Wed, 18 Feb 2026 01:42:03 +0900 Subject: [PATCH 06/10] fix _get_status_multi_document --- modules/weko-swordserver/tests/test_views.py | 251 +++++++++--------- .../weko_swordserver/views.py | 3 +- 2 files changed, 129 insertions(+), 125 deletions(-) diff --git a/modules/weko-swordserver/tests/test_views.py b/modules/weko-swordserver/tests/test_views.py index 158e8217e5..c621e19e73 100644 --- a/modules/weko-swordserver/tests/test_views.py +++ b/modules/weko-swordserver/tests/test_views.py @@ -1348,51 +1348,22 @@ def test__sort_links_for_status(): 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): +# .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', '')}" - elif endpoint == "weko_swordserver.get_status_document": + if endpoint == "weko_swordserver.get_status_document": return f"/dummy/status/{kwargs.get('recid', '')}" - elif endpoint == "weko_swordserver.get_service_document": + 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. where recid==last_recid is False (recids=["1", "2"]) - class MockRecordA: - revision_id = 1 - def get(self, key, default=None): - return default - def items(self): - return [] - class MockRecordB: - revision_id = 2 - def get(self, key, default=None): - return default - def items(self): - return [] - def get_record_multi(recid): - if recid == "1": - return MockRecordA() - return MockRecordB() - 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=[]) - with app.test_request_context("/test_req"): - result = _get_status_multi_document(["1", "2"], [], register_type="Direct") - assert isinstance(result, dict) - assert "@context" in result - assert len(result["links"]) >= 2 - - # 2. With file, with permalink, reverse reference as int type + # 1. With file, with permalink, reverse reference as int type class MockRecord1: revision_id = 1 def get(self, key, default=None): @@ -1404,7 +1375,7 @@ def items(self): ("file_attr", { "attribute_type": "file", "attribute_value_mlt": [ - {"url": {"url": "http://example.com/files/test.pdf", "label": "test.pdf"}, "mimetype": "application/pdf", "format": None} + {"url": {"url": "http://TEST_SERVER.localdomain/files/test.pdf", "label": "test.pdf"}, "mimetype": "application/pdf", "format": None} ] }) ] @@ -1413,84 +1384,96 @@ def items(self): 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://example.com/files/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" } }) - class MockRef: - src_item_pid = "1" - reference_type = "cites" + 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") - assert isinstance(result, dict) - assert "@context" in result + 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 any(link["@id"] == "http://example.com/files/test.pdf" for link in result["links"]) - assert any(link["@id"] == "http://example.com/permalink" for link in result["links"]) - expected_log = [{"type": "cites", "url": "http://TEST_SERVER.localdomain/records/1"}] - log_links = [link for link in result["links"] if link.get("log")] - assert len(log_links) == 1 - import ast - log_value = log_links[0]["log"] - if isinstance(log_value, str): - log_value = ast.literal_eval(log_value) - assert log_value == expected_log + 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 - class MockRef2: - src_item_pid = "2" - reference_type = "isReferencedBy" - class MockRef3: - src_item_pid = "3" - reference_type = "isSupplementedBy" - class MockRef4: - src_item_pid = "4" - reference_type = "otherType" - class MockRecord1: + 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 [] - class MockRecord2: - revision_id = 2 - def get(self, key, default=None): - return default - def items(self): - return [] - class MockRecord3: - revision_id = 3 - def get(self, key, default=None): - return default - def items(self): - return [] + def get(self, key, default=None): return default + def items(self): return [] def get_record_multi(recid): - if recid == "1": - return MockRecord1() - elif recid == "2": - return MockRecord2() - return MockRecord3() + 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()]) - expected_multi_log = [ - {"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"} - ] with app.test_request_context("/test_req"): result_multi = _get_status_multi_document(["1", "2", "3"], [], register_type="Direct") - log_links_multi = [link for link in result_multi["links"] if link.get("log")] - assert len(log_links_multi) == 3 - log_raw = log_links_multi[0]["log"] - import ast - log_value = ast.literal_eval(log_raw) - assert log_value == expected_multi_log + 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: @@ -1503,8 +1486,7 @@ 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 [] + 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) @@ -1512,21 +1494,29 @@ def items(self): 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") - assert isinstance(result, dict) - assert "@context" in result + 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 any(link["@id"] == "http://example.com/doi_subitem" for link in result["links"]) + 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 [] - class MockRefFloat: - src_item_pid = "10.5" - reference_type = "cites" + 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) @@ -1534,21 +1524,23 @@ class MockRefFloat: 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") - assert isinstance(result, dict) - assert "@context" in result + expected_links = [ + { + "@id": "http://TEST_SERVER.localdomain/records/3", + "contentType": "text/html", + "rel": ["alternate"] + } + ] assert "links" in result - assert all( - not link.get("log") - for link in result["links"] - ) + 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 [] + 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) @@ -1556,16 +1548,27 @@ def items(self): 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") - assert isinstance(result, dict) - assert "@context" in result + 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 any(link["@id"] == "/workflow/activity/detail/A-0001" for link in result["links"]) - assert any(link["@id"] == "/workflow/activity/detail/A-0002" for link in result["links"]) - assert any( - s.get("@id") == "http://purl.org/net/sword/3.0/state/inWorkflow" - for s in result.get("state", []) - ) - assert "eTag" not 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/views.py b/modules/weko-swordserver/weko_swordserver/views.py index 8085ba34ef..1b879d1177 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 @@ -959,7 +960,7 @@ def _get_status_multi_document(recids, activity_ids, register_type="Direct"): }) if logs: - all_links[-1]["log"] = logs + all_links[-1]["log"] = json.dumps(logs) # Add file links if files_info is not None: From a0ba11e36df173e332a7c361daea00d22b4cf8ad Mon Sep 17 00:00:00 2001 From: "kenji.shiokawa" Date: Thu, 19 Feb 2026 00:38:46 +0900 Subject: [PATCH 07/10] fix _get_status_workflow_document --- modules/weko-swordserver/weko_swordserver/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/weko-swordserver/weko_swordserver/views.py b/modules/weko-swordserver/weko_swordserver/views.py index 1b879d1177..c2881107d1 100644 --- a/modules/weko-swordserver/weko_swordserver/views.py +++ b/modules/weko-swordserver/weko_swordserver/views.py @@ -1056,7 +1056,7 @@ def _get_status_workflow_document(activity_id, recid): raise WekoSwordserverException("Activity created, but not found.", ErrorType.NotFound) # Get record uri - record_url = url_for("weko_swordserver.get_status_document", recid=recid, _external=True) + record_url = "{}records/{}".format(request.url_root, recid) # Get file info files_info = _get_file_info(record, record_url) From 38af698dbb330f183cde3c7ff277e5561622b932 Mon Sep 17 00:00:00 2001 From: "kenji.shiokawa" Date: Thu, 19 Feb 2026 12:00:18 +0900 Subject: [PATCH 08/10] fix _get_status_workflow_document --- .../weko_swordserver/views.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/modules/weko-swordserver/weko_swordserver/views.py b/modules/weko-swordserver/weko_swordserver/views.py index c2881107d1..bcd71340c5 100644 --- a/modules/weko-swordserver/weko_swordserver/views.py +++ b/modules/weko-swordserver/weko_swordserver/views.py @@ -1058,7 +1058,35 @@ def _get_status_workflow_document(activity_id, recid): # Get record uri record_url = "{}records/{}".format(request.url_root, recid) # Get file info - files_info = _get_file_info(record, record_url) + 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": record_url + } + if not files_info: + files_info = None + else: + files_info = _get_file_info(record, record_url) raw_data = { "@id": record_url, From 902ce35bbf18ec4dbcf705412ad124bb004a200b Mon Sep 17 00:00:00 2001 From: "kenji.shiokawa" Date: Thu, 19 Feb 2026 13:17:27 +0900 Subject: [PATCH 09/10] fix _get_status_workflow_document record_url --- modules/weko-swordserver/weko_swordserver/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/weko-swordserver/weko_swordserver/views.py b/modules/weko-swordserver/weko_swordserver/views.py index bcd71340c5..7812155489 100644 --- a/modules/weko-swordserver/weko_swordserver/views.py +++ b/modules/weko-swordserver/weko_swordserver/views.py @@ -1056,7 +1056,8 @@ def _get_status_workflow_document(activity_id, recid): raise WekoSwordserverException("Activity created, but not found.", ErrorType.NotFound) # Get record uri - record_url = "{}records/{}".format(request.url_root, recid) + 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 @@ -1081,7 +1082,7 @@ def _get_status_workflow_document(activity_id, recid): "@id": url, "contentType": content_type, "rel": [file_rel], - "derivedFrom": record_url + "derivedFrom": links_record_url } if not files_info: files_info = None @@ -1125,7 +1126,7 @@ def _get_status_workflow_document(activity_id, recid): "contentType" : "text/html" }, { - "@id" : record_url, + "@id" : links_record_url, "rel" : ["alternate"], "contentType" : "text/html" } From 035b20434aafa90f82ac04c53fb5ffef3bd22744 Mon Sep 17 00:00:00 2001 From: "kenji.shiokawa" Date: Thu, 19 Feb 2026 14:15:02 +0900 Subject: [PATCH 10/10] refix get_status_workflow_document --- modules/weko-swordserver/weko_swordserver/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/weko-swordserver/weko_swordserver/views.py b/modules/weko-swordserver/weko_swordserver/views.py index 7812155489..c3f727a9fd 100644 --- a/modules/weko-swordserver/weko_swordserver/views.py +++ b/modules/weko-swordserver/weko_swordserver/views.py @@ -1087,7 +1087,7 @@ def _get_status_workflow_document(activity_id, recid): if not files_info: files_info = None else: - files_info = _get_file_info(record, record_url) + files_info = _get_file_info(record, links_record_url) raw_data = { "@id": record_url,