Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ nosetests.xml
.venv

.idea
src/
168 changes: 166 additions & 2 deletions packtools/sps/validation/author_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,106 @@

class AuthorNotesFnValidation(BaseFnValidation):

def validate_fn_type_presence(self):
"""
Validate that @fn-type attribute is mandatory for <fn> in <author-notes>.

SPS 1.10 Rule 1: @fn-type is required for all <fn> elements in <author-notes>.
"""
fn_type = self.fn_data.get("fn_type")
is_valid = fn_type is not None

if not is_valid:
advice = 'Add mandatory @fn-type attribute to <fn> in <author-notes>. Valid values for author notes: abbr, coi-statement, corresp'
advice_text = 'Add mandatory @fn-type attribute to <fn> in <author-notes>. Valid values for author notes: {values}'
advice_params = {"values": "abbr, coi-statement, corresp"}

return build_response(
title="@fn-type attribute presence",
parent=self.fn_data,
item="fn",
sub_item="@fn-type",
validation_type="exist",
is_valid=False,
expected="@fn-type attribute",
obtained=None,
advice=advice,
data=self.fn_data,
error_level="CRITICAL",
advice_text=advice_text,
advice_params=advice_params,
)

def validate_fn_type_author_notes_values(self):
"""
Validate that @fn-type has allowed values for <fn> in <author-notes>.

SPS 1.10 Rule 2: For author notes, only specific values are allowed:
abbr, coi-statement, corresp
"""
fn_type = self.fn_data.get("fn_type")

# Only validate if fn_type exists
if fn_type is None:
return None

# SPS 1.10 allowed values for author-notes context
allowed_values = ["abbr", "coi-statement", "corresp"]
is_valid = fn_type in allowed_values

if not is_valid:
advice = f'@fn-type="{fn_type}" is not valid for <fn> in <author-notes>. Use one of: {", ".join(allowed_values)}'
advice_text = '@fn-type="{fn_type}" is not valid for <fn> in <author-notes>. Use one of: {values}'
advice_params = {
"fn_type": fn_type,
"values": ", ".join(allowed_values)
}

return build_response(
title="@fn-type value in author-notes",
parent=self.fn_data,
item="fn",
sub_item="@fn-type",
validation_type="value in list",
is_valid=False,
expected=allowed_values,
obtained=fn_type,
advice=advice,
data=self.fn_data,
error_level="ERROR",
advice_text=advice_text,
advice_params=advice_params,
)

def validate_corresp_type_recommendation(self):
"""
Warn when @fn-type="corresp" is used in <author-notes>.

SPS 1.10 Rule 8: Recommend using <corresp> element instead of <fn fn-type="corresp">.
"""
fn_type = self.fn_data.get("fn_type")

if fn_type == "corresp":
advice = 'For corresponding author information, use <corresp> element instead of <fn fn-type="corresp">'
advice_text = 'For corresponding author information, use <corresp> element instead of <fn fn-type="corresp">'
advice_params = {}

return build_response(
title="corresp element recommendation",
parent=self.fn_data,
item="fn",
sub_item="@fn-type",
validation_type="recommendation",
is_valid=False,
expected="<corresp> element",
obtained='<fn fn-type="corresp">',
advice=advice,
data=self.fn_data,
error_level="WARNING",
advice_text=advice_text,
advice_params=advice_params,
)

