diff --git a/docker-compose.yml b/docker-compose.yml index 21e531718..09c8614b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ x-server: &base_server_setup build: context: . tags: - - ifrcgo/go-api:latest + - ifrcgo/go-api:latest # To attach to container with stdin `docker attach ` # Used for python debugging. stdin_open: true @@ -23,7 +23,7 @@ x-server: &base_server_setup FRONTEND_URL: ${FRONTEND_URL:-http://localhost:3000} PLAYWRIGHT_SERVER_URL: ${PLAYWRIGHT_SERVER_URL:-ws://playwright:3000/} GO_WEB_INTERNAL_URL: ${GO_WEB_INTERNAL_URL:-http://host.docker.internal:3000} - DJANGO_ADDITIONAL_ALLOWED_HOSTS: ${DJANGO_ADDITIONAL_ALLOWED_HOSTS:-host.docker.internal} + DJANGO_ADDITIONAL_ALLOWED_HOSTS: ${DJANGO_ADDITIONAL_ALLOWED_HOSTS:-host.docker.internal,127.0.0.1,localhost} DEBUG_EMAIL: ${DEBUG_EMAIL:-true} MOLNIX_API_BASE: ${MOLNIX_API_BASE:-https://api.ifrc-staging.rpm.molnix.com/api/} ERP_API_ENDPOINT: ${ERP_API_ENDPOINT:-https://ifrctintapim001.azure-api.net/GoAPI/ExtractGoEmergency} @@ -57,11 +57,11 @@ x-server: &base_server_setup POWERBI_DATASET_IDS: ${POWERBI_DATASET_IDS:-} extra_hosts: - - "host.docker.internal:host-gateway" + - "host.docker.internal:host-gateway" env_file: - .env volumes: - - '.:/home/ifrc/go-api' + - ".:/home/ifrc/go-api" depends_on: - db - redis @@ -76,7 +76,7 @@ services: POSTGRES_USER: test POSTGRES_DB: test volumes: - - './.db/pg-15:/var/lib/postgresql/data' + - "./.db/pg-15:/var/lib/postgresql/data" extra_hosts: - "host.docker.internal:host-gateway" @@ -118,13 +118,13 @@ services: - 9200:9200 kibana: - image: 'docker.elastic.co/kibana/kibana:7.0.0' + image: "docker.elastic.co/kibana/kibana:7.0.0" container_name: kibana environment: SERVER_NAME: kibana.local ELASTICSEARCH_URL: http://elasticsearch:9200 ports: - - '5601:5601' + - "5601:5601" depends_on: - elasticsearch profiles: [elasticsearch] @@ -253,7 +253,6 @@ services: command: python manage.py triggers_to_db profiles: [cli] - volumes: redis-data: elastic-search-data: diff --git a/dref/serializers.py b/dref/serializers.py index 752ac5abd..a304690f5 100644 --- a/dref/serializers.py +++ b/dref/serializers.py @@ -1689,6 +1689,8 @@ class BaseDref3Serializer(serializers.ModelSerializer): people_targeted = serializers.SerializerMethodField() people_assisted = serializers.SerializerMethodField() population_disaggregation = serializers.SerializerMethodField() + + # Sector fields sector_shelter_and_basic_household_items = serializers.SerializerMethodField() sector_shelter_and_basic_household_items_budget = serializers.SerializerMethodField() sector_shelter_and_basic_household_items_people_targeted = serializers.SerializerMethodField() @@ -1731,6 +1733,7 @@ class BaseDref3Serializer(serializers.ModelSerializer): sector_national_society_strengthening = serializers.SerializerMethodField() sector_national_society_strengthening_budget = serializers.SerializerMethodField() sector_national_society_strengthening_people_targeted = serializers.SerializerMethodField() + public = serializers.SerializerMethodField(read_only=True) is_latest_stage = serializers.SerializerMethodField(read_only=True) status = serializers.IntegerField(read_only=True) @@ -1739,8 +1742,61 @@ class BaseDref3Serializer(serializers.ModelSerializer): indicators_id = serializers.SerializerMethodField() link_to_emergency_page = serializers.SerializerMethodField() - # get_id removed: numeric ids are injected post-serialization + # ----------------------------- + # Per-object caches + # ----------------------------- + def _get_cached_list(self, obj, attr_name, qs_fn): + cache_attr = f"_{attr_name}_cache" + if hasattr(obj, cache_attr): + return getattr(obj, cache_attr) + data = list(qs_fn()) + setattr(obj, cache_attr, data) + return data + + def _planned_interventions(self, obj): + # If prefetched, this is memory-only. If not, this is 1 query per obj. + return self._get_cached_list(obj, "planned_interventions", lambda: obj.planned_interventions.all()) + def _districts_list(self, obj): + return self._get_cached_list(obj, "districts", lambda: obj.district.all()) + + def _sector_index(self, obj): + """ + One pass per object. + PlannedIntervention.title -> {"any": bool, "budget": number, "people": number} + """ + cache_attr = "_sector_index_cache" + if hasattr(obj, cache_attr): + return getattr(obj, cache_attr) + + idx = {} + for p in self._planned_interventions(obj): + t = p.title + rec = idx.setdefault(t, {"any": False, "budget": 0, "people": 0}) + rec["any"] = True + rec["budget"] += p.budget or 0 + rec["people"] += p.person_targeted or 0 + + setattr(obj, cache_attr, idx) + return idx + + def _sector_any(self, obj, topic): + return self._sector_index(obj).get(topic, {}).get("any", False) + + def _sector_budget(self, obj, topic): + return self._sector_index(obj).get(topic, {}).get("budget", None) + + def _sector_people(self, obj, topic): + return self._sector_index(obj).get(topic, {}).get("people", None) + + def _appeal_cache(self): + if not hasattr(self, "_appeal_by_code"): + self._appeal_by_code = {} + return self._appeal_by_code + + # ----------------------------- + # Context-driven fields + # ----------------------------- def get_public(self, obj): return self.context.get("public") @@ -1753,6 +1809,9 @@ def get_stage(self, obj): def get_allocation(self, obj): return self.context.get("allocation") + # ----------------------------- + # Simple computed fields + # ----------------------------- def get_pillar(self, obj): return "Anticipatory" if obj.type_of_dref == Dref.DrefType.IMMINENT else "Response" @@ -1767,13 +1826,13 @@ def get_allocation_type(self, obj): return "Loan" if obj.type_of_dref == Dref.DrefType.LOAN else "Grant" def get_districts(self, obj): - return ", ".join(d.name for d in obj.district.all()) + return ", ".join(d.name for d in self._districts_list(obj)) def get_district_codes(self, obj): - return ", ".join(d.code for d in obj.district.all()) + return ", ".join(d.code for d in self._districts_list(obj)) def get_type_of_onset(self, obj): - type_of_onset = obj.type_of_onset if obj.type_of_onset != 0 else 1 # Default to "Slow Onset" if not set + type_of_onset = obj.type_of_onset if obj.type_of_onset != 0 else 1 return Dref.OnsetType(type_of_onset).label def get_crisis_categorization(self, obj): @@ -1800,7 +1859,6 @@ def get_date_of_appeal_request_from_ns(self, obj): return obj.ns_request_date def get_date_of_approval(self, obj): - # if type(obj).__name__ == "Dref" and hasattr(obj, "date_of_approval"): – instead of this, just return for all types if hasattr(obj, "date_of_approval"): return obj.date_of_approval @@ -1822,16 +1880,12 @@ def get_end_date_of_operation(self, obj): return obj.operation_end_date def get_operation_status(self, obj): - """Return 'active' if current date is between start and end date (inclusive), else 'closed'. - Returns None if either boundary date is missing. - """ start = self.get_start_date_of_operation(obj) end = self.get_end_date_of_operation(obj) if not start or not end: return None try: today = timezone.now().date() - # Ensure we are comparing date objects (convert datetimes if present) if hasattr(start, "date") and callable(getattr(start, "date")): start = start.date() if hasattr(end, "date") and callable(getattr(end, "date")): @@ -1844,22 +1898,22 @@ def get_operation_timeframe(self, obj): t = type(obj).__name__ if t == "Dref" and hasattr(obj, "operation_timeframe"): return obj.operation_timeframe - if t != "Dref" and hasattr(obj, "total_operation_timeframe"): # OU + FR: + if t != "Dref" and hasattr(obj, "total_operation_timeframe"): return obj.total_operation_timeframe def get_data_origin(self, obj): - return "DREF process in GO" # Hardcoded for now, later can be also "DREF published report" + return "DREF process in GO" def get_people_affected(self, obj): t = type(obj).__name__ if t == "Dref" and hasattr(obj, "num_affected"): return obj.num_affected - if t != "Dref" and hasattr(obj, "number_of_people_affected"): # OU + FR: + if t != "Dref" and hasattr(obj, "number_of_people_affected"): return obj.number_of_people_affected def get_people_targeted(self, obj): t = type(obj).__name__ - if t != "DrefOperationalUpdate" and hasattr(obj, "total_targeted_population"): # A + FR: + if t != "DrefOperationalUpdate" and hasattr(obj, "total_targeted_population"): return obj.total_targeted_population if t == "DrefOperationalUpdate" and hasattr(obj, "number_of_people_targeted"): return obj.number_of_people_targeted @@ -1894,7 +1948,6 @@ def pct(val): if val is None: return None try: - # Keep as int if float-ish, then append % return f"{int(val)}%" except (ValueError, TypeError): return None @@ -1919,213 +1972,161 @@ def pct(val): return data or None + # ----------------------------- + # Sector fields (O(1) lookups after one pass) + # ----------------------------- def get_sector_shelter_and_basic_household_items(self, obj): - topic = PlannedIntervention.Title.SHELTER_HOUSING_AND_SETTLEMENTS - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.SHELTER_HOUSING_AND_SETTLEMENTS) def get_sector_shelter_and_basic_household_items_budget(self, obj): - topic = PlannedIntervention.Title.SHELTER_HOUSING_AND_SETTLEMENTS - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.SHELTER_HOUSING_AND_SETTLEMENTS) def get_sector_shelter_and_basic_household_items_people_targeted(self, obj): - topic = PlannedIntervention.Title.SHELTER_HOUSING_AND_SETTLEMENTS - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.SHELTER_HOUSING_AND_SETTLEMENTS) def get_sector_livelihoods(self, obj): - topic = PlannedIntervention.Title.LIVELIHOODS_AND_BASIC_NEEDS - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.LIVELIHOODS_AND_BASIC_NEEDS) def get_sector_livelihoods_budget(self, obj): - topic = PlannedIntervention.Title.LIVELIHOODS_AND_BASIC_NEEDS - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.LIVELIHOODS_AND_BASIC_NEEDS) def get_sector_livelihoods_people_targeted(self, obj): - topic = PlannedIntervention.Title.LIVELIHOODS_AND_BASIC_NEEDS - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.LIVELIHOODS_AND_BASIC_NEEDS) def get_sector_multi_purpose_cash_grants(self, obj): - topic = PlannedIntervention.Title.MULTI_PURPOSE_CASH - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.MULTI_PURPOSE_CASH) def get_sector_multi_purpose_cash_grants_budget(self, obj): - topic = PlannedIntervention.Title.MULTI_PURPOSE_CASH - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.MULTI_PURPOSE_CASH) def get_sector_multi_purpose_cash_grants_people_targeted(self, obj): - topic = PlannedIntervention.Title.MULTI_PURPOSE_CASH - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.MULTI_PURPOSE_CASH) def get_sector_health(self, obj): - topic = PlannedIntervention.Title.HEALTH - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.HEALTH) def get_sector_health_budget(self, obj): - topic = PlannedIntervention.Title.HEALTH - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.HEALTH) def get_sector_health_people_targeted(self, obj): - topic = PlannedIntervention.Title.HEALTH - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.HEALTH) def get_sector_water_sanitation_and_hygiene(self, obj): - topic = PlannedIntervention.Title.WATER_SANITATION_AND_HYGIENE - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.WATER_SANITATION_AND_HYGIENE) def get_sector_water_sanitation_and_hygiene_budget(self, obj): - topic = PlannedIntervention.Title.WATER_SANITATION_AND_HYGIENE - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.WATER_SANITATION_AND_HYGIENE) def get_sector_water_sanitation_and_hygiene_people_targeted(self, obj): - topic = PlannedIntervention.Title.WATER_SANITATION_AND_HYGIENE - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.WATER_SANITATION_AND_HYGIENE) def get_sector_protection_gender_and_inclusion(self, obj): - topic = PlannedIntervention.Title.PROTECTION_GENDER_AND_INCLUSION - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.PROTECTION_GENDER_AND_INCLUSION) def get_sector_protection_gender_and_inclusion_budget(self, obj): - topic = PlannedIntervention.Title.PROTECTION_GENDER_AND_INCLUSION - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.PROTECTION_GENDER_AND_INCLUSION) def get_sector_protection_gender_and_inclusion_people_targeted(self, obj): - topic = PlannedIntervention.Title.PROTECTION_GENDER_AND_INCLUSION - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.PROTECTION_GENDER_AND_INCLUSION) def get_sector_education(self, obj): - topic = PlannedIntervention.Title.EDUCATION - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.EDUCATION) def get_sector_education_budget(self, obj): - topic = PlannedIntervention.Title.EDUCATION - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.EDUCATION) def get_sector_education_people_targeted(self, obj): - topic = PlannedIntervention.Title.EDUCATION - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.EDUCATION) def get_sector_migration_and_displacement(self, obj): - topic = PlannedIntervention.Title.MIGRATION_AND_DISPLACEMENT - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.MIGRATION_AND_DISPLACEMENT) def get_sector_migration_and_displacement_budget(self, obj): - topic = PlannedIntervention.Title.MIGRATION_AND_DISPLACEMENT - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.MIGRATION_AND_DISPLACEMENT) def get_sector_migration_and_displacement_people_targeted(self, obj): - topic = PlannedIntervention.Title.MIGRATION_AND_DISPLACEMENT - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.MIGRATION_AND_DISPLACEMENT) def get_sector_risk_reduction_climate_adaptation_and_recovery(self, obj): - topic = PlannedIntervention.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY) def get_sector_risk_reduction_climate_adaptation_and_recovery_budget(self, obj): - topic = PlannedIntervention.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY) def get_sector_risk_reduction_climate_adaptation_and_recovery_people_targeted(self, obj): - topic = PlannedIntervention.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY) def get_sector_community_engagement_and_accountability(self, obj): - topic = PlannedIntervention.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY) def get_sector_community_engagement_and_accountability_budget(self, obj): - topic = PlannedIntervention.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY) def get_sector_community_engagement_and_accountability_people_targeted(self, obj): - topic = PlannedIntervention.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY) def get_sector_environmental_sustainability(self, obj): - topic = PlannedIntervention.Title.ENVIRONMENTAL_SUSTAINABILITY - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.ENVIRONMENTAL_SUSTAINABILITY) def get_sector_environmental_sustainability_budget(self, obj): - topic = PlannedIntervention.Title.ENVIRONMENTAL_SUSTAINABILITY - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.ENVIRONMENTAL_SUSTAINABILITY) def get_sector_environmental_sustainability_people_targeted(self, obj): - topic = PlannedIntervention.Title.ENVIRONMENTAL_SUSTAINABILITY - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.ENVIRONMENTAL_SUSTAINABILITY) def get_sector_coordination_and_partnerships(self, obj): - topic = PlannedIntervention.Title.COORDINATION_AND_PARTNERSHIPS - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.COORDINATION_AND_PARTNERSHIPS) def get_sector_coordination_and_partnerships_budget(self, obj): - topic = PlannedIntervention.Title.COORDINATION_AND_PARTNERSHIPS - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.COORDINATION_AND_PARTNERSHIPS) def get_sector_coordination_and_partnerships_people_targeted(self, obj): - topic = PlannedIntervention.Title.COORDINATION_AND_PARTNERSHIPS - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.COORDINATION_AND_PARTNERSHIPS) def get_sector_secretariat_services(self, obj): - topic = PlannedIntervention.Title.SECRETARIAT_SERVICES - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.SECRETARIAT_SERVICES) def get_sector_secretariat_services_budget(self, obj): - topic = PlannedIntervention.Title.SECRETARIAT_SERVICES - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.SECRETARIAT_SERVICES) def get_sector_secretariat_services_people_targeted(self, obj): - topic = PlannedIntervention.Title.SECRETARIAT_SERVICES - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.SECRETARIAT_SERVICES) def get_sector_national_society_strengthening(self, obj): - topic = PlannedIntervention.Title.NATIONAL_SOCIETY_STRENGTHENING - return True if any(p.title == topic for p in obj.planned_interventions.all()) else False + return self._sector_any(obj, PlannedIntervention.Title.NATIONAL_SOCIETY_STRENGTHENING) def get_sector_national_society_strengthening_budget(self, obj): - topic = PlannedIntervention.Title.NATIONAL_SOCIETY_STRENGTHENING - if obj.planned_interventions.count(): - return sum([(p.budget or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_budget(obj, PlannedIntervention.Title.NATIONAL_SOCIETY_STRENGTHENING) def get_sector_national_society_strengthening_people_targeted(self, obj): - topic = PlannedIntervention.Title.NATIONAL_SOCIETY_STRENGTHENING - if obj.planned_interventions.count(): - return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) + return self._sector_people(obj, PlannedIntervention.Title.NATIONAL_SOCIETY_STRENGTHENING) + # ----------------------------- + # Other method fields + # ----------------------------- def get_approved(self, obj): return True if obj.status == Dref.Status.APPROVED else False def get_indicators_id(self, obj): - return None # Placeholder for future implementation + return None def get_link_to_emergency_page(self, obj): - try: - appeal = Appeal.objects.get(code=obj.appeal_code) - except Appeal.DoesNotExist: - return None # f"No Appeal (and no Event) found with code: {obj.appeal_code}" + code = getattr(obj, "appeal_code", None) + if not code: + return None + + cache = self._appeal_cache() + if code in cache: + appeal = cache[code] + else: + try: + appeal = Appeal.objects.only("event_id").get(code=code) + except Appeal.DoesNotExist: + appeal = None + cache[code] = appeal + + if not appeal or not getattr(appeal, "event_id", None): + return None return f"https://go.ifrc.org/emergencies/{appeal.event_id}/details" class Meta: diff --git a/dref/views.py b/dref/views.py index 0ddca8169..2eedbf972 100644 --- a/dref/views.py +++ b/dref/views.py @@ -388,6 +388,11 @@ class Dref3ViewSet(RevisionMixin, viewsets.ModelViewSet): # type: ignore[misc] # Previous: [permissions.IsAuthenticated, DenyGuestUserPermission, UseBySuperAdminOnly] lookup_field = "appeal_code" + def _excluded_codes(self): + if not hasattr(self, "_excluded_codes_cache"): + self._excluded_codes_cache = self.get_nonsuperusers_excluded_codes() + return self._excluded_codes_cache + def get_queryset(self): # type: ignore[override] # just to give something to rest_framework/generics.py:63 – not used in retrieve return Dref.objects.none() @@ -573,7 +578,7 @@ def list(self, request): # Exclude codes for non-superusers if not getattr(self.request.user, "is_superuser", False): - excluded_codes = self.get_nonsuperusers_excluded_codes() + excluded_codes = self._excluded_codes() if excluded_codes: codes = [c for c in codes if c and c.upper() not in excluded_codes] @@ -609,7 +614,7 @@ def list(self, request): self.kwargs = old_kwargs # Restore old kwargs # Assign ephemeral numeric ids (1-based sequence) per request and silent_operation flag - silents = self.get_nonsuperusers_excluded_codes() + silents = self._excluded_codes() for idx, row in enumerate(data, start=1): row["id"] = idx row["public"] = row["appeal_id"] not in silents @@ -668,54 +673,45 @@ def get_objects_by_appeal_code(self, appeal_code): if not getattr(user, "is_superuser", False): # If code is in the excluded list, return no results for anonymous users - excluded_codes = self.get_nonsuperusers_excluded_codes() + excluded_codes = self._excluded_codes() if appeal_code and appeal_code.upper() in excluded_codes: return [] # Light users: only published records are visible drefs = ( Dref.objects.filter(appeal_code=appeal_code, status=Dref.Status.APPROVED) - .prefetch_related("planned_interventions", "needs_identified", "national_society_actions", "users") + .prefetch_related("planned_interventions") .order_by("created_at") - .distinct() ) if drefs.exists(): results.extend(drefs) operational_updates = ( DrefOperationalUpdate.objects.filter(appeal_code=appeal_code, status=Dref.Status.APPROVED) - .prefetch_related("planned_interventions", "needs_identified", "national_society_actions", "users") + .prefetch_related("planned_interventions") .order_by("created_at") - .distinct() ) if operational_updates.exists(): results.extend(operational_updates) final_reports = ( DrefFinalReport.objects.filter(appeal_code=appeal_code, status=Dref.Status.APPROVED) - .prefetch_related("planned_interventions", "needs_identified", "national_society_actions", "users") + .prefetch_related("planned_interventions") .order_by("created_at") - .distinct() ) if final_reports.exists(): results.extend(final_reports) return results # Strong users: allow more access - drefs = ( - Dref.objects.filter(appeal_code=appeal_code) - .prefetch_related("planned_interventions", "needs_identified", "national_society_actions", "users") - .order_by("created_at") - .distinct() - ) + drefs = Dref.objects.filter(appeal_code=appeal_code).prefetch_related("planned_interventions").order_by("created_at") drefs = filter_dref_queryset_by_user_access(user, drefs) if drefs.exists(): results.extend(drefs) operational_updates = ( DrefOperationalUpdate.objects.filter(appeal_code=appeal_code) - .prefetch_related("planned_interventions", "needs_identified", "national_society_actions", "users") + .prefetch_related("planned_interventions") .order_by("created_at") - .distinct() ) operational_updates = filter_dref_queryset_by_user_access(user, operational_updates) if operational_updates.exists(): @@ -723,9 +719,8 @@ def get_objects_by_appeal_code(self, appeal_code): final_reports = ( DrefFinalReport.objects.filter(appeal_code=appeal_code) - .prefetch_related("planned_interventions", "needs_identified", "national_society_actions", "users") + .prefetch_related("planned_interventions") .order_by("created_at") - .distinct() ) final_reports = filter_dref_queryset_by_user_access(user, final_reports) if final_reports.exists(): @@ -742,7 +737,7 @@ def retrieve(self, request, *args, **kwargs): serialized_data = [] ops_update_count = 0 allocation_count = 1 # Dref Application is always the first allocation - public = code not in self.get_nonsuperusers_excluded_codes() + public = code not in self._excluded_codes() a = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Seventh", "Eighth", "Ninth", "Tenth"] # is_latest_stage: the last APPROVED-status instance and next instance either absent or not APPROVED