diff --git a/packtools/sps/validation/funding_group.py b/packtools/sps/validation/funding_group.py index e2c0d6d36..e28bc79f0 100644 --- a/packtools/sps/validation/funding_group.py +++ b/packtools/sps/validation/funding_group.py @@ -107,50 +107,456 @@ def validate_required_award_ids(self): def validate_funding_statement(self): """ - Validates the existence of funding sources and award IDs. + Validates that each has a consistent + with the reference texts found in the document (fn elements, ack, etc.). + + Each is evaluated individually so that a second group + without is not silently skipped (bug C6). Reference + texts are whitespace-normalised before use in advice strings to avoid + raw concatenated whitespace from multiple elements (bug C7). Yields ------ dict - Validation results for each funding source and award ID. + Validation result per node. """ - if self.funding.award_groups: - for lang, statements in self.funding.statements_by_lang.items(): - parent_id = statements.get("parent_id") - xml = f'' if parent_id else "
" - advice = None - funding_statement = statements["funding_statement"] - items = {k: v for k, v in statements["texts"].items() if v} - texts = [] - valid = False - if items: - texts = list(items.values()) - - if funding_statement and texts: - best_score, best_matches = most_similar(similarity(texts, funding_statement, 0.8)) - - if best_matches: - valid = True - else: - valid = False - advice = f'Replace {funding_statement} by {texts[0]} for {xml}' - elif texts: - valid = False - advice = f'Add {texts[0]} in for {xml}. Consult SPS documentation for more detail' + if not self.funding.award_groups: + return + + funding_groups = self.xml_tree.xpath(".//article-meta/funding-group") + if not funding_groups: + return + + funding_data = self.funding.data + + # Collect document-level reference texts (fn elements, ack, etc.) + # and normalise whitespace to prevent C7 (raw concatenated whitespace + # from multiple nodes appearing in advice strings). + all_texts = [] + for lang, statements in self.funding.statements_by_lang.items(): + items = {k: v for k, v in statements["texts"].items() if v} + for v in items.values(): + normalized = " ".join(v.split()) + if normalized: + all_texts.append(normalized) + + # Iterate each individually (C6 fix: each node is + # evaluated; the second group is no longer silently skipped). + for fg_node in funding_groups: + # Infer parent context from the node itself so that sub-article + # scopes are correctly reported (mirrors validate_funding_group_uniqueness). + article_meta = fg_node.getparent() + parent_elem = article_meta.getparent() if article_meta is not None else None + if parent_elem is not None: + parent_tag = parent_elem.tag + if "}" in parent_tag: + parent_tag = parent_tag.split("}", 1)[1] + parent_id = parent_elem.get("id") + else: + parent_tag = "article" + parent_id = None + parent = { + "parent": parent_tag, + "parent_id": parent_id, + "parent_article_type": funding_data.get("article_type"), + "parent_lang": funding_data.get("article_lang"), + } + + fs_nodes = fg_node.xpath("funding-statement") + funding_statement = None + if fs_nodes: + # Concatenate text from ALL nodes in this group + # (not just the first) to avoid false-negatives when multiple nodes + # are present — mirrors the approach in validate_funding_statement_presence(). + raw = "".join("".join(node.itertext()) for node in fs_nodes) + funding_statement = " ".join(raw.split()) or None + + texts = all_texts + valid = False + advice = None + + if funding_statement and texts: + # Both a and reference texts exist: compare them. + best_score, best_matches = most_similar( + similarity(texts, funding_statement, 0.8) + ) + if best_matches: + valid = True else: - valid = False - advice = f'Add funding statement with inside for {xml}. Consult SPS documentation for more detail' + advice = ( + f"Replace {funding_statement}" + f" by {texts[0]}" + ) + elif funding_statement and not texts: + # is present but no reference texts (fn/ack) were + # found to compare against. We cannot invalidate the statement, so + # treat as valid and emit an informational advice only. + valid = True + advice = ( + "No reference texts (fn/ack elements) were found to compare with" + " . Verify manually that the statement is correct." + ) + elif texts: + # Reference texts exist but is absent. + advice = ( + f"Add {texts[0]}" + " in . Consult SPS documentation for more detail" + ) + else: + # Neither nor reference texts are present. + advice = ( + "Add funding statement with inside" + " . Consult SPS documentation for more detail" + ) - yield build_response( - title="funding-statement", - parent=statements, - item="funding-statement", - sub_item=None, - validation_type="match", - is_valid=valid, - expected="funding-statement", - obtained=statements, - advice=advice, - data=statements, - error_level=self.params["funding_statement_error_level"], + yield build_response( + title="funding-statement", + parent=parent, + item="funding-statement", + sub_item=None, + validation_type="match", + is_valid=valid, + expected="funding-statement", + obtained=funding_statement or "None", + advice=advice, + data={"funding_statement": funding_statement, "texts": texts}, + error_level=self.params["funding_statement_error_level"], + ) + + def validate_funding_group_uniqueness(self, error_level="ERROR"): + """ + Rule 1: Validates that appears at most once in . + + According to SPS 1.10, only one is allowed per . + + Params + ------ + error_level : str, optional + The severity level of the validation error, by default "ERROR". + + Yields + ------ + dict + Validation result for funding-group uniqueness. + """ + article_metas = self.xml_tree.xpath(".//article-meta") + funding_data = self.funding.data + + for article_meta in article_metas: + funding_groups = article_meta.xpath("./funding-group") + count = len(funding_groups) + + parent_elem = article_meta.getparent() + if parent_elem is not None: + parent_tag = parent_elem.tag + if "}" in parent_tag: + parent_tag = parent_tag.split("}", 1)[1] + parent_id = parent_elem.get("id") + else: + parent_tag = "article" + parent_id = None + + parent = { + "parent": parent_tag, + "parent_id": parent_id, + "parent_article_type": funding_data.get("article_type"), + "parent_lang": funding_data.get("article_lang"), + } + + is_valid = count <= 1 + advice = None + if not is_valid: + advice = ( + f"Found {count} elements in . " + "Only one is allowed. Merge them into a single ." ) + + yield build_response( + title="funding-group uniqueness", + parent=parent, + item="funding-group", + sub_item=None, + validation_type="unique", + is_valid=is_valid, + expected="At most one in ", + obtained=f"{count} element(s) found", + advice=advice, + data={"count": count}, + error_level=error_level, + ) + + def validate_funding_statement_presence(self, error_level="CRITICAL"): + """ + Rule 2: Validates that is present in EACH . + + According to SPS 1.10, is mandatory in all cases. + Each must have its own . + + Params + ------ + error_level : str, optional + The severity level of the validation error, by default "CRITICAL". + + Yields + ------ + dict + Validation result for funding-statement presence in each funding-group. + """ + funding_groups = self.xml_tree.xpath(".//article-meta/funding-group") + + if not funding_groups: + # No funding-group means no validation needed + return + + funding_data = self.funding.data + parent = { + "parent": "article", + "parent_id": None, + "parent_article_type": funding_data.get("article_type"), + "parent_lang": funding_data.get("article_lang"), + } + + # Validate each funding-group individually + for idx, funding_group_node in enumerate(funding_groups): + funding_statements = funding_group_node.xpath("funding-statement") + is_valid = len(funding_statements) > 0 + + if is_valid: + # Get the text from funding-statement(s) + text_parts = [] + for fs in funding_statements: + raw_text = "".join(fs.itertext()) + cleaned = " ".join(raw_text.split()) + if cleaned: + text_parts.append(cleaned) + funding_statement_text = " ".join(text_parts) + obtained = funding_statement_text if funding_statement_text else "Present but empty" + advice = None + else: + obtained = "None" + advice = f"Add element inside (index {idx + 1}). It is mandatory according to SPS 1.10." + + yield build_response( + title="funding-statement presence", + parent=parent, + item="funding-statement", + sub_item=None, + validation_type="exist", + is_valid=is_valid, + expected=" present in ", + obtained=obtained, + advice=advice, + data={"funding_group_index": idx + 1, "has_funding_statement": is_valid}, + error_level=error_level, + ) + + def validate_funding_source_in_award_group(self, error_level="CRITICAL"): + """ + Rule 3: Validates that is present when exists. + + According to SPS 1.10, when there are institutions declared via , + is mandatory. + + Params + ------ + error_level : str, optional + The severity level of the validation error, by default "CRITICAL". + + Yields + ------ + dict + Validation results for each award-group. + """ + funding_data = self.funding.data + parent = { + "parent": "article", + "parent_id": None, + "parent_article_type": funding_data.get("article_type"), + "parent_lang": funding_data.get("article_lang"), + } + + for item in self.funding.award_groups: + funding_sources = item["funding-source"] + + is_valid = len(funding_sources) > 0 + advice = None + if not is_valid: + advice = "Add at least one element inside this . It is mandatory when exists." + + yield build_response( + title="funding-source in award-group", + parent=parent, + item="award-group", + sub_item="funding-source", + validation_type="exist", + is_valid=is_valid, + expected="At least one in ", + obtained=f"{len(funding_sources)} element(s) found", + advice=advice, + data=item, + error_level=error_level, + ) + + def validate_label_absence(self, error_level="ERROR"): + """ + Rule 5: Validates that + +
+ """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_title_absence()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "ERROR") + self.assertIn("Remove", results[0]["advice"]) + self.assertIn("", results[0]["advice"]) + + +class TestAwardIdFundingSourceConsistency(TestFundingValidationBase): + """Rule 7: Test <award-id> and <funding-source> consistency validation""" + + def test_support_without_contract_valid(self): + """Support without contract (0 award-ids) should be valid""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + </award-group> + <funding-statement>Funded by FAPESP</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_award_id_funding_source_consistency()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_single_contract_valid(self): + """Single contract (1 award-id) for multiple sources should be valid""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + <funding-source>CAPES</funding-source> + <award-id>04/08142-0</award-id> + </award-group> + <funding-statement>Funded by FAPESP and CAPES</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_award_id_funding_source_consistency()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_matching_quantities_valid(self): + """Matching quantities (N sources, N awards) should be valid""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + <funding-source>CAPES</funding-source> + <award-id>04/08142-0</award-id> + <award-id>05/09876-5</award-id> + </award-group> + <funding-statement>Funded by FAPESP and CAPES</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_award_id_funding_source_consistency()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_inconsistent_quantities_warning(self): + """Inconsistent quantities should trigger warning""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + <award-id>04/08142-0</award-id> + <award-id>05/09876-5</award-id> + <award-id>06/12345-6</award-id> + </award-group> + <funding-statement>Funded by FAPESP</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_award_id_funding_source_consistency()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "WARNING") + self.assertIn("Inconsistent quantities", results[0]["advice"]) + + +class TestCompleteValidExamples(TestFundingValidationBase): + """Test complete valid XML examples from the issue""" + + def test_example_1_funding_with_contract(self): + """Example 1: Funding with contract number""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>Fundação de Amparo à Pesquisa do Estado de São Paulo (FAPESP)</funding-source> + <award-id>04/08142-0</award-id> + </award-group> + <funding-statement>This study was supported by Fundação de Amparo à Pesquisa do Estado de São Paulo (FAPESP - Grant no. 04/08142-0; São Paulo, Brazil)</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + + # All validations should pass + uniqueness = list(validator.validate_funding_group_uniqueness()) + statement = list(validator.validate_funding_statement_presence()) + source = list(validator.validate_funding_source_in_award_group()) + label = list(validator.validate_label_absence()) + title = list(validator.validate_title_absence()) + consistency = list(validator.validate_award_id_funding_source_consistency()) + + self.assertEqual(uniqueness[0]["response"], "OK") + self.assertEqual(statement[0]["response"], "OK") + self.assertEqual(source[0]["response"], "OK") + self.assertEqual(label[0]["response"], "OK") + self.assertEqual(title[0]["response"], "OK") + self.assertEqual(consistency[0]["response"], "OK") + + def test_example_6_negative_funding_declaration(self): + """Example 6: Negative funding declaration""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <funding-statement>Não houve financiamento para esta publicação</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + + # Should pass all checks (no award-group means no source validation) + uniqueness = list(validator.validate_funding_group_uniqueness()) + statement = list(validator.validate_funding_statement_presence()) + source = list(validator.validate_funding_source_in_award_group()) + label = list(validator.validate_label_absence()) + title = list(validator.validate_title_absence()) + + self.assertEqual(uniqueness[0]["response"], "OK") + self.assertEqual(statement[0]["response"], "OK") + self.assertEqual(len(source), 0) # No award-group, so no validation + self.assertEqual(label[0]["response"], "OK") + self.assertEqual(title[0]["response"], "OK") + + +class TestValidateFundingStatement(TestFundingValidationBase): + """ + Tests for validate_funding_statement — covers the two bugs fixed on 06/03/2026: + + C6 — second <funding-group> without <funding-statement> was silently skipped + because the old implementation iterated statements_by_lang (one entry + per language) instead of per <funding-group> node. + C7 — whitespace from multiple <fn> elements was concatenated raw into the + advice string; fixed by normalising with " ".join(v.split()). + """ + + # XML with two <funding-group>: first has a statement, second does not (C6) + XML_TWO_FG_SECOND_MISSING = """ + <article article-type="research-article" xml:lang="pt"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + <award-id>2022/12345-6</award-id> + </award-group> + <funding-statement>Financiado pela FAPESP processo 2022/12345-6</funding-statement> + </funding-group> + <funding-group> + <award-group> + <funding-source>CNPq</funding-source> + <award-id>123456</award-id> + </award-group> + </funding-group> + </article-meta> + </front> + <back> + <fn-group> + <fn fn-type="financial-disclosure" id="fn-fd1"> + <p>Financiado pela FAPESP processo 2022/12345-6</p> + </fn> + <fn fn-type="financial-disclosure" id="fn-fd2"> + <p>Apoio CNPq 123456</p> + </fn> + </fn-group> + </back> + </article> + """ + + # XML with a single <funding-group> whose statement matches the fn text (valid) + XML_SINGLE_FG_VALID = """ + <article article-type="research-article" xml:lang="pt"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + <award-id>2022/12345-6</award-id> + </award-group> + <funding-statement>Financiado pela FAPESP processo 2022/12345-6</funding-statement> + </funding-group> + </article-meta> + </front> + <back> + <fn-group> + <fn fn-type="financial-disclosure" id="fn-fd1"> + <p>Financiado pela FAPESP processo 2022/12345-6</p> + </fn> + </fn-group> + </back> + </article> + """ + + # XML where the fn text has multi-line / extra whitespace (C7 scenario) + XML_WHITESPACE_FN = """ + <article article-type="research-article" xml:lang="pt"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + <award-id>2022/12345-6</award-id> + </award-group> + <funding-statement>Outro texto completamente diferente</funding-statement> + </funding-group> + </article-meta> + </front> + <back> + <fn-group> + <fn fn-type="financial-disclosure" id="fn-fd1"> + <p>Financiado + pela FAPESP + processo 2022/12345-6</p> + </fn> + <fn fn-type="financial-disclosure" id="fn-fd2"> + <p>Apoio CNPq 123456</p> + </fn> + </fn-group> + </back> + </article> + """ + + def test_c6_second_funding_group_without_statement_is_flagged(self): + """ + C6: When two <funding-group> exist and the second has no + <funding-statement>, validate_funding_statement must yield TWO results + — one OK for the first group and one ERROR/CRITICAL for the second. + The old implementation only yielded one result (silently skipping C6). + """ + xml_tree = etree.fromstring(self.XML_TWO_FG_SECOND_MISSING) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_funding_statement()) + + self.assertEqual(len(results), 2, "Must yield one result per <funding-group>") + # First group has a matching statement → OK + self.assertEqual(results[0]["response"], "OK") + # Second group has no statement → should be invalid + self.assertNotEqual(results[1]["response"], "OK") + self.assertIsNotNone(results[1]["advice"]) + self.assertIn("<funding-statement>", results[1]["advice"]) + + def test_c7_advice_string_has_no_raw_whitespace(self): + """ + C7: When the reference text in an <fn> element contains extra/multi-line + whitespace, the advice string must NOT contain sequences of multiple + spaces or newline characters. Normalization via ' '.join(v.split()) is + required before building the advice. + """ + xml_tree = etree.fromstring(self.XML_WHITESPACE_FN) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_funding_statement()) + + self.assertEqual(len(results), 1) + result = results[0] + advice = result.get("advice", "") or "" + # Must not contain raw runs of whitespace / newlines in the advice + self.assertNotIn("\n", advice, "Advice must not contain newline characters") + self.assertNotRegex(advice, r" +", "Advice must not contain consecutive spaces") + + def test_valid_matching_statement_yields_ok(self): + """ + When the <funding-statement> closely matches the reference fn text, + the result must be OK and advice must be None. + """ + xml_tree = etree.fromstring(self.XML_SINGLE_FG_VALID) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_funding_statement()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + self.assertIsNone(results[0]["advice"]) + + def test_early_exit_when_no_award_groups(self): + """ + When there are no <award-group> elements, validate_funding_statement + must yield nothing (early return). + """ + xml = """ + <article article-type="research-article" xml:lang="pt"> + <front> + <article-meta> + <funding-group> + <funding-statement>Estudo realizado sem apoio financeiro externo.</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_funding_statement()) + + self.assertEqual(len(results), 0, "No award-groups → no results expected") + + +# ======================================== +# Sugestão 3: Testes para o orquestrador validate_funding_data +# Requer: from packtools.sps.validation.xml_validations import validate_funding_data +# ======================================== + + +class TestValidateFundingDataOrchestrator(TestFundingValidationBase): + """ + Sugestão 3: Testes de integração para validate_funding_data em xml_validations.py. + + Verificam que: + (a) todas as novas validações SPS 1.10 são emitidas pelo orquestrador; + (b) os níveis configuráveis via funding_data_rules são propagados corretamente, + especialmente a chave funding_statement_error_level (não + funding_statement_presence_error_level, que era o nome incorreto no PR). + """ + + # Importação condicional: o teste é ignorado se xml_validations não estiver disponível + try: + from packtools.sps.validation.xml_validations import validate_funding_data as _vfd + _orchestrator_available = True + except ImportError: + _orchestrator_available = False + + def setUp(self): + self.xml_full = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + <award-id>04/08142-0</award-id> + </award-group> + <funding-statement>Funded by FAPESP grant 04/08142-0</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + self.xml_missing_statement = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>CNPq</funding-source> + <award-id>123456</award-id> + </award-group> + </funding-group> + </article-meta> + </front> + </article> + """ + + @unittest.skipUnless(_orchestrator_available, "xml_validations não disponível no path") + def test_orchestrator_emits_all_new_validations(self): + """ + Sugestão 3a: validate_funding_data deve emitir resultados para todas as + validações SPS 1.10 (uniqueness, statement presence, source in award-group, + label absence, title absence, consistency). + """ + from packtools.sps.validation.xml_validations import validate_funding_data + + xml_tree = etree.fromstring(self.xml_full) + params = { + "funding_data_rules": { + "special_chars_award_id": ["/", ".", "-"], + "award_id_error_level": "CRITICAL", + "funding_statement_error_level": "CRITICAL", + "funding_group_uniqueness_error_level": "ERROR", + "funding_source_in_award_group_error_level": "CRITICAL", + "label_absence_error_level": "ERROR", + "title_absence_error_level": "ERROR", + "award_id_consistency_error_level": "WARNING", + } + } + results = list(validate_funding_data(xml_tree, params)) + titles = {r["title"] for r in results} + + self.assertIn("funding-group uniqueness", titles) + self.assertIn("funding-statement presence", titles) + self.assertIn("funding-source in award-group", titles) + self.assertIn("label absence in funding-group", titles) + self.assertIn("title absence in funding-group", titles) + self.assertIn("award-id and funding-source consistency", titles) + + @unittest.skipUnless(_orchestrator_available, "xml_validations não disponível no path") + def test_orchestrator_propagates_funding_statement_error_level(self): + """ + Sugestão 3b / Sugestão 2: o orquestrador deve ler a chave + 'funding_statement_error_level' (não 'funding_statement_presence_error_level'). + Configurar como WARNING e verificar que o resultado reflete WARNING, + não o fallback CRITICAL. + """ + from packtools.sps.validation.xml_validations import validate_funding_data + + xml_tree = etree.fromstring(self.xml_missing_statement) + params = { + "funding_data_rules": { + "special_chars_award_id": ["/", ".", "-"], + "award_id_error_level": "CRITICAL", + "funding_statement_error_level": "WARNING", # chave correta + } + } + results = list(validate_funding_data(xml_tree, params)) + statement_results = [r for r in results if r["title"] == "funding-statement presence"] + + self.assertTrue( + len(statement_results) > 0, + "Nenhum resultado de 'funding-statement presence' emitido pelo orquestrador" + ) + for r in statement_results: + self.assertEqual( + r["response"], "WARNING", + "O nível configurado via 'funding_statement_error_level' não foi propagado " + "como WARNING; provavelmente o orquestrador ainda usa a chave incorreta " + "'funding_statement_presence_error_level' ou está caindo no fallback CRITICAL." + ) + + @unittest.skipUnless(_orchestrator_available, "xml_validations não disponível no path") + def test_orchestrator_propagates_uniqueness_error_level(self): + """ + Sugestão 3b: funding_group_uniqueness_error_level configurado como WARNING + deve ser refletido no resultado de uniqueness. + """ + from packtools.sps.validation.xml_validations import validate_funding_data + + xml_duplicate = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group><funding-statement>A</funding-statement></funding-group> + <funding-group><funding-statement>B</funding-statement></funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml_duplicate) + params = { + "funding_data_rules": { + "special_chars_award_id": ["/", ".", "-"], + "funding_group_uniqueness_error_level": "WARNING", + } + } + results = list(validate_funding_data(xml_tree, params)) + uniqueness_results = [r for r in results if r["title"] == "funding-group uniqueness"] + + invalid = [r for r in uniqueness_results if r["response"] != "OK"] + self.assertTrue(len(invalid) > 0) + for r in invalid: + self.assertEqual(r["response"], "WARNING") + + if __name__ == "__main__": unittest.main()