From df2b135dbe70ee70d8305aeb3f95fa87c2d40dc8 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:27:16 -0300 Subject: [PATCH 01/32] add cryptography e PyJWT --- requirements/base.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 00582587..07e140b8 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -166,4 +166,9 @@ wagtail-2fa-new==1.8.0 # https://pypi.org/project/wagtail-2fa-new/ xlwt==1.3.0 -django-silk==5.3.2 \ No newline at end of file +django-silk==5.3.2 + +# JWT +# ------------------------------------------------------------------------------ +cryptography==46.0.1 +PyJWT==2.8.0 From 02f061e374e6d6556c7e6c04f94340878d35bcb2 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:28:09 -0300 Subject: [PATCH 02/32] Remove field size de BaseLogo --- core/models.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/core/models.py b/core/models.py index fa4cbbb0..7ff55341 100755 --- a/core/models.py +++ b/core/models.py @@ -594,15 +594,6 @@ class BaseLogo(models.Model): blank=True, verbose_name=_("Logo Image") ) - - size = models.CharField( - _("Logo Size"), - max_length=20, - choices=choices.LOGO_SIZE_CHOICES, - default='medium', - help_text=_("Select the size/purpose of this logo") - ) - language = models.ForeignKey( Language, verbose_name=_("Logo language"), @@ -614,16 +605,12 @@ class BaseLogo(models.Model): panels = [ FieldPanel("logo"), - FieldPanel("size"), FieldPanel("language"), ] class Meta: abstract = True - ordering = ['sort_order', 'language', 'size'] - - def __str__(self): - return f"{self.collection} - {self.language} ({self.size})" + ordering = ['sort_order', 'language'] ICON_MAP = { From 22f9159da88aa1d0ce1f586f836489762dd53ab6 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:29:07 -0300 Subject: [PATCH 03/32] Add purpose em Collection --- collection/models.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/collection/models.py b/collection/models.py index fed413e7..924353c9 100755 --- a/collection/models.py +++ b/collection/models.py @@ -278,6 +278,7 @@ def get_name_for_language(self, lang_code=None): return name_obj.text return self.main_name or (self.collection_name.first().text if self.collection_name.exists() else "") + class CollectionSocialNetwork(Orderable, SocialNetwork): page = ParentalKey( Collection, @@ -309,6 +310,7 @@ class CollectionSupportingOrganization(Orderable, ClusterableModel, BaseHistory) class Meta: verbose_name = _("Supporting Organization") verbose_name_plural = _("Supporting Organizations") + ordering = ['sort_order'] # Ordena os icons no opac_5 def __str__(self): return str(self.organization) @@ -336,6 +338,7 @@ class CollectionExecutingOrganization(Orderable, ClusterableModel, BaseHistory): class Meta: verbose_name = _("Executing Organization") verbose_name_plural = _("Executing Organizations") + ordering = ['sort_order'] # Ordena os icons no opac_5 def __str__(self): return str(self.organization) @@ -352,14 +355,25 @@ class CollectionLogo(Orderable, BaseLogo): related_name='logos', verbose_name=_("Collection") ) - + purpose = models.CharField( + max_length=20, + choices=choices.LOGO_PURPOSE, + blank=True, + help_text=_("Select the purpose of this logo"), + ) + + panels = [ + FieldPanel("logo"), + AutocompletePanel("language"), + FieldPanel("purpose"), + ] class Meta: verbose_name = _("Collection Logo") verbose_name_plural = _("Collection Logos") - ordering = ['sort_order', 'language', 'size'] + ordering = ['sort_order', 'language'] unique_together = [ - ('collection', 'size', 'language', ), + ('collection', 'language', 'purpose'), ] def __str__(self): - return f"{self.collection} - {self.language} ({self.size})" + return f"{self.collection} - {self.language} ({self.purpose})" From cf7b1a182aaab9b623e3c8a548d39bf0e36554fc Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:29:22 -0300 Subject: [PATCH 04/32] Adiciona url e logo_url --- organization/api/v1/serializers.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/organization/api/v1/serializers.py b/organization/api/v1/serializers.py index 4eabb42c..6266b7e8 100644 --- a/organization/api/v1/serializers.py +++ b/organization/api/v1/serializers.py @@ -1,9 +1,17 @@ from rest_framework import serializers from organization import models - +from wagtail.models.sites import Site class OrganizationSerializer(serializers.ModelSerializer): location = serializers.SerializerMethodField() + logo_url = serializers.SerializerMethodField() + + def get_logo_url(self, obj): + if obj.logo: + domain = Site.objects.get(is_default_site=True).hostname + domain = f"http://{domain}" + return f"{domain}{obj.logo.url}" + return None def get_location(self, obj): if obj.location: @@ -14,5 +22,7 @@ class Meta: fields = [ "name", "acronym", + "url", + "logo_url", "location", ] \ No newline at end of file From 1d730b2aa9e786ad2c9ff8580a8a9a4b08ebaddd Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:29:34 -0300 Subject: [PATCH 05/32] Remove LOGO_SIZE --- core/choices.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/core/choices.py b/core/choices.py index b0ae9d90..2f2adc77 100644 --- a/core/choices.py +++ b/core/choices.py @@ -233,12 +233,3 @@ ("bsky", "Discover Blue Sky"), ("youtube", "Youtube"), ] - - -LOGO_SIZE_CHOICES = [ - ('thumbnail', _('Thumbnail (150x150)')), - ('small', _('Small (300x300)')), - ('medium', _('Medium (600x600)')), - ('large', _('Large (1200x1200)')), - ('original', _('Original')), -] From a57d96203ed732030c82e166bce40dfffda82b37 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:29:51 -0300 Subject: [PATCH 06/32] Add LOGO_PURPOSE --- collection/choices.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/collection/choices.py b/collection/choices.py index e17b93d7..7c1dbe2f 100644 --- a/collection/choices.py +++ b/collection/choices.py @@ -14,3 +14,11 @@ ("books", _("Books")), ("data", _("Data repository")), ] + +LOGO_PURPOSE = [ + ("homepage", _("Homepage")), + ("header", _("Header")), + ("logo_drop_menu", _("Logo drop menu")), + ("footer", _("Footer")), + ("menu", _("Menu")), +] \ No newline at end of file From 16d4349a53415467c1a079b4afb622fa0f4a99ce Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:39:29 -0300 Subject: [PATCH 07/32] Var de ambiente local --- .envs/.local/.django | 6 +++++- .envs/.local/.jwt | 6 ++++++ jwt_private_local.pem | 27 +++++++++++++++++++++++++++ local.yml | 1 + 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .envs/.local/.jwt create mode 100644 jwt_private_local.pem diff --git a/.envs/.local/.django b/.envs/.local/.django index 6e391215..4a113b70 100755 --- a/.envs/.local/.django +++ b/.envs/.local/.django @@ -19,4 +19,8 @@ CELERY_FLOWER_PASSWORD=QgScyefPrYhHgO6onW61u0nazc5xdBuP4sM7jMRrBBFuA2RjsFhZLp7xb FETCH_DATA_TIMEOUT=2 DJANGO_PROFILING_ENABLED=True -RUN_ASYNC=False \ No newline at end of file +RUN_ASYNC=False + +# ENDPOINT PARA ATUALIZAÇÃO DE COLEÇÕES +# ------------------------------------------------------------------------------ +ENDPOINT_COLLECTION="/api/v1/update_collection/" \ No newline at end of file diff --git a/.envs/.local/.jwt b/.envs/.local/.jwt new file mode 100644 index 00000000..845f753c --- /dev/null +++ b/.envs/.local/.jwt @@ -0,0 +1,6 @@ +JWT_ISS="https://api.seu-django.com" +JWT_AUD="seu-flask-servico" +JWT_EXP_SECONDS=600 +JWT_ALG="RS256" +# JWT_PRIVATE_KEY JA CORRESPONDENTE AO jwt_public OPAC_5 PARA TESTES. +JWT_PRIVATE_KEY_PATH="/app/jwt_private_local.pem" \ No newline at end of file diff --git a/jwt_private_local.pem b/jwt_private_local.pem new file mode 100644 index 00000000..99ba64cc --- /dev/null +++ b/jwt_private_local.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQB2+n5vaPzgUNvvnyDQ8NJiXCpUx1gktxJ9MUP5RTR+v9z5h4K0 +I+ktSHtydgOr43d/TlWztOT5sVubC3AsLxV9Be5D1zRZ5PqJ881fDnydhpedyjrX +dfhvKdMRSPGptD7lYeQZh5M46yH1GZHNMX5ZcVrRynUAOWr7js0HLgRuWPgTvUJm +4YGztTBJGw27O4h5v9UvDpe42LFEeVLxtmWTLPJncGSQdcE1uBBGcQ3SYrkOhcJ/ +fwDAlAMvHu5CrYnk5cQ2Ge1jTqOfbtcWTFlyE5EbZFqUHzwu4yHYmN8LDp0/siF/ +2dGnMANRVkEHASsqlbRgBtZUBdDOrPJA6ElBAgMBAAECggEAIIjVBCAgbjKOQdMw +xRHlZdIwHSi5uKh7HNLY9JPxC+vpQC7HFf1v6NsWLrGIxXsZFS3Vj6OarZDLLPWO +wfZKGPsDMXfqr95GiGyrrx+mAelpElPXjU2MFtHIDsT0mGRagp0nI7gIERtzGLPa +Q9MR6uFtZUrIfbw1vf5JoHn76jR1hw8Mi4Cvv6PMUBPs05VG2ccQoIFB9U/P8qNN +pJLKZKt38Acf2k7Uy1zqeujGZ3wNKcNYXDKgkPHNNojGe62Ac2SCiU6H2MqQVl6P +npMHliI351tBnZ/eSz2K6JXJEZtf7l7ues266KmQp2kIlHB/RDpyJvWg1YJaEESg +10z1wQKBgQDgHWmvWUIPMilUiPKSsZJjLdKE6598PyQRODrSH8hX+E/zWVK8hTU6 +h1gV2wsl5yldEBo5WPiIGXud922boB16nw5PksgfQ5OsJOMFjaLRFDIAmQXQ7evU +3QBkMkfNNQDgKwzYn5zn9EwKZSnSE3fEPVmxcYqAyCSnr5WjmQfmWQKBgQCH59yk +1H6Ar7FtrD3WgtaUbrZEmUm7OtOep9/u28fBeVpS0Jb+hMdCjblGhIzXlmTKYEO1 +J5F5W9n0KXNlvePWv7iZ6PF60vFrQkeCF+5BT6ufxYjg6uAx8yotdLWj83IYw88r +kSr9SsZ4L+5v34IXpjprnrUiSkfRcjDY3QTtKQKBgQCBK9K3Ex4E/ideJtSRAjHa +YhW9MMUqaoYSOrOEWncxZNhH7QrHx4snYcy+RBLH1NU9Y0OKHCKa5v0dzfdpxD5Z +Z4VrDbG0e1GI3XXPxJO8KASt3YTBp+/OL1bDaUuDFfKcQZPU4yIfeum88BPLlhbo +j2e74zIuCa0+zO/VbCVmeQKBgDKSaf2AAQ7b0fdQp7Yh+71CMVr3e4NLUaHxMBgO ++pcZFC5K5lURjaBLYo1GF67FjP3tsmQ/tBdnwYPkxMcwUQ8BSq0jDHC6/BEAmeFk +DEmLXv7WH6gqXoDXznMZwdmfZm6mtnmszEVyxMXGeEBy+FLajVSsuxRmdbEPf/PC +0ul5AoGAaeo2b3MiPNSlWlK9z1pH/z5LdUcrFAYJmY2uQncwSSpTl7fu1WRT+pBm +HVVw7nyiLZ6NHpRIolc+9F0jUL02hzy5f7FVTJTiwG95mHJTKet+aSbtn90H7jn9 +j5i+vDTqRimiob9KTkITTZ4uWKvG4Rlymxd4PkMN1TgBn7y4XQk= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/local.yml b/local.yml index e5bbfdaf..9cec7c9d 100755 --- a/local.yml +++ b/local.yml @@ -20,6 +20,7 @@ services: env_file: - ./.envs/.local/.django - ./.envs/.local/.postgres + - ./.envs/.local/.jwt ports: - "8009:8000" command: /start From 03e82f72198d450de15b18c90cb2fe499833c6f4 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:42:53 -0300 Subject: [PATCH 08/32] Adiciona variaveis de ambiente relacionado a JWT --- config/settings/base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/config/settings/base.py b/config/settings/base.py index 635bdb19..3313875e 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -579,3 +579,18 @@ PROFILING_LOG_SLOW_REQUESTS = env.float('DJANGO_PROFILING_LOG_SLOW_REQUESTS', default=0.2) PROFILING_LOG_HIGH_MEMORY = env.int('DJANGO_PROFILING_LOG_HIGH_MEMORY', default=20) PROFILING_LOG_ALL = env.bool('DJANGO_PROFILING_LOG_ALL', default=True) + + +# JWT para dados de colleção no opac_5 +JWT_PRIVATE_KEY_PATH = env.str("JWT_PRIVATE_KEY_PATH", default="/app/jwt_private.pem") + +with open(JWT_PRIVATE_KEY_PATH, "rb") as f: + JWT_PRIVATE_KEY = f.read() + +JWT_ALG = "RS256" +JWT_ISS = env.str("JWT_ISS", default="https://api.seu-django.com") +JWT_AUD = env.str("JWT_AUD", default="seu-flask-servico") +JWT_EXP_SECONDS = env.int("JWT_EXP_SECONDS", default=3600) + +# ENDPOINT PARA ATUALIZAÇÃO DOS DADOS DE COLEÇÃO NO OPAC_5 +ENDPOINT_COLLECTION = env.str("ENDPOINT_COLLECTION", default="/api/v1/update_collection/") From 45b4f04491fcb373e6925116311393862202519c Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:44:26 -0300 Subject: [PATCH 09/32] =?UTF-8?q?Adiciona=20classe=20ListSerializer=20para?= =?UTF-8?q?=20agrupar=20logos=20por=20purpose=20e=20ficar=20melhor=20extra?= =?UTF-8?q?=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- collection/api/v1/serializers.py | 56 +++++++++++++++++++------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/collection/api/v1/serializers.py b/collection/api/v1/serializers.py index 303db021..0be55797 100644 --- a/collection/api/v1/serializers.py +++ b/collection/api/v1/serializers.py @@ -2,6 +2,8 @@ from collection import models from core.api.v1.serializers import LanguageSerializer from organization.api.v1.serializers import OrganizationSerializer +from wagtail.models.sites import Site + class CollectionNameSerializer(serializers.ModelSerializer): """Serializer para nomes traduzidos da coleção""" @@ -14,6 +16,32 @@ class Meta: "language", ] +class CollectionLogoListSerializer(serializers.ListSerializer): + """ + Agrupa os logos por 'purpose' e consolida por idioma, + eliminando duplicatas por (purpose, lang_code2). + """ + def to_representation(self, data): + items = super().to_representation(data) + grouped = {} + seen = set() + + for item in items: + purpose = item.get("purpose") + url = item.get("logo_url") + lang = item.get("language", {}) + code2 = lang.get("code2") + + if not purpose or not code2 or not url: + continue + + key = (purpose, code2) + if key in seen: + continue + seen.add(key) + + grouped.setdefault(purpose, {}).update({code2: url}) + return grouped class CollectionLogoSerializer(serializers.ModelSerializer): """Serializer para logos da coleção""" @@ -22,38 +50,21 @@ class CollectionLogoSerializer(serializers.ModelSerializer): class Meta: model = models.CollectionLogo + list_serializer_class = CollectionLogoListSerializer fields = [ - "logo", "logo_url", - "size", + "purpose", "language", ] def get_logo_url(self, obj): - """Retorna a URL do logo renderizado no tamanho apropriado""" if obj.logo: - # Ajusta o rendition baseado no tamanho - rendition_specs = { - 'small': 'fill-100x100', - 'medium': 'fill-200x200', - 'large': 'fill-400x400', - 'banner': 'width-1200', - 'thumbnail': 'fill-150x150', - 'header': 'height-80', - 'footer': 'height-60', - } - spec = rendition_specs.get(obj.size, 'fill-200x200') - rendition = obj.logo.get_rendition(spec) - - # Retorna URL completa se houver request no contexto - request = self.context.get('request') - if request: - return request.build_absolute_uri(rendition.url) - return rendition.url + domain = Site.objects.get(is_default_site=True).hostname + domain = f"http://{domain}" + return f"{domain}{obj.logo.file.url}" return None - class SupportingOrganizationSerializer(serializers.ModelSerializer): """Serializer para organizações de suporte""" organization = OrganizationSerializer(read_only=True, many=False) @@ -93,7 +104,6 @@ class Meta: class CollectionSerializer(serializers.ModelSerializer): """Serializer principal para Collection com todos os relacionamentos""" - # Campos relacionados (read-only por padrão) collection_names = CollectionNameSerializer(source='collection_name', many=True, read_only=True) logos = CollectionLogoSerializer(many=True, read_only=True) supporting_organizations = SupportingOrganizationSerializer(source='supporting_organization', many=True, read_only=True) From 247bfdd824b905709cbabaeca78b86c1df501ed5 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:46:32 -0300 Subject: [PATCH 10/32] =?UTF-8?q?Adiciona=20signal=20e=20codifica=C3=A7?= =?UTF-8?q?=C3=A3o=20do=20token=20para=20mandar=20requisi=C3=A7=C3=B5es=20?= =?UTF-8?q?post=20para=20endpoint=20no=20opac=20ao=20salvar=20objeto=20col?= =?UTF-8?q?lection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- collection/apps.py | 3 ++ collection/signals.py | 20 ++++++++++ collection/tasks.py | 91 +++++++++++++++++++++++++++++++++++++++++++ core/utils/jwt.py | 28 +++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 collection/signals.py create mode 100644 core/utils/jwt.py diff --git a/collection/apps.py b/collection/apps.py index 040c2d31..3cd73cc2 100755 --- a/collection/apps.py +++ b/collection/apps.py @@ -4,3 +4,6 @@ class CollectionConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "collection" + + def ready(self): + from . import signals \ No newline at end of file diff --git a/collection/signals.py b/collection/signals.py new file mode 100644 index 00000000..2fd32490 --- /dev/null +++ b/collection/signals.py @@ -0,0 +1,20 @@ +from django.db import transaction +from django.db.models.signals import post_save +from django.dispatch import receiver + +from .models import Collection +from .tasks import build_collection_webhook + + +@receiver(post_save, sender=Collection, dispatch_uid="collection.signals.post_save") +def collection_post_save(sender, instance, created, **kwargs): + def _on_commit(): + event = "collection.created" if created else "collection.updated" + build_collection_webhook.apply_async( + kwargs=dict( + event=event, + collection_acron=instance.acron3, + # headers=headers, + ) + ) + transaction.on_commit(_on_commit) \ No newline at end of file diff --git a/collection/tasks.py b/collection/tasks.py index 264e9494..2cda7b74 100644 --- a/collection/tasks.py +++ b/collection/tasks.py @@ -1,10 +1,19 @@ +import logging +import sys + +import requests +from django.conf import settings from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ from collection.models import Collection from config import celery_app +from core.utils.jwt import issue_jwt_for_flask + +from .api.v1.serializers import CollectionSerializer User = get_user_model() +from tracker.models import UnexpectedEvent @celery_app.task(bind=True) @@ -14,3 +23,85 @@ def task_load_collections(self, user_id=None, username=None): if username: user = User.objects.get(username=username) Collection.load(user) + + +def fetch_with_schema_guess(host_or_url, timeout=10): + """ + Algumas coleções não possuem o schema no domínio, por isso é necessário + tentar os schemas http e https para obter o resultado correto. + """ + if "://" in host_or_url: + return host_or_url + + for schema in ["http", "https"]: + url = f"{schema}://{host_or_url}" + try: + resp = requests.post(url, timeout=timeout) + resp.raise_for_status() + return url + except requests.exceptions.SSLError: + continue + except requests.exceptions.RequestException: + continue + + +def _send_payload(url, headers, payload): + url_with_schema = fetch_with_schema_guess(url) + pattern_url = "http://172.23.0.1:8000" + settings.ENDPOINT_COLLECTION + + try: + resp = requests.post(pattern_url, json=payload, headers=headers, timeout=5) + if resp.status_code != 200: + logging.error(f"Erro ao enviar dados de coleção para {url}. Status: {resp.status_code}. Body: {resp.text}") + resp.raise_for_status() + return resp.json() + except Exception as e: + exc_type, exc_value, exc_traceback = sys.exc_info() + UnexpectedEvent.create( + exception=e, + exc_traceback=exc_traceback, + detail={ + "task": "collection.tasks.build_collection_webhook", + "url": url, + "payload": payload, + "headers": headers, + "pattern_url": pattern_url, + }, + ) + + +@celery_app.task +def build_collection_webhook_for_all(event=False, headers=None): + collections = Collection.objects.filter(domain__isnull=False, is_active=True) + for collection in collections: + build_collection_webhook.apply_async( + kwargs=dict( + event=event, + collection_acron=collection.acron3, + headers=headers, + ) + ) + +@celery_app.task +def build_collection_webhook(event, collection_acron, headers=None): + collection = Collection.objects.get(acron3=collection_acron) + if not collection.domain: + return None + serializer = CollectionSerializer(collection) + event = "collection.created" if event else "collection.updated" + payload = { + "event": event, + "results": serializer.data, + } + token = issue_jwt_for_flask( + sub="service:django", + claims={"roles": ["m2m"], "scope": "ingest:write"} + ) + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + _send_payload(url=collection.domain, headers=headers, payload=payload) + + diff --git a/core/utils/jwt.py b/core/utils/jwt.py new file mode 100644 index 00000000..671df21d --- /dev/null +++ b/core/utils/jwt.py @@ -0,0 +1,28 @@ +import time +import jwt +from django.conf import settings + + +def issue_jwt_for_flask(sub="service:django", claims=None): + now = int(time.time()) + payload = { + "iss": settings.JWT_ISS, + "aud": settings.JWT_AUD, + "iat": now, + "nbf": now, + "exp": now + settings.JWT_EXP_SECONDS, + "sub": sub, + } + if claims: + payload.update(claims) + + with open(settings.JWT_PRIVATE_KEY_PATH, "rb") as f: + private_key = f.read() + + token = jwt.encode( + payload, + private_key, + algorithm="RS256", + headers={"alg": "RS256", "typ": "JWT"}, + ) + return token \ No newline at end of file From 4cf1d9bfa345d6aacfbbd3da064e9faf3559af4a Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:47:17 -0300 Subject: [PATCH 11/32] fix header --- core/templates/home/scieloorg/header.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/templates/home/scieloorg/header.html b/core/templates/home/scieloorg/header.html index adfea801..920d1192 100644 --- a/core/templates/home/scieloorg/header.html +++ b/core/templates/home/scieloorg/header.html @@ -86,7 +86,12 @@
- {% trans 'Sobre o SciELO' %} + {% if page_about %} + {% trans 'Sobre o SciELO' %} + {% else %} + {% trans 'Sobre o SciELO' %} + {% endif %} +
From 9511cc5486b891af868f7d5507f4cb5ef19b6f0a Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 14:47:29 -0300 Subject: [PATCH 12/32] migration --- ...nexecutingorganization_options_and_more.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 collection/migrations/0007_alter_collectionexecutingorganization_options_and_more.py diff --git a/collection/migrations/0007_alter_collectionexecutingorganization_options_and_more.py b/collection/migrations/0007_alter_collectionexecutingorganization_options_and_more.py new file mode 100644 index 00000000..40dfd444 --- /dev/null +++ b/collection/migrations/0007_alter_collectionexecutingorganization_options_and_more.py @@ -0,0 +1,65 @@ +# Generated by Django 5.2.3 on 2025-09-25 17:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "collection", + "0006_alter_collection_options_alter_collection_acron2_and_more", + ), + ("core", "0007_alter_language_options_alter_license_options_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="collectionexecutingorganization", + options={ + "ordering": ["sort_order"], + "verbose_name": "Executing Organization", + "verbose_name_plural": "Executing Organizations", + }, + ), + migrations.AlterModelOptions( + name="collectionlogo", + options={ + "ordering": ["sort_order", "language"], + "verbose_name": "Collection Logo", + "verbose_name_plural": "Collection Logos", + }, + ), + migrations.AlterModelOptions( + name="collectionsupportingorganization", + options={ + "ordering": ["sort_order"], + "verbose_name": "Supporting Organization", + "verbose_name_plural": "Supporting Organizations", + }, + ), + migrations.AlterUniqueTogether( + name="collectionlogo", + unique_together={("collection", "language", "purpose")}, + ), + migrations.AddField( + model_name="collectionlogo", + name="purpose", + field=models.CharField( + blank=True, + choices=[ + ("homepage", "Homepage"), + ("header", "Header"), + ("logo_drop_menu", "Logo drop menu"), + ("footer", "Footer"), + ("menu", "Menu"), + ], + help_text="Select the purpose of this logo", + max_length=20, + ), + ), + migrations.RemoveField( + model_name="collectionlogo", + name="size", + ), + ] From 823d7fe5750df84a62ba6c7fa526b97bcb51742d Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 15:37:07 -0300 Subject: [PATCH 13/32] remove jwt_private_local.pem --- jwt_private_local.pem | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 jwt_private_local.pem diff --git a/jwt_private_local.pem b/jwt_private_local.pem deleted file mode 100644 index 99ba64cc..00000000 --- a/jwt_private_local.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQB2+n5vaPzgUNvvnyDQ8NJiXCpUx1gktxJ9MUP5RTR+v9z5h4K0 -I+ktSHtydgOr43d/TlWztOT5sVubC3AsLxV9Be5D1zRZ5PqJ881fDnydhpedyjrX -dfhvKdMRSPGptD7lYeQZh5M46yH1GZHNMX5ZcVrRynUAOWr7js0HLgRuWPgTvUJm -4YGztTBJGw27O4h5v9UvDpe42LFEeVLxtmWTLPJncGSQdcE1uBBGcQ3SYrkOhcJ/ -fwDAlAMvHu5CrYnk5cQ2Ge1jTqOfbtcWTFlyE5EbZFqUHzwu4yHYmN8LDp0/siF/ -2dGnMANRVkEHASsqlbRgBtZUBdDOrPJA6ElBAgMBAAECggEAIIjVBCAgbjKOQdMw -xRHlZdIwHSi5uKh7HNLY9JPxC+vpQC7HFf1v6NsWLrGIxXsZFS3Vj6OarZDLLPWO -wfZKGPsDMXfqr95GiGyrrx+mAelpElPXjU2MFtHIDsT0mGRagp0nI7gIERtzGLPa -Q9MR6uFtZUrIfbw1vf5JoHn76jR1hw8Mi4Cvv6PMUBPs05VG2ccQoIFB9U/P8qNN -pJLKZKt38Acf2k7Uy1zqeujGZ3wNKcNYXDKgkPHNNojGe62Ac2SCiU6H2MqQVl6P -npMHliI351tBnZ/eSz2K6JXJEZtf7l7ues266KmQp2kIlHB/RDpyJvWg1YJaEESg -10z1wQKBgQDgHWmvWUIPMilUiPKSsZJjLdKE6598PyQRODrSH8hX+E/zWVK8hTU6 -h1gV2wsl5yldEBo5WPiIGXud922boB16nw5PksgfQ5OsJOMFjaLRFDIAmQXQ7evU -3QBkMkfNNQDgKwzYn5zn9EwKZSnSE3fEPVmxcYqAyCSnr5WjmQfmWQKBgQCH59yk -1H6Ar7FtrD3WgtaUbrZEmUm7OtOep9/u28fBeVpS0Jb+hMdCjblGhIzXlmTKYEO1 -J5F5W9n0KXNlvePWv7iZ6PF60vFrQkeCF+5BT6ufxYjg6uAx8yotdLWj83IYw88r -kSr9SsZ4L+5v34IXpjprnrUiSkfRcjDY3QTtKQKBgQCBK9K3Ex4E/ideJtSRAjHa -YhW9MMUqaoYSOrOEWncxZNhH7QrHx4snYcy+RBLH1NU9Y0OKHCKa5v0dzfdpxD5Z -Z4VrDbG0e1GI3XXPxJO8KASt3YTBp+/OL1bDaUuDFfKcQZPU4yIfeum88BPLlhbo -j2e74zIuCa0+zO/VbCVmeQKBgDKSaf2AAQ7b0fdQp7Yh+71CMVr3e4NLUaHxMBgO -+pcZFC5K5lURjaBLYo1GF67FjP3tsmQ/tBdnwYPkxMcwUQ8BSq0jDHC6/BEAmeFk -DEmLXv7WH6gqXoDXznMZwdmfZm6mtnmszEVyxMXGeEBy+FLajVSsuxRmdbEPf/PC -0ul5AoGAaeo2b3MiPNSlWlK9z1pH/z5LdUcrFAYJmY2uQncwSSpTl7fu1WRT+pBm -HVVw7nyiLZ6NHpRIolc+9F0jUL02hzy5f7FVTJTiwG95mHJTKet+aSbtn90H7jn9 -j5i+vDTqRimiob9KTkITTZ4uWKvG4Rlymxd4PkMN1TgBn7y4XQk= ------END RSA PRIVATE KEY----- \ No newline at end of file From 36dd9fb88a7db9090edd381b3bc7b8203bb0ac0e Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 16:13:58 -0300 Subject: [PATCH 14/32] =?UTF-8?q?Adiciona=20vari=C3=A1vel=20para=20ativar?= =?UTF-8?q?=20update=20da=20cole=C3=A7=C3=A3o=20em=20opac=5F5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- collection/signals.py | 17 +++++++++-------- config/settings/base.py | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/collection/signals.py b/collection/signals.py index 2fd32490..ec16632e 100644 --- a/collection/signals.py +++ b/collection/signals.py @@ -1,7 +1,7 @@ from django.db import transaction from django.db.models.signals import post_save from django.dispatch import receiver - +from django.conf import settings from .models import Collection from .tasks import build_collection_webhook @@ -9,12 +9,13 @@ @receiver(post_save, sender=Collection, dispatch_uid="collection.signals.post_save") def collection_post_save(sender, instance, created, **kwargs): def _on_commit(): - event = "collection.created" if created else "collection.updated" - build_collection_webhook.apply_async( - kwargs=dict( - event=event, - collection_acron=instance.acron3, - # headers=headers, + if settings.ACTIVATE_UPDATE_COLLECTION_WEBHOOK: + event = "collection.created" if created else "collection.updated" + build_collection_webhook.apply_async( + kwargs=dict( + event=event, + collection_acron=instance.acron3, + # headers=headers, + ) ) - ) transaction.on_commit(_on_commit) \ No newline at end of file diff --git a/config/settings/base.py b/config/settings/base.py index 3313875e..f8b4cd7d 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -594,3 +594,4 @@ # ENDPOINT PARA ATUALIZAÇÃO DOS DADOS DE COLEÇÃO NO OPAC_5 ENDPOINT_COLLECTION = env.str("ENDPOINT_COLLECTION", default="/api/v1/update_collection/") +ACTIVATE_UPDATE_COLLECTION_WEBHOOK = env.bool("ACTIVATE_UPDATE_COLLECTION_WEBHOOK", default=False) \ No newline at end of file From 5bb31a7ba43270aa047e8ba90f04b44c73f42618 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 16:14:07 -0300 Subject: [PATCH 15/32] remove event em build_collection_webhook --- collection/tasks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/collection/tasks.py b/collection/tasks.py index 2cda7b74..14c9ad97 100644 --- a/collection/tasks.py +++ b/collection/tasks.py @@ -88,7 +88,6 @@ def build_collection_webhook(event, collection_acron, headers=None): if not collection.domain: return None serializer = CollectionSerializer(collection) - event = "collection.created" if event else "collection.updated" payload = { "event": event, "results": serializer.data, From 4e5e7b41165e69a1b37251fe41249fb570b9c217 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Thu, 25 Sep 2025 22:02:34 -0300 Subject: [PATCH 16/32] remove ip fixo --- collection/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collection/tasks.py b/collection/tasks.py index 14c9ad97..c3605025 100644 --- a/collection/tasks.py +++ b/collection/tasks.py @@ -47,7 +47,7 @@ def fetch_with_schema_guess(host_or_url, timeout=10): def _send_payload(url, headers, payload): url_with_schema = fetch_with_schema_guess(url) - pattern_url = "http://172.23.0.1:8000" + settings.ENDPOINT_COLLECTION + pattern_url = url_with_schema + settings.ENDPOINT_COLLECTION try: resp = requests.post(pattern_url, json=payload, headers=headers, timeout=5) From 37c06aa6fbb534237863b3cff912c415ee4aa18d Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Mon, 6 Oct 2025 15:34:34 -0300 Subject: [PATCH 17/32] remove value JWT_PRIVATE_KEY_PATH --- .envs/.local/.jwt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.envs/.local/.jwt b/.envs/.local/.jwt index 845f753c..86d5da92 100644 --- a/.envs/.local/.jwt +++ b/.envs/.local/.jwt @@ -3,4 +3,4 @@ JWT_AUD="seu-flask-servico" JWT_EXP_SECONDS=600 JWT_ALG="RS256" # JWT_PRIVATE_KEY JA CORRESPONDENTE AO jwt_public OPAC_5 PARA TESTES. -JWT_PRIVATE_KEY_PATH="/app/jwt_private_local.pem" \ No newline at end of file +JWT_PRIVATE_KEY_PATH= \ No newline at end of file From f79d60dd40788f8eb573f13759b5f269a4072fd1 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Mon, 6 Oct 2025 15:45:32 -0300 Subject: [PATCH 18/32] =?UTF-8?q?Cria=20fun=C3=A7=C3=A3o=20para=20ler=20ch?= =?UTF-8?q?ave=20privada?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings/base.py | 6 +----- core/utils/jwt.py | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index f8b4cd7d..333add13 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -581,12 +581,8 @@ PROFILING_LOG_ALL = env.bool('DJANGO_PROFILING_LOG_ALL', default=True) -# JWT para dados de colleção no opac_5 +# JWT para dados de coleção no opac_5 JWT_PRIVATE_KEY_PATH = env.str("JWT_PRIVATE_KEY_PATH", default="/app/jwt_private.pem") - -with open(JWT_PRIVATE_KEY_PATH, "rb") as f: - JWT_PRIVATE_KEY = f.read() - JWT_ALG = "RS256" JWT_ISS = env.str("JWT_ISS", default="https://api.seu-django.com") JWT_AUD = env.str("JWT_AUD", default="seu-flask-servico") diff --git a/core/utils/jwt.py b/core/utils/jwt.py index 671df21d..fb4c25c7 100644 --- a/core/utils/jwt.py +++ b/core/utils/jwt.py @@ -1,4 +1,6 @@ +import logging import time + import jwt from django.conf import settings @@ -16,8 +18,10 @@ def issue_jwt_for_flask(sub="service:django", claims=None): if claims: payload.update(claims) - with open(settings.JWT_PRIVATE_KEY_PATH, "rb") as f: - private_key = f.read() + private_key = get_jwt_private_key() + + if not private_key: + return None token = jwt.encode( payload, @@ -25,4 +29,12 @@ def issue_jwt_for_flask(sub="service:django", claims=None): algorithm="RS256", headers={"alg": "RS256", "typ": "JWT"}, ) - return token \ No newline at end of file + return token + +def get_jwt_private_key(): + try: + with open(settings.JWT_PRIVATE_KEY_PATH, "rb") as f: + return f.read() + except (FileNotFoundError, IOError): + logging.error("JWT_PRIVATE_KEY_PATH not found") + return None From cdffec6a7d6ddacb59b1b1ce92d1e34909a16727 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Mon, 6 Oct 2025 15:46:15 -0300 Subject: [PATCH 19/32] muda metodo post para get em fetch_with_schema_guess --- collection/tasks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/collection/tasks.py b/collection/tasks.py index c3605025..2bfbdf68 100644 --- a/collection/tasks.py +++ b/collection/tasks.py @@ -36,7 +36,7 @@ def fetch_with_schema_guess(host_or_url, timeout=10): for schema in ["http", "https"]: url = f"{schema}://{host_or_url}" try: - resp = requests.post(url, timeout=timeout) + resp = requests.get(url, timeout=timeout) resp.raise_for_status() return url except requests.exceptions.SSLError: @@ -84,9 +84,11 @@ def build_collection_webhook_for_all(event=False, headers=None): @celery_app.task def build_collection_webhook(event, collection_acron, headers=None): - collection = Collection.objects.get(acron3=collection_acron) + collection = Collection.objects.get(acron3=collection_acron, is_active=True) + if not collection.domain: return None + serializer = CollectionSerializer(collection) payload = { "event": event, From c76f5ed859a010468fd04778599be3b2e608e256 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Mon, 6 Oct 2025 15:47:27 -0300 Subject: [PATCH 20/32] Muda nome da funcao fetch_with_schema_guess para fetch_with_protocol_guess --- collection/tasks.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/collection/tasks.py b/collection/tasks.py index 2bfbdf68..31fff29c 100644 --- a/collection/tasks.py +++ b/collection/tasks.py @@ -25,16 +25,16 @@ def task_load_collections(self, user_id=None, username=None): Collection.load(user) -def fetch_with_schema_guess(host_or_url, timeout=10): +def fetch_with_protocol_guess(host_or_url, timeout=10): """ - Algumas coleções não possuem o schema no domínio, por isso é necessário - tentar os schemas http e https para obter o resultado correto. + Algumas coleções não possuem o protocolo no domínio, por isso é necessário + tentar os protocolos http e https para obter o resultado correto. """ if "://" in host_or_url: return host_or_url - for schema in ["http", "https"]: - url = f"{schema}://{host_or_url}" + for protocol in ["http", "https"]: + url = f"{protocol}://{host_or_url}" try: resp = requests.get(url, timeout=timeout) resp.raise_for_status() @@ -46,7 +46,7 @@ def fetch_with_schema_guess(host_or_url, timeout=10): def _send_payload(url, headers, payload): - url_with_schema = fetch_with_schema_guess(url) + url_with_schema = fetch_with_protocol_guess(url) pattern_url = url_with_schema + settings.ENDPOINT_COLLECTION try: From cdee2ec44a81fc082c4dd2355afc9d5e15e60b8f Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Mon, 6 Oct 2025 15:49:48 -0300 Subject: [PATCH 21/32] Move logging.error para handler exception --- collection/tasks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/collection/tasks.py b/collection/tasks.py index 31fff29c..dda0a280 100644 --- a/collection/tasks.py +++ b/collection/tasks.py @@ -51,11 +51,10 @@ def _send_payload(url, headers, payload): try: resp = requests.post(pattern_url, json=payload, headers=headers, timeout=5) - if resp.status_code != 200: - logging.error(f"Erro ao enviar dados de coleção para {url}. Status: {resp.status_code}. Body: {resp.text}") resp.raise_for_status() return resp.json() except Exception as e: + logging.error(f"Erro ao enviar dados de coleção para {url}. Status: {resp.status_code}. Body: {resp.text}") exc_type, exc_value, exc_traceback = sys.exc_info() UnexpectedEvent.create( exception=e, From a9a4b3e1f850211a65caf10256ccce177ede1d5b Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Mon, 6 Oct 2025 15:51:58 -0300 Subject: [PATCH 22/32] remove span duplicado --- core/templates/home/scieloorg/header.html | 1 - 1 file changed, 1 deletion(-) diff --git a/core/templates/home/scieloorg/header.html b/core/templates/home/scieloorg/header.html index 920d1192..abd9a1ed 100644 --- a/core/templates/home/scieloorg/header.html +++ b/core/templates/home/scieloorg/header.html @@ -92,7 +92,6 @@ {% trans 'Sobre o SciELO' %} {% endif %} - From e6e6f50ab838c59b8fd85f623d2f9ffd0d68eb51 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Mon, 6 Oct 2025 16:04:29 -0300 Subject: [PATCH 23/32] remove self.size em BaseLogo.__str__ --- core/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/models.py b/core/models.py index df3d49a7..dd0c8fa8 100755 --- a/core/models.py +++ b/core/models.py @@ -635,7 +635,7 @@ class Meta: ordering = ['sort_order', 'language'] def __str__(self): - return f"{self.collection} - {self.language} ({self.size})" + return f"{self.collection} - {self.language}" ICON_MAP = { From 36bdc838e9a4853a5c1335f36fdd596056277def Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Tue, 28 Oct 2025 00:52:06 -0300 Subject: [PATCH 24/32] refactor get_logo_url --- collection/api/v1/serializers.py | 10 ++++++---- core/utils/utils.py | 15 ++++++++++++++- organization/api/v1/serializers.py | 7 ++++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/collection/api/v1/serializers.py b/collection/api/v1/serializers.py index 0be55797..5009e980 100644 --- a/collection/api/v1/serializers.py +++ b/collection/api/v1/serializers.py @@ -1,8 +1,10 @@ from rest_framework import serializers +from wagtail.models.sites import Site + from collection import models from core.api.v1.serializers import LanguageSerializer +from core.utils.utils import get_hostname from organization.api.v1.serializers import OrganizationSerializer -from wagtail.models.sites import Site class CollectionNameSerializer(serializers.ModelSerializer): @@ -59,9 +61,9 @@ class Meta: def get_logo_url(self, obj): if obj.logo: - domain = Site.objects.get(is_default_site=True).hostname - domain = f"http://{domain}" - return f"{domain}{obj.logo.file.url}" + domain = get_hostname() + if domain: + return f"{domain}{obj.logo.url}" return None diff --git a/core/utils/utils.py b/core/utils/utils.py index 71e754f8..22752111 100644 --- a/core/utils/utils.py +++ b/core/utils/utils.py @@ -10,7 +10,7 @@ stop_after_attempt, wait_exponential, ) -from urllib3.util import Retry +from wagtail.models.sites import Site from config.settings.base import FETCH_DATA_TIMEOUT @@ -113,3 +113,16 @@ def formated_date_api_params(query_params): except (ValueError, AttributeError): continue return formated_date + + +def get_default_site(): + try: + return Site.objects.get(is_default_site=True) + except Site.DoesNotExist: + return None + +def get_hostname(): + default_site = get_default_site() + if not default_site: + return None + return f"http://{default_site.hostname}" diff --git a/organization/api/v1/serializers.py b/organization/api/v1/serializers.py index 6266b7e8..b2708c26 100644 --- a/organization/api/v1/serializers.py +++ b/organization/api/v1/serializers.py @@ -1,6 +1,7 @@ from rest_framework import serializers from organization import models from wagtail.models.sites import Site +from core.utils.utils import get_hostname class OrganizationSerializer(serializers.ModelSerializer): location = serializers.SerializerMethodField() @@ -8,9 +9,9 @@ class OrganizationSerializer(serializers.ModelSerializer): def get_logo_url(self, obj): if obj.logo: - domain = Site.objects.get(is_default_site=True).hostname - domain = f"http://{domain}" - return f"{domain}{obj.logo.url}" + domain = get_hostname() + if domain: + return f"{domain}{obj.logo.url}" return None def get_location(self, obj): From 3e8c2a960cac93a16bcbde20b10e18e7d233fa74 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Tue, 28 Oct 2025 01:02:33 -0300 Subject: [PATCH 25/32] Refactor get_logo_url --- collection/api/v1/serializers.py | 6 ++---- core/utils/utils.py | 14 ++++++++++++++ journal/api/v1/serializers.py | 5 ++--- organization/api/v1/serializers.py | 7 +++---- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/collection/api/v1/serializers.py b/collection/api/v1/serializers.py index 5009e980..ceb982e5 100644 --- a/collection/api/v1/serializers.py +++ b/collection/api/v1/serializers.py @@ -3,7 +3,7 @@ from collection import models from core.api.v1.serializers import LanguageSerializer -from core.utils.utils import get_hostname +from core.utils.utils import get_url_file from organization.api.v1.serializers import OrganizationSerializer @@ -61,9 +61,7 @@ class Meta: def get_logo_url(self, obj): if obj.logo: - domain = get_hostname() - if domain: - return f"{domain}{obj.logo.url}" + return get_url_file(obj.logo) return None diff --git a/core/utils/utils.py b/core/utils/utils.py index 22752111..fac9f7e8 100644 --- a/core/utils/utils.py +++ b/core/utils/utils.py @@ -126,3 +126,17 @@ def get_hostname(): if not default_site: return None return f"http://{default_site.hostname}" + + +def get_url_file(file): + """ + Return the URL of the file. + Parameters: + file: File object + Returns: + URL of the file + """ + domain = get_hostname() + if domain: + return f"{domain}{file.file.url}" + return None \ No newline at end of file diff --git a/journal/api/v1/serializers.py b/journal/api/v1/serializers.py index b19c0b5d..e1e2abe1 100644 --- a/journal/api/v1/serializers.py +++ b/journal/api/v1/serializers.py @@ -3,6 +3,7 @@ from core.api.v1.serializers import LanguageSerializer from journal import models +from core.utils.utils import get_url_file class OfficialJournalSerializer(serializers.ModelSerializer): @@ -165,9 +166,7 @@ def get_title_in_database(self, obj): def get_url_logo(self, obj): if obj.logo: - domain = Site.objects.get(is_default_site=True).hostname - domain = f"http://{domain}" - return f"{domain}{obj.logo.file.url}" + return get_url_file(obj.logo) return None def get_email(self, obj): diff --git a/organization/api/v1/serializers.py b/organization/api/v1/serializers.py index b2708c26..8b404b65 100644 --- a/organization/api/v1/serializers.py +++ b/organization/api/v1/serializers.py @@ -1,7 +1,8 @@ from rest_framework import serializers from organization import models from wagtail.models.sites import Site -from core.utils.utils import get_hostname +from core.utils.utils import get_url_file + class OrganizationSerializer(serializers.ModelSerializer): location = serializers.SerializerMethodField() @@ -9,9 +10,7 @@ class OrganizationSerializer(serializers.ModelSerializer): def get_logo_url(self, obj): if obj.logo: - domain = get_hostname() - if domain: - return f"{domain}{obj.logo.url}" + return get_url_file(obj.logo) return None def get_location(self, obj): From 24640cfee3ddcdf76e5abca13a282e76bee30b80 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Tue, 28 Oct 2025 01:07:19 -0300 Subject: [PATCH 26/32] =?UTF-8?q?fix:=20corrige=20valida=C3=A7=C3=B5es=20e?= =?UTF-8?q?=20chamadas=20duplicadas=20em=20webhooks=20de=20cole=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- collection/tasks.py | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/collection/tasks.py b/collection/tasks.py index dda0a280..b7a86f6f 100644 --- a/collection/tasks.py +++ b/collection/tasks.py @@ -43,18 +43,19 @@ def fetch_with_protocol_guess(host_or_url, timeout=10): continue except requests.exceptions.RequestException: continue - + return None def _send_payload(url, headers, payload): - url_with_schema = fetch_with_protocol_guess(url) - pattern_url = url_with_schema + settings.ENDPOINT_COLLECTION - + resp = None try: - resp = requests.post(pattern_url, json=payload, headers=headers, timeout=5) + resp = requests.post(url, json=payload, headers=headers, timeout=5) resp.raise_for_status() return resp.json() except Exception as e: - logging.error(f"Erro ao enviar dados de coleção para {url}. Status: {resp.status_code}. Body: {resp.text}") + if resp is not None: + logging.error(f"Erro ao enviar dados de coleção para {url}. Status: {resp.status_code}. Body: {resp.text}") + else: + logging.error(f"Erro ao enviar dados de coleção para {url}. Exception {e}") exc_type, exc_value, exc_traceback = sys.exc_info() UnexpectedEvent.create( exception=e, @@ -64,7 +65,6 @@ def _send_payload(url, headers, payload): "url": url, "payload": payload, "headers": headers, - "pattern_url": pattern_url, }, ) @@ -84,24 +84,28 @@ def build_collection_webhook_for_all(event=False, headers=None): @celery_app.task def build_collection_webhook(event, collection_acron, headers=None): collection = Collection.objects.get(acron3=collection_acron, is_active=True) + url_with_schema = fetch_with_protocol_guess(collection.domain) - if not collection.domain: + if not url_with_schema: return None + pattern_url = url_with_schema + settings.ENDPOINT_COLLECTION + serializer = CollectionSerializer(collection) payload = { "event": event, "results": serializer.data, } - token = issue_jwt_for_flask( - sub="service:django", - claims={"roles": ["m2m"], "scope": "ingest:write"} - ) - - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } - _send_payload(url=collection.domain, headers=headers, payload=payload) + + if not headers: + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + token = issue_jwt_for_flask( + sub="service:django", + claims={"roles": ["m2m"], "scope": "ingest:write"} + ) + _send_payload(url=pattern_url, headers=headers, payload=payload) From 22344ae3e021c5819a9dbcf3ad001c25ab6202a8 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Tue, 28 Oct 2025 01:21:53 -0300 Subject: [PATCH 27/32] organiza e remove imports --- pid_provider/models.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pid_provider/models.py b/pid_provider/models.py index d7d845ba..2bbfe7da 100644 --- a/pid_provider/models.py +++ b/pid_provider/models.py @@ -1,10 +1,8 @@ -import json import logging import os import sys -import traceback from datetime import datetime -from functools import lru_cache, cached_property +from functools import cached_property from zlib import crc32 from django.core.files.base import ContentFile @@ -16,8 +14,6 @@ from packtools.sps.pid_provider import v3_gen, xml_sps_adapter from packtools.sps.pid_provider.xml_sps_lib import XMLWithPre from wagtail.admin.panels import FieldPanel, InlinePanel, ObjectList, TabbedInterface -from wagtail.fields import RichTextField -from wagtail.models import Orderable from wagtailautocomplete.edit_handlers import AutocompletePanel from collection.models import Collection @@ -32,14 +28,10 @@ from core.utils.similarity import is_similar from pid_provider import choices, exceptions from pid_provider.query_params import ( - get_article_q_expression, - get_issue_kwargs, - get_journal_q_expression, - get_valid_query_parameters, zero_to_none, QueryBuilderPidProviderXML, ) -from tracker.models import BaseEvent, EventSaveError, UnexpectedEvent +from tracker.models import UnexpectedEvent try: from django_prometheus.models import ExportModelOperationsMixin From cd1e4cdf278d5aa006f1a950d58c325eed4f39cb Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Tue, 28 Oct 2025 01:22:04 -0300 Subject: [PATCH 28/32] migration merge --- collection/migrations/0008_merge_20251028_0412.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 collection/migrations/0008_merge_20251028_0412.py diff --git a/collection/migrations/0008_merge_20251028_0412.py b/collection/migrations/0008_merge_20251028_0412.py new file mode 100644 index 00000000..3b738628 --- /dev/null +++ b/collection/migrations/0008_merge_20251028_0412.py @@ -0,0 +1,13 @@ +# Generated by Django 5.2.3 on 2025-10-28 04:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("collection", "0007_alter_collectionexecutingorganization_options_and_more"), + ("collection", "0007_collection_platform_status"), + ] + + operations = [] From b5091f48af642ffd5b0241dc457e68a5f7edc1cd Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Tue, 28 Oct 2025 23:48:00 -0300 Subject: [PATCH 29/32] Transforma leitura da chave privada em variavel de ambiente --- config/settings/base.py | 2 +- core/utils/jwt.py | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index 45dff4e7..d299bf5c 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -581,7 +581,7 @@ PROFILING_LOG_ALL = env.bool('DJANGO_PROFILING_LOG_ALL', default=True) -JWT_PRIVATE_KEY_PATH = env.str("JWT_PRIVATE_KEY_PATH", default="/app/jwt_private.pem") +JWT_PRIVATE_KEY = env.str("JWT_PRIVATE_KEY", default=None) JWT_ALG = "RS256" JWT_ISS = env.str("JWT_ISS", default="https://api.seu-django.com") JWT_AUD = env.str("JWT_AUD", default="seu-flask-servico") diff --git a/core/utils/jwt.py b/core/utils/jwt.py index fb4c25c7..31639bcd 100644 --- a/core/utils/jwt.py +++ b/core/utils/jwt.py @@ -5,6 +5,19 @@ from django.conf import settings +def get_jwt_private_key(): + key = settings.JWT_PRIVATE_KEY + if not key: + logging.error("JWT_PRIVATE_KEY not found") + return None + if isinstance(key, bytes): + return key + if isinstance(key, str): + return key.encode() + logging.error("JWT_PRIVATE_KEY is not bytes or str") + return None + + def issue_jwt_for_flask(sub="service:django", claims=None): now = int(time.time()) payload = { @@ -30,11 +43,3 @@ def issue_jwt_for_flask(sub="service:django", claims=None): headers={"alg": "RS256", "typ": "JWT"}, ) return token - -def get_jwt_private_key(): - try: - with open(settings.JWT_PRIVATE_KEY_PATH, "rb") as f: - return f.read() - except (FileNotFoundError, IOError): - logging.error("JWT_PRIVATE_KEY_PATH not found") - return None From e530db298a68c8cda90e784441072e21832d72ee Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Mon, 24 Nov 2025 11:12:05 -0300 Subject: [PATCH 30/32] =?UTF-8?q?Add=20valida=C3=A7=C3=A3o=20de=20tamanho?= =?UTF-8?q?=20para=20logos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- collection/models.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/collection/models.py b/collection/models.py index 3a6d7837..15e34b96 100755 --- a/collection/models.py +++ b/collection/models.py @@ -7,6 +7,7 @@ from wagtail.admin.panels import FieldPanel, InlinePanel, ObjectList, TabbedInterface from wagtail.models import Orderable from wagtailautocomplete.edit_handlers import AutocompletePanel +from django.core.exceptions import ValidationError from core.forms import CoreAdminModelForm from core.models import ( @@ -419,5 +420,44 @@ class Meta: ('collection', 'language', 'purpose'), ] + def clean(self): + validation_logo_dimensions = { + 'homepage': { + 'max_width': 200, + 'max_height': 100, + }, + 'header': { + 'max_width': 200, + 'max_height': 100, + }, + 'logo_drop_menu': { + 'max_width': 200, + 'max_height': 100, + }, + 'footer': { + 'max_width': 200, + 'max_height': 100, + }, + 'menu': { + 'max_width': 200, + 'max_height': 100, + }, + } + if self.logo: + self.validate_image_dimensions(self.logo, validation_logo_dimensions[self.purpose]) + + @staticmethod + def validate_image_dimensions(image, validation_logo_dimensions): + width = image.width + height = image.height + max_width = validation_logo_dimensions['max_width'] + max_height = validation_logo_dimensions['max_height'] + if width < max_width or height > max_height: + raise ValidationError({ + 'logo': _(f'Image dimensions ({width}x{height}px) exceed the maximum allowed size. ' + f' Width must be ≤ {max_width}px and height must be ≤ {max_height}px. ' + ) + }) + def __str__(self): return f"{self.collection} - {self.language} ({self.purpose})" From 627ab2cedf671e6ad974276f7fd6719d987f0b48 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Mon, 24 Nov 2025 11:12:13 -0300 Subject: [PATCH 31/32] Fix build_collection_webhook --- collection/tasks.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/collection/tasks.py b/collection/tasks.py index b7a86f6f..b5d9641a 100644 --- a/collection/tasks.py +++ b/collection/tasks.py @@ -98,14 +98,15 @@ def build_collection_webhook(event, collection_acron, headers=None): } if not headers: - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } token = issue_jwt_for_flask( sub="service:django", claims={"roles": ["m2m"], "scope": "ingest:write"} ) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + _send_payload(url=pattern_url, headers=headers, payload=payload) From d43f61d7029e4ba55431922e1978632b31e19599 Mon Sep 17 00:00:00 2001 From: Samuel Veiga Rangel Date: Mon, 24 Nov 2025 11:12:48 -0300 Subject: [PATCH 32/32] =?UTF-8?q?Cria=20fun=C3=A7=C3=B5es=20para=20obter?= =?UTF-8?q?=20url=20de=20Imagefield=20e=20wagtail.images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- collection/api/v1/serializers.py | 4 ++-- core/utils/utils.py | 20 ++++++++++++++++++-- journal/api/v1/serializers.py | 4 ++-- organization/api/v1/serializers.py | 4 ++-- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/collection/api/v1/serializers.py b/collection/api/v1/serializers.py index ceb982e5..25027d27 100644 --- a/collection/api/v1/serializers.py +++ b/collection/api/v1/serializers.py @@ -3,7 +3,7 @@ from collection import models from core.api.v1.serializers import LanguageSerializer -from core.utils.utils import get_url_file +from core.utils.utils import get_url_file_from_wagtail_images from organization.api.v1.serializers import OrganizationSerializer @@ -61,7 +61,7 @@ class Meta: def get_logo_url(self, obj): if obj.logo: - return get_url_file(obj.logo) + return get_url_file_from_wagtail_images(obj.logo) return None diff --git a/core/utils/utils.py b/core/utils/utils.py index fac9f7e8..0eaabe23 100644 --- a/core/utils/utils.py +++ b/core/utils/utils.py @@ -128,15 +128,31 @@ def get_hostname(): return f"http://{default_site.hostname}" -def get_url_file(file): +def get_url_file_from_wagtail_images(file): """ - Return the URL of the file. + Return the url of the file from Wagtail ImageField Parameters: file: File object Returns: URL of the file """ domain = get_hostname() + domain = "http://172.20.0.1:8009" if domain: return f"{domain}{file.file.url}" + return None + + +def get_url_file_from_image_field(file): + """ + Return the url of the file from ImageField + Parameters: + file: ImageField object + Returns: + URL of the file + """ + domain = get_hostname() + domain = "http://172.20.0.1:8009" + if domain: + return f"{domain}{file.url}" return None \ No newline at end of file diff --git a/journal/api/v1/serializers.py b/journal/api/v1/serializers.py index e1e2abe1..3f1317c5 100644 --- a/journal/api/v1/serializers.py +++ b/journal/api/v1/serializers.py @@ -3,7 +3,7 @@ from core.api.v1.serializers import LanguageSerializer from journal import models -from core.utils.utils import get_url_file +from core.utils.utils import get_url_file_from_wagtail_images class OfficialJournalSerializer(serializers.ModelSerializer): @@ -166,7 +166,7 @@ def get_title_in_database(self, obj): def get_url_logo(self, obj): if obj.logo: - return get_url_file(obj.logo) + return get_url_file_from_wagtail_images(obj.logo) return None def get_email(self, obj): diff --git a/organization/api/v1/serializers.py b/organization/api/v1/serializers.py index 8b404b65..8074821c 100644 --- a/organization/api/v1/serializers.py +++ b/organization/api/v1/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from organization import models from wagtail.models.sites import Site -from core.utils.utils import get_url_file +from core.utils.utils import get_url_file_from_image_field class OrganizationSerializer(serializers.ModelSerializer): @@ -10,7 +10,7 @@ class OrganizationSerializer(serializers.ModelSerializer): def get_logo_url(self, obj): if obj.logo: - return get_url_file(obj.logo) + return get_url_file_from_image_field(obj.logo) return None def get_location(self, obj):