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/
249 changes: 248 additions & 1 deletion packtools/sps/validation/front_articlemeta_issue.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from packtools.sps.models.front_articlemeta_issue import ArticleMetaIssue
from packtools.sps.validation.utils import build_response
import re


def is_valid_value(value, zero_is_allowed):
Expand Down Expand Up @@ -242,6 +243,244 @@ def validate_expected_issues(self):
error_level=self.params["expected_issues_error_level"],
)

def validate_issue_element_uniqueness(self):
"""
Validates that <issue> element appears at most once in <article-meta>.
According to SPS 1.10, only one <issue> element is allowed.

Returns:
dict: Validation response with results
"""
issue_elements = self.xml_tree.findall(".//front/article-meta/issue")
count = len(issue_elements)
is_valid = count <= 1

return build_response(
title="issue element uniqueness",
parent={"parent": "article"},
item="issue",
sub_item=None,
validation_type="unique",
is_valid=is_valid,
expected="at most one <issue> element in <article-meta>",
obtained=f"{count} <issue> element(s) found",
advice=f"Remove duplicate <issue> elements from <article-meta>. Found {count} elements, expected at most 1.",
data={"issue_count": count, "issue_values": [elem.text for elem in issue_elements]},
error_level=self.params.get("issue_element_uniqueness_error_level", "ERROR"),
)

def validate_issue_no_punctuation(self):
"""
Validates that <issue> value does not contain punctuation marks.
According to SPS 1.10, punctuation like . , - / : ; are not allowed.

Returns:
dict: Validation response with results
"""
if not self.article_issue.issue:
return None

issue_value = self.article_issue.issue
# Check for common punctuation marks
punctuation_marks = ['.', ',', '-', '/', ':', ';', '!', '?', '(', ')', '[', ']', '{', '}', '"', "'"]
found_punctuation = [p for p in punctuation_marks if p in issue_value]
is_valid = len(found_punctuation) == 0

return build_response(
title="issue value without punctuation",
parent={"parent": "article"},
item="issue",
sub_item=None,
validation_type="format",
is_valid=is_valid,
expected="issue value without punctuation marks",
obtained=issue_value,
advice=f"Remove punctuation marks {found_punctuation} from <issue> value '{issue_value}'",
data={"issue": issue_value, "punctuation_found": found_punctuation},
error_level=self.params.get("issue_no_punctuation_error_level", "ERROR"),
)

def validate_issue_no_uppercase(self):
"""
Validates that <issue> value does not contain uppercase letters.
According to SPS 1.10, all letters must be lowercase.

Returns:
dict: Validation response with results
"""
if not self.article_issue.issue:
return None

issue_value = self.article_issue.issue
has_uppercase = any(c.isupper() for c in issue_value)
is_valid = not has_uppercase

return build_response(
title="issue value without uppercase",
parent={"parent": "article"},
item="issue",
sub_item=None,
validation_type="format",
is_valid=is_valid,
expected="issue value in lowercase only",
obtained=issue_value,
advice=f"Convert uppercase letters to lowercase in <issue> value '{issue_value}'. Expected: '{issue_value.lower()}'",
data={"issue": issue_value, "expected": issue_value.lower()},
error_level=self.params.get("issue_no_uppercase_error_level", "ERROR"),
)

def validate_issue_supplement_nomenclature(self):
"""
Validates that supplement uses correct nomenclature 'suppl'.
According to SPS 1.10, must use 'suppl' not 'supl', 'supplement', 'sup'.

Returns:
dict: Validation response with results
"""
if not self.article_issue.issue:
return None

issue_value = self.article_issue.issue
issue_lower = issue_value.lower()

# Check if issue contains supplement-related terms
if "sup" not in issue_lower:
return None

# Check for invalid supplement nomenclatures using regex
invalid_patterns = []

# Check for specific invalid patterns
if re.search(r'\bsupl\b', issue_lower):
invalid_patterns.append('supl')
if re.search(r'\bsupplement\b', issue_lower):
invalid_patterns.append('supplement')
if re.search(r'\bsup\b', issue_lower):
invalid_patterns.append('sup')

is_valid = len(invalid_patterns) == 0

return build_response(
title="issue supplement nomenclature",
parent={"parent": "article"},
item="issue",
sub_item="supplement nomenclature",
validation_type="format",
is_valid=is_valid,
expected="supplement nomenclature as 'suppl'",
obtained=issue_value,
advice=f"Use 'suppl' for supplement nomenclature in <issue> value '{issue_value}'. Invalid terms found: {invalid_patterns}",
data={"issue": issue_value, "invalid_terms": invalid_patterns},
error_level=self.params.get("issue_supplement_nomenclature_error_level", "ERROR"),
)

