-
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": 0, | ||
| "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 |
||
| 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 |
|---|---|---|
|
|
@@ -19,6 +19,10 @@ | |
| "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_future_tolerance_days":0, | ||
|
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 número maior entre 4 a 7 dias |
||
| "pub_date_past_collection_tolerance_months":12, | ||
| "required_events":[ | ||
| "received", | ||
| "accepted" | ||
|
|
||
| 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,152 @@ 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. |
||
| """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 | ||
|
|
||
| # 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"]) | ||
|
|
||
| # 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... |
||
|
|
||
| # 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"]) | ||
| # 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