From f43a013e987e66cc98ed0a1b7168262912a26b10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:02:33 +0000 Subject: [PATCH 1/4] chore(deps): bump pypdf from 6.9.2 to 6.10.0 Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.9.2 to 6.10.0. - [Release notes](https://github.com/py-pdf/pypdf/releases) - [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md) - [Commits](https://github.com/py-pdf/pypdf/compare/6.9.2...6.10.0) --- updated-dependencies: - dependency-name: pypdf dependency-version: 6.10.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 427ba7b85..ec2c2f098 100644 --- a/uv.lock +++ b/uv.lock @@ -2516,11 +2516,11 @@ crypto = [ [[package]] name = "pypdf" -version = "6.9.2" +version = "6.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/83/691bdb309306232362503083cb15777491045dd54f45393a317dc7d8082f/pypdf-6.9.2.tar.gz", hash = "sha256:7f850faf2b0d4ab936582c05da32c52214c2b089d61a316627b5bfb5b0dab46c", size = 5311837, upload-time = "2026-03-23T14:53:27.983Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/9f/ca96abf18683ca12602065e4ed2bec9050b672c87d317f1079abc7b6d993/pypdf-6.10.0.tar.gz", hash = "sha256:4c5a48ba258c37024ec2505f7e8fd858525f5502784a2e1c8d415604af29f6ef", size = 5314833, upload-time = "2026-04-10T09:34:57.102Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/7e/c85f41243086a8fe5d1baeba527cb26a1918158a565932b41e0f7c0b32e9/pypdf-6.9.2-py3-none-any.whl", hash = "sha256:662cf29bcb419a36a1365232449624ab40b7c2d0cfc28e54f42eeecd1fd7e844", size = 333744, upload-time = "2026-03-23T14:53:26.573Z" }, + { url = "https://files.pythonhosted.org/packages/55/f2/7ebe366f633f30a6ad105f650f44f24f98cb1335c4157d21ae47138b3482/pypdf-6.10.0-py3-none-any.whl", hash = "sha256:90005e959e1596c6e6c84c8b0ad383285b3e17011751cedd17f2ce8fcdfc86de", size = 334459, upload-time = "2026-04-10T09:34:54.966Z" }, ] [[package]] From c13906e0361ad3cd373e0caf9eac5ee9348d551d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 00:25:35 +0000 Subject: [PATCH 2/4] chore(deps-dev): bump pytest from 8.3.4 to 9.0.3 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.4 to 9.0.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.4...9.0.3) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.0.3 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- uv.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index ec2c2f098..17fe375aa 100644 --- a/uv.lock +++ b/uv.lock @@ -2534,17 +2534,18 @@ wheels = [ [[package]] name = "pytest" -version = "8.3.4" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919, upload-time = "2024-12-01T12:54:25.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083, upload-time = "2024-12-01T12:54:19.735Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] From 8923dcfe0d01fe6ae9b29ba63c2193507c05b4f3 Mon Sep 17 00:00:00 2001 From: Szabo Zoltan Date: Fri, 17 Apr 2026 13:20:51 +0200 Subject: [PATCH 3/4] Fix N+1 issue in ops-learning and fix test --- api/test_views.py | 12 +++++++++--- assets | 2 +- per/drf_views.py | 24 +++++++++++++++++++++++- per/serializers.py | 36 ++++++++++++++++++++++++------------ 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/api/test_views.py b/api/test_views.py index 0e4fc61c3..9d695410b 100644 --- a/api/test_views.py +++ b/api/test_views.py @@ -22,6 +22,7 @@ from deployments.factories.user import UserFactory from dref.models import DrefFile from main.test_case import APITestCase +from per.factories import OpsLearningFactory class AuthPowerBITest(APITestCase): @@ -144,14 +145,19 @@ def setUp(self): # Create public field reports event_pub = EventFactory.create(visibility=VisibilityChoices.PUBLIC, parent_event=None) - FieldReportFactory.create_batch(4, event=event_pub, visibility=VisibilityChoices.PUBLIC) + self.public_field_report = FieldReportFactory.create(event=event_pub, visibility=VisibilityChoices.PUBLIC) + FieldReportFactory.create_batch(3, event=event_pub, visibility=VisibilityChoices.PUBLIC) # Create non-public field reports event_non_pub = EventFactory.create(visibility=VisibilityChoices.IFRC, parent_event=None) FieldReportFactory.create_batch(5, event=event_non_pub, visibility=VisibilityChoices.IFRC) + self.ops_learning = OpsLearningFactory.create(is_validated=True) + def test_guest_user_permission(self): body = {} id = 1 # NOTE: id is used just to test api that requires id, it doesnot indicate real id. It can be any number. + field_report_id = self.public_field_report.id + ops_learning_id = self.ops_learning.id guest_apis = [ "/api/v2/add_subscription/", @@ -161,12 +167,12 @@ def test_guest_user_permission(self): guest_get_apis = [ "/api/v2/user/me/", "/api/v2/field-report/", - f"/api/v2/field-report/{id}/", + f"/api/v2/field-report/{field_report_id}/", "/api/v2/language/", f"/api/v2/language/{id}/", "/api/v2/event/", "/api/v2/ops-learning/", - f"/api/v2/ops-learning/{id}/", + f"/api/v2/ops-learning/{ops_learning_id}/", ] go_post_apis = [ diff --git a/assets b/assets index 6614f72f5..5600ee76f 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 6614f72f5174333c67f43ca533513972c2581e9c +Subproject commit 5600ee76f61fb69c1dec91feb18907490052145c diff --git a/per/drf_views.py b/per/drf_views.py index 1c54c72b9..e8b7c3a78 100644 --- a/per/drf_views.py +++ b/per/drf_views.py @@ -20,7 +20,7 @@ from rest_framework.response import Response from rest_framework.settings import api_settings -from api.models import Country, Region +from api.models import AppealDocument, Country, Region from deployments.models import SectorTag from main.permissions import DenyGuestUserMutationPermission, DenyGuestUserPermission from main.utils import SpreadSheetContentNegotiation @@ -1202,6 +1202,7 @@ def get_queryset(self): return qs.select_related( "appeal_code", ).prefetch_related( + "appeal_code__event__appeals", "sector", "sector_validated", "organization", @@ -1216,6 +1217,7 @@ def get_queryset(self): "appeal_code", ) .prefetch_related( + "appeal_code__event__appeals", "sector", "sector_validated", "organization", @@ -1226,6 +1228,26 @@ def get_queryset(self): ) ) + def get_serializer_context(self): + context = super().get_serializer_context() + if getattr(self, "swagger_fake_view", False): + return context + view_action = getattr(self, "action", None) + if view_action == "list": + queryset = self.filter_queryset(self.get_queryset()) + appeal_document_ids = list( + queryset.exclude(appeal_document_id__isnull=True).values_list("appeal_document_id", flat=True).distinct() + ) + if appeal_document_ids: + context["appeal_documents_map"] = AppealDocument.objects.in_bulk(appeal_document_ids) + elif view_action == "retrieve": + lookup_kwarg = self.lookup_url_kwarg or self.lookup_field + if lookup_kwarg in self.kwargs: + obj = self.get_object() + if obj.appeal_document_id: + context["appeal_documents_map"] = AppealDocument.objects.in_bulk([obj.appeal_document_id]) + return context + def get_serializer_class(self): if self.request.method == "GET": request_format = self.request.GET.get("format", "json") diff --git a/per/serializers.py b/per/serializers.py index 18b29dc4a..fbade675f 100644 --- a/per/serializers.py +++ b/per/serializers.py @@ -1054,14 +1054,20 @@ class Meta: fields = "__all__" read_only_fields = ("created_at", "modified_at") - @staticmethod - def get_document_url(obj): - if obj.appeal_document_id and (document := AppealDocument.objects.filter(id=obj.appeal_document_id).first()): + def _get_document(self, obj): + if not obj.appeal_document_id: + return None + document_map = self.context.get("appeal_documents_map") + if document_map is not None: + return document_map.get(obj.appeal_document_id) + return AppealDocument.objects.filter(id=obj.appeal_document_id).first() + + def get_document_url(self, obj): + if document := self._get_document(obj): return document.document_url - @staticmethod - def get_document_name(obj): - if obj.appeal_document_id and (document := AppealDocument.objects.filter(id=obj.appeal_document_id).first()): + def get_document_name(self, obj): + if document := self._get_document(obj): return document.name @@ -1088,14 +1094,20 @@ class Meta: ) exclude = ("learning", "type", "organization", "sector", "per_component") - @staticmethod - def get_document_url(obj): - if obj.appeal_document_id and (document := AppealDocument.objects.filter(id=obj.appeal_document_id).first()): + def _get_document(self, obj): + if not obj.appeal_document_id: + return None + document_map = self.context.get("appeal_documents_map") + if document_map is not None: + return document_map.get(obj.appeal_document_id) + return AppealDocument.objects.filter(id=obj.appeal_document_id).first() + + def get_document_url(self, obj): + if document := self._get_document(obj): return document.document_url - @staticmethod - def get_document_name(obj): - if obj.appeal_document_id and (document := AppealDocument.objects.filter(id=obj.appeal_document_id).first()): + def get_document_name(self, obj): + if document := self._get_document(obj): return document.name From 900f8a161b2b422a1861a59d94371ee8d0a29743 Mon Sep 17 00:00:00 2001 From: Szabo Zoltan Date: Fri, 17 Apr 2026 14:34:07 +0200 Subject: [PATCH 4/4] Show year for no-date-in-name events on Admin --- api/admin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/admin.py b/api/admin.py index 47b126344..defad87f3 100644 --- a/api/admin.py +++ b/api/admin.py @@ -222,7 +222,11 @@ class EventAdmin(CompareVersionAdmin, RegionRestrictedAdmin, TranslationAdmin): @admin.display(ordering="ifrc_severity_level_update_date") def level_updated_at(self, obj): - return obj.ifrc_severity_level_update_date + if obj.ifrc_severity_level_update_date: + return obj.ifrc_severity_level_update_date + if obj.created_at and str(obj.created_at.year)[:3] not in obj.name: + return f"{obj.created_at.year}" + return None country_in = "countries__pk__in" region_in = "regions__pk__in"