Skip to content
Merged
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
121 changes: 104 additions & 17 deletions packtools/sps/models/v2/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,21 +304,108 @@ def data(self):
}


import warnings as _warnings


def __getattr__(name):
_moved = {
"XMLAbstracts": "packtools.sps.validation.models.abstract",
}
if name in _moved:
import importlib
_warnings.warn(
f"{name} has moved to {_moved[name]}. "
f"Importing from packtools.sps.models.v2.abstract is deprecated.",
DeprecationWarning,
stacklevel=2,
class XMLAbstracts:
"""
Collects the <abstract>/<trans-abstract> elements of an article, in the
main article and in every <sub-article>, as plain data (no validation
rule or expected-value logic lives here).

PR #1180 (2026-05) had moved this class into
packtools.sps.validation.models.abstract on the premise that it was
used only by validation code; that premise was wrong (scms-upload's
TOC builder consumes it directly for presentation data, not
validation), so it moved back here. See issues #1153/#1181.
"""

def __init__(self, xmltree):
self.xmltree = xmltree
self.lang = xmltree.find(".").get("{http://www.w3.org/XML/1998/namespace}lang")
self.tags_to_keep = None
self.tags_to_keep_with_content = None
self.tags_to_remove_with_content = None
self.tags_to_convert_to_html = None

def configure(
self,
tags_to_keep=None,
tags_to_keep_with_content=None,
tags_to_remove_with_content=None,
tags_to_convert_to_html=None,
):
self.tags_to_keep = tags_to_keep
self.tags_to_keep_with_content = tags_to_keep_with_content
self.tags_to_remove_with_content = tags_to_remove_with_content
self.tags_to_convert_to_html = tags_to_convert_to_html

def _build_abstract(self, node, lang):
abstract = Abstract(
node,
lang,
tags_to_keep=self.tags_to_keep,
tags_to_keep_with_content=self.tags_to_keep_with_content,
tags_to_remove_with_content=self.tags_to_remove_with_content,
tags_to_convert_to_html=self.tags_to_convert_to_html,
)
return abstract.data

def get_abstracts(self, abstract_type=None):
type_filter = f'[@abstract-type="{abstract_type}"]' if abstract_type else "[not(@abstract-type)]"

# Abstracts do artigo principal: exclui qualquer coisa dentro de
# sub-article (traduções), para nunca misturar os dois casos.
main_xpath = (
f".//abstract{type_filter}[not(ancestor::sub-article)] | "
f".//trans-abstract{type_filter}[not(ancestor::sub-article)]"
)
for node in self.xmltree.xpath(main_xpath):
lang = node.get("{http://www.w3.org/XML/1998/namespace}lang") or self.lang
yield self._build_abstract(node, lang)

# Abstracts de sub-article: o lang vem do próprio nó, com fallback
# para o xml:lang do sub-article que o contém. Nunca cai para o
# lang do artigo principal, já que um sub-article representa outro
# idioma.
sub_xpath = (
f".//sub-article//abstract{type_filter} | "
f".//sub-article//trans-abstract{type_filter}"
)
mod = importlib.import_module(_moved[name])
return getattr(mod, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
for node in self.xmltree.xpath(sub_xpath):
sub_article = node.xpath("ancestor::sub-article[1]")
sub_lang = (
sub_article[0].get("{http://www.w3.org/XML/1998/namespace}lang")
if sub_article else None
)
lang = node.get("{http://www.w3.org/XML/1998/namespace}lang") or sub_lang
yield self._build_abstract(node, lang)

@property
def standard_abstracts(self):
return self.get_abstracts()

@property
def visual_abstracts(self):
return self.get_abstracts("graphical")

@property
def key_points_abstracts(self):
return self.get_abstracts("key-points")

@property
def summary_abstracts(self):
return self.get_abstracts("summary")

@property
def abstracts(self):
yield from self.standard_abstracts
yield from self.key_points_abstracts
yield from self.visual_abstracts
yield from self.summary_abstracts

def abstracts_by_lang_and_type(self):
langs = {}
for item in self.abstracts:
lang = item["lang"]
abstract_type = item["abstract_type"]
langs.setdefault(lang, {})
langs[lang][abstract_type] = item
return langs
2 changes: 1 addition & 1 deletion packtools/sps/validation/article_abstract.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from packtools.sps.validation.models.abstract import XMLAbstracts
from packtools.sps.models.v2.abstract import XMLAbstracts
from packtools.sps.validation.utils import build_response
from packtools.sps import i18n

Expand Down
100 changes: 28 additions & 72 deletions packtools/sps/validation/models/abstract.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,28 @@
from packtools.sps.models.v2.abstract import Abstract

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Rossi-Luciano remover todo o conteúdo atual deste arquivo e colocar:

from packtools.sps.models.v2.abstract import Abstract, XMLAbstracts



class XMLAbstracts:
def __init__(self, xmltree):
self.xmltree = xmltree
self.lang = xmltree.find(".").get("{http://www.w3.org/XML/1998/namespace}lang")
self.tags_to_keep = None
self.tags_to_keep_with_content = None
self.tags_to_remove_with_content = None
self.tags_to_convert_to_html = None

def configure(
self,
tags_to_keep=None,
tags_to_keep_with_content=None,
tags_to_remove_with_content=None,
tags_to_convert_to_html=None,
):
self.tags_to_keep = tags_to_keep
self.tags_to_keep_with_content = tags_to_keep_with_content
self.tags_to_remove_with_content = tags_to_remove_with_content
self.tags_to_convert_to_html = tags_to_convert_to_html

def get_abstracts(self, abstract_type=None):
if abstract_type:
xpath = f'.//abstract[@abstract-type="{abstract_type}"] | .//trans-abstract[@abstract-type="{abstract_type}"]'
else:
xpath = ".//abstract[not(@abstract-type)] | .//trans-abstract[not(@abstract-type)]"

for node in self.xmltree.xpath(xpath):
abstract = Abstract(
node,
node.get("{http://www.w3.org/XML/1998/namespace}lang") or self.lang,
tags_to_keep=self.tags_to_keep,
tags_to_keep_with_content=self.tags_to_keep_with_content,
tags_to_remove_with_content=self.tags_to_remove_with_content,
tags_to_convert_to_html=self.tags_to_convert_to_html,
)
yield abstract.data

@property
def standard_abstracts(self):
return self.get_abstracts()

@property
def visual_abstracts(self):
return self.get_abstracts("graphical")

@property
def key_points_abstracts(self):
return self.get_abstracts("key-points")

@property
def summary_abstracts(self):
return self.get_abstracts("summary")

@property
def abstracts(self):
yield from self.standard_abstracts
yield from self.key_points_abstracts
yield from self.visual_abstracts
yield from self.summary_abstracts

def abstracts_by_lang_and_type(self):
langs = {}
for item in self.abstracts:
lang = item["lang"]
abstract_type = item["abstract_type"]
langs.setdefault(lang, {})
langs[lang][abstract_type] = item
return langs
"""
XMLAbstracts moved back to packtools.sps.models.v2.abstract.

PR #1180 (2026-05) moved it here believing it was used only by validation
code; that premise was wrong (scms-upload's TOC builder consumes it
directly for presentation data via packtools.sps.models.v2.abstract, not
for validation). Kept as a compatibility redirect only: this module
should not gain new logic, since the class itself carries no
validation-rule/expected-value concerns and doesn't belong here.
"""
import warnings as _warnings


def __getattr__(name):
_moved = {
"XMLAbstracts": "packtools.sps.models.v2.abstract",
}
if name in _moved:
import importlib
_warnings.warn(
f"{name} has moved to {_moved[name]}. "
f"Importing from packtools.sps.validation.models.abstract is deprecated.",
DeprecationWarning,
stacklevel=2,
)
mod = importlib.import_module(_moved[name])
return getattr(mod, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
2 changes: 1 addition & 1 deletion packtools/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Single source to the version across setup.py and the whole project.
"""
from __future__ import unicode_literals
__version__ = '4.17.0'
__version__ = '4.17.2'
79 changes: 79 additions & 0 deletions tests/sps/validation/test_article_abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,85 @@ def test_document_with_all_abstract_types(self):
self.assertEqual(validation["response"], "OK")


class SubArticleAbstractLangTest(TestCase):
"""
Regression tests for packtools#1329: a sub-article's translated
<abstract> (which usually has no xml:lang of its own — the language
lives on the parent <sub-article xml:lang="..."> element) must be
reported with its own language, never the main article's language.
"""

def test_sub_article_abstract_without_own_lang_uses_sub_article_lang(self):
"""Sub-article abstract without xml:lang must use the sub-article's own lang, not the main article's."""
self.maxDiff = None
xmltree = ET.fromstring(
"""
<article article-type="research-article" xml:lang="en">
<front>
<article-meta>
<abstract>
<title>Abstract</title>
<p>Main abstract text.</p>
</abstract>
</article-meta>
</front>
<sub-article article-type="translation" id="s1" xml:lang="pt">
<front-stub>
<abstract>
<title>Resumo</title>
<p>Texto do resumo.</p>
</abstract>
</front-stub>
</sub-article>
</article>
"""
)

abstracts = {a["lang"]: a for a in XMLAbstracts(xmltree).standard_abstracts}

self.assertEqual(abstracts["en"]["text"], "Main abstract text.")
self.assertEqual(abstracts["pt"]["text"], "Texto do resumo.")

def test_multiple_sub_articles_each_keep_own_lang(self):
"""Reuses the shared fixture with an en main article plus pt/es sub-article translations."""
xmltree = ET.parse("tests/samples/article-abstract-en-sub-articles-pt-es.xml")

abstracts = list(XMLAbstracts(xmltree).standard_abstracts)

self.assertEqual({a["lang"] for a in abstracts}, {"en", "pt", "es"})
self.assertEqual(len(abstracts), 3)

def test_validation_pipeline_does_not_break_with_sub_article_abstracts(self):
"""XMLAbstractsValidation (the production entry point) must keep working with sub-article abstracts present."""
xmltree = ET.fromstring(
"""
<article article-type="research-article" xml:lang="en">
<front>
<article-meta>
<abstract>
<title>Abstract</title>
<p>Main abstract text.</p>
</abstract>
</article-meta>
</front>
<sub-article article-type="translation" id="s1" xml:lang="pt">
<front-stub>
<abstract>
<title>Resumo</title>
<p>Texto do resumo.</p>
</abstract>
</front-stub>
</sub-article>
</article>
"""
)

validator = XMLAbstractsValidation(xmltree)
obtained = list(validator.validate())

self.assertTrue(obtained)


class TransAbstractValidationTest(TestCase):
"""
Tests for translated abstracts (<trans-abstract>).
Expand Down