def validate_issue_special_nomenclature(self):
"""
Validates that special issues use correct nomenclature 'spe'.
According to SPS 1.10, must use 'spe' not 'esp', 'nesp', 'nspe', 'especial', 'noesp'.

Returns:
dict: Validation response with results
"""
if not self.article_issue.issue:
return None

issue_value = self.article_issue.issue
issue_lower = issue_value.lower()

# Check if issue contains special issue indicators
special_indicators = ['esp', 'especial', 'nesp', 'nspe', 'noesp']
found_invalid = []

for indicator in special_indicators:
if indicator in issue_lower:
found_invalid.append(indicator)

# If no special issue indicators found, check if 'spe' is present
if not found_invalid and 'spe' not in issue_lower:
return None

is_valid = len(found_invalid) == 0

return build_response(
title="issue special nomenclature",
parent={"parent": "article"},
item="issue",
sub_item="special issue nomenclature",
validation_type="format",
is_valid=is_valid,
expected="special issue nomenclature as 'spe'",
obtained=issue_value,
advice=f"Use 'spe' for special issue nomenclature in <issue> value '{issue_value}'. Invalid terms found: {found_invalid}",
data={"issue": issue_value, "invalid_terms": found_invalid},
error_level=self.params.get("issue_special_nomenclature_error_level", "ERROR"),
)

def validate_no_supplement_element(self):
"""
Validates that <supplement> element does not exist in <article-meta>.
According to SPS 1.10, <supplement> is not allowed in <article-meta>.
Supplements should be identified in <issue> element instead.

Returns:
dict: Validation response with results
"""
supplement_elements = self.xml_tree.findall(".//front/article-meta/supplement")
count = len(supplement_elements)
is_valid = count == 0

return build_response(
title="supplement element not allowed",
parent={"parent": "article"},
item="supplement",
sub_item=None,
validation_type="unexpected",
is_valid=is_valid,
expected="no <supplement> element in <article-meta>",
obtained=f"{count} <supplement> element(s) found",
advice="Remove <supplement> element(s) from <article-meta>. Use <issue> element to indicate supplements (e.g., '4 suppl 1').",
data={"supplement_count": count, "supplement_values": [elem.text for elem in supplement_elements]},
error_level=self.params.get("no_supplement_element_error_level", "CRITICAL"),
)

def validate_issue_no_leading_zeros(self):
"""
Validates that numeric parts of <issue> do not have leading zeros.
According to SPS 1.10, should use '4' not '04'.

Returns:
dict: Validation response with results
"""
if not self.article_issue.issue:
return None

issue_value = self.article_issue.issue
parts = issue_value.split()

# Check each numeric part for leading zeros
issues_found = []
for part in parts:
# Check if part is numeric and has leading zero
if part.isdigit() and len(part) > 1 and part[0] == '0':
issues_found.append(part)

is_valid = len(issues_found) == 0
expected_value = ' '.join([(part.lstrip('0') or '0') if part.isdigit() else part for part in parts])

return build_response(
title="issue value without leading zeros",
parent={"parent": "article"},
item="issue",
sub_item=None,
validation_type="format",
is_valid=is_valid,
expected="numeric values without leading zeros",
obtained=issue_value,
advice=f"Remove leading zeros from numeric parts in <issue> value '{issue_value}'. Expected: '{expected_value}'",
data={"issue": issue_value, "parts_with_leading_zeros": issues_found, "expected": expected_value},
error_level=self.params.get("issue_no_leading_zeros_error_level", "WARNING"),
)

def validate(self):
"""
Performs all validation checks for the issue.
Expand All @@ -255,7 +494,15 @@ def validate(self):
yield self.validate_number_format()
yield self.validate_supplement_format()
yield self.validate_issue_format()
yield self.validate_expected_issues()
yield self.validate_expected_issues()
# New SPS 1.10 validations for <issue> element
yield self.validate_issue_element_uniqueness()
yield self.validate_issue_no_punctuation()
yield self.validate_issue_no_uppercase()
yield self.validate_issue_supplement_nomenclature()
yield self.validate_issue_special_nomenclature()
yield self.validate_no_supplement_element()
yield self.validate_issue_no_leading_zeros()


class PaginationValidation:
Expand Down
Loading