def validate_current_affiliation_attrib_type_deprecation(self):
if "current-aff" == self.fn_data.get("fn_type"):
return build_response(
Expand Down Expand Up @@ -49,10 +149,12 @@ def validate(self):
list[dict]: A list of validation responses (excluding None responses).
"""
validations = [
self.validate_fn_type_presence,
self.validate_fn_type_author_notes_values, # Replaces validate_type from BaseFnValidation with author-notes-specific values
self.validate_corresp_type_recommendation,
self.validate_label,
self.validate_title,
self.validate_bold,
self.validate_type,
self.validate_current_affiliation_attrib_type_deprecation,
self.validate_contribution_attrib_type_deprecation,
]
Expand Down Expand Up @@ -82,7 +184,7 @@ def __init__(self, xml_tree, rules):

xml_article = XMLAuthorNotes(xml_tree)
self.article_author_notes = xml_article.article_author_notes()
self.sub_article_author_notes = xml_article.sub_article_author_notes()
self.sub_article_author_notes = list(xml_article.sub_article_author_notes())

def validate(self):
"""
Expand All @@ -91,12 +193,74 @@ def validate(self):
Yields:
dict: Validation results for each footnote (excluding None).
"""
# Validate uniqueness of <author-notes> in article
yield from self.validate_author_notes_uniqueness()

fn_group = self.article_author_notes
yield from self.validate_fn_group(fn_group)

for fn_group in self.sub_article_author_notes:
yield from self.validate_fn_group(fn_group)

def validate_author_notes_uniqueness(self):
"""
Validate that <author-notes> appears at most once in the document.

SPS 1.10 Rule 4: <author-notes> should appear at most once per article/sub-article.
"""
# Check article level - find root is already the article element
article_author_notes_count = len(self.xml_tree.xpath("./front//author-notes"))

if article_author_notes_count > 1:
advice = f'<author-notes> element should appear at most once in the article. Found {article_author_notes_count} occurrences.'
advice_text = '<author-notes> element should appear at most once in the article. Found {count} occurrences.'
advice_params = {"count": str(article_author_notes_count)}

yield build_response(
title="author-notes uniqueness",
parent={},
item="author-notes",
sub_item=None,
validation_type="uniqueness",
is_valid=False,
expected="at most 1 <author-notes>",
obtained=f"{article_author_notes_count} <author-notes> elements",
advice=advice,
data={"count": article_author_notes_count},
error_level="ERROR",
advice_text=advice_text,
advice_params=advice_params,
)

# Check sub-article level
for sub_article in self.xml_tree.xpath(".//sub-article"):
sub_article_id = sub_article.get("id", "unknown")
sub_author_notes_count = len(sub_article.xpath(".//author-notes"))

if sub_author_notes_count > 1:
advice = f'<author-notes> element should appear at most once in sub-article (id={sub_article_id}). Found {sub_author_notes_count} occurrences.'
advice_text = '<author-notes> element should appear at most once in sub-article (id={id}). Found {count} occurrences.'
advice_params = {
"id": sub_article_id,
"count": str(sub_author_notes_count)
}

yield build_response(
title="author-notes uniqueness in sub-article",
parent={"parent_id": sub_article_id},
item="author-notes",
sub_item=None,
validation_type="uniqueness",
is_valid=False,
expected="at most 1 <author-notes>",
obtained=f"{sub_author_notes_count} <author-notes> elements",
advice=advice,
data={"count": sub_author_notes_count, "sub_article_id": sub_article_id},
error_level="ERROR",
advice_text=advice_text,
advice_params=advice_params,
)

def validate_fn_group(self, fn_group):
for corresp_data in fn_group.get("corresp_data"):
corresp_validator = CorrespValidation(corresp_data, self.rules)
Expand Down
98 changes: 98 additions & 0 deletions packtools/sps/validation/fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,41 @@

class FnValidation(BaseFnValidation):

def validate_fn_type_presence_in_fn_group(self):
"""
Validate that @fn-type attribute is mandatory for <fn> in <fn-group>.

SPS 1.10 Rule 3: @fn-type is required for all <fn> elements in <fn-group>.
"""
# Check if this fn is in fn-group context
fn_parent = self.fn_data.get("fn_parent")
if fn_parent != "fn-group":
return None

fn_type = self.fn_data.get("fn_type")
is_valid = fn_type is not None

if not is_valid:
advice = 'Add mandatory @fn-type attribute to <fn> in <fn-group>'
advice_text = 'Add mandatory @fn-type attribute to <fn> in <fn-group>'
advice_params = {}

return build_response(
title="@fn-type attribute presence in fn-group",
parent=self.fn_data,
item="fn",
sub_item="@fn-type",
validation_type="exist",
is_valid=False,
expected="@fn-type attribute",
obtained=None,
advice=advice,
data=self.fn_data,
error_level="CRITICAL",
advice_text=advice_text,
advice_params=advice_params,
)

def validate(self):
"""
Execute all registered validations for the footnote.
Expand All @@ -13,6 +48,7 @@ def validate(self):
list[dict]: A list of validation responses (excluding None responses).
"""
validations = [
self.validate_fn_type_presence_in_fn_group,
self.validate_label,
self.validate_title,
self.validate_bold,
Expand Down Expand Up @@ -75,6 +111,9 @@ def validate(self):
Yields:
dict: Validation results for each footnote.
"""
# Validate uniqueness of <fn-group> elements
yield from self.validate_fn_group_uniqueness()

fn_types = []
for fn_group in self.article_fn_groups:
fn_types.append(fn_group["fn_type"])
Expand All @@ -86,6 +125,65 @@ def validate(self):

yield from self.validate_edited_by()

def validate_fn_group_uniqueness(self):
"""
Validate that <fn-group> appears at most once in the document.

SPS 1.10 Rule 6: <fn-group> should appear at most once per article/sub-article.
"""
# Check article level - xml_tree root is already the article element
article_fn_group_count = len(self.xml_tree.xpath("./front//fn-group | ./body//fn-group | ./back//fn-group"))

if article_fn_group_count > 1:
advice = f'<fn-group> element should appear at most once in the article. Found {article_fn_group_count} occurrences.'
advice_text = '<fn-group> element should appear at most once in the article. Found {count} occurrences.'
advice_params = {"count": str(article_fn_group_count)}

yield build_response(
title="fn-group uniqueness",
parent={},
item="fn-group",
sub_item=None,
validation_type="uniqueness",
is_valid=False,
expected="at most 1 <fn-group>",
obtained=f"{article_fn_group_count} <fn-group> elements",
advice=advice,
data={"count": article_fn_group_count},
error_level="ERROR",
advice_text=advice_text,
advice_params=advice_params,
)

# Check sub-article level
for sub_article in self.xml_tree.xpath(".//sub-article"):
sub_article_id = sub_article.get("id", "unknown")
sub_fn_group_count = len(sub_article.xpath(".//fn-group"))

if sub_fn_group_count > 1:
advice = f'<fn-group> element should appear at most once in sub-article (id={sub_article_id}). Found {sub_fn_group_count} occurrences.'
advice_text = '<fn-group> element should appear at most once in sub-article (id={id}). Found {count} occurrences.'
advice_params = {
"id": sub_article_id,
"count": str(sub_fn_group_count)
}

yield build_response(
title="fn-group uniqueness in sub-article",
parent={"parent_id": sub_article_id},
item="fn-group",
sub_item=None,
validation_type="uniqueness",
is_valid=False,
expected="at most 1 <fn-group>",
obtained=f"{sub_fn_group_count} <fn-group> elements",
advice=advice,
data={"count": sub_fn_group_count, "sub_article_id": sub_article_id},
error_level="ERROR",
advice_text=advice_text,
advice_params=advice_params,
)

def validate_fn(self, fn):
"""
Validate an individual footnote and update the response with context.
Expand Down
Loading