Files
polylan-submitter/polylan_submitter/polylan_submitter/settings.py
T

388 lines
12 KiB
Python

"""
Django settings for polylan_submitter project.
Generated by 'django-admin startproject' using Django 5.2.7.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
import os
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Optional: point this at a KEY=value file (see
# deploy/docker/polylan-submitter.env.dist for the template - unused in the
# Docker deploy itself, which passes env vars directly via `docker run
# --env-file` instead) to fill in the env vars read throughout this file.
# override=False so an already-exported env var always wins over the file -
# this only fills gaps, same as every os.environ.get() below already
# treating a merely-blank value as "use my own default" rather than "".
if os.environ.get("SETTINGS_ENV_FILE_LOCATION"):
load_dotenv(os.environ["SETTINGS_ENV_FILE_LOCATION"], override=False)
os.environ.setdefault("AWS_REQUEST_CHECKSUM_CALCULATION", "when_required")
os.environ.setdefault("AWS_RESPONSE_CHECKSUM_VALIDATION", "when_required")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
#
# All of the settings below that read from os.environ fall back to
# quick-start/dev-friendly defaults when the env var is unset, EXCEPT
# DJANGO_DEBUG, which defaults to False (secure by default per Django's own
# deployment checklist) - export DJANGO_DEBUG=true for local development.
#
# These all use `os.environ.get(X) or default` rather than
# `os.environ.get(X, default)`: an env var that's *defined but blank* (e.g.
# systemd's EnvironmentFile represents "not configured" as `KEY=`, not by
# omitting the line) must fall through to the default too, not become "".
# That distinction matters a lot here - `int(os.environ.get("REDIS_PORT",
# "6379"))` would still crash on an empty string.
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = (
os.environ.get("DJANGO_SECRET_KEY")
or "django-insecure-m0ivj_3gpbf281jl1$&n5pdo!5le(bp4z31u(1&4s=n#!tpy=n"
)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true"
ALLOWED_HOSTS = [
host
for host in (os.environ.get("DJANGO_ALLOWED_HOSTS") or "127.0.0.1,localhost").split(
","
)
if host
]
# Application definition
INSTALLED_APPS = [
"daphne",
"admin_interface",
"colorfield",
# "palette" provides the admin/base.html override (the command-palette
# UI) and must precede "palette.apps.PaletteAdminConfig", which stands
# in for "django.contrib.admin" (same app name, custom AdminSite) to
# wire the palette into the admin.
"palette.apps.PaletteConfig",
"palette.apps.PaletteAdminConfig",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_vite",
"django_rq",
"django_tasks_rq",
"django_object_actions",
"accounts",
"events",
"animations",
"opus_magnum",
"noita",
"games",
"market",
"submissions",
"clock",
"place",
]
if DEBUG:
# Was unconditionally added by settingsLocal.py before it was retired in
# favor of env-var-driven settings; kept dev-only since it pulls in
# shell_plus and similar dev tooling that has no place in production.
INSTALLED_APPS += ["django_extensions"]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
# Removed CASMiddleware to avoid conflicts with our custom backend
"events.middleware.EventContextMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "polylan_submitter.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.template.context_processors.i18n",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
ASGI_APPLICATION = "polylan_submitter.asgi.application"
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
#
# Falls back to sqlite (the quick-start default) when POSTGRES_DB isn't set,
# so a fresh checkout with no env configured still works out of the box.
if os.environ.get("POSTGRES_DB"):
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["POSTGRES_DB"],
"USER": os.environ.get("POSTGRES_USER") or "postgres",
"PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""),
"HOST": os.environ.get("POSTGRES_HOST") or "localhost",
"PORT": os.environ.get("POSTGRES_PORT") or "5432",
}
}
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
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
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = "en-us"
LANGUAGES = [
("en", "English"),
]
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = "static/"
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Custom User Model
AUTH_USER_MODEL = "accounts.CustomUser"
# Simple CAS Configuration
CAS_SERVER_URL = "https://polylan.ch/cas/"
# Enables POST /testing/login/ (see testing_auth_views.py), a
# username/password login for local-only accounts that authenticate through
# ModelBackend instead of SimpleCASBackend. This exists so load testing (see
# loadtest/locustfile.py) never sends traffic to the real CAS server at
# polylan.ch. Leave this off everywhere except a dedicated load-test
# environment - the "Create load test users" admin action on CustomUser
# (see accounts/admin.py) also hides itself without it, as a second layer
# of the same guard rail.
LOAD_TEST_LOGIN_ENABLED = (
os.environ.get("LOAD_TEST_LOGIN_ENABLED", "false").lower() == "true"
)
# Steam API Configuration
STEAM_API_KEY = os.environ.get("STEAM_API_KEY") or None # Set via environment variable
# Steam store info (price, discount, blurb) for the event's Steam games, refreshed
# in the background every STEAM_STORE_REFRESH_MINUTES (see games.tasks)
STEAM_STORE_REFRESH_MINUTES = int(os.environ.get("STEAM_STORE_REFRESH_MINUTES") or 360)
# Store region the prices are fetched for (ISO country code, drives the currency)
STEAM_STORE_COUNTRY = os.environ.get("STEAM_STORE_COUNTRY") or "ch"
# Base URL of the legacy PolyLAN site, used to sync AAndT matches/members
# (see submissions.services.aandt_sync)
AANDT_API_BASE_URL = os.environ.get("AANDT_API_BASE_URL") or "https://polylan.ch"
# Base URL of this site, used to build submission links pushed back to the
# legacy AAndT match messages (see submissions.services.aandt_sync)
SUBMITTER_BASE_URL = os.environ.get("SUBMITTER_BASE_URL") or "http://localhost:7777"
# File Upload Settings
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
# File Upload Limits
FILE_UPLOAD_MAX_MEMORY_SIZE = 256 * 1024 * 1024 # 256MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 256 * 1024 * 1024 # 256MB
# Redis host/port shared by the cache, channel layer, RQ, and place's canvas
# below - each still uses its own DB number, just on a common host.
REDIS_HOST = os.environ.get("REDIS_HOST") or "127.0.0.1"
REDIS_PORT = int(os.environ.get("REDIS_PORT") or "6379")
# Caching Configuration
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": f"redis://{REDIS_HOST}:{REDIS_PORT}/11",
"TIMEOUT": 300, # 5 minutes default
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
# Django Channels Configuration
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [(REDIS_HOST, REDIS_PORT)],
"serializer_format": "cjson",
},
},
}
# Redis for place's live pixel canvas (SETRANGE on the hot path - not a fit
# for django-redis's cache API, so it gets its own raw redis-py client).
PLACE_REDIS_URL = (
os.environ.get("PLACE_REDIS_URL") or f"redis://{REDIS_HOST}:{REDIS_PORT}/15"
)
# Django Tasks Configuration (with RQ backend)
TASKS = {
"default": {
"BACKEND": "django_tasks_rq.RQBackend",
}
}
RQ_QUEUES = {
"default": {
"HOST": REDIS_HOST,
"PORT": REDIS_PORT,
"DB": 12,
"DEFAULT_RESULT_TTL": 500,
}
}
# Allowed file types for submissions
ALLOWED_SUBMISSION_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
"video/mp4",
"video/webm",
]
# Authentication backends
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"simple_cas_backend.SimpleCASBackend",
]
# Login/Logout URLs
LOGIN_URL = "/cas/login/"
LOGOUT_URL = "/cas/logout/"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
DJANGO_VITE = {
"default": {
"dev_mode": DEBUG,
"dev_server_port": 5273,
}
}
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static_source"),
os.path.join(BASE_DIR, "static_source/vite"),
]
# Submission/media uploads always go to S3 - there's no local fallback for
# that, so these must be set (via env) wherever the app runs, dev included.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
AWS_S3_ENDPOINT_URL = (
os.environ.get("AWS_S3_ENDPOINT_URL") or "https://storage.polylan.ch"
)
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_STORAGE_BUCKET_NAME") or "dev-submissions"
_s3_storage_options = {
"bucket_name": AWS_STORAGE_BUCKET_NAME,
"endpoint_url": AWS_S3_ENDPOINT_URL,
"access_key": AWS_ACCESS_KEY_ID,
"secret_key": AWS_SECRET_ACCESS_KEY,
}
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": _s3_storage_options,
},
# Static files are always collected to local disk and served by
# WhiteNoise (see WhiteNoiseMiddleware in MIDDLEWARE above, which is
# likewise unconditional) - S3 is only for user-uploaded content
# ("default", above), never for the app's own CSS/JS/images.
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
SENTRY_ENVIRONMENT = os.environ.get("SENTRY_ENVIRONMENT") or (
"development" if DEBUG else "production"
)
SENTRY_DSN = (
os.environ.get("SENTRY_DSN")
or "https://cc62a4ce3f3470890b43accf02cc6d8c@sentry2.polylan.ch/12"
)
import sentry_sdk # noqa
sentry_sdk.init(
dsn=SENTRY_DSN,
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for tracing.
traces_sample_rate=1.0,
environment=SENTRY_ENVIRONMENT,
)