-
Notifications
You must be signed in to change notification settings - Fork 25
Valida pub-date "pub" contra hoje e contra collection (#1268) #1273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| from datetime import date, datetime | ||
| from datetime import date, datetime, timedelta | ||
|
|
||
| from packtools.sps.models.dates import FulltextDates | ||
| from packtools.sps.validation.utils import build_response, get_future_date | ||
|
|
@@ -284,6 +284,12 @@ def _get_default_params(self): | |
| "pub_date_uniqueness_error_level": "ERROR", | ||
| "day_value_error_level": "ERROR", | ||
| "month_value_error_level": "ERROR", | ||
| "pub_date_future_error_level": "CRITICAL", | ||
| "pub_date_past_collection_error_level": "CRITICAL", | ||
| # pub-date sanity tolerances (issue #1268) | ||
| "pub_date_future_tolerance_days": 7, | ||
| "pub_date_past_collection_tolerance_months": 12, | ||
| "today": None, | ||
| # Event lists — alinhados com article_dates_rules.json | ||
| "required_events": ["received", "accepted"], | ||
| "pre_pub_ordered_events": [ | ||
|
|
@@ -329,6 +335,8 @@ def validate(self): | |
| yield from self.validate_pub_date_collection_required_year() | ||
| yield from self.validate_pub_date_collection_no_day() | ||
| yield from self.validate_day_month_values() | ||
| yield from self.validate_pub_date_not_in_future() | ||
| yield from self.validate_pub_date_not_too_far_before_collection() | ||
| yield from self.validate_article_date() | ||
| yield from self.validate_collection_date() | ||
| yield from self.validate_history_dates() | ||
|
|
@@ -553,6 +561,85 @@ def validate_day_month_values(self): | |
| error_level=self.params["month_value_error_level"], | ||
| ) | ||
|
|
||
| def validate_pub_date_not_in_future(self): | ||
| """Rule 9: Validate that pub-date[@date-type='pub'] is not later than | ||
| today + tolerance (days). Catches typos such as year 2029 instead of | ||
| 2026, which OPAC silently hides from public access (issue #1268). | ||
| Only applies to main article (not sub-articles). | ||
| """ | ||
| if self.fulltext.tag != "article": | ||
| return | ||
| epub_date_model = self.fulltext.epub_date_model | ||
| pub_date = epub_date_model and epub_date_model.date | ||
| if not pub_date: | ||
| return | ||
|
|
||
| tolerance_days = self.params["pub_date_future_tolerance_days"] | ||
| today = self.params.get("today") or date.today() | ||
| limit = today + timedelta(days=tolerance_days) | ||
| is_valid = pub_date <= limit | ||
|
|
||
| yield build_response( | ||
| title="pub-date pub not in future", | ||
| parent=self.params["parent"], | ||
| item="pub-date", | ||
| sub_item="pub", | ||
| validation_type="value", | ||
| is_valid=is_valid, | ||
| expected=f'<pub-date date-type="pub"> no later than {limit.isoformat()}', | ||
| obtained=pub_date.isoformat(), | ||
| advice=f'<pub-date date-type="pub"> ({pub_date.isoformat()}) must not be later than {limit.isoformat()}', | ||
| advice_text=i18n._('<pub-date date-type="pub"> ({pub_date}) must not be later than {limit}'), | ||
| advice_params={ | ||
| "pub_date": pub_date.isoformat(), | ||
| "limit": limit.isoformat(), | ||
| }, | ||
| data=self.fulltext.epub_date, | ||
| error_level=self.params["pub_date_future_error_level"], | ||
| ) | ||
|
|
||
| def validate_pub_date_not_too_far_before_collection(self): | ||
| """Rule 10: Validate that pub-date[@date-type='pub'] is not more than | ||
| N months before the collection year (issue #1268, regra 2). | ||
| Only applies to main article (not sub-articles). | ||
| """ | ||
| if self.fulltext.tag != "article": | ||
| return | ||
| epub_date_model = self.fulltext.epub_date_model | ||
| pub_date = epub_date_model and epub_date_model.date | ||
| collection_date = self.fulltext.collection_date | ||
| collection_year = collection_date and collection_date.get("year") | ||
| if not pub_date or not collection_year: | ||
| return | ||
| try: | ||
| collection_start = date(int(collection_year), 1, 1) | ||
| except (ValueError, TypeError): | ||
| return | ||
|
|
||
| tolerance_months = self.params["pub_date_past_collection_tolerance_months"] | ||
| earliest_allowed = collection_start - timedelta(days=30 * tolerance_months) | ||
| is_valid = pub_date >= earliest_allowed | ||
|
|
||
| yield build_response( | ||
| title="pub-date pub not too far before collection", | ||
| parent=self.params["parent"], | ||
| item="pub-date", | ||
| sub_item="pub", | ||
| validation_type="value", | ||
| is_valid=is_valid, | ||
| expected=f'<pub-date date-type="pub"> no earlier than {earliest_allowed.isoformat()} ({tolerance_months} months before collection year {collection_year})', | ||
| obtained=pub_date.isoformat(), | ||
| advice=f'<pub-date date-type="pub"> ({pub_date.isoformat()}) must not be more than {tolerance_months} months before <pub-date date-type="collection"> year ({collection_year})', | ||
| advice_text=i18n._('<pub-date date-type="pub"> ({pub_date}) must not be more than {tolerance_months} months before <pub-date date-type="collection"> year ({collection_year})'), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Rossi-Luciano mesmo problema do caso anterior
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @robertatakenaka mesma explicação do comentário na linha 592 (padrão |
||
| advice_params={ | ||
| "pub_date": pub_date.isoformat(), | ||
| "tolerance_months": tolerance_months, | ||
| "collection_year": collection_year, | ||
| }, | ||
| data=self.fulltext.epub_date, | ||
| error_level=self.params["pub_date_past_collection_error_level"], | ||
| ) | ||
|
|
||
| def validate_article_date(self): | ||
| """Validate the main article date.""" | ||
| if article_date := self.fulltext.article_date: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| from datetime import date | ||
| from datetime import date, timedelta | ||
| from unittest import TestCase | ||
| from unittest.mock import Mock, patch | ||
|
|
||
|
|
@@ -1452,3 +1452,176 @@ def test_missing_only_absent_events(self): | |
| "'accepted' deve constar em missing_events") | ||
| self.assertNotIn("received", validator.missing_events, | ||
| "'received' está presente no histórico e não deve aparecer em missing_events") | ||
|
|
||
|
|
||
| class TestPubDateFutureAndCollectionDistanceValidation(TestCase): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Rossi-Luciano tem muitos testes em que é OK. Gostaria de ver mais testes apresentando a mensagem de erro, pois ajuda a visualizar a lógica.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @robertatakenaka aplicado: adicionei |
||
| """Testes para as regras 9 e 10 (issue #1268): | ||
|
|
||
| - pub-date[@date-type="pub"] não pode estar no futuro além de uma | ||
| tolerância em dias (regra 1 da issue). Reproduz o bug real: o artigo | ||
| 0102-6720-abcd-39-e1948 teve pub-date pub=2029 (digitado por engano | ||
| no lugar de 2026) e ficou oculto na produção sem gerar erro. | ||
| - pub-date pub não pode ser mais de N meses anterior ao ano de | ||
| pub-date[@date-type="collection"] (regra 2 da issue). | ||
| - Coleções retrospectivas (pub muito posterior ao collection, mas não | ||
| no futuro) continuam permitidas (regra 3 da issue) — testado como | ||
| guarda de regressão, já que não há checagem que bloqueie esse caso. | ||
|
|
||
| O parâmetro "today" é injetado nos params para tornar os testes | ||
| determinísticos, sem depender do relógio real da máquina. | ||
| """ | ||
|
|
||
| TODAY = date(2026, 6, 15) | ||
|
|
||
| def _make_params(self, **overrides): | ||
| params = { | ||
| "parent": {"parent": "article"}, | ||
| "required_events": [], | ||
| "pre_pub_ordered_events": [ | ||
| "preprint", "received", "rev-request", "rev-recd", "revised", "accepted" | ||
| ], | ||
| "pos_pub_ordered_events": ["pub", "resubmitted", "corrected", "retracted"], | ||
| "required_history_events_for_article_type": {}, | ||
| "required_history_events_for_related_article_type": {}, | ||
| "today": self.TODAY, | ||
| } | ||
| params.update(overrides) | ||
| return params | ||
|
|
||
| def _article_xml(self, pub_date, collection_year=None): | ||
| collection_block = "" | ||
| if collection_year is not None: | ||
| collection_block = f""" | ||
| <pub-date publication-format="electronic" date-type="collection"> | ||
| <year>{collection_year}</year> | ||
| </pub-date>""" | ||
| return f""" | ||
| <article article-type="research-article" xml:lang="pt"> | ||
| <front> | ||
| <article-meta> | ||
| <pub-date publication-format="electronic" date-type="pub"> | ||
| <day>{pub_date.day:02d}</day><month>{pub_date.month:02d}</month><year>{pub_date.year}</year> | ||
| </pub-date>{collection_block} | ||
| </article-meta> | ||
| </front> | ||
| </article> | ||
| """ | ||
|
|
||
| def _results(self, pub_date, collection_year=None, **param_overrides): | ||
| tree = etree.fromstring(self._article_xml(pub_date, collection_year)) | ||
| validator = FulltextDatesValidation(tree, self._make_params(**param_overrides)) | ||
| results = list(validator.validate()) | ||
| future = [r for r in results if r["title"] == "pub-date pub not in future"] | ||
| distance = [r for r in results if r["title"] == "pub-date pub not too far before collection"] | ||
| return future, distance | ||
|
|
||
| @staticmethod | ||
| def _rendered_advice(result): | ||
| """Renderiza adv_text + adv_params como um consumidor real faria | ||
| (ex.: spsvalidator), confirmando que os placeholders do template | ||
| i18n batem com as chaves de adv_params.""" | ||
| return result["adv_text"].format(**result["adv_params"]) | ||
|
|
||
| # Regra 1: pub não pode estar no futuro ----------------------------- | ||
|
|
||
| def test_pub_equal_today_is_ok(self): | ||
| future, _ = self._results(self.TODAY, collection_year=self.TODAY.year) | ||
| self.assertEqual(1, len(future)) | ||
| self.assertEqual("OK", future[0]["response"]) | ||
|
|
||
| def test_pub_within_future_tolerance_is_ok(self): | ||
| pub = self.TODAY + timedelta(days=5) | ||
| future, _ = self._results( | ||
| pub, collection_year=self.TODAY.year, pub_date_future_tolerance_days=5 | ||
| ) | ||
| self.assertEqual("OK", future[0]["response"]) | ||
|
|
||
| def test_pub_far_in_future_is_error(self): | ||
| """Reproduz o bug real da issue: pub 3 anos à frente (2029 vs 2026).""" | ||
| pub = date(self.TODAY.year + 3, 1, 1) | ||
| future, _ = self._results(pub, collection_year=self.TODAY.year) | ||
| self.assertEqual("CRITICAL", future[0]["response"]) | ||
| self.assertIn("must not be later than", future[0]["advice"]) | ||
| self.assertEqual( | ||
| f'<pub-date date-type="pub"> ({pub.isoformat()}) must not be ' | ||
| f"later than {(self.TODAY + timedelta(days=7)).isoformat()}", | ||
| self._rendered_advice(future[0]), | ||
| ) | ||
|
|
||
| # Regra 4: pub == collection ----------------------------------------- | ||
|
|
||
| def test_pub_equal_collection_year_is_ok(self): | ||
| pub = date(self.TODAY.year, 3, 1) | ||
| future, distance = self._results(pub, collection_year=self.TODAY.year) | ||
| self.assertEqual("OK", future[0]["response"]) | ||
| self.assertEqual("OK", distance[0]["response"]) | ||
|
|
||
| # Regra 2: pub não pode ser muito anterior ao collection ------------- | ||
|
|
||
| def test_pub_up_to_12_months_before_collection_is_ok(self): | ||
| collection_year = self.TODAY.year | ||
| pub = date(collection_year - 1, 2, 1) # dentro da tolerância de 12 meses | ||
| _, distance = self._results(pub, collection_year=collection_year) | ||
| self.assertEqual("OK", distance[0]["response"]) | ||
|
|
||
| def test_pub_more_than_12_months_before_collection_is_error(self): | ||
| collection_year = self.TODAY.year | ||
| pub = date(collection_year - 2, 1, 1) # bem além da tolerância de 12 meses | ||
| _, distance = self._results(pub, collection_year=collection_year) | ||
| self.assertEqual("CRITICAL", distance[0]["response"]) | ||
| self.assertIn("must not be more than", distance[0]["advice"]) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Rossi-Luciano alguns lugares está more than e later than...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @robertatakenaka boa observação, mas não é acidental: são duas comparações diferentes. "later than {limit}" para a regra 1 (pub comparado a uma data-limite) e "more than {N} months before {collection}" para a regra 2 (pub comparado a uma duração/intervalo). Achei que cada frase fica mais natural para o tipo de comparação que descreve, mas se preferir uma redação única para as duas mensagens (facilita grep/padronização), me diga qual formato prefere que eu unifico. |
||
| self.assertEqual( | ||
| f'<pub-date date-type="pub"> ({pub.isoformat()}) must not be more ' | ||
| f'than 12 months before <pub-date date-type="collection"> year ' | ||
| f"({collection_year})", | ||
| self._rendered_advice(distance[0]), | ||
| ) | ||
|
|
||
| # Regra 3: coleção retrospectiva (pub muito posterior ao collection) - | ||
|
|
||
| def test_pub_many_years_after_collection_but_not_future_is_ok(self): | ||
| """Coleção retrospectiva: pub muito posterior ao collection, mas <= hoje.""" | ||
| collection_year = self.TODAY.year - 100 | ||
| future, distance = self._results(self.TODAY, collection_year=collection_year) | ||
| self.assertEqual("OK", future[0]["response"]) | ||
| self.assertEqual("OK", distance[0]["response"]) | ||
|
|
||
| def test_pub_many_years_after_collection_and_in_future_is_error(self): | ||
| """Mesmo em coleção retrospectiva, pub não pode estar no futuro.""" | ||
| collection_year = self.TODAY.year - 100 | ||
| pub = date(self.TODAY.year + 3, 1, 1) | ||
| future, distance = self._results(pub, collection_year=collection_year) | ||
| self.assertEqual("CRITICAL", future[0]["response"]) | ||
| self.assertIn("must not be later than", future[0]["advice"]) | ||
| self.assertEqual( | ||
| f'<pub-date date-type="pub"> ({pub.isoformat()}) must not be ' | ||
| f"later than {(self.TODAY + timedelta(days=7)).isoformat()}", | ||
| self._rendered_advice(future[0]), | ||
| ) | ||
| # A distância para trás não é violada (pub é muito posterior ao collection) | ||
| self.assertEqual("OK", distance[0]["response"]) | ||
|
|
||
| # Casos sem collection ------------------------------------------------- | ||
|
|
||
| def test_no_collection_date_skips_distance_rule(self): | ||
| _, distance = self._results(self.TODAY, collection_year=None) | ||
| self.assertEqual([], distance) | ||
|
|
||
| def test_no_pub_date_skips_both_rules(self): | ||
| tree = etree.fromstring(""" | ||
| <article article-type="research-article" xml:lang="pt"> | ||
| <front> | ||
| <article-meta> | ||
| <pub-date publication-format="electronic" date-type="collection"> | ||
| <year>2026</year> | ||
| </pub-date> | ||
| </article-meta> | ||
| </front> | ||
| </article> | ||
| """) | ||
| validator = FulltextDatesValidation(tree, self._make_params()) | ||
| results = list(validator.validate()) | ||
| future = [r for r in results if r["title"] == "pub-date pub not in future"] | ||
| distance = [r for r in results if r["title"] == "pub-date pub not too far before collection"] | ||
| self.assertEqual([], future) | ||
| self.assertEqual([], distance) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Rossi-Luciano suspeito que isso não funciona:
teria que ser
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@robertatakenaka checando com calma, isso segue o mesmo padrão usado em todo o módulo
sps/validation/(não é específico dessas duas linhas):advice_text=i18n._("...{pub_date}...{limit}")é armazenado como template (sem.format()), e os valores ficam separados emadvice_params. Obuild_response()retorna os dois comoadv_text(template traduzido) eadv_params(valores), para o consumidor renderizar comadv_text.format(**adv_params), exatamente comomsg_text/msg_paramsjá fazem, documentado emtests/sps/validation/test_i18n_message_rendering.py::render_message.Testei manualmente as duas mensagens (regra 1 e regra 2) chamando
adv_text.format(**adv_params)e ambas renderizam corretamente, semKeyError:Se eu aplicasse
.format()aqui dentro dodates.py, oadv_textpassaria a conter a string já interpolada em inglês, quebrando a tradução dinâmica para outros locales (diferente de todos os outros validators do módulo). Por isso preferi manter como está, mas se você já sabia disso e mesmo assim prefere mudar o padrão aqui, me avisa que ajusto.Também adicionei em
test_dates.pya renderização explícita deadv_text.format(**adv_params)nos casos de erro, para isso ficar coberto automaticamente daqui pra frente.