""" Base Django settings for thrillwiki project. Common settings shared across all environments. """ from datetime import timedelta import sys import warnings from pathlib import Path from typing import List from decouple import config # Suppress django-allauth deprecation warnings for dj_rest_auth compatibility # TODO: Remove this once dj_rest_auth is updated to work with the new ACCOUNT_SIGNUP_FIELDS format warnings.filterwarnings( "ignore", message=r"app_settings\.(USERNAME|EMAIL)_REQUIRED is deprecated", module="dj_rest_auth.registration.serializers" ) # Initialize environment variables with better defaults DEBUG = config("DEBUG", default=True, cast=bool) SECRET_KEY = config("SECRET_KEY") ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost,127.0.0.1", cast=lambda v: [s.strip() for s in str(v).split(',') if s.strip()]) DATABASE_URL = config("DATABASE_URL") CACHE_URL = config("CACHE_URL", default="locmem://") EMAIL_URL = config("EMAIL_URL", default="console://") REDIS_URL = config("REDIS_URL", default="redis://127.0.0.1:6379/1") CORS_ALLOWED_ORIGINS = config("CORS_ALLOWED_ORIGINS", default="", cast=lambda v: [s.strip() for s in str(v).split(',') if s.strip()]) API_RATE_LIMIT_PER_MINUTE = config("API_RATE_LIMIT_PER_MINUTE", default=60) API_RATE_LIMIT_PER_HOUR = config("API_RATE_LIMIT_PER_HOUR", default=1000) CACHE_MIDDLEWARE_SECONDS = config("CACHE_MIDDLEWARE_SECONDS", default=300) CACHE_MIDDLEWARE_KEY_PREFIX = config( "CACHE_MIDDLEWARE_KEY_PREFIX", default="thrillwiki" ) GDAL_LIBRARY_PATH = config( "GDAL_LIBRARY_PATH", default="/opt/homebrew/opt/gdal/lib/libgdal.dylib" ) GEOS_LIBRARY_PATH = config( "GEOS_LIBRARY_PATH", default="/opt/homebrew/opt/geos/lib/libgeos_c.dylib" ) # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent.parent # Add apps directory to sys.path so Django can find the apps apps_dir = BASE_DIR / "apps" if apps_dir.exists() and str(apps_dir) not in sys.path: sys.path.insert(0, str(apps_dir)) # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = config("SECRET_KEY") # Allowed hosts (already configured above) # CSRF trusted origins CSRF_TRUSTED_ORIGINS = config( "CSRF_TRUSTED_ORIGINS", default="", cast=lambda v: [s.strip() for s in str(v).split(',') if s.strip()] ) # Application definition DJANGO_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.sites", # "django.contrib.gis", # GeoDjango - Disabled temporarily for setup ] THIRD_PARTY_APPS = [ # Django Cloudflare Images Toolkit - moved to top to avoid circular imports "django_cloudflareimages_toolkit", "rest_framework", # Django REST Framework # Token authentication (kept for backward compatibility) "rest_framework.authtoken", "rest_framework_simplejwt", # JWT authentication "rest_framework_simplejwt.token_blacklist", # JWT token blacklist "dj_rest_auth", # REST authentication with JWT support "dj_rest_auth.registration", # REST registration support "drf_spectacular", # OpenAPI 3.0 documentation "corsheaders", # CORS headers for API "pghistory", # django-pghistory "pgtrigger", # Required by django-pghistory "allauth", "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.google", "allauth.socialaccount.providers.discord", "django_cleanup", "django_filters", "django_htmx", "whitenoise", "django_tailwind_cli", "autocomplete", # Django HTMX Autocomplete "django_cotton", # Django Cotton for component templates "health_check", # Health checks "health_check.db", "health_check.cache", "health_check.storage", "health_check.contrib.migrations", "health_check.contrib.redis", "django_celery_beat", # Celery beat scheduler "django_celery_results", # Celery result backend "django_extensions", # Django Extensions for enhanced development tools ] LOCAL_APPS = [ "apps.core", "apps.accounts", "apps.parks", "apps.rides", "django_forwardemail", # New PyPI package for email service "apps.moderation", ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS MIDDLEWARE = [ "django.middleware.cache.UpdateCacheMiddleware", "corsheaders.middleware.CorsMiddleware", # CORS middleware for API "django.middleware.security.SecurityMiddleware", "apps.core.middleware.security_headers.SecurityHeadersMiddleware", # Modern security headers "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "apps.core.middleware.analytics.PgHistoryContextMiddleware", # Add history context tracking "allauth.account.middleware.AccountMiddleware", "django.middleware.cache.FetchFromCacheMiddleware", "django_htmx.middleware.HtmxMiddleware", ] ROOT_URLCONF = "thrillwiki.urls" # Add a toggle to enable/disable Django template support via env var # Use a distinct environment variable name so it doesn't collide with Django's TEMPLATES setting TEMPLATES_ENABLED = config("TEMPLATES_ENABLED", default=True, cast=bool) # Conditional TEMPLATES configuration if TEMPLATES_ENABLED: TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", "apps.moderation.context_processors.moderation_access", ] }, } ] else: # When templates are disabled, we still need APP_DIRS=True for DRF Spectacular to work TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, # Changed from False to True to support DRF Spectacular templates "DIRS": [BASE_DIR / "templates/" / "404"], "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", "apps.moderation.context_processors.moderation_access", ] }, } ] WSGI_APPLICATION = "thrillwiki.wsgi.application" # Cloudflare Images Settings - Updated for django-cloudflareimages-toolkit CLOUDFLARE_IMAGES = { 'ACCOUNT_ID': config("CLOUDFLARE_IMAGES_ACCOUNT_ID", default="development"), 'API_TOKEN': config("CLOUDFLARE_IMAGES_API_TOKEN", default="development"), 'ACCOUNT_HASH': config("CLOUDFLARE_IMAGES_ACCOUNT_HASH", default="development"), # Optional settings 'DEFAULT_VARIANT': 'public', 'UPLOAD_TIMEOUT': 300, 'WEBHOOK_SECRET': config("CLOUDFLARE_IMAGES_WEBHOOK_SECRET", default="development"), 'CLEANUP_EXPIRED_HOURS': 24, 'MAX_FILE_SIZE': 10 * 1024 * 1024, # 10MB 'ALLOWED_FORMATS': ['jpeg', 'png', 'gif', 'webp'], 'REQUIRE_SIGNED_URLS': False, 'DEFAULT_METADATA': {}, } # Storage configuration STORAGES = { "default": { "BACKEND": "django.core.files.storage.FileSystemStorage", "OPTIONS": { "location": str(BASE_DIR.parent / "shared" / "media"), }, }, "staticfiles": { "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", "OPTIONS": { "location": str(BASE_DIR / "staticfiles"), }, }, } # Password validation AUTH_PASSWORD_VALIDATORS = [ { "NAME": ( "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" ), }, { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization LANGUAGE_CODE = "en-us" TIME_ZONE = "America/New_York" USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) STATIC_URL = "static/" STATICFILES_DIRS = [BASE_DIR / "static"] STATIC_ROOT = BASE_DIR / "staticfiles" # Media files - point to shared/media directory MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR.parent / "shared" / "media" # Default primary key field type DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # Authentication settings AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "allauth.account.auth_backends.AuthenticationBackend", ] # django-allauth settings SITE_ID = 1 # CORRECTED: Django allauth still expects the old format with asterisks for required fields # The deprecation warnings are from dj_rest_auth, not our configuration ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"] ACCOUNT_LOGIN_METHODS = {"email", "username"} ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_CHANGE = True ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_RESEND = True ACCOUNT_REAUTHENTICATION_REQUIRED = True ACCOUNT_EMAIL_NOTIFICATIONS = True ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False LOGIN_REDIRECT_URL = "/" ACCOUNT_LOGOUT_REDIRECT_URL = "/" # Custom adapters ACCOUNT_ADAPTER = "apps.accounts.adapters.CustomAccountAdapter" SOCIALACCOUNT_ADAPTER = "apps.accounts.adapters.CustomSocialAccountAdapter" # Social account settings SOCIALACCOUNT_PROVIDERS = { "google": { "SCOPE": [ "profile", "email", ], "AUTH_PARAMS": {"access_type": "online"}, }, "discord": { "SCOPE": ["identify", "email"], "OAUTH_PKCE_ENABLED": True, }, } # Additional social account settings SOCIALACCOUNT_LOGIN_ON_GET = True SOCIALACCOUNT_AUTO_SIGNUP = False SOCIALACCOUNT_STORE_TOKENS = True # Custom User Model AUTH_USER_MODEL = "accounts.User" # Autocomplete configuration AUTOCOMPLETE_BLOCK_UNAUTHENTICATED = False # Tailwind configuration TAILWIND_CLI_CONFIG_FILE = "tailwind.config.js" TAILWIND_CLI_SRC_CSS = "static/css/src/input.css" TAILWIND_CLI_DIST_CSS = "css/tailwind.css" # Test runner TEST_RUNNER = "django.test.runner.DiscoverRunner" # Road Trip Service Settings ROADTRIP_CACHE_TIMEOUT = 3600 * 24 # 24 hours for geocoding ROADTRIP_ROUTE_CACHE_TIMEOUT = 3600 * 6 # 6 hours for routes ROADTRIP_MAX_REQUESTS_PER_SECOND = 1 # Respect OSM rate limits ROADTRIP_USER_AGENT = config("ROADTRIP_USER_AGENT", default="ThrillWiki/1.0 (+https://thrillwiki.com)") ROADTRIP_REQUEST_TIMEOUT = 10 # seconds ROADTRIP_MAX_RETRIES = 3 ROADTRIP_BACKOFF_FACTOR = 2 # Frontend URL Configuration FRONTEND_DOMAIN = config("FRONTEND_DOMAIN", default="https://thrillwiki.com") # ForwardEmail Configuration FORWARD_EMAIL_BASE_URL = config( "FORWARD_EMAIL_BASE_URL", default="https://api.forwardemail.net") FORWARD_EMAIL_API_KEY = config("FORWARD_EMAIL_API_KEY", default="") FORWARD_EMAIL_DOMAIN = config("FORWARD_EMAIL_DOMAIN", default="") # Django REST Framework Settings REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework_simplejwt.authentication.JWTAuthentication", "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.TokenAuthentication", # Kept for backward compatibility ], "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticated", ], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 20, "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning", "DEFAULT_VERSION": "v1", "ALLOWED_VERSIONS": ["v1"], "DEFAULT_RENDERER_CLASSES": [ "rest_framework.renderers.JSONRenderer", "rest_framework.renderers.BrowsableAPIRenderer", ], "DEFAULT_PARSER_CLASSES": [ "rest_framework.parsers.JSONParser", "rest_framework.parsers.FormParser", "rest_framework.parsers.MultiPartParser", ], "EXCEPTION_HANDLER": "apps.core.api.exceptions.custom_exception_handler", "DEFAULT_FILTER_BACKENDS": [ "django_filters.rest_framework.DjangoFilterBackend", "rest_framework.filters.SearchFilter", "rest_framework.filters.OrderingFilter", ], "TEST_REQUEST_DEFAULT_FORMAT": "json", "NON_FIELD_ERRORS_KEY": "non_field_errors", "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } # CORS Settings for API CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_ALL_ORIGINS = config( "CORS_ALLOW_ALL_ORIGINS", default=False, cast=bool ) # type: ignore[arg-type] API_RATE_LIMIT_PER_MINUTE = config( "API_RATE_LIMIT_PER_MINUTE", default=60, cast=int ) # type: ignore[arg-type] API_RATE_LIMIT_PER_HOUR = config( "API_RATE_LIMIT_PER_HOUR", default=1000, cast=int ) # type: ignore[arg-type] SPECTACULAR_SETTINGS = { "TITLE": "ThrillWiki API", "DESCRIPTION": "Comprehensive theme park and ride information API", "VERSION": "1.0.0", "SERVE_INCLUDE_SCHEMA": False, "COMPONENT_SPLIT_REQUEST": True, "TAGS": [ {"name": "Parks", "description": "Theme park operations"}, {"name": "Rides", "description": "Ride information and management"}, { "name": "Statistics", "description": "Statistical endpoints providing aggregated data and insights", }, ], "SCHEMA_PATH_PREFIX": "/api/", "DEFAULT_GENERATOR_CLASS": "drf_spectacular.generators.SchemaGenerator", "DEFAULT_AUTO_SCHEMA": "drf_spectacular.openapi.AutoSchema", "PREPROCESSING_HOOKS": [ "api.v1.schema.custom_preprocessing_hook", ], # "POSTPROCESSING_HOOKS": [ # "api.v1.schema.custom_postprocessing_hook", # ], "SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"], "SWAGGER_UI_SETTINGS": { "deepLinking": True, "persistAuthorization": True, "displayOperationId": False, "displayRequestDuration": True, }, "REDOC_UI_SETTINGS": { "hideDownloadButton": False, "hideHostname": False, "hideLoading": False, "hideSchemaPattern": True, "scrollYOffset": 0, "theme": {"colors": {"primary": {"main": "#1976d2"}}}, }, } # Health Check Configuration HEALTH_CHECK = { "DISK_USAGE_MAX": 90, # Fail if disk usage is over 90% "MEMORY_MIN": 100, # Fail if less than 100MB available memory } # Custom health check backends HEALTH_CHECK_BACKENDS = [ "health_check.db", "health_check.cache", "health_check.storage", "core.health_checks.custom_checks.CacheHealthCheck", "core.health_checks.custom_checks.DatabasePerformanceCheck", "core.health_checks.custom_checks.ApplicationHealthCheck", "core.health_checks.custom_checks.ExternalServiceHealthCheck", "core.health_checks.custom_checks.DiskSpaceHealthCheck", ] # Enhanced Cache Configuration DJANGO_REDIS_CACHE_BACKEND = "django_redis.cache.RedisCache" DJANGO_REDIS_CLIENT_CLASS = "django_redis.client.DefaultClient" CACHES = { "default": { "BACKEND": DJANGO_REDIS_CACHE_BACKEND, # pyright: ignore[reportArgumentType] # type: ignore "LOCATION": config("REDIS_URL", default="redis://127.0.0.1:6379/1"), "OPTIONS": { "CLIENT_CLASS": DJANGO_REDIS_CLIENT_CLASS, "PARSER_CLASS": "redis.connection.HiredisParser", "CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool", "CONNECTION_POOL_CLASS_KWARGS": { "max_connections": 50, "timeout": 20, }, "COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor", "IGNORE_EXCEPTIONS": True, }, "KEY_PREFIX": "thrillwiki", "VERSION": 1, }, "sessions": { "BACKEND": DJANGO_REDIS_CACHE_BACKEND, "LOCATION": config("REDIS_URL", default="redis://127.0.0.1:6379/2"), "OPTIONS": { "CLIENT_CLASS": DJANGO_REDIS_CLIENT_CLASS, }, }, "api": { "BACKEND": DJANGO_REDIS_CACHE_BACKEND, "LOCATION": config("REDIS_URL", default="redis://127.0.0.1:6379/3"), "OPTIONS": { "CLIENT_CLASS": DJANGO_REDIS_CLIENT_CLASS, }, }, } # Use Redis for sessions SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "sessions" SESSION_COOKIE_AGE = 86400 # 24 hours # Cache middleware settings CACHE_MIDDLEWARE_SECONDS = 300 # 5 minutes CACHE_MIDDLEWARE_KEY_PREFIX = "thrillwiki" # JWT Settings SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), # 1 hour "REFRESH_TOKEN_LIFETIME": timedelta(days=7), # 7 days "ROTATE_REFRESH_TOKENS": True, "BLACKLIST_AFTER_ROTATION": True, "UPDATE_LAST_LOGIN": True, "ALGORITHM": "HS256", "SIGNING_KEY": SECRET_KEY, "VERIFYING_KEY": None, "AUDIENCE": None, "ISSUER": None, "JWK_URL": None, "LEEWAY": 0, "AUTH_HEADER_TYPES": ("Bearer",), "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", "USER_ID_FIELD": "id", "USER_ID_CLAIM": "user_id", "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule", "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), "TOKEN_TYPE_CLAIM": "token_type", "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", "JTI_CLAIM": "jti", "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", "SLIDING_TOKEN_LIFETIME": timedelta(minutes=60), "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), } # dj-rest-auth settings REST_AUTH = { "USE_JWT": True, "JWT_AUTH_COOKIE": "thrillwiki-auth", "JWT_AUTH_REFRESH_COOKIE": "thrillwiki-refresh", "JWT_AUTH_SECURE": not DEBUG, # Use secure cookies in production "JWT_AUTH_HTTPONLY": True, "JWT_AUTH_SAMESITE": "Lax", "JWT_AUTH_RETURN_EXPIRATION": True, "JWT_TOKEN_CLAIMS_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer", }