feat(validation): Implementar validações para elemento <ext-link> conforme SPS 1.10 com i18n completo#1091
Conversation
…sing) Co-authored-by: robertatakenaka <[email protected]>
Co-authored-by: robertatakenaka <[email protected]>
<ext-link> conforme SPS 1.10
|
Complementar os seguintes arquivos para considerar a nova validação:
Considere o padrão apresentado para a validação do elemento article_xref. |
| self.params.setdefault("xlink_href_format_error_level", "ERROR") | ||
| self.params.setdefault("ext_link_type_value_error_level", "ERROR") | ||
| self.params.setdefault("descriptive_text_error_level", "WARNING") | ||
| self.params.setdefault("xlink_title_error_level", "WARNING") |
There was a problem hiding this comment.
Adicionar dicionário com níveis de validação em um módulo separado, como em packtools/sps/validation_rules/article_ids_rules.json
There was a problem hiding this comment.
Done in commit 47ec6b7. Created packtools/sps/validation_rules/ext_link_rules.json with validation error level settings following the pattern from article_ids_rules.json.
| self.params.setdefault("xlink_href_format_error_level", "ERROR") | ||
| self.params.setdefault("ext_link_type_value_error_level", "ERROR") | ||
| self.params.setdefault("descriptive_text_error_level", "WARNING") | ||
| self.params.setdefault("xlink_title_error_level", "WARNING") |
There was a problem hiding this comment.
@copilot Adicionar dicionário com níveis de validação em um módulo separado, como em packtools/sps/validation_rules/article_ids_rules.json
There was a problem hiding this comment.
Done in commit 47ec6b7. Created packtools/sps/validation_rules/ext_link_rules.json with validation error level settings following the pattern from article_ids_rules.json.
|
|
||
| is_valid = bool(xlink_href) | ||
|
|
||
| advice_text = 'Add @xlink:href attribute to <ext-link> with text "{text}"' |
There was a problem hiding this comment.
@copilot todos os advice_text tem que ter a internacionalização (uso de _( e ))
There was a problem hiding this comment.
Done in commit 1f61b9a. Added internationalization to all advice_text strings:
- Imported
gettextand defined_ = gettext.gettext - Wrapped all 6 advice_text strings with
_()for i18n support - All 42 tests still passing
|
@copilot Complementar os seguintes arquivos para considerar a nova validação: packtools/sps/validation/xml_validations.py |
- Created ext_link_rules.json with validation error levels - Integrated ExtLinkValidation into xml_validations.py and xml_validator.py - Added 16 comprehensive model tests for ext_link.py - All tests passing (26 validation + 16 model tests) Co-authored-by: robertatakenaka <[email protected]>
Done in commit 47ec6b7. Completed all requested integrations following the article_xref pattern:
All 42 tests passing (26 validation + 16 model tests). Integration verified working. |
- Import gettext and define _ = gettext.gettext - Wrap all 6 advice_text strings with _() for i18n support - All 42 tests still passing (26 validation + 16 model) Co-authored-by: robertatakenaka <[email protected]>
<ext-link> conforme SPS 1.10<ext-link> conforme SPS 1.10 com i18n completo
There was a problem hiding this comment.
Pull request overview
Implementa validações SPS 1.10 para o elemento <ext-link> e integra o novo conjunto de checagens ao pipeline de validação XML do Packtools, incluindo configuração de níveis por rules JSON e cobertura por testes unitários.
Changes:
- Adiciona modelo para extração de dados de
<ext-link>no artigo principal e sub-artigos. - Implementa 6 validações (atributos obrigatórios, formato, valores permitidos e regras de acessibilidade) e integra no
xml_validator. - Inclui regras
ext_link_rules.jsone testes unitários para validação e extração do modelo.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
packtools/sps/validation/ext_link.py |
Implementa validações de <ext-link> com suporte a advice_text/advice_params. |
packtools/sps/models/ext_link.py |
Extrai <ext-link> (atributos, texto e contexto do parent) do artigo e sub-artigos. |
packtools/sps/validation_rules/ext_link_rules.json |
Define níveis de severidade para as regras de <ext-link>. |
packtools/sps/validation/xml_validations.py |
Adiciona validate_ext_links() para executar as 6 validações. |
packtools/sps/validation/xml_validator.py |
Inclui o novo grupo "ext-link" no pipeline. |
tests/sps/validation/test_ext_link.py |
Testes das validações (atributos, formato, valores e acessibilidade). |
tests/sps/models/test_ext_link.py |
Testes de extração do modelo (atributos, texto, seções e sub-artigos). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| tostring, | ||
| remove_namespaces, |
There was a problem hiding this comment.
In ExtLink the imports tostring and remove_namespaces are never used in this module. Please remove them to avoid dead code and keep the module dependencies minimal.
| tostring, | |
| remove_namespaces, |
There was a problem hiding this comment.
Concordo. Os imports foram removidos do módulo. Nenhuma função do arquivo
os utilizava, e nenhuma outra classe importa esses símbolos a partir daqui.
| # Sub-articles ext-links (recursively) | ||
| for fulltext in self._ext_link.fulltexts: | ||
| ext_links.extend(list(fulltext.ext_links)) | ||
| # Handle nested sub-articles | ||
| for nested_fulltext in fulltext.fulltexts: | ||
| ext_links.extend(list(nested_fulltext.ext_links)) |
There was a problem hiding this comment.
ArticleExtLinks.ext_links only descends into sub-articles two levels deep (fulltext and nested_fulltext). This will miss <ext-link> elements if there are deeper nested <sub-article> structures. Consider iterating xmltree.xpath('.//sub-article') (like other extractors) or implementing a true recursive traversal so all depths are covered.
| # Sub-articles ext-links (recursively) | |
| for fulltext in self._ext_link.fulltexts: | |
| ext_links.extend(list(fulltext.ext_links)) | |
| # Handle nested sub-articles | |
| for nested_fulltext in fulltext.fulltexts: | |
| ext_links.extend(list(nested_fulltext.ext_links)) | |
| # All sub-articles ext-links at any depth | |
| for sub_article_node in self.xmltree.xpath(".//sub-article"): | |
| sub_article_ext_link = ExtLink(sub_article_node) | |
| ext_links.extend(list(sub_article_ext_link.ext_links)) |
There was a problem hiding this comment.
Concordo com o diagnóstico, mas não com a solução sugerida.
Usar xpath(".//sub-article") diretamente em ArticleExtLinks ignora o mecanismo
de fulltexts já existente em ExtLink, e mais importante: cada sub-article
instanciado isoladamente perde o parent_data correto (parent_id, parent_lang
etc.), pois esse contexto é derivado do nó raiz passado ao construtor.
A correção adotada foi um método recursivo _collect_ext_links() que desce
via fulltext.fulltexts, preservando o contexto em cada nível:
def _collect_ext_links(self, fulltext):
yield from fulltext.ext_links
for child in fulltext.fulltexts:
yield from self._collect_ext_links(child)
Isso cobre qualquer profundidade de aninhamento e mantém a coerência do
modelo. Um teste de regressão com 3 níveis de profundidade foi adicionado
em test_ext_link.py (test_ext_links_deeply_nested_sub_articles).
| error_level = error_level or self.params.get("ext_link_type_error_level", "CRITICAL") | ||
|
|
||
| for ext_link in self.ext_links_model.ext_links: | ||
| ext_link_type = ext_link.get("ext_link_type") | ||
| text = ext_link.get("text", "")[:50] # First 50 chars for context | ||
|
|
||
| is_valid = bool(ext_link_type) |
There was a problem hiding this comment.
Each validation method iterates self.ext_links_model.ext_links, but ArticleExtLinks.ext_links builds a new list every time the property is accessed. Since validate_ext_links() calls 6 validations, this re-parses the XML repeatedly and can become expensive on large documents. Cache the extracted ext-links once (e.g., compute a list in __init__ and reuse it, or memoize ArticleExtLinks.ext_links).
There was a problem hiding this comment.
Válido. ArticleExtLinks.ext_links foi convertido de @Property para
@cached_property (functools), resolvendo o problema na origem — no modelo —
em vez de adicionar cache local no validador.
Com isso, independentemente de quantas validações acessem ext_links_model.ext_links,
o XML é percorrido uma única vez. Um teste foi adicionado para garantir esse
comportamento (test_ext_links_cached_property_returns_same_object), que usa
assertIs para confirmar que as duas chamadas retornam o mesmo objeto em memória.
| reason = "generic" if is_generic else "URL" | ||
| advice_text = _('Add @xlink:title attribute to <ext-link> with {reason} text "{text}" to describe link destination') | ||
| advice_params = { | ||
| "reason": reason, | ||
| "text": text, |
There was a problem hiding this comment.
For i18n, reason is interpolated into a translated advice_text, but the values assigned are hard-coded English strings ("generic" / "URL"). This makes the final localized message partially untranslated. Prefer using separate advice_text messages per case, or pass a localized reason value (or a neutral key that translations can map without leaking English into the output).
There was a problem hiding this comment.
Correto. O problema é que "generic" e "URL" eram strings em inglês passadas
como parâmetro {reason} dentro de uma mensagem traduzível, o que resultaria
em mensagens parcialmente em inglês em qualquer outro locale.
A correção foi separar em dois advice_text completamente distintos e
traduzíveis, um para cada caso, eliminando o parâmetro {reason}:
if is_generic:
advice_text = _('Add @xlink:title ... with generic text "{text}" ...')
else:
advice_text = _('Add @xlink:title ... where the text is the URL "{text}" ...')
Ambas as mensagens precisam apenas de {text} como parâmetro. O teste que
verificava assertIn("reason", adv_params) foi atualizado para
assertIn("text", adv_params) com comentário explicando a mudança.
| type_presence_results = filter_results(validator.validate_ext_link_type_presence()) | ||
| href_presence_results = filter_results(validator.validate_xlink_href_presence()) | ||
| href_format_results = filter_results(validator.validate_xlink_href_format()) | ||
| type_value_results = filter_results(validator.validate_ext_link_type_value()) | ||
| text_results = filter_results(validator.validate_descriptive_text()) |
There was a problem hiding this comment.
In this multi-link test, href_presence_results is computed but never asserted, so the test doesn't actually verify validate_xlink_href_presence() behavior for the mixed cases. Add assertions for href_presence_results (e.g., total count and number of non-OK responses) to make this test cover the intended validation.
There was a problem hiding this comment.
Correto. href_presence_results era calculado mas não tinha nenhum assert,
o que significa que a validação de presença do @xLink:href nunca era
de fato testada nesse cenário misto.
Foram adicionadas duas asserções:
self.assertEqual(len(href_presence_results), 3)
self.assertEqual(len([r for r in href_presence_results if r["response"] != "OK"]), 0)
No XML do teste, todos os três ext-links possuem @xLink:href — portanto
3 resultados esperados e 0 falhas. Isso agora está explicitamente verificado.
…onforme SPS 1.10 com i18n completo (scieloorg#1091) * Initial plan * Implement ext-link validations with model and tests (all 26 tests passing) Co-authored-by: robertatakenaka <[email protected]> * Fix whitespace in test file (code review feedback) Co-authored-by: robertatakenaka <[email protected]> * Add ext-link validation integration and model tests - Created ext_link_rules.json with validation error levels - Integrated ExtLinkValidation into xml_validations.py and xml_validator.py - Added 16 comprehensive model tests for ext_link.py - All tests passing (26 validation + 16 model tests) Co-authored-by: robertatakenaka <[email protected]> * Add internationalization (_) to all advice_text strings in ext_link.py - Import gettext and define _ = gettext.gettext - Wrap all 6 advice_text strings with _() for i18n support - All 42 tests still passing (26 validation + 16 model) Co-authored-by: robertatakenaka <[email protected]> * Correções aplicadas ao modelo e validação do elemento <ext-link> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: robertatakenaka <[email protected]> Co-authored-by: Rossi-Luciano <[email protected]>
O que esse PR faz?
Implementa 6 validações para o elemento
<ext-link>conforme especificação SPS 1.10, aumentando conformidade de 0% para 67%. Inclui integração completa no pipeline de validação XML do packtools com suporte completo a internacionalização (i18n).Validações P0 (Critical/Error):
@ext-link-typee@xlink:hrefhttp://ouhttps://)uri,doi,pmid,pmcid,clinical-trialValidações P1 (Warning):
@xlink:titleobrigatório quando texto é genérico ou URLCaracterísticas técnicas:
_()wrapper em todos os advice_textadvice_text+advice_paramspara internacionalizaçãoOnde a revisão poderia começar?
packtools/sps/validation/ext_link.py- Lógica de validação (6 métodos geradores) com i18n completo usando_()packtools/sps/models/ext_link.py- Extração de dados (herda deFulltext)packtools/sps/validation_rules/ext_link_rules.json- Configuração de níveis de erropacktools/sps/validation/xml_validations.py- Funçãovalidate_ext_links()para integraçãopacktools/sps/validation/xml_validator.py- Grupo "ext-link" no pipelinetests/sps/validation/test_ext_link.py- 26 testes de validaçãotests/sps/models/test_ext_link.py- 16 testes do modeloComo este poderia ser testado manualmente?
Teste 1: Validação direta
Teste 2: Integração com xml_validator
Executar testes:
Algum cenário de contexto que queira dar?
Padrões seguidos:
build_response()com i18n completo (análogo aarticle_doi.py,article_contribs.py,accessibility_data.py)advice_textenvolvidos com_()para internacionalizaçãoFulltextpara contexto de parent (análogo arelated_articles.py){http://www.w3.org/1999/xlink}href→xlink_hrefarticle_xrefInternacionalização (i18n):
gettexte definido_ = gettext.gettextadvice_textenvolvidas com_()para suporte multilíngueaccessibility_data.pye outras validações do packtoolsIntegração no pipeline de validação:
ext_link_rules.jsoncriado empacktools/sps/validation_rules/validate_ext_links()adicionada emxml_validations.pyxml_validator.pyvalidate_xml_content()Frases genéricas detectadas:
leia mais,clique aqui,acesse,veja mais,saiba mais,click here,read more,see more,learn more,more infoRegras não implementadas (fora de escopo):
utils.validate_doi_format())Garantias de qualidade:
_()Screenshots
N/A - Validações backend sem interface gráfica.
Quais são tickets relevantes?
TK #1082
Referências
<ext-link><ext-link>Original prompt
This section details on the original issue you should resolve
<issue_title>Criar validações para o elemento </issue_title>
<issue_description>## Objetivo
Implementar validações para o elemento
<ext-link>conforme a especificação SPS 1.10, aumentando a conformidade de 0% para 67% (6 de 9 regras).Contexto
O elemento
<ext-link>é usado para marcar links ou hyperlink text externos em artigos científicos, garantindo acessibilidade e interoperabilidade. Validações corretas asseguram que os links estejam bem formados, possuam atributos obrigatórios e sigam boas práticas de acessibilidade (texto descritivo ao invés de URLs genéricas).Conformidade atual: 0 de 9 regras implementadas (0%)
Meta após implementação: 6 de 9 regras (67%)
Documentação SPS
Referência oficial: https://docs.google.com/document/d/1GTv4Inc2LS_AXY-ToHT3HmO66UT0VAHWJNOIqzBNSgA/edit?tab=t.0#heading=h.n2z5yrri2aba
Regras principais conforme SPS 1.10:
Atributos obrigatórios:
@ext-link-type(obrigatório)@xlink:href(obrigatório)Valores permitidos para
@ext-link-type:uri- Link para websitedoi- Link DOI (deve incluir doi.org/ ou dx.doi.org/)pmid- Link para PubMedpmcid- Link para PubMed Centralclinical-trial- Link para registro de ensaio clínicoFormato do
@xlink:href:http://ouhttps://Acessibilidade:
<ext-link>deve ser descritivo@xlink:titlecom descriçãoRestrições:
<uri>ou<self-uri>(substituídos por<ext-link>)Regras a Implementar
P0 – Críticas (implementar obrigatoriamente)
@ext-link-type@ext-link-typeé obrigatório em todos os<ext-link>@xlink:href@xlink:hrefé obrigatório em todos os<ext-link>@xlink:href@xlink:hrefdeve ser URL completa começando comhttp://ouhttps://@ext-link-type@ext-link-typedeve ser um dos permitidos:uri,doi,pmid,pmcid,clinical-trialP1 – Importantes (implementar se possível)
@xlink:titlequando texto genérico@xlink:titledeve estar presente com descrição do destino do linkP2 – Futuras (fora do escopo deste Issue)
@xlink:hrefutils.validate_doi_format()Arquivos a Criar/Modificar
Criar:
packtools/sps/models/ext_link.py– Modelo de extração de dadospacktools/sps/validation/ext_link.py– Validaçõespacktools/sps/validation/rules/ext_link_rules.json– Configuração de níveis de errotests/sps/validation/test_ext_link.py– Testes unitáriosReferenciar (implementações similares):
packtools/sps/models/related_article.py– Modelo similar (também é link)packtools/sps/validation/article_doi.py– Validação de atributos obrigatórios e formatopacktools/sps/validation/utils.py– Funções auxiliares (build_response,is_valid_url_format)Exemplos de XML
XML Válido (deve passar sem erros):