From 83afec18f4744bc0967b08e9cc25ac5a0f01411a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:12:52 +0000 Subject: [PATCH 1/4] Initial plan From 5f62caddf15451d19548d088f694725d3c705be0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:31:53 +0000 Subject: [PATCH 2/4] Initial commit: plan for journal-meta validations Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> --- src/scielo-scholarly-data | 1 + 1 file changed, 1 insertion(+) create mode 160000 src/scielo-scholarly-data diff --git a/src/scielo-scholarly-data b/src/scielo-scholarly-data new file mode 160000 index 000000000..a2899ce8a --- /dev/null +++ b/src/scielo-scholarly-data @@ -0,0 +1 @@ +Subproject commit a2899ce8a1fa77396c516640d36686351210d606 From f12de5af55d1e5fcaa31c39f8310e544e7d16cf1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:36:26 +0000 Subject: [PATCH 3/4] Add journal-meta validation classes with comprehensive tests Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> --- packtools/sps/validation/journal_meta.py | 391 +++++++++++++++++++ tests/sps/validation/test_journal_meta.py | 453 ++++++++++++++++++++++ 2 files changed, 844 insertions(+) diff --git a/packtools/sps/validation/journal_meta.py b/packtools/sps/validation/journal_meta.py index bd26964ea..b210cf548 100644 --- a/packtools/sps/validation/journal_meta.py +++ b/packtools/sps/validation/journal_meta.py @@ -1,3 +1,4 @@ +import re from packtools.sps.models.journal_meta import ISSN, Acronym, Title, Publisher, JournalID from packtools.sps.validation.exceptions import ( ValidationPublisherException, @@ -381,3 +382,393 @@ def validate(self, expected_values): list(nlm_ta.nlm_ta_id_validation(expected_values['nlm-ta'])) return resp_journal_meta + + +class JournalMetaPresenceValidation: + """ + Validates presence and uniqueness of journal-meta and its required elements. + Implements SPS 1.10 rules for structural validation. + """ + def __init__(self, xmltree): + self.xmltree = xmltree + + def validate_journal_meta_presence(self, error_level="CRITICAL"): + """ + Rule 1: Validates that element exists in . + + Returns + ------- + generator of dict + Validation result indicating presence of journal-meta. + """ + journal_meta = self.xmltree.find('.//front/journal-meta') + is_valid = journal_meta is not None + + yield format_response( + title='Journal meta presence', + parent='article', + parent_id=None, + parent_article_type=self.xmltree.get("article-type"), + parent_lang=self.xmltree.get("{http://www.w3.org/XML/1998/namespace}lang"), + item='journal-meta', + sub_item=None, + validation_type='exist', + is_valid=is_valid, + expected=' element', + obtained='' if is_valid else None, + advice='Add element inside ', + data=None, + error_level=error_level, + ) + + def validate_journal_meta_uniqueness(self, error_level="CRITICAL"): + """ + Rule 2: Validates that appears exactly once in . + + Returns + ------- + generator of dict + Validation result indicating uniqueness of journal-meta. + """ + journal_meta_list = self.xmltree.xpath('.//front/journal-meta') + count = len(journal_meta_list) + is_valid = count == 1 + + if count == 0: + obtained = 'No found' + elif count == 1: + obtained = 'One element' + else: + obtained = f'{count} elements found' + + yield format_response( + title='Journal meta uniqueness', + parent='article', + parent_id=None, + parent_article_type=self.xmltree.get("article-type"), + parent_lang=self.xmltree.get("{http://www.w3.org/XML/1998/namespace}lang"), + item='journal-meta', + sub_item=None, + validation_type='exist', + is_valid=is_valid, + expected='exactly one element', + obtained=obtained, + advice='Ensure exactly one element exists inside ', + data={'count': count}, + error_level=error_level, + ) + + def validate_publisher_id_presence(self, error_level="CRITICAL"): + """ + Rule 3: Validates presence of . + + Returns + ------- + generator of dict + Validation result for publisher-id presence. + """ + publisher_id = self.xmltree.findtext('.//journal-meta//journal-id[@journal-id-type="publisher-id"]') + is_valid = publisher_id is not None and publisher_id.strip() != '' + + yield format_response( + title='Journal publisher ID presence', + parent='article', + parent_id=None, + parent_article_type=self.xmltree.get("article-type"), + parent_lang=self.xmltree.get("{http://www.w3.org/XML/1998/namespace}lang"), + item='journal-id', + sub_item='@journal-id-type="publisher-id"', + validation_type='exist', + is_valid=is_valid, + expected=' with non-empty value', + obtained=publisher_id if is_valid else None, + advice='Add ACRONYM inside ', + data={'publisher_id': publisher_id}, + error_level=error_level, + ) + + def validate_journal_title_presence(self, error_level="CRITICAL"): + """ + Rule 4: Validates presence of . + + Returns + ------- + generator of dict + Validation result for journal-title presence. + """ + journal_title = self.xmltree.findtext('.//journal-meta//journal-title-group//journal-title') + is_valid = journal_title is not None and journal_title.strip() != '' + + yield format_response( + title='Journal title presence', + parent='article', + parent_id=None, + parent_article_type=self.xmltree.get("article-type"), + parent_lang=self.xmltree.get("{http://www.w3.org/XML/1998/namespace}lang"), + item='journal-title-group', + sub_item='journal-title', + validation_type='exist', + is_valid=is_valid, + expected=' with non-empty value', + obtained=journal_title if is_valid else None, + advice='Add Title inside ', + data={'journal_title': journal_title}, + error_level=error_level, + ) + + def validate_abbrev_journal_title_presence(self, error_level="CRITICAL"): + """ + Rule 5: Validates presence of . + + Returns + ------- + generator of dict + Validation result for abbreviated journal title presence. + """ + abbrev_title = self.xmltree.findtext('.//journal-meta//journal-title-group//abbrev-journal-title[@abbrev-type="publisher"]') + is_valid = abbrev_title is not None and abbrev_title.strip() != '' + + yield format_response( + title='Abbreviated journal title presence', + parent='article', + parent_id=None, + parent_article_type=self.xmltree.get("article-type"), + parent_lang=self.xmltree.get("{http://www.w3.org/XML/1998/namespace}lang"), + item='journal-title-group', + sub_item='abbrev-journal-title', + validation_type='exist', + is_valid=is_valid, + expected=' with non-empty value', + obtained=abbrev_title if is_valid else None, + advice='Add Abbrev. Title inside ', + data={'abbrev_title': abbrev_title}, + error_level=error_level, + ) + + def validate_issn_presence(self, error_level="CRITICAL"): + """ + Rule 6: Validates presence of at least one (epub or ppub). + + Returns + ------- + generator of dict + Validation result for ISSN presence. + """ + issn_list = self.xmltree.xpath('.//journal-meta//issn') + is_valid = len(issn_list) > 0 + + issn_data = [{'type': node.get('pub-type'), 'value': node.text} for node in issn_list] + + yield format_response( + title='ISSN presence', + parent='article', + parent_id=None, + parent_article_type=self.xmltree.get("article-type"), + parent_lang=self.xmltree.get("{http://www.w3.org/XML/1998/namespace}lang"), + item='issn', + sub_item=None, + validation_type='exist', + is_valid=is_valid, + expected='at least one element', + obtained=f'{len(issn_list)} ISSN(s) found' if is_valid else 'No ISSN found', + advice='Add at least one XXXX-XXXX or XXXX-XXXX inside ', + data=issn_data, + error_level=error_level, + ) + + def validate_publisher_name_presence(self, error_level="CRITICAL"): + """ + Rule 7: Validates presence of . + + Returns + ------- + generator of dict + Validation result for publisher-name presence. + """ + publisher_name = self.xmltree.findtext('.//journal-meta//publisher//publisher-name') + is_valid = publisher_name is not None and publisher_name.strip() != '' + + yield format_response( + title='Publisher name presence', + parent='article', + parent_id=None, + parent_article_type=self.xmltree.get("article-type"), + parent_lang=self.xmltree.get("{http://www.w3.org/XML/1998/namespace}lang"), + item='publisher', + sub_item='publisher-name', + validation_type='exist', + is_valid=is_valid, + expected=' with non-empty value', + obtained=publisher_name if is_valid else None, + advice='Add Publisher Name inside ', + data={'publisher_name': publisher_name}, + error_level=error_level, + ) + + +class ISSNFormatValidation: + """ + Validates ISSN format and attributes. + Implements SPS 1.10 format validation rules. + """ + def __init__(self, xmltree): + self.xmltree = xmltree + self.journal_issns = ISSN(xmltree) + + def validate_issn_format(self, error_level="ERROR"): + """ + Rule 8: Validates ISSN format (XXXX-XXXX pattern). + ISSN must be 4 digits, hyphen, 4 digits (last digit can be X). + + Returns + ------- + generator of dict + Validation results for each ISSN format. + """ + # Regex pattern for ISSN: 4 digits, hyphen, 3 digits + (digit or X) + issn_pattern = re.compile(r'^\d{4}-\d{3}[\dXx]$') + + for issn_data in self.journal_issns.data: + issn_value = issn_data.get('value', '') + issn_type = issn_data.get('type', '') + + is_valid = bool(issn_pattern.match(issn_value)) if issn_value else False + + yield format_response( + title='ISSN format', + parent='article', + parent_id=None, + parent_article_type=self.xmltree.get("article-type"), + parent_lang=self.xmltree.get("{http://www.w3.org/XML/1998/namespace}lang"), + item='issn', + sub_item=f'@pub-type="{issn_type}"' if issn_type else None, + validation_type='format', + is_valid=is_valid, + expected='ISSN with format XXXX-XXXX (where X can be a digit or letter X)', + obtained=issn_value, + advice=f'Correct ISSN format to XXXX-XXXX pattern. Current value: {issn_value}', + data=issn_data, + error_level=error_level, + ) + + +class JournalMetaAttributeValidation: + """ + Validates allowed attribute values in journal-meta elements. + Implements SPS 1.10 attribute validation rules. + """ + def __init__(self, xmltree): + self.xmltree = xmltree + + def validate_journal_id_type_values(self, error_level="ERROR"): + """ + Rule 9: Validates allowed values for @journal-id-type (publisher-id, nlm-ta). + + Returns + ------- + generator of dict + Validation results for each journal-id type attribute. + """ + allowed_types = ['publisher-id', 'nlm-ta'] + journal_ids = self.xmltree.xpath('.//journal-meta//journal-id') + + for journal_id in journal_ids: + id_type = journal_id.get('journal-id-type') + id_value = journal_id.text + + is_valid = id_type in allowed_types if id_type else False + + yield format_response( + title='Journal ID type attribute', + parent='article', + parent_id=None, + parent_article_type=self.xmltree.get("article-type"), + parent_lang=self.xmltree.get("{http://www.w3.org/XML/1998/namespace}lang"), + item='journal-id', + sub_item='@journal-id-type', + validation_type='value in list', + is_valid=is_valid, + expected=f'{allowed_types}', + obtained=id_type, + advice=f'Set @journal-id-type to one of {allowed_types}. Current value: {id_type}', + data={'journal_id_type': id_type, 'value': id_value}, + error_level=error_level, + ) + + def validate_issn_pub_type_values(self, error_level="ERROR"): + """ + Rule 10: Validates allowed values for @pub-type in (epub, ppub). + + Returns + ------- + generator of dict + Validation results for each ISSN pub-type attribute. + """ + allowed_types = ['epub', 'ppub'] + issns = self.xmltree.xpath('.//journal-meta//issn') + + for issn in issns: + pub_type = issn.get('pub-type') + issn_value = issn.text + + is_valid = pub_type in allowed_types if pub_type else False + + yield format_response( + title='ISSN pub-type attribute', + parent='article', + parent_id=None, + parent_article_type=self.xmltree.get("article-type"), + parent_lang=self.xmltree.get("{http://www.w3.org/XML/1998/namespace}lang"), + item='issn', + sub_item='@pub-type', + validation_type='value in list', + is_valid=is_valid, + expected=f'{allowed_types}', + obtained=pub_type, + advice=f'Set @pub-type to one of {allowed_types}. Current value: {pub_type}', + data={'pub_type': pub_type, 'value': issn_value}, + error_level=error_level, + ) + + def validate_issn_type_uniqueness(self, error_level="WARNING"): + """ + Rule 11: Validates that there are no duplicate ISSN pub-types. + + Returns + ------- + generator of dict + Validation results for ISSN type uniqueness. + """ + issns = self.xmltree.xpath('.//journal-meta//issn') + pub_types = [issn.get('pub-type') for issn in issns if issn.get('pub-type')] + + # Count occurrences of each type + type_counts = {} + for pub_type in pub_types: + type_counts[pub_type] = type_counts.get(pub_type, 0) + 1 + + # Check for duplicates + duplicates = [pt for pt, count in type_counts.items() if count > 1] + is_valid = len(duplicates) == 0 + + if duplicates: + obtained = f'Duplicate pub-types found: {duplicates}' + else: + obtained = 'All ISSN pub-types are unique' + + yield format_response( + title='ISSN type uniqueness', + parent='article', + parent_id=None, + parent_article_type=self.xmltree.get("article-type"), + parent_lang=self.xmltree.get("{http://www.w3.org/XML/1998/namespace}lang"), + item='issn', + sub_item='@pub-type', + validation_type='uniqueness', + is_valid=is_valid, + expected='unique pub-type values for each ISSN', + obtained=obtained, + advice=f'Remove duplicate ISSN elements with same pub-type. Duplicates: {duplicates}' if duplicates else None, + data={'type_counts': type_counts, 'duplicates': duplicates}, + error_level=error_level, + ) diff --git a/tests/sps/validation/test_journal_meta.py b/tests/sps/validation/test_journal_meta.py index da1995709..c8ed8de1d 100644 --- a/tests/sps/validation/test_journal_meta.py +++ b/tests/sps/validation/test_journal_meta.py @@ -718,3 +718,456 @@ def test_journal_meta_match(self): for i, item in enumerate(expected): with self.subTest(i): self.assertDictEqual(obtained[i], item) + + +class JournalMetaPresenceTest(TestCase): + """Tests for JournalMetaPresenceValidation class""" + + def test_validate_journal_meta_presence_success(self): + """Test journal-meta presence validation when element exists""" + xmltree = etree.fromstring( + """ +
+ + + test + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaPresenceValidation + validation = JournalMetaPresenceValidation(xmltree) + result = list(validation.validate_journal_meta_presence()) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['response'], 'OK') + self.assertEqual(result[0]['validation_type'], 'exist') + + def test_validate_journal_meta_presence_failure(self): + """Test journal-meta presence validation when element is missing""" + xmltree = etree.fromstring( + """ +
+ + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaPresenceValidation + validation = JournalMetaPresenceValidation(xmltree) + result = list(validation.validate_journal_meta_presence()) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['response'], 'CRITICAL') + self.assertIsNotNone(result[0]['advice']) + + def test_validate_journal_meta_uniqueness_success(self): + """Test journal-meta uniqueness validation when exactly one exists""" + xmltree = etree.fromstring( + """ +
+ + + test + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaPresenceValidation + validation = JournalMetaPresenceValidation(xmltree) + result = list(validation.validate_journal_meta_uniqueness()) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['response'], 'OK') + self.assertEqual(result[0]['data']['count'], 1) + + def test_validate_journal_meta_uniqueness_failure_multiple(self): + """Test journal-meta uniqueness validation when multiple exist""" + xmltree = etree.fromstring( + """ +
+ + + test1 + + + test2 + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaPresenceValidation + validation = JournalMetaPresenceValidation(xmltree) + result = list(validation.validate_journal_meta_uniqueness()) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['response'], 'CRITICAL') + self.assertEqual(result[0]['data']['count'], 2) + + def test_validate_publisher_id_presence_success(self): + """Test publisher-id presence validation when element exists""" + xmltree = etree.fromstring( + """ +
+ + + bjmbr + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaPresenceValidation + validation = JournalMetaPresenceValidation(xmltree) + result = list(validation.validate_publisher_id_presence()) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['response'], 'OK') + self.assertEqual(result[0]['data']['publisher_id'], 'bjmbr') + + def test_validate_publisher_id_presence_failure(self): + """Test publisher-id presence validation when element is missing""" + xmltree = etree.fromstring( + """ +
+ + + Test + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaPresenceValidation + validation = JournalMetaPresenceValidation(xmltree) + result = list(validation.validate_publisher_id_presence()) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['response'], 'CRITICAL') + + def test_validate_journal_title_presence_success(self): + """Test journal-title presence validation when element exists""" + xmltree = etree.fromstring( + """ +
+ + + + Test Journal + + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaPresenceValidation + validation = JournalMetaPresenceValidation(xmltree) + result = list(validation.validate_journal_title_presence()) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['response'], 'OK') + + def test_validate_abbrev_journal_title_presence_success(self): + """Test abbreviated journal title presence validation""" + xmltree = etree.fromstring( + """ +
+ + + + Test J. + + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaPresenceValidation + validation = JournalMetaPresenceValidation(xmltree) + result = list(validation.validate_abbrev_journal_title_presence()) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['response'], 'OK') + + def test_validate_issn_presence_success(self): + """Test ISSN presence validation when at least one exists""" + xmltree = etree.fromstring( + """ +
+ + + 1234-5678 + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaPresenceValidation + validation = JournalMetaPresenceValidation(xmltree) + result = list(validation.validate_issn_presence()) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['response'], 'OK') + + def test_validate_issn_presence_failure(self): + """Test ISSN presence validation when none exist""" + xmltree = etree.fromstring( + """ +
+ + + test + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaPresenceValidation + validation = JournalMetaPresenceValidation(xmltree) + result = list(validation.validate_issn_presence()) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['response'], 'CRITICAL') + + def test_validate_publisher_name_presence_success(self): + """Test publisher-name presence validation when element exists""" + xmltree = etree.fromstring( + """ +
+ + + + Test Publisher + + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaPresenceValidation + validation = JournalMetaPresenceValidation(xmltree) + result = list(validation.validate_publisher_name_presence()) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['response'], 'OK') + + +class ISSNFormatTest(TestCase): + """Tests for ISSNFormatValidation class""" + + def test_validate_issn_format_valid_standard(self): + """Test ISSN format validation with valid standard format""" + xmltree = etree.fromstring( + """ +
+ + + 1234-5678 + 0103-5053 + + +
+ """ + ) + from packtools.sps.validation.journal_meta import ISSNFormatValidation + validation = ISSNFormatValidation(xmltree) + results = list(validation.validate_issn_format()) + + self.assertEqual(len(results), 2) + for result in results: + self.assertEqual(result['response'], 'OK') + self.assertEqual(result['validation_type'], 'format') + + def test_validate_issn_format_valid_with_x(self): + """Test ISSN format validation with X as check digit""" + xmltree = etree.fromstring( + """ +
+ + + 1234-567X + + +
+ """ + ) + from packtools.sps.validation.journal_meta import ISSNFormatValidation + validation = ISSNFormatValidation(xmltree) + results = list(validation.validate_issn_format()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]['response'], 'OK') + + def test_validate_issn_format_invalid_no_hyphen(self): + """Test ISSN format validation with missing hyphen""" + xmltree = etree.fromstring( + """ +
+ + + 12345678 + + +
+ """ + ) + from packtools.sps.validation.journal_meta import ISSNFormatValidation + validation = ISSNFormatValidation(xmltree) + results = list(validation.validate_issn_format()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]['response'], 'ERROR') + + def test_validate_issn_format_invalid_wrong_length(self): + """Test ISSN format validation with wrong length""" + xmltree = etree.fromstring( + """ +
+ + + 123-456 + + +
+ """ + ) + from packtools.sps.validation.journal_meta import ISSNFormatValidation + validation = ISSNFormatValidation(xmltree) + results = list(validation.validate_issn_format()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]['response'], 'ERROR') + + +class JournalMetaAttributeTest(TestCase): + """Tests for JournalMetaAttributeValidation class""" + + def test_validate_journal_id_type_valid(self): + """Test journal-id-type validation with valid values""" + xmltree = etree.fromstring( + """ +
+ + + bjmbr + Rev Test + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaAttributeValidation + validation = JournalMetaAttributeValidation(xmltree) + results = list(validation.validate_journal_id_type_values()) + + self.assertEqual(len(results), 2) + for result in results: + self.assertEqual(result['response'], 'OK') + + def test_validate_journal_id_type_invalid(self): + """Test journal-id-type validation with invalid value""" + xmltree = etree.fromstring( + """ +
+ + + test + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaAttributeValidation + validation = JournalMetaAttributeValidation(xmltree) + results = list(validation.validate_journal_id_type_values()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]['response'], 'ERROR') + + def test_validate_issn_pub_type_valid(self): + """Test ISSN pub-type validation with valid values""" + xmltree = etree.fromstring( + """ +
+ + + 1234-5678 + 0103-5053 + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaAttributeValidation + validation = JournalMetaAttributeValidation(xmltree) + results = list(validation.validate_issn_pub_type_values()) + + self.assertEqual(len(results), 2) + for result in results: + self.assertEqual(result['response'], 'OK') + + def test_validate_issn_pub_type_invalid(self): + """Test ISSN pub-type validation with invalid value""" + xmltree = etree.fromstring( + """ +
+ + + 1234-5678 + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaAttributeValidation + validation = JournalMetaAttributeValidation(xmltree) + results = list(validation.validate_issn_pub_type_values()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]['response'], 'ERROR') + + def test_validate_issn_type_uniqueness_success(self): + """Test ISSN type uniqueness with unique types""" + xmltree = etree.fromstring( + """ +
+ + + 1234-5678 + 0103-5053 + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaAttributeValidation + validation = JournalMetaAttributeValidation(xmltree) + results = list(validation.validate_issn_type_uniqueness()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]['response'], 'OK') + + def test_validate_issn_type_uniqueness_failure(self): + """Test ISSN type uniqueness with duplicate types""" + xmltree = etree.fromstring( + """ +
+ + + 1234-5678 + 8765-4321 + + +
+ """ + ) + from packtools.sps.validation.journal_meta import JournalMetaAttributeValidation + validation = JournalMetaAttributeValidation(xmltree) + results = list(validation.validate_issn_type_uniqueness()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]['response'], 'WARNING') + self.assertIn('epub', results[0]['data']['duplicates']) From a4cce0aec5b3802dbeda3e72467967ca80dd5835 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:40:21 +0000 Subject: [PATCH 4/4] Address code review feedback: enforce uppercase X in ISSN, clean up data fields Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> --- packtools/sps/validation/journal_meta.py | 13 ++++++------ tests/sps/validation/test_journal_meta.py | 25 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packtools/sps/validation/journal_meta.py b/packtools/sps/validation/journal_meta.py index b210cf548..6fe45dc37 100644 --- a/packtools/sps/validation/journal_meta.py +++ b/packtools/sps/validation/journal_meta.py @@ -483,7 +483,7 @@ def validate_publisher_id_presence(self, error_level="CRITICAL"): expected=' with non-empty value', obtained=publisher_id if is_valid else None, advice='Add ACRONYM inside ', - data={'publisher_id': publisher_id}, + data={'publisher_id': publisher_id} if is_valid else None, error_level=error_level, ) @@ -512,7 +512,7 @@ def validate_journal_title_presence(self, error_level="CRITICAL"): expected=' with non-empty value', obtained=journal_title if is_valid else None, advice='Add Title inside ', - data={'journal_title': journal_title}, + data={'journal_title': journal_title} if is_valid else None, error_level=error_level, ) @@ -541,7 +541,7 @@ def validate_abbrev_journal_title_presence(self, error_level="CRITICAL"): expected=' with non-empty value', obtained=abbrev_title if is_valid else None, advice='Add Abbrev. Title inside ', - data={'abbrev_title': abbrev_title}, + data={'abbrev_title': abbrev_title} if is_valid else None, error_level=error_level, ) @@ -601,7 +601,7 @@ def validate_publisher_name_presence(self, error_level="CRITICAL"): expected=' with non-empty value', obtained=publisher_name if is_valid else None, advice='Add Publisher Name inside ', - data={'publisher_name': publisher_name}, + data={'publisher_name': publisher_name} if is_valid else None, error_level=error_level, ) @@ -619,14 +619,15 @@ def validate_issn_format(self, error_level="ERROR"): """ Rule 8: Validates ISSN format (XXXX-XXXX pattern). ISSN must be 4 digits, hyphen, 4 digits (last digit can be X). + According to ISO 3297, the check digit X must be uppercase. Returns ------- generator of dict Validation results for each ISSN format. """ - # Regex pattern for ISSN: 4 digits, hyphen, 3 digits + (digit or X) - issn_pattern = re.compile(r'^\d{4}-\d{3}[\dXx]$') + # Regex pattern for ISSN: 4 digits, hyphen, 3 digits + (digit or uppercase X) + issn_pattern = re.compile(r'^\d{4}-\d{3}[\dX]$') for issn_data in self.journal_issns.data: issn_value = issn_data.get('value', '') diff --git a/tests/sps/validation/test_journal_meta.py b/tests/sps/validation/test_journal_meta.py index c8ed8de1d..151e8b604 100644 --- a/tests/sps/validation/test_journal_meta.py +++ b/tests/sps/validation/test_journal_meta.py @@ -1041,6 +1041,28 @@ def test_validate_issn_format_invalid_wrong_length(self): self.assertEqual(len(results), 1) self.assertEqual(results[0]['response'], 'ERROR') + def test_validate_issn_format_invalid_lowercase_x(self): + """Test ISSN format validation rejects lowercase x (must be uppercase X)""" + xmltree = etree.fromstring( + """ +
+ + + 1234-567x + + +
+ """ + ) + from packtools.sps.validation.journal_meta import ISSNFormatValidation + validation = ISSNFormatValidation(xmltree) + results = list(validation.validate_issn_format()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]['response'], 'ERROR') + + + class JournalMetaAttributeTest(TestCase): """Tests for JournalMetaAttributeValidation class""" @@ -1171,3 +1193,6 @@ def test_validate_issn_type_uniqueness_failure(self): self.assertEqual(len(results), 1) self.assertEqual(results[0]['response'], 'WARNING') self.assertIn('epub', results[0]['data']['duplicates']) + + def test_validate_issn_format_invalid_lowercase_x(self): + """Test ISSN format validation rejects lowercase x (must be uppercase X)"""