From 12bfe0f955b484e2e113f012864f307933d7d0bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:05:03 +0300 Subject: [PATCH 01/13] Update website staging image tag to stage-8fda0318-1733752993 --- k8s/website/values-stage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/website/values-stage.yaml b/k8s/website/values-stage.yaml index 1287978b00..01bfb5e66b 100644 --- a/k8s/website/values-stage.yaml +++ b/k8s/website/values-stage.yaml @@ -6,7 +6,7 @@ app: replicaCount: 2 image: repository: eu.gcr.io/airqo-250220/airqo-stage-website-api - tag: stage-100bea2d-1733743125 + tag: stage-8fda0318-1733752993 nameOverride: '' fullnameOverride: '' podAnnotations: {} From 3cbf2732fb05e964e6541ec08871aa8273104589 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:05:49 +0300 Subject: [PATCH 02/13] Update AirQo exceedance production image tag to prod-aa6aa4ba-1733753092 --- k8s/exceedance/values-prod-airqo.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/exceedance/values-prod-airqo.yaml b/k8s/exceedance/values-prod-airqo.yaml index ea3f6a3fbf..85ddc8196a 100644 --- a/k8s/exceedance/values-prod-airqo.yaml +++ b/k8s/exceedance/values-prod-airqo.yaml @@ -4,6 +4,6 @@ app: configmap: env-exceedance-production image: repository: eu.gcr.io/airqo-250220/airqo-exceedance-job - tag: prod-d862897c-1733743392 + tag: prod-aa6aa4ba-1733753092 nameOverride: '' fullnameOverride: '' From 9d1ad5aff2df722452f48e454e0d9b1d41c8cae1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:05:59 +0300 Subject: [PATCH 03/13] Update KCCA exceedance production image tag to prod-aa6aa4ba-1733753092 --- k8s/exceedance/values-prod-kcca.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/exceedance/values-prod-kcca.yaml b/k8s/exceedance/values-prod-kcca.yaml index a570a40013..9b6a6d3b1c 100644 --- a/k8s/exceedance/values-prod-kcca.yaml +++ b/k8s/exceedance/values-prod-kcca.yaml @@ -4,6 +4,6 @@ app: configmap: env-exceedance-production image: repository: eu.gcr.io/airqo-250220/kcca-exceedance-job - tag: prod-d862897c-1733743392 + tag: prod-aa6aa4ba-1733753092 nameOverride: '' fullnameOverride: '' From 0b69ed5c6291d9cece237b51898e821ed61b9ec6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:06:23 +0300 Subject: [PATCH 04/13] Update auth service production image tag to prod-aa6aa4ba-1733753092 --- k8s/auth-service/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/auth-service/values-prod.yaml b/k8s/auth-service/values-prod.yaml index f4469ca70e..f7461e5376 100644 --- a/k8s/auth-service/values-prod.yaml +++ b/k8s/auth-service/values-prod.yaml @@ -6,7 +6,7 @@ app: replicaCount: 3 image: repository: eu.gcr.io/airqo-250220/airqo-auth-api - tag: prod-d862897c-1733743392 + tag: prod-aa6aa4ba-1733753092 nameOverride: '' fullnameOverride: '' podAnnotations: {} From 0f8c498d8496bbd1d1378471499c130b23667a64 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:06:46 +0300 Subject: [PATCH 05/13] Update website production image tag to prod-aa6aa4ba-1733753092 --- k8s/website/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/website/values-prod.yaml b/k8s/website/values-prod.yaml index 903c1227a1..31bec7c83a 100644 --- a/k8s/website/values-prod.yaml +++ b/k8s/website/values-prod.yaml @@ -6,7 +6,7 @@ app: replicaCount: 3 image: repository: eu.gcr.io/airqo-250220/airqo-website-api - tag: prod-d862897c-1733743392 + tag: prod-aa6aa4ba-1733753092 nameOverride: '' fullnameOverride: '' podAnnotations: {} From b5f47d8ab3b796d423b9ec6435f47400e013af15 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:07:44 +0300 Subject: [PATCH 06/13] Update workflows prod image tag to prod-aa6aa4ba-1733753092 --- k8s/workflows/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/workflows/values-prod.yaml b/k8s/workflows/values-prod.yaml index 6006b1c0cd..fec033074d 100644 --- a/k8s/workflows/values-prod.yaml +++ b/k8s/workflows/values-prod.yaml @@ -10,7 +10,7 @@ images: initContainer: eu.gcr.io/airqo-250220/airqo-workflows-xcom redisContainer: eu.gcr.io/airqo-250220/airqo-redis containers: eu.gcr.io/airqo-250220/airqo-workflows - tag: prod-d862897c-1733743392 + tag: prod-aa6aa4ba-1733753092 nameOverride: '' fullnameOverride: '' podAnnotations: {} From 9adeefb9a48d7bdc346d588edc305e3287150a4b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:08:20 +0300 Subject: [PATCH 07/13] Update predict production image tag to prod-aa6aa4ba-1733753092 --- k8s/predict/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/predict/values-prod.yaml b/k8s/predict/values-prod.yaml index 504847908e..a554ba7165 100644 --- a/k8s/predict/values-prod.yaml +++ b/k8s/predict/values-prod.yaml @@ -7,7 +7,7 @@ images: predictJob: eu.gcr.io/airqo-250220/airqo-predict-job trainJob: eu.gcr.io/airqo-250220/airqo-train-job predictPlaces: eu.gcr.io/airqo-250220/airqo-predict-places-air-quality - tag: prod-d862897c-1733743392 + tag: prod-aa6aa4ba-1733753092 api: name: airqo-prediction-api label: prediction-api From 415e39315857f88f2febc5b1b78930aacf1c7042 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:14:44 +0300 Subject: [PATCH 08/13] Update spatial production image tag to prod-aa6aa4ba-1733753092 --- k8s/spatial/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/spatial/values-prod.yaml b/k8s/spatial/values-prod.yaml index e5b90c5b37..7e876b40f0 100644 --- a/k8s/spatial/values-prod.yaml +++ b/k8s/spatial/values-prod.yaml @@ -6,7 +6,7 @@ app: replicaCount: 3 image: repository: eu.gcr.io/airqo-250220/airqo-spatial-api - tag: prod-d862897c-1733743392 + tag: prod-aa6aa4ba-1733753092 nameOverride: '' fullnameOverride: '' podAnnotations: {} From 4bfbb940f4704354657776981554d2b253e489a9 Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Tue, 10 Dec 2024 12:50:44 +0300 Subject: [PATCH 09/13] edits to the settings.py file --- src/website/core/settings.py | 96 ++++++------------------------------ 1 file changed, 14 insertions(+), 82 deletions(-) diff --git a/src/website/core/settings.py b/src/website/core/settings.py index 4f853070e7..b9cd32a381 100644 --- a/src/website/core/settings.py +++ b/src/website/core/settings.py @@ -52,7 +52,8 @@ def require_env_var(env_var: str) -> str: # Core Settings # --------------------------------------------------------- SECRET_KEY = require_env_var('SECRET_KEY') -DEBUG = get_env_bool('DEBUG', default=False) +# DEBUG = get_env_bool('DEBUG', default=False) +DEBUG = True # ALLOWED_HOSTS = parse_env_list("ALLOWED_HOSTS") ALLOWED_HOSTS = ['*'] @@ -121,65 +122,16 @@ def require_env_var(env_var: str) -> str: CORS_ORIGIN_REGEX_WHITELIST = [] # Allow all CSRF origins during development - CSRF_TRUSTED_ORIGINS = [ - "https://website-trigger-3-website-preview-w7kzhvlewq-ew.a.run.app", - ] + CSRF_TRUSTED_ORIGINS = [] # Optionally, you can add more relaxed settings # For example, allow specific subdomains or ports if needed else: # Restrict CORS origins in production CORS_ORIGIN_ALLOW_ALL = False - CORS_ALLOWED_ORIGINS = [ - "https://staging-dot-airqo-frontend.appspot.com", - "https://staging.airqo.net", - "https://airqo.net", - "https://airqo.africa", - "https://airqo.org", - "https://airqo.mak.ac.ug", - "http://127.0.0.1:8000", - "http://localhost:3000", - "https://staging-platform.airqo.net", - "https://staging-analytics.airqo.net", - "https://analytics.airqo.net", - "https://platform.airqo.net", - ] - CORS_ORIGIN_REGEX_WHITELIST = [ - # Matches subdomains under airqo.net, airqo.africa, airqo.org, airqo.io - r"^https://[a-zA-Z0-9_\-]+\.airqo\.(net|africa|org|io)$", - # Matches airqo.africa, airqo.org, and airqo.mak.ac.ug - r"^https://airqo\.(africa|org|mak\.ac\.ug)$", - # Matches staging-dot-airqo-frontend.appspot.com - r"^https://staging-dot-airqo-frontend\.appspot\.com$", - r"^https://staging-platform\.airqo\.net$", # Matches staging-platform.airqo.net - # Matches staging-analytics.airqo.net - r"^https://staging-analytics\.airqo\.net$", - r"^https://analytics\.airqo\.net$", # Matches analytics.airqo.net - r"^https://platform\.airqo\.net$", # Matches platform.airqo.net - # Matches any subpath under https://platform.airqo.net/website/admin - r"^https://platform\.airqo\.net/website/admin.*$", - # Matches any subpath under https://staging-platform.airqo.net/website/admin - r"^https://staging-platform\.airqo\.net/website/admin.*$", - ] - - # Trust specific origins for CSRF protection in production - # CSRF_TRUSTED_ORIGINS = parse_env_list("CSRF_TRUSTED_ORIGINS") - CSRF_TRUSTED_ORIGINS = [ - "https://staging-dot-airqo-frontend.appspot.com", - "https://staging.airqo.net", - "https://airqo.net", - "https://airqo.africa", - "https://airqo.org", - "https://airqo.mak.ac.ug", - "http://127.0.0.1:8000", - "http://localhost:3000", - "https://*.cloudshell.dev", - "https://staging-platform.airqo.net", - "https://staging-analytics.airqo.net", - "https://analytics.airqo.net", - "https://platform.airqo.net", - "https://website-trigger-3-website-preview-w7kzhvlewq-ew.a.run.app", - ] + CORS_ALLOWED_ORIGINS = parse_env_list("CORS_ALLOWED_ORIGINS") + CORS_ORIGIN_REGEX_WHITELIST = parse_env_list("CORS_ORIGIN_REGEX_WHITELIST") + CSRF_TRUSTED_ORIGINS = parse_env_list("CSRF_TRUSTED_ORIGINS") # Security settings @@ -254,24 +206,15 @@ def require_env_var(env_var: str) -> str: STATICFILES_DIRS = [BASE_DIR / 'static'] STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' -if DEBUG: - # Local file storage for development - MEDIA_URL = '/media/' - DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' - MEDIA_ROOT = BASE_DIR / 'assets' - print("DEBUG=True: Using local file storage for media.") -else: - # Cloudinary setup for production - CLOUDINARY_STORAGE = { - 'CLOUD_NAME': require_env_var('CLOUDINARY_CLOUD_NAME'), - 'API_KEY': require_env_var('CLOUDINARY_API_KEY'), - 'API_SECRET': require_env_var('CLOUDINARY_API_SECRET'), - 'SECURE': True, - 'TIMEOUT': 600, - } +CLOUDINARY_STORAGE = { + 'CLOUD_NAME': require_env_var('CLOUDINARY_CLOUD_NAME'), + 'API_KEY': require_env_var('CLOUDINARY_API_KEY'), + 'API_SECRET': require_env_var('CLOUDINARY_API_SECRET'), + 'SECURE': True, + 'TIMEOUT': 600, +} - DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage' - print("DEBUG=False: Using Cloudinary for media storage.") +DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage' # --------------------------------------------------------- # Default Primary Key Field Type @@ -291,17 +234,6 @@ def require_env_var(env_var: str) -> str: ], } -# --------------------------------------------------------- -# File Upload Limits -# --------------------------------------------------------- -# Define a constant for maximum upload size -MAX_UPLOAD_SIZE_MB = 10 # Maximum upload size in MB -MAX_UPLOAD_SIZE = MAX_UPLOAD_SIZE_MB * 1024 * 1024 # Convert to bytes - -# Apply the maximum upload size to Django settings -DATA_UPLOAD_MAX_MEMORY_SIZE = MAX_UPLOAD_SIZE -FILE_UPLOAD_MAX_MEMORY_SIZE = MAX_UPLOAD_SIZE - # --------------------------------------------------------- # Admin and Authentication Settings # --------------------------------------------------------- From 1afd91730b1ba31e96bc5031be5edcaa5427f3f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:29:15 +0300 Subject: [PATCH 10/13] Update website staging image tag to stage-aff08031-1733826449 --- k8s/website/values-stage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/website/values-stage.yaml b/k8s/website/values-stage.yaml index 01bfb5e66b..bdf124819b 100644 --- a/k8s/website/values-stage.yaml +++ b/k8s/website/values-stage.yaml @@ -6,7 +6,7 @@ app: replicaCount: 2 image: repository: eu.gcr.io/airqo-250220/airqo-stage-website-api - tag: stage-8fda0318-1733752993 + tag: stage-aff08031-1733826449 nameOverride: '' fullnameOverride: '' podAnnotations: {} From 18f06adc3698d1502ea1d24ce72f510870fbb0eb Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Tue, 10 Dec 2024 14:30:42 +0300 Subject: [PATCH 11/13] updates to settings and events model --- src/website/apps/event/models.py | 94 ++++++++++++++++++++++++--- src/website/apps/event/views.py | 106 ++++++++++++++++++++++++++++--- src/website/core/settings.py | 97 +++++++++++----------------- 3 files changed, 222 insertions(+), 75 deletions(-) diff --git a/src/website/apps/event/models.py b/src/website/apps/event/models.py index a93214cd31..bda5d0fba8 100644 --- a/src/website/apps/event/models.py +++ b/src/website/apps/event/models.py @@ -1,15 +1,12 @@ - +import logging from django.db import models from django.contrib.auth import get_user_model from django_quill.fields import QuillField from utils.models import BaseModel from cloudinary.models import CloudinaryField from cloudinary.uploader import destroy -import logging User = get_user_model() - -# Configure logger logger = logging.getLogger(__name__) @@ -65,7 +62,6 @@ class EventCategory(models.TextChoices): blank=True, ) - # Image fields using CloudinaryField event_image = CloudinaryField( 'image', folder='website/uploads/events/images', @@ -94,8 +90,18 @@ class Meta: def __str__(self): return self.title + def save(self, *args, **kwargs): + is_new = self.pk is None + super().save(*args, **kwargs) + if is_new: + logger.info(f"Created new Event: ID={self.pk}, Title={self.title}") + else: + logger.info(f"Updated Event: ID={self.pk}, Title={self.title}") + def delete(self, *args, **kwargs): - # Delete files from Cloudinary + logger.debug( + f"Attempting to delete Event: ID={self.pk}, Title={self.title}") + # Attempt to delete images from Cloudinary if self.event_image: try: destroy(self.event_image.public_id) @@ -112,7 +118,9 @@ def delete(self, *args, **kwargs): except Exception as e: logger.error( f"Error deleting background_image from Cloudinary: {e}") + super().delete(*args, **kwargs) + logger.info(f"Deleted Event: ID={self.pk}, Title={self.title}") class Inquiry(BaseModel): @@ -134,6 +142,22 @@ class Meta: def __str__(self): return f"Inquiry - {self.inquiry}" + def save(self, *args, **kwargs): + is_new = self.pk is None + super().save(*args, **kwargs) + if is_new: + logger.info( + f"Created new Inquiry: ID={self.pk}, Inquiry={self.inquiry}") + else: + logger.info( + f"Updated Inquiry: ID={self.pk}, Inquiry={self.inquiry}") + + def delete(self, *args, **kwargs): + logger.debug( + f"Attempting to delete Inquiry: ID={self.pk}, Inquiry={self.inquiry}") + super().delete(*args, **kwargs) + logger.info(f"Deleted Inquiry: ID={self.pk}, Inquiry={self.inquiry}") + class Program(BaseModel): date = models.DateField() @@ -153,6 +177,20 @@ class Meta: def __str__(self): return f"Program - {self.date}" + def save(self, *args, **kwargs): + is_new = self.pk is None + super().save(*args, **kwargs) + if is_new: + logger.info(f"Created new Program: ID={self.pk}, Date={self.date}") + else: + logger.info(f"Updated Program: ID={self.pk}, Date={self.date}") + + def delete(self, *args, **kwargs): + logger.debug( + f"Attempting to delete Program: ID={self.pk}, Date={self.date}") + super().delete(*args, **kwargs) + logger.info(f"Deleted Program: ID={self.pk}, Date={self.date}") + class Session(BaseModel): start_time = models.TimeField() @@ -175,6 +213,23 @@ class Meta: def __str__(self): return f"Session - {self.session_title}" + def save(self, *args, **kwargs): + is_new = self.pk is None + super().save(*args, **kwargs) + if is_new: + logger.info( + f"Created new Session: ID={self.pk}, Title={self.session_title}") + else: + logger.info( + f"Updated Session: ID={self.pk}, Title={self.session_title}") + + def delete(self, *args, **kwargs): + logger.debug( + f"Attempting to delete Session: ID={self.pk}, Title={self.session_title}") + super().delete(*args, **kwargs) + logger.info( + f"Deleted Session: ID={self.pk}, Title={self.session_title}") + class PartnerLogo(BaseModel): partner_logo = CloudinaryField( @@ -201,7 +256,18 @@ class Meta: def __str__(self): return f"Partner - {self.name}" + def save(self, *args, **kwargs): + is_new = self.pk is None + super().save(*args, **kwargs) + if is_new: + logger.info( + f"Created new PartnerLogo: ID={self.pk}, Name={self.name}") + else: + logger.info(f"Updated PartnerLogo: ID={self.pk}, Name={self.name}") + def delete(self, *args, **kwargs): + logger.debug( + f"Attempting to delete PartnerLogo: ID={self.pk}, Name={self.name}") if self.partner_logo: try: destroy(self.partner_logo.public_id) @@ -211,6 +277,7 @@ def delete(self, *args, **kwargs): logger.error( f"Error deleting partner_logo from Cloudinary: {e}") super().delete(*args, **kwargs) + logger.info(f"Deleted PartnerLogo: ID={self.pk}, Name={self.name}") class Resource(BaseModel): @@ -239,13 +306,24 @@ class Meta: def __str__(self): return f"Resource - {self.title}" + def save(self, *args, **kwargs): + is_new = self.pk is None + super().save(*args, **kwargs) + if is_new: + logger.info( + f"Created new Resource: ID={self.pk}, Title={self.title}") + else: + logger.info(f"Updated Resource: ID={self.pk}, Title={self.title}") + def delete(self, *args, **kwargs): + logger.debug( + f"Attempting to delete Resource: ID={self.pk}, Title={self.title}") if self.resource: try: destroy(self.resource.public_id) logger.info( f"Deleted resource from Cloudinary: {self.resource.public_id}") except Exception as e: - logger.error( - f"Error deleting resource from Cloudinary: {e}") + logger.error(f"Error deleting resource from Cloudinary: {e}") super().delete(*args, **kwargs) + logger.info(f"Deleted Resource: ID={self.pk}, Title={self.title}") diff --git a/src/website/apps/event/views.py b/src/website/apps/event/views.py index 82cf33e439..569a381706 100644 --- a/src/website/apps/event/views.py +++ b/src/website/apps/event/views.py @@ -1,6 +1,7 @@ -# backend/apps/event/views.py - +import logging from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticatedOrReadOnly + from .models import Event, Inquiry, Program, Session, PartnerLogo, Resource from .serializers import ( EventListSerializer, @@ -11,39 +12,75 @@ PartnerLogoSerializer, ResourceSerializer, ) -from rest_framework.permissions import IsAuthenticatedOrReadOnly + +logger = logging.getLogger(__name__) class EventViewSet(viewsets.ReadOnlyModelViewSet): """ - A viewset that provides the standard actions for Event model. + A viewset that provides the standard actions for the Event model. """ permission_classes = [IsAuthenticatedOrReadOnly] - lookup_field = 'id' # Use 'id' as the lookup field + lookup_field = 'id' def get_serializer_class(self): if self.action == 'retrieve': - return EventDetailSerializer - return EventListSerializer + serializer_class = EventDetailSerializer + else: + serializer_class = EventListSerializer + logger.debug( + f"Selected serializer_class={serializer_class.__name__} for action={self.action}") + return serializer_class def get_queryset(self): + category = self.request.query_params.get('category', None) + logger.debug(f"Fetching Event queryset with category={category}") queryset = Event.objects.prefetch_related( 'inquiries', 'programs__sessions', 'partner_logos', 'resources' ).all() - category = self.request.query_params.get('category', None) + if category in ['airqo', 'cleanair']: queryset = queryset.filter(website_category=category) + logger.info( + f"Filtered Event queryset by category={category}, count={queryset.count()}") + + logger.info(f"Retrieved Event queryset, count={queryset.count()}") return queryset + def list(self, request, *args, **kwargs): + logger.debug("Handling Event list request") + response = super().list(request, *args, **kwargs) + logger.info(f"Listed Events, returned {len(response.data)} records") + return response + + def retrieve(self, request, *args, **kwargs): + logger.debug(f"Handling Event retrieve request, id={kwargs.get('id')}") + response = super().retrieve(request, *args, **kwargs) + logger.info(f"Retrieved Event detail for ID={kwargs.get('id')}") + return response + class InquiryViewSet(viewsets.ReadOnlyModelViewSet): queryset = Inquiry.objects.select_related('event').all() serializer_class = InquirySerializer lookup_field = 'id' + def list(self, request, *args, **kwargs): + logger.debug("Handling Inquiry list request") + response = super().list(request, *args, **kwargs) + logger.info(f"Listed Inquiries, returned {len(response.data)} records") + return response + + def retrieve(self, request, *args, **kwargs): + logger.debug( + f"Handling Inquiry retrieve request, id={kwargs.get('id')}") + response = super().retrieve(request, *args, **kwargs) + logger.info(f"Retrieved Inquiry detail for ID={kwargs.get('id')}") + return response + class ProgramViewSet(viewsets.ReadOnlyModelViewSet): queryset = Program.objects.select_related( @@ -51,20 +88,73 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = ProgramSerializer lookup_field = 'id' + def list(self, request, *args, **kwargs): + logger.debug("Handling Program list request") + response = super().list(request, *args, **kwargs) + logger.info(f"Listed Programs, returned {len(response.data)} records") + return response + + def retrieve(self, request, *args, **kwargs): + logger.debug( + f"Handling Program retrieve request, id={kwargs.get('id')}") + response = super().retrieve(request, *args, **kwargs) + logger.info(f"Retrieved Program detail for ID={kwargs.get('id')}") + return response + class SessionViewSet(viewsets.ReadOnlyModelViewSet): queryset = Session.objects.select_related('program__event').all() serializer_class = SessionSerializer lookup_field = 'id' + def list(self, request, *args, **kwargs): + logger.debug("Handling Session list request") + response = super().list(request, *args, **kwargs) + logger.info(f"Listed Sessions, returned {len(response.data)} records") + return response + + def retrieve(self, request, *args, **kwargs): + logger.debug( + f"Handling Session retrieve request, id={kwargs.get('id')}") + response = super().retrieve(request, *args, **kwargs) + logger.info(f"Retrieved Session detail for ID={kwargs.get('id')}") + return response + class PartnerLogoViewSet(viewsets.ReadOnlyModelViewSet): queryset = PartnerLogo.objects.select_related('event').all() serializer_class = PartnerLogoSerializer lookup_field = 'id' + def list(self, request, *args, **kwargs): + logger.debug("Handling PartnerLogo list request") + response = super().list(request, *args, **kwargs) + logger.info( + f"Listed PartnerLogos, returned {len(response.data)} records") + return response + + def retrieve(self, request, *args, **kwargs): + logger.debug( + f"Handling PartnerLogo retrieve request, id={kwargs.get('id')}") + response = super().retrieve(request, *args, **kwargs) + logger.info(f"Retrieved PartnerLogo detail for ID={kwargs.get('id')}") + return response + class ResourceViewSet(viewsets.ReadOnlyModelViewSet): queryset = Resource.objects.select_related('event').all() serializer_class = ResourceSerializer lookup_field = 'id' + + def list(self, request, *args, **kwargs): + logger.debug("Handling Resource list request") + response = super().list(request, *args, **kwargs) + logger.info(f"Listed Resources, returned {len(response.data)} records") + return response + + def retrieve(self, request, *args, **kwargs): + logger.debug( + f"Handling Resource retrieve request, id={kwargs.get('id')}") + response = super().retrieve(request, *args, **kwargs) + logger.info(f"Retrieved Resource detail for ID={kwargs.get('id')}") + return response diff --git a/src/website/core/settings.py b/src/website/core/settings.py index b9cd32a381..80b50328d9 100644 --- a/src/website/core/settings.py +++ b/src/website/core/settings.py @@ -6,25 +6,24 @@ from dotenv import load_dotenv # --------------------------------------------------------- -# Load Environment Variables from .env +# Load Environment Variables # --------------------------------------------------------- load_dotenv() # --------------------------------------------------------- -# Base Directory and Python Path Adjustments +# Base Directory & Python Path # --------------------------------------------------------- BASE_DIR = Path(__file__).resolve().parent.parent -sys.path.append(str(BASE_DIR / 'apps')) # Allow referencing apps directly +sys.path.append(str(BASE_DIR / 'apps')) # --------------------------------------------------------- -# Helper Functions for Environment Variables +# Environment Variable Helpers # --------------------------------------------------------- def parse_env_list(env_var: str, default: str = "") -> list: """ - Parse a comma-separated string from an environment variable into a list. - Trims whitespace and ignores empty entries. + Convert a comma-separated list in an env var to a Python list. """ raw_value = os.getenv(env_var, default) return [item.strip() for item in raw_value.split(',') if item.strip()] @@ -32,15 +31,14 @@ def parse_env_list(env_var: str, default: str = "") -> list: def get_env_bool(env_var: str, default: bool = False) -> bool: """ - Convert an environment variable to a boolean. - Accepts 'true', '1', 't' (case-insensitive) as True. + Convert an environment variable to boolean. """ - return os.getenv(env_var, str(default)).lower() in ['true', '1', 't'] + return os.getenv(env_var, str(default)).lower() in ['true', '1', 't', 'yes'] def require_env_var(env_var: str) -> str: """ - Ensure an environment variable is set. Raise an error if not set. + Ensure an environment variable is set. Raise ValueError if not. """ value = os.getenv(env_var) if not value: @@ -52,17 +50,15 @@ def require_env_var(env_var: str) -> str: # Core Settings # --------------------------------------------------------- SECRET_KEY = require_env_var('SECRET_KEY') -# DEBUG = get_env_bool('DEBUG', default=False) -DEBUG = True +DEBUG = get_env_bool('DEBUG', default=False) -# ALLOWED_HOSTS = parse_env_list("ALLOWED_HOSTS") -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = parse_env_list('ALLOWED_HOSTS', default='localhost,127.0.0.1') # --------------------------------------------------------- -# Application Definitions +# Applications # --------------------------------------------------------- INSTALLED_APPS = [ - # Django defaults + # Django Defaults 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -70,7 +66,7 @@ def require_env_var(env_var: str) -> str: 'django.contrib.messages', 'django.contrib.staticfiles', - # Third-party apps + # Third-party Apps 'corsheaders', 'cloudinary', 'cloudinary_storage', @@ -79,9 +75,9 @@ def require_env_var(env_var: str) -> str: 'django_extensions', 'nested_admin', 'drf_yasg', - 'django_quill', # Re-added django_quill + 'django_quill', - # Custom apps + # Custom Apps 'apps.externalteams', 'apps.event', 'apps.cleanair', @@ -101,7 +97,7 @@ def require_env_var(env_var: str) -> str: # Middleware # --------------------------------------------------------- MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', # Must be first + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -115,26 +111,15 @@ def require_env_var(env_var: str) -> str: # --------------------------------------------------------- # CORS and CSRF Configuration # --------------------------------------------------------- -if DEBUG: - # Allow all CORS origins during development - CORS_ORIGIN_ALLOW_ALL = True - CORS_ALLOWED_ORIGINS = [] - CORS_ORIGIN_REGEX_WHITELIST = [] +CORS_ALLOWED_ORIGINS = parse_env_list('CORS_ALLOWED_ORIGINS') +CORS_ALLOWED_ORIGIN_REGEXES = parse_env_list('CORS_ORIGIN_REGEX_WHITELIST') +CSRF_TRUSTED_ORIGINS = parse_env_list('CSRF_TRUSTED_ORIGINS') - # Allow all CSRF origins during development - CSRF_TRUSTED_ORIGINS = [] +# If no CORS settings provided, consider defaulting to empty lists +CORS_ALLOWED_ORIGINS = CORS_ALLOWED_ORIGINS if CORS_ALLOWED_ORIGINS else [] +CORS_ALLOWED_ORIGIN_REGEXES = CORS_ALLOWED_ORIGIN_REGEXES if CORS_ALLOWED_ORIGIN_REGEXES else [] - # Optionally, you can add more relaxed settings - # For example, allow specific subdomains or ports if needed -else: - # Restrict CORS origins in production - CORS_ORIGIN_ALLOW_ALL = False - CORS_ALLOWED_ORIGINS = parse_env_list("CORS_ALLOWED_ORIGINS") - CORS_ORIGIN_REGEX_WHITELIST = parse_env_list("CORS_ORIGIN_REGEX_WHITELIST") - CSRF_TRUSTED_ORIGINS = parse_env_list("CSRF_TRUSTED_ORIGINS") - - -# Security settings +# Security cookies CSRF_COOKIE_SECURE = not DEBUG SESSION_COOKIE_SECURE = not DEBUG @@ -167,17 +152,21 @@ def require_env_var(env_var: str) -> str: # Database Configuration # --------------------------------------------------------- DATABASE_URL = os.getenv('DATABASE_URL') - -DATABASES = { - 'default': dj_database_url.parse( - DATABASE_URL, - conn_max_age=600, - ssl_require=True - ) if DATABASE_URL else { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', +if DATABASE_URL: + DATABASES = { + 'default': dj_database_url.parse( + DATABASE_URL, + conn_max_age=600, + ssl_require=True + ) + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } } -} # --------------------------------------------------------- # Password Validation @@ -195,7 +184,6 @@ def require_env_var(env_var: str) -> str: LANGUAGE_CODE = os.getenv('LANGUAGE_CODE', 'en-us') TIME_ZONE = os.getenv('TIME_ZONE', 'UTC') USE_I18N = True -USE_L10N = True USE_TZ = True # --------------------------------------------------------- @@ -206,6 +194,7 @@ def require_env_var(env_var: str) -> str: STATICFILES_DIRS = [BASE_DIR / 'static'] STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' +# Cloudinary Configuration CLOUDINARY_STORAGE = { 'CLOUD_NAME': require_env_var('CLOUDINARY_CLOUD_NAME'), 'API_KEY': require_env_var('CLOUDINARY_API_KEY'), @@ -213,7 +202,6 @@ def require_env_var(env_var: str) -> str: 'SECURE': True, 'TIMEOUT': 600, } - DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage' # --------------------------------------------------------- @@ -281,12 +269,3 @@ def require_env_var(env_var: str) -> str: 'scrollingContainer': '#scrolling-container', }, } - -# --------------------------------------------------------- -# Mode-Specific Logging -# --------------------------------------------------------- -if DEBUG: - print(f"Debug mode is: {DEBUG}") - print(f"Media files are stored in: {BASE_DIR / 'assets'}") -else: - print("Production mode is ON") From dcc34528cfe8f907e64e1a33cf1dba0a18bc44b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:40:16 +0300 Subject: [PATCH 12/13] Update website staging image tag to stage-ac4d3c16-1733830718 --- k8s/website/values-stage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/website/values-stage.yaml b/k8s/website/values-stage.yaml index bdf124819b..61f8603b8d 100644 --- a/k8s/website/values-stage.yaml +++ b/k8s/website/values-stage.yaml @@ -6,7 +6,7 @@ app: replicaCount: 2 image: repository: eu.gcr.io/airqo-250220/airqo-stage-website-api - tag: stage-aff08031-1733826449 + tag: stage-ac4d3c16-1733830718 nameOverride: '' fullnameOverride: '' podAnnotations: {} From 93914f84113b2375f1304637ba399141e5acc997 Mon Sep 17 00:00:00 2001 From: baalmart Date: Tue, 10 Dec 2024 15:08:31 +0300 Subject: [PATCH 13/13] just fixing list cohorts --- src/device-registry/models/Cohort.js | 71 ++++++++++++++++------------ 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/src/device-registry/models/Cohort.js b/src/device-registry/models/Cohort.js index ac5a481261..788811e60a 100644 --- a/src/device-registry/models/Cohort.js +++ b/src/device-registry/models/Cohort.js @@ -160,8 +160,6 @@ cohortSchema.statics.register = async function(args, next) { response.errors = { message: error.message }; } - return response; - logger.error(`🐛🐛 Internal Server Error ${error.message}`); next(new HttpError(response.message, response.status, response.errors)); } @@ -196,12 +194,22 @@ cohortSchema.statics.list = async function( foreignField: "cohorts", as: "devices", }) - .unwind("$devices") - .lookup({ - from: "sites", - localField: "devices.site_id", - foreignField: "_id", - as: "devices.site", + .project({ + _id: 1, + visibility: 1, + cohort_tags: 1, + cohort_codes: 1, + name: 1, + createdAt: 1, + network: 1, + group: 1, + devices: { + $cond: { + if: { $eq: [{ $size: "$devices" }, 0] }, + then: [], + else: "$devices", + }, + }, }) .sort({ createdAt: -1 }) .project(inclusionProjection) @@ -215,11 +223,10 @@ cohortSchema.statics.list = async function( createdAt: { $first: "$createdAt" }, network: { $first: "$network" }, group: { $first: "$group" }, - numberOfDevices: { $sum: 1 }, - devices: { $push: "$devices" }, + devices: { $first: "$devices" }, }) - .skip(skip ? skip : 0) - .limit(limit ? limit : 1000) + .skip(skip ? parseInt(skip) : 0) + .limit(limit ? parseInt(limit) : 1000) .allowDiskUse(true); const cohorts = await pipeline.exec(); @@ -234,24 +241,28 @@ cohortSchema.statics.list = async function( network: cohort.network, createdAt: cohort.createdAt, group: cohort.group, - numberOfDevices: cohort.numberOfDevices, - devices: cohort.devices.map((device) => ({ - _id: device._id, - status: device.status, - name: device.name, - network: device.network, - group: device.group, - device_number: device.device_number, - description: device.description, - long_name: device.long_name, - createdAt: device.createdAt, - host_id: device.host_id, - site: device.site && - device.site[0] && { - _id: device.site[0]._id, - name: device.site[0].name, - }, - })), + numberOfDevices: cohort.devices ? cohort.devices.length : 0, + devices: cohort.devices + ? cohort.devices + .filter((device) => Object.keys(device).length > 0) + .map((device) => ({ + _id: device._id, + status: device.status, + name: device.name, + network: device.network, + group: device.group, + device_number: device.device_number, + description: device.description, + long_name: device.long_name, + createdAt: device.createdAt, + host_id: device.host_id, + site: device.site && + device.site[0] && { + _id: device.site[0]._id, + name: device.site[0].name, + }, + })) + : [], })) .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));