-
Notifications
You must be signed in to change notification settings - Fork 24
Add <response> element validation for SPS 1.10 compliance #1138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,294 @@ | ||||||
| """ | ||||||
| Validations for the <response> element according to SPS 1.10 specification. | ||||||
|
|
||||||
| This module implements validations for the <response> element, which identifies | ||||||
| a set of responses related to a letter or commentary, mandatorily published | ||||||
| alongside the letter/commentary. | ||||||
|
|
||||||
| Reference: https://docs.google.com/document/d/1GTv4Inc2LS_AXY-ToHT3HmO66UT0VAHWJNOIqzBNSgA/edit#heading=h.response | ||||||
| """ | ||||||
|
|
||||||
| from packtools.sps.validation.utils import build_response | ||||||
|
|
||||||
|
|
||||||
| XML_LANG = "{http://www.w3.org/XML/1998/namespace}lang" | ||||||
|
|
||||||
|
|
||||||
| class ResponseValidation: | ||||||
| """ | ||||||
| Validates <response> elements according to SPS 1.10 rules. | ||||||
|
|
||||||
| Validation rules: | ||||||
| - Presence of @response-type attribute | ||||||
| - Value of @response-type must be "reply" | ||||||
| - Presence of @xml:lang attribute | ||||||
| - Presence of @id attribute | ||||||
| - Uniqueness of @id across all <response> elements | ||||||
| - Presence of <front-stub> child element | ||||||
| - Presence of <body> child element | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, xmltree, params=None): | ||||||
| self.xmltree = xmltree | ||||||
| self.params = params or {} | ||||||
|
|
||||||
| def _get_response_elements(self): | ||||||
| """ | ||||||
| Yield context dicts for each <response> element found in the document. | ||||||
|
|
||||||
| Searches for <response> elements as children of <article> and | ||||||
| <sub-article>. | ||||||
| """ | ||||||
| root = self.xmltree.find(".") | ||||||
| if root is None: | ||||||
| return | ||||||
|
|
||||||
| for response_node in root.xpath(".//response"): | ||||||
| parent_node = response_node.getparent() | ||||||
| if parent_node is not None: | ||||||
| parent_tag = parent_node.tag | ||||||
| if parent_tag == "article": | ||||||
| parent_id = None | ||||||
| parent_article_type = parent_node.get("article-type") | ||||||
| parent_lang = parent_node.get(XML_LANG) | ||||||
| elif parent_tag == "sub-article": | ||||||
| parent_id = parent_node.get("id") | ||||||
| parent_article_type = parent_node.get("article-type") | ||||||
| parent_lang = parent_node.get(XML_LANG) | ||||||
| else: | ||||||
| parent_id = None | ||||||
| parent_article_type = None | ||||||
| parent_lang = None | ||||||
| else: | ||||||
| parent_tag = None | ||||||
| parent_id = None | ||||||
| parent_article_type = None | ||||||
| parent_lang = None | ||||||
|
|
||||||
| yield { | ||||||
| "node": response_node, | ||||||
| "parent": parent_tag, | ||||||
| "parent_id": parent_id, | ||||||
| "parent_article_type": parent_article_type, | ||||||
| "parent_lang": parent_lang, | ||||||
| "response_type": (response_node.get("response-type") or "").strip() or None, | ||||||
| "xml_lang": (response_node.get(XML_LANG) or "").strip() or None, | ||||||
| "id": (response_node.get("id") or "").strip() or None, | ||||||
| "has_front_stub": response_node.find("front-stub") is not None, | ||||||
| "has_body": response_node.find("body") is not None, | ||||||
| } | ||||||
|
|
||||||
| def _build_parent_info(self, ctx): | ||||||
| return { | ||||||
| "parent": ctx["parent"], | ||||||
| "parent_id": ctx["parent_id"], | ||||||
| "parent_article_type": ctx["parent_article_type"], | ||||||
| "parent_lang": ctx["parent_lang"], | ||||||
| } | ||||||
|
|
||||||
| def validate(self): | ||||||
| yield from self.validate_response_type_presence() | ||||||
| yield from self.validate_response_type_value() | ||||||
| yield from self.validate_xml_lang_presence() | ||||||
| yield from self.validate_id_presence() | ||||||
| yield from self.validate_id_uniqueness() | ||||||
| yield from self.validate_front_stub_presence() | ||||||
| yield from self.validate_body_presence() | ||||||
|
|
||||||
| def validate_response_type_presence(self): | ||||||
| """ | ||||||
| Rule 1: Validate that @response-type attribute is present in <response>. | ||||||
| """ | ||||||
| error_level = self.params.get( | ||||||
| "response_type_presence_error_level", "CRITICAL" | ||||||
| ) | ||||||
| for ctx in self._get_response_elements(): | ||||||
| response_type = ctx["response_type"] | ||||||
| is_valid = bool(response_type) | ||||||
| yield build_response( | ||||||
| title="response @response-type presence", | ||||||
| parent=self._build_parent_info(ctx), | ||||||
| item="response", | ||||||
| sub_item="@response-type", | ||||||
| validation_type="exist", | ||||||
| is_valid=is_valid, | ||||||
| expected="reply", | ||||||
| obtained=response_type, | ||||||
| advice='Add @response-type="reply" to <response>.', | ||||||
| data=ctx.get("id"), | ||||||
| error_level=error_level, | ||||||
| element_name="response", | ||||||
| attribute_name="response-type", | ||||||
|
Comment on lines
+120
to
+121
|
||||||
| element_name="response", | |
| attribute_name="response-type", |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "response_rules": { | ||
| "response_type_presence_error_level": "CRITICAL", | ||
| "response_type_value_error_level": "ERROR", | ||
| "xml_lang_presence_error_level": "CRITICAL", | ||
| "id_presence_error_level": "CRITICAL", | ||
| "id_uniqueness_error_level": "ERROR", | ||
| "front_stub_presence_error_level": "WARNING", | ||
| "body_presence_error_level": "WARNING" | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_get_response_elements() docstring says it searches for as children of
/, but the XPath.//responsematches any descendant. Since parent info is derived from the direct parent tag, a nested would be attributed to the wrong parent (or lose article-type/lang context). Either restrict the XPath to the intended locations (e.g., direct children of article/sub-article) or, if descendants are intended, resolve the nearest ancestor / for parent metadata and update the docstring accordingly.