Skip to content
Open
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
10 changes: 10 additions & 0 deletions packtools/sps/locale/es/LC_MESSAGES/packtools_sps.po
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,16 @@ msgstr "Agregue <pub-date publication-format=\"electronic\" date-type=\"pub\"> c
msgid "Add <pub-date publication-format=\"electronic\" date-type=\"collection\"> with <year>"
msgstr "Agregue <pub-date publication-format=\"electronic\" date-type=\"collection\"> con <year>"

#: packtools/sps/validation/dates.py
#, python-brace-format
msgid "<pub-date date-type=\"pub\"> ({pub_date}) must not be later than {limit}"
msgstr "<pub-date date-type=\"pub\"> ({pub_date}) no debe ser posterior a {limit}"

#: packtools/sps/validation/dates.py
#, python-brace-format
msgid "<pub-date date-type=\"pub\"> ({pub_date}) must not be more than {tolerance_months} months before <pub-date date-type=\"collection\"> year ({collection_year})"
msgstr "<pub-date date-type=\"pub\"> ({pub_date}) no debe ser más de {tolerance_months} meses anterior al año de <pub-date date-type=\"collection\"> ({collection_year})"

#: packtools/sps/validation/dates.py:411
#, python-brace-format
msgid "Set @publication-format=\"electronic\" in <pub-date date-type=\"{date_type}\">"
Expand Down
10 changes: 10 additions & 0 deletions packtools/sps/locale/pt_BR/LC_MESSAGES/packtools_sps.po
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,16 @@ msgstr "Adicione <pub-date publication-format=\"electronic\" date-type=\"pub\">
msgid "Add <pub-date publication-format=\"electronic\" date-type=\"collection\"> with <year>"
msgstr "Adicione <pub-date publication-format=\"electronic\" date-type=\"collection\"> com <year>"

#: packtools/sps/validation/dates.py
#, python-brace-format
msgid "<pub-date date-type=\"pub\"> ({pub_date}) must not be later than {limit}"
msgstr "<pub-date date-type=\"pub\"> ({pub_date}) não deve ser posterior a {limit}"

#: packtools/sps/validation/dates.py
#, python-brace-format
msgid "<pub-date date-type=\"pub\"> ({pub_date}) must not be more than {tolerance_months} months before <pub-date date-type=\"collection\"> year ({collection_year})"
msgstr "<pub-date date-type=\"pub\"> ({pub_date}) não deve ser mais de {tolerance_months} meses anterior ao ano de <pub-date date-type=\"collection\"> ({collection_year})"

#: packtools/sps/validation/dates.py:411
#, python-brace-format
msgid "Set @publication-format=\"electronic\" in <pub-date date-type=\"{date_type}\">"
Expand Down
89 changes: 88 additions & 1 deletion packtools/sps/validation/dates.py
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
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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}'),

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 suspeito que isso não funciona:

i18n._('<pub-date date-type="pub"> ({pub_date}) must not be later than {limit}')

teria que ser

i18n._('<pub-date date-type="pub"> ({pub_date}) must not be later than {limit}').format(
    pub_date=pub_date, 
    limit=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})'),

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 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:
Expand Down
4 changes: 4 additions & 0 deletions packtools/sps/validation_rules/article_dates_rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,

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 número maior entre 4 a 7 dias

"pub_date_past_collection_tolerance_months":12,
"required_events":[
"received",
"accepted"
Expand Down
151 changes: 150 additions & 1 deletion tests/sps/validation/test_dates.py
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

Expand Down Expand Up @@ -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):

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 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"])

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 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)