diff --git a/k8s/auth-service/values-prod.yaml b/k8s/auth-service/values-prod.yaml index f4469ca70e..4cb0498ecc 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-ee15b958-1733833086 nameOverride: '' fullnameOverride: '' podAnnotations: {} diff --git a/k8s/device-registry/values-prod.yaml b/k8s/device-registry/values-prod.yaml index 0377f748bf..b4c32c4d56 100644 --- a/k8s/device-registry/values-prod.yaml +++ b/k8s/device-registry/values-prod.yaml @@ -6,7 +6,7 @@ app: replicaCount: 3 image: repository: eu.gcr.io/airqo-250220/airqo-device-registry-api - tag: prod-03a25cf1-1733249950 + tag: prod-ee15b958-1733833086 nameOverride: '' fullnameOverride: '' podAnnotations: {} diff --git a/k8s/device-registry/values-stage.yaml b/k8s/device-registry/values-stage.yaml index 00149d1545..c3d8dccf77 100644 --- a/k8s/device-registry/values-stage.yaml +++ b/k8s/device-registry/values-stage.yaml @@ -6,7 +6,7 @@ app: replicaCount: 2 image: repository: eu.gcr.io/airqo-250220/airqo-stage-device-registry-api - tag: stage-1838aa7a-1733249623 + tag: stage-dfe6eb16-1733832983 nameOverride: '' fullnameOverride: '' podAnnotations: {} diff --git a/k8s/exceedance/values-prod-airqo.yaml b/k8s/exceedance/values-prod-airqo.yaml index ea3f6a3fbf..8fd10c0c0c 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-ee15b958-1733833086 nameOverride: '' fullnameOverride: '' diff --git a/k8s/exceedance/values-prod-kcca.yaml b/k8s/exceedance/values-prod-kcca.yaml index a570a40013..72030a5345 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-ee15b958-1733833086 nameOverride: '' fullnameOverride: '' diff --git a/k8s/predict/values-prod.yaml b/k8s/predict/values-prod.yaml index 504847908e..70cf680436 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-ee15b958-1733833086 api: name: airqo-prediction-api label: prediction-api diff --git a/k8s/spatial/values-prod.yaml b/k8s/spatial/values-prod.yaml index e5b90c5b37..3e448286e8 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-ee15b958-1733833086 nameOverride: '' fullnameOverride: '' podAnnotations: {} diff --git a/k8s/website/values-prod.yaml b/k8s/website/values-prod.yaml index 903c1227a1..6460bba388 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-ee15b958-1733833086 nameOverride: '' fullnameOverride: '' podAnnotations: {} diff --git a/k8s/website/values-stage.yaml b/k8s/website/values-stage.yaml index 1287978b00..a85c7a78ca 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-7a1d5dd3-1733836788 nameOverride: '' fullnameOverride: '' podAnnotations: {} diff --git a/k8s/workflows/values-prod.yaml b/k8s/workflows/values-prod.yaml index 6006b1c0cd..723152603a 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-ee15b958-1733833086 nameOverride: '' fullnameOverride: '' podAnnotations: {} 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)); 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 149e8af3d7..5bffef8789 100644 --- a/src/website/core/settings.py +++ b/src/website/core/settings.py @@ -1,4 +1,3 @@ - import os import sys from pathlib import Path @@ -7,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()] @@ -33,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: @@ -55,13 +52,13 @@ def require_env_var(env_var: str) -> str: SECRET_KEY = require_env_var('SECRET_KEY') DEBUG = get_env_bool('DEBUG', default=False) -ALLOWED_HOSTS = parse_env_list("ALLOWED_HOSTS") +ALLOWED_HOSTS = parse_env_list('ALLOWED_HOSTS', default='localhost,127.0.0.1') # --------------------------------------------------------- -# Application Definitions +# Installed Apps # --------------------------------------------------------- INSTALLED_APPS = [ - # Django defaults + # Django Defaults 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -69,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', @@ -78,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', @@ -114,11 +111,17 @@ def require_env_var(env_var: str) -> str: # --------------------------------------------------------- # CORS and CSRF Configuration # --------------------------------------------------------- -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") +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') + +# Ensure no trailing slashes and correct schemes +CORS_ALLOWED_ORIGINS = [origin.rstrip('/') for origin in CORS_ALLOWED_ORIGINS] +CORS_ALLOWED_ORIGIN_REGEXES = [regex.rstrip( + '/') for regex in CORS_ALLOWED_ORIGIN_REGEXES] +CSRF_TRUSTED_ORIGINS = [origin.rstrip('/') for origin in CSRF_TRUSTED_ORIGINS] +# Security cookies CSRF_COOKIE_SECURE = not DEBUG SESSION_COOKIE_SECURE = not DEBUG @@ -151,17 +154,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 @@ -179,7 +186,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 # --------------------------------------------------------- @@ -190,25 +196,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, - } - - DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage' - print("DEBUG=False: Using Cloudinary for media storage.") +# Cloudinary Configuration +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' # --------------------------------------------------------- # Default Primary Key Field Type @@ -228,13 +224,6 @@ def require_env_var(env_var: str) -> str: ], } -# --------------------------------------------------------- -# File Upload Limits -# --------------------------------------------------------- -MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB -DATA_UPLOAD_MAX_MEMORY_SIZE = MAX_UPLOAD_SIZE -FILE_UPLOAD_MAX_MEMORY_SIZE = MAX_UPLOAD_SIZE - # --------------------------------------------------------- # Admin and Authentication Settings # --------------------------------------------------------- @@ -254,6 +243,9 @@ def require_env_var(env_var: str) -> str: }, } +# --------------------------------------------------------- +# Quill Editor Configuration +# --------------------------------------------------------- QUILL_CONFIGS = { 'default': { 'theme': 'snow', @@ -281,10 +273,148 @@ def require_env_var(env_var: str) -> str: } # --------------------------------------------------------- -# Mode-Specific Logging +# File Upload Settings # --------------------------------------------------------- -if DEBUG: - print(f"Debug mode is: {DEBUG}") - print(f"Media files are stored in: {BASE_DIR / 'assets'}") -else: - print("Production mode is ON") +# Increase these values as needed to handle larger uploads +FILE_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10 MB +DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10 MB + +# --------------------------------------------------------- +# SSL and Proxy Settings (if behind a reverse proxy) +# --------------------------------------------------------- +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +USE_X_FORWARDED_HOST = True + +# --------------------------------------------------------- +# Logging Configuration +# --------------------------------------------------------- +LOG_DIR = BASE_DIR / 'logs' +LOG_DIR.mkdir(exist_ok=True) # Ensure log directory exists + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + # Formatters + 'formatters': { + 'verbose': { + 'format': '[%(asctime)s] %(levelname)s %(name)s [%(filename)s:%(lineno)d] %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S' + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + # Handlers + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + 'level': 'DEBUG' if DEBUG else 'INFO', + }, + 'file': { + 'class': 'logging.FileHandler', + 'filename': LOG_DIR / 'django.log', + 'formatter': 'verbose', + 'level': 'INFO', + }, + 'error_file': { + 'class': 'logging.FileHandler', + 'filename': LOG_DIR / 'django_errors.log', + 'formatter': 'verbose', + 'level': 'ERROR', + }, + }, + # Loggers + 'loggers': { + # Django Logs + 'django': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'INFO', + 'propagate': True, + }, + # Cloudinary Logs + 'cloudinary': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'INFO', + 'propagate': True, + }, + # Event App Logs + 'apps.event': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # CleanAir App Logs + 'apps.cleanair': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # AfricanCities App Logs + 'apps.africancities': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # Publications App Logs + 'apps.publications': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # Press App Logs + 'apps.press': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # Impact App Logs + 'apps.impact': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # FAQs App Logs + 'apps.faqs': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # Highlights App Logs + 'apps.highlights': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # Career App Logs + 'apps.career': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # Partners App Logs + 'apps.partners': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # Board App Logs + 'apps.board': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # Team App Logs + 'apps.team': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + # ExternalTeams App Logs + 'apps.externalteams': { + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + } +} diff --git a/src/website/entrypoint.sh b/src/website/entrypoint.sh index 783590129b..217438654c 100644 --- a/src/website/entrypoint.sh +++ b/src/website/entrypoint.sh @@ -13,4 +13,4 @@ python manage.py collectstatic --noinput # Start Gunicorn server to serve the Django application echo "Starting Gunicorn server..." -exec gunicorn core.wsgi:application --bind 0.0.0.0:8000 --timeout 600 --log-level info +exec gunicorn core.wsgi:application --bind 0.0.0.0:8000 --timeout 600 --workers 3 --log-level info