""" Django base settings for ThrillWiki project. These settings are common across all environments. """ from pathlib import Path import environ # Build paths BASE_DIR = Path(__file__).resolve().parent.parent.parent # Initialize environment variables env = environ.Env( DEBUG=(bool, False), ALLOWED_HOSTS=(list, []), ) # Read .env file if it exists environ.Env.read_env(BASE_DIR / '.env') # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = env('SECRET_KEY', default='django-insecure-change-this-in-production') # Application definition INSTALLED_APPS = [ # Django Unfold (must come before django.contrib.admin) 'unfold', 'unfold.contrib.filters', 'unfold.contrib.forms', 'unfold.contrib.import_export', # Django GIS (must come before admin for proper admin integration) 'django.contrib.gis', # Django apps 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Third-party apps 'import_export', 'rest_framework', 'rest_framework_simplejwt', 'ninja', 'django_filters', 'corsheaders', 'guardian', 'django_otp', 'django_otp.plugins.otp_totp', 'allauth', 'allauth.account', 'allauth.socialaccount', 'allauth.socialaccount.providers.google', 'allauth.socialaccount.providers.discord', 'django_celery_beat', 'django_celery_results', 'django_extensions', 'channels', 'storages', 'defender', # Local apps 'apps.core', 'apps.users', 'apps.entities', 'apps.moderation', 'apps.versioning', 'apps.media', 'apps.notifications', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django_otp.middleware.OTPMiddleware', 'allauth.account.middleware.AccountMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'defender.middleware.FailedLoginMiddleware', ] ROOT_URLCONF = 'config.urls' 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', ], }, }, ] WSGI_APPLICATION = 'config.wsgi.application' ASGI_APPLICATION = 'config.asgi.application' # Database DATABASES = { 'default': env.db('DATABASE_URL', default='postgresql://localhost/thrillwiki') } # 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 = 'UTC' USE_I18N = True USE_TZ = True # Static files STATIC_URL = 'static/' STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_DIRS = [BASE_DIR / 'static'] # Media files MEDIA_URL = 'media/' MEDIA_ROOT = BASE_DIR / 'media' # Default primary key field type DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # Custom User Model AUTH_USER_MODEL = 'users.User' # Authentication Backends AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', 'allauth.account.auth_backends.AuthenticationBackend', 'guardian.backends.ObjectPermissionBackend', ] # Django REST Framework REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticatedOrReadOnly', ], 'DEFAULT_FILTER_BACKENDS': [ 'django_filters.rest_framework.DjangoFilterBackend', ], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 50, } # JWT Settings from datetime import timedelta SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, 'ALGORITHM': 'HS256', 'SIGNING_KEY': SECRET_KEY, 'AUTH_HEADER_TYPES': ('Bearer',), } # CORS Settings CORS_ALLOWED_ORIGINS = env.list( 'CORS_ALLOWED_ORIGINS', default=['http://localhost:5173', 'http://localhost:3000'] ) CORS_ALLOW_CREDENTIALS = True # Redis Configuration REDIS_URL = env('REDIS_URL', default='redis://localhost:6379/0') # Caching CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': REDIS_URL, 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', 'PARSER_CLASS': 'redis.connection.HiredisParser', }, 'KEY_PREFIX': 'thrillwiki', 'TIMEOUT': 300, } } # Session Configuration SESSION_ENGINE = 'django.contrib.sessions.backends.cache' SESSION_CACHE_ALIAS = 'default' SESSION_COOKIE_AGE = 86400 * 30 # 30 days # Celery Configuration CELERY_BROKER_URL = env('CELERY_BROKER_URL', default=REDIS_URL) CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND', default='redis://localhost:6379/1') CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = TIME_ZONE CELERY_TASK_TRACK_STARTED = True CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes # Celery Beat Schedule (Periodic Tasks) from celery.schedules import crontab CELERY_BEAT_SCHEDULE = { # Clean up expired moderation locks every 5 minutes 'cleanup-expired-locks': { 'task': 'apps.moderation.tasks.cleanup_expired_locks', 'schedule': crontab(minute='*/5'), }, # Clean up expired JWT tokens daily at 2 AM 'cleanup-expired-tokens': { 'task': 'apps.users.tasks.cleanup_expired_tokens', 'schedule': crontab(hour=2, minute=0), }, # Update entity statistics every 6 hours 'update-all-statistics': { 'task': 'apps.entities.tasks.update_all_statistics', 'schedule': crontab(hour='*/6', minute=0), }, # Clean up old rejected photos weekly on Monday at 3 AM 'cleanup-rejected-photos': { 'task': 'apps.media.tasks.cleanup_rejected_photos', 'schedule': crontab(day_of_week=1, hour=3, minute=0), }, # Auto-unlock stale reviews every 30 minutes 'auto-unlock-stale-reviews': { 'task': 'apps.moderation.tasks.auto_unlock_stale_reviews', 'schedule': crontab(minute='*/30'), }, # Check moderation queue size every hour 'check-moderation-queue': { 'task': 'apps.moderation.tasks.notify_moderators_of_queue_size', 'schedule': crontab(minute=0), }, # Update photo statistics daily at 1 AM 'update-photo-statistics': { 'task': 'apps.media.tasks.update_photo_statistics', 'schedule': crontab(hour=1, minute=0), }, # Update moderation statistics daily at 1:30 AM 'update-moderation-statistics': { 'task': 'apps.moderation.tasks.update_moderation_statistics', 'schedule': crontab(hour=1, minute=30), }, # Update user statistics daily at 4 AM 'update-user-statistics': { 'task': 'apps.users.tasks.update_user_statistics', 'schedule': crontab(hour=4, minute=0), }, # Calculate global statistics every 12 hours 'calculate-global-statistics': { 'task': 'apps.entities.tasks.calculate_global_statistics', 'schedule': crontab(hour='*/12', minute=0), }, } # Django Channels CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { 'hosts': [REDIS_URL], }, }, } # Django Cacheops CACHEOPS_REDIS = REDIS_URL CACHEOPS_DEFAULTS = { 'timeout': 60*15 # 15 minutes } CACHEOPS = { 'entities.park': {'ops': 'all', 'timeout': 60*15}, 'entities.ride': {'ops': 'all', 'timeout': 60*15}, 'entities.company': {'ops': 'all', 'timeout': 60*15}, 'core.*': {'ops': 'all', 'timeout': 60*60}, # 1 hour for reference data '*.*': {'timeout': 60*60}, } # Django Allauth ACCOUNT_AUTHENTICATION_METHOD = 'email' ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_EMAIL_VERIFICATION = 'optional' SITE_ID = 1 # Site Configuration SITE_URL = env('SITE_URL', default='http://localhost:8000') DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL', default='noreply@thrillwiki.com') # CloudFlare Images CLOUDFLARE_ACCOUNT_ID = env('CLOUDFLARE_ACCOUNT_ID', default='') CLOUDFLARE_IMAGE_TOKEN = env('CLOUDFLARE_IMAGE_TOKEN', default='') CLOUDFLARE_IMAGE_HASH = env('CLOUDFLARE_IMAGE_HASH', default='') # Novu NOVU_API_KEY = env('NOVU_API_KEY', default='') NOVU_API_URL = env('NOVU_API_URL', default='https://api.novu.co') # Sentry SENTRY_DSN = env('SENTRY_DSN', default='') if SENTRY_DSN: import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.celery import CeleryIntegration sentry_sdk.init( dsn=SENTRY_DSN, integrations=[ DjangoIntegration(), CeleryIntegration(), ], traces_sample_rate=0.1, send_default_pii=False, ) # Logging Configuration LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', 'style': '{', }, 'simple': { 'format': '{levelname} {message}', 'style': '{', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'verbose', }, 'file': { 'class': 'logging.FileHandler', 'filename': BASE_DIR / 'logs' / 'django.log', 'formatter': 'verbose', }, }, 'root': { 'handlers': ['console'], 'level': 'INFO', }, 'loggers': { 'django': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False, }, 'apps': { 'handlers': ['console', 'file'], 'level': 'INFO', 'propagate': False, }, }, } # Rate Limiting RATELIMIT_ENABLE = True RATELIMIT_USE_CACHE = 'default' # Django Defender DEFENDER_LOGIN_FAILURE_LIMIT = 5 DEFENDER_COOLOFF_TIME = 300 # 5 minutes DEFENDER_LOCKOUT_TEMPLATE = 'defender/lockout.html' # Django Unfold Configuration UNFOLD = { "SITE_TITLE": "ThrillWiki Admin", "SITE_HEADER": "ThrillWiki Administration", "SITE_URL": "/", "SITE_ICON": { "light": lambda request: "/static/logo-light.svg", "dark": lambda request: "/static/logo-dark.svg", }, "SITE_SYMBOL": "🎢", "SHOW_HISTORY": True, "SHOW_VIEW_ON_SITE": True, "ENVIRONMENT": "django.conf.settings.DEBUG", "DASHBOARD_CALLBACK": "apps.entities.admin.dashboard_callback", "COLORS": { "primary": { "50": "220 252 231", "100": "187 247 208", "200": "134 239 172", "300": "74 222 128", "400": "34 197 94", "500": "22 163 74", "600": "21 128 61", "700": "22 101 52", "800": "22 78 43", "900": "20 83 45", "950": "5 46 22", }, }, "EXTENSIONS": { "modeltranslation": { "flags": { "en": "🇬🇧", "fr": "🇫🇷", "nl": "🇧🇪", }, }, }, "SIDEBAR": { "show_search": True, "show_all_applications": False, "navigation": [ { "title": "Dashboard", "icon": "dashboard", "link": lambda request: "/admin/", }, { "title": "Entities", "icon": "category", "items": [ { "title": "Parks", "icon": "park", "link": lambda request: "/admin/entities/park/", }, { "title": "Rides", "icon": "roller_skating", "link": lambda request: "/admin/entities/ride/", }, { "title": "Companies", "icon": "business", "link": lambda request: "/admin/entities/company/", }, { "title": "Ride Models", "icon": "construction", "link": lambda request: "/admin/entities/ridemodel/", }, ], }, { "title": "User Management", "icon": "people", "items": [ { "title": "Users", "icon": "person", "link": lambda request: "/admin/users/user/", }, { "title": "Groups", "icon": "group", "link": lambda request: "/admin/auth/group/", }, ], }, { "title": "Content", "icon": "folder", "items": [ { "title": "Media", "icon": "image", "link": lambda request: "/admin/media/", }, { "title": "Moderation", "icon": "verified_user", "link": lambda request: "/admin/moderation/", }, ], }, ], }, }