Skip to content

Commit ca491f4

Browse files
committed
[MarkAPI-112] fix: merge
2 parents a4ded03 + 92e3c5e commit ca491f4

13 files changed

Lines changed: 726 additions & 135 deletions

config/settings/base.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,16 @@
323323
"CORE_JOURNAL_API_ENDPOINT",
324324
default="/api/v2/pid/journal/",
325325
)
326+
CORE_ISSUE_API_ENDPOINT = env(
327+
"CORE_ISSUE_API_ENDPOINT",
328+
default="/api/v1/issue/",
329+
)
330+
CORE_ISSUE_FROM_DATE_CREATED = env(
331+
"CORE_ISSUE_FROM_DATE_CREATED",
332+
default="2019-01-01",
333+
)
326334
CORE_COLLECTION_API_URL = f"{CORE_API_DOMAIN}{CORE_COLLECTION_API_ENDPOINT}"
327335
CORE_JOURNAL_API_URL = f"{CORE_API_DOMAIN}{CORE_JOURNAL_API_ENDPOINT}"
328336

329337
#Aumento en el límite de campos
330-
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000
338+
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000

config/urls.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1+
from core.search import views as search_views
12
from django.conf import settings
2-
from django.urls import include, path
3-
from django.contrib import admin
4-
from django.conf.urls.static import static
53
from django.conf.urls.i18n import i18n_patterns # ← Adicionar esta linha
6-
from wagtail.admin import urls as wagtailadmin_urls
4+
from django.conf.urls.static import static
5+
from django.contrib import admin
6+
from django.urls import include, path
7+
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
78
from wagtail import urls as wagtail_urls
9+
from wagtail.admin import urls as wagtailadmin_urls
810
from wagtail.documents import urls as wagtaildocs_urls
9-
from wagtailautocomplete.urls.admin import urlpatterns as autocomplete_admin_urls
10-
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
1111

12-
from core.search import views as search_views
13-
from reference import views as reference_views
1412
from config import api_router as api_router
13+
from markup_doc.autocomplete import urlpatterns as autocomplete_admin_urls
1514

1615
urlpatterns = [
1716
path("django-admin/", admin.site.urls),
@@ -20,10 +19,12 @@
2019
path("search/", search_views.search, name="search"),
2120
# JWT
2221
path("api/v1/auth/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
23-
path("api/v1/auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
22+
path(
23+
"api/v1/auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"
24+
),
2425
path("api/v1/", include(api_router)),
2526
# URL para trocar idioma
26-
path('i18n/', include('django.conf.urls.i18n')),
27+
path("i18n/", include("django.conf.urls.i18n")),
2728
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
2829

2930
# URLs com prefixo de idioma
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Generated by Django 6.0.5 on 2026-05-31 17:41
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('core', '0002_wagtailsearch_indexentry_text_defaults'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='CoreSyncState',
15+
fields=[
16+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('resource', models.CharField(max_length=50, unique=True, verbose_name='Resource')),
18+
('last_updated_at', models.DateTimeField(blank=True, null=True, verbose_name='Last updated at')),
19+
('last_success_at', models.DateTimeField(blank=True, null=True, verbose_name='Last success at')),
20+
],
21+
options={
22+
'verbose_name': 'Core sync state',
23+
'verbose_name_plural': 'Core sync states',
24+
},
25+
),
26+
migrations.AlterField(
27+
model_name='flexibledate',
28+
name='id',
29+
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
30+
),
31+
migrations.AlterField(
32+
model_name='gender',
33+
name='id',
34+
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
35+
),
36+
migrations.AlterField(
37+
model_name='language',
38+
name='id',
39+
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
40+
),
41+
migrations.AlterField(
42+
model_name='license',
43+
name='id',
44+
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
45+
),
46+
migrations.AlterField(
47+
model_name='licensestatement',
48+
name='id',
49+
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
50+
),
51+
]

core/models.py

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import os
2-
from django.db import models, IntegrityError
3-
from django.db.models import Case, When, Value, IntegerField
2+
43
from django.contrib.auth import get_user_model
4+
from django.db import IntegrityError, models
5+
from django.db.models import Case, IntegerField, Value, When
6+
from django.utils import timezone
57
from django.utils.translation import gettext_lazy as _
68
from wagtail.admin.panels import FieldPanel
79
from wagtail.fields import RichTextField
8-
from wagtail.search import index
910
from wagtailautocomplete.edit_handlers import AutocompletePanel
1011

1112
from . import choices
1213
from .utils.utils import language_iso
1314

1415
User = get_user_model()
1516

17+
1618
class CommonControlField(models.Model):
1719
"""
1820
Class with common control fields.
@@ -77,11 +79,9 @@ def autocomplete_label(self):
7779
FieldPanel("gender"),
7880
]
7981

80-
8182
class Meta:
8283
unique_together = [("code", "gender")]
8384

84-
8585
def __unicode__(self):
8686
return self.gender or self.code
8787

@@ -224,17 +224,20 @@ def get_object_in_preferred_language(self, language):
224224
mission = self.filter(language=language)
225225
if mission:
226226
return mission
227-
228-
language_order = ['pt', 'es', 'en']
227+
228+
language_order = ["pt", "es", "en"]
229229
langs = self.all().values_list("language", flat=True)
230230
languages = Language.objects.filter(id__in=langs)
231-
231+
232232
# Define a ordem baseado na lista language_order
233-
order = [When(code2=lang, then=Value(i)) for i, lang in enumerate(language_order)]
233+
order = [
234+
When(code2=lang, then=Value(i)) for i, lang in enumerate(language_order)
235+
]
234236
ordered_languages = languages.annotate(
235-
language_order=Case(*order, default=Value(len(language_order)), output_field=IntegerField())
236-
).order_by('language_order')
237-
237+
language_order=Case(
238+
*order, default=Value(len(language_order)), output_field=IntegerField()
239+
)
240+
).order_by("language_order")
238241

239242
for lang in ordered_languages:
240243
mission = self.filter(language=lang)
@@ -257,7 +260,7 @@ class RichTextWithLanguage(models.Model):
257260
AutocompletePanel("language"),
258261
FieldPanel("rich_text"),
259262
]
260-
263+
261264
objects = LanguageFallbackManager()
262265

263266
class Meta:
@@ -297,7 +300,7 @@ def autocomplete_label(self):
297300
]
298301

299302
class Meta:
300-
unique_together = [("license_type", )]
303+
unique_together = [("license_type",)]
301304
verbose_name = _("License")
302305
verbose_name_plural = _("Licenses")
303306
indexes = [
@@ -326,9 +329,7 @@ def get(
326329
):
327330
if not license_type:
328331
raise ValueError("License.get requires license_type parameters")
329-
filters = dict(
330-
license_type__iexact=license_type
331-
)
332+
filters = dict(license_type__iexact=license_type)
332333
try:
333334
return cls.objects.get(**filters)
334335
except cls.MultipleObjectsReturned:
@@ -368,7 +369,8 @@ class LicenseStatement(CommonControlField):
368369
Language, on_delete=models.SET_NULL, null=True, blank=True
369370
)
370371
license = models.ForeignKey(
371-
License, on_delete=models.SET_NULL, null=True, blank=True)
372+
License, on_delete=models.SET_NULL, null=True, blank=True
373+
)
372374

373375
panels = [
374376
FieldPanel("url"),
@@ -406,7 +408,8 @@ def get(
406408
raise ValueError("LicenseStatement.get requires url or license_p")
407409
try:
408410
return cls.objects.get(
409-
url__iexact=url, license_p__iexact=license_p, language=language)
411+
url__iexact=url, license_p__iexact=license_p, language=language
412+
)
410413
except cls.MultipleObjectsReturned:
411414
return cls.objects.filter(
412415
url__iexact=url, license_p__iexact=license_p, language=language
@@ -447,9 +450,7 @@ def create_or_update(
447450
):
448451
try:
449452
data = dict(
450-
url=url,
451-
license_p=license_p,
452-
language=language and language.code2
453+
url=url, license_p=license_p, language=language and language.code2
453454
)
454455
try:
455456
obj = cls.get(url, license_p, language)
@@ -464,7 +465,9 @@ def create_or_update(
464465
except cls.DoesNotExist:
465466
return cls.create(user, url, license_p, language, license)
466467
except Exception as e:
467-
raise ValueError(f"Unable to create or update LicenseStatement for {data}: {type(e)} {e}")
468+
raise ValueError(
469+
f"Unable to create or update LicenseStatement for {data}: {type(e)} {e}"
470+
)
468471

469472
@staticmethod
470473
def parse_url(url):
@@ -513,7 +516,7 @@ class FileWithLang(models.Model):
513516
blank=True,
514517
on_delete=models.SET_NULL,
515518
verbose_name=_("File"),
516-
help_text='',
519+
help_text="",
517520
related_name="+",
518521
)
519522

@@ -536,3 +539,52 @@ def filename(self):
536539

537540
class Meta:
538541
abstract = True
542+
543+
544+
class CoreSyncState(models.Model):
545+
"""
546+
Guarda o checkpoint da última coleta da API Core por recurso.
547+
548+
A próxima coleta deve sempre retomar a partir de ``last_updated_at``.
549+
"""
550+
551+
resource = models.CharField(_("Resource"), max_length=50, unique=True)
552+
last_updated_at = models.DateTimeField(_("Last updated at"), null=True, blank=True)
553+
last_success_at = models.DateTimeField(_("Last success at"), null=True, blank=True)
554+
555+
class Meta:
556+
verbose_name = _("Core sync state")
557+
verbose_name_plural = _("Core sync states")
558+
559+
def __unicode__(self):
560+
return self.resource
561+
562+
def __str__(self):
563+
return self.resource
564+
565+
@classmethod
566+
def get_for_resource(cls, resource):
567+
obj, _ = cls.objects.get_or_create(resource=resource)
568+
return obj
569+
570+
def get_from_date_updated(self, default):
571+
"""
572+
Retorna a data inicial para o filtro ``from_date_created`` da API.
573+
574+
Usa sempre a última data coletada; se ainda não houver checkpoint,
575+
retorna ``default``.
576+
"""
577+
if self.last_updated_at:
578+
return self.last_updated_at.date().isoformat()
579+
return default
580+
581+
def update_checkpoint(self, max_updated_at=None):
582+
"""
583+
Atualiza o checkpoint após uma execução bem-sucedida de sync.
584+
"""
585+
update_fields = ["last_success_at"]
586+
if max_updated_at:
587+
self.last_updated_at = max_updated_at
588+
update_fields.append("last_updated_at")
589+
self.last_success_at = timezone.now()
590+
self.save(update_fields=update_fields)

core/utils/sync_state.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from django.utils import timezone
2+
from django.utils.dateparse import parse_datetime
3+
4+
5+
def _normalize_datetime(value):
6+
if value is None:
7+
return None
8+
if hasattr(value, "utcoffset"):
9+
dt = value
10+
else:
11+
dt = parse_datetime(str(value))
12+
if dt is None:
13+
return None
14+
if timezone.is_naive(dt):
15+
dt = timezone.make_aware(dt, timezone.utc)
16+
return dt
17+
18+
19+
def track_max_from_item(current_max, item, field="updated"):
20+
"""
21+
Retorna o timestamp mais recente encontrado ao iterar resultados da API.
22+
23+
Converte ``item[field]`` e ``current_max`` para ``datetime`` antes de
24+
comparar, evitando erro ao misturar string ISO da API com ``DateTimeField``.
25+
26+
Args:
27+
current_max: ``datetime`` já processado, ou None.
28+
item: Dicionário retornado pela API Core.
29+
field: Nome do campo de data em ``item`` (padrão: ``created``).
30+
31+
Returns:
32+
O ``datetime`` mais recente entre ``current_max`` e ``item[field]``.
33+
"""
34+
value = _normalize_datetime(item.get(field))
35+
current_max = _normalize_datetime(current_max)
36+
if value and (current_max is None or value > current_max):
37+
return value
38+
return current_max
39+
40+
41+
def finalize_core_sync_state(sync_state, max_updated_at):
42+
"""
43+
Persiste o checkpoint após uma execução bem-sucedida de sync da API Core.
44+
45+
Args:
46+
sync_state: Instância de ``CoreSyncState`` do recurso sincronizado.
47+
max_updated_at: Maior ``created`` (ou equivalente) visto na execução.
48+
"""
49+
sync_state.update_checkpoint(max_updated_at)

0 commit comments

Comments
 (0)