feat(config): add email backend and update settings for production
Support email sending via SMTP, configure static/media handling, set correct timezone, enable Swagger UI, and add external access routes.
This commit is contained in:
6 files changed
+643
-170
No files matched your search
+6
-2
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import environ
|
import environ
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
@@ -9,5 +10,8 @@ env_path = Path(__file__).resolve().parent.parent / ".env"
|
|||||||
env = environ.Env()
|
env = environ.Env()
|
||||||
environ.Env.read_env(env_path)
|
environ.Env.read_env(env_path)
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="config.settings.production"))
|
os.environ.setdefault(
|
||||||
application = get_asgi_application()
|
"DJANGO_SETTINGS_MODULE",
|
||||||
|
env("DJANGO_SETTINGS_MODULE", default="config.settings.production"),
|
||||||
|
)
|
||||||
|
application = get_asgi_application()
|
||||||
+71
-73
@@ -1,9 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
generated with djinit
|
Common settings shared between development and production environments.
|
||||||
Common settings shared between development and production environment
|
Environment-specific settings (DEBUG, SECRET_KEY, DATABASES, etc.)
|
||||||
|
live in development.py and production.py respectively.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -16,19 +16,18 @@ environ.Env.read_env(env_path)
|
|||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# ── Installed Apps ────────────────────────────────────────────────────────────
|
||||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
THIRD_PARTY_APPS = [
|
THIRD_PARTY_APPS = [
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"rest_framework_simplejwt",
|
"rest_framework_simplejwt",
|
||||||
"rest_framework_simplejwt.token_blacklist",
|
|
||||||
"corsheaders",
|
"corsheaders",
|
||||||
"drf_yasg",
|
|
||||||
"django_tailwind_cli",
|
"django_tailwind_cli",
|
||||||
"django_htmx",
|
"django_htmx",
|
||||||
|
"django_filters",
|
||||||
|
"import_export",
|
||||||
|
"drf_spectacular",
|
||||||
|
"django_q",
|
||||||
]
|
]
|
||||||
|
|
||||||
USER_DEFINED_APPS = [
|
USER_DEFINED_APPS = [
|
||||||
@@ -53,6 +52,8 @@ BUILT_IN_APPS = [
|
|||||||
INSTALLED_APPS = BUILT_IN_APPS + THIRD_PARTY_APPS + USER_DEFINED_APPS
|
INSTALLED_APPS = BUILT_IN_APPS + THIRD_PARTY_APPS + USER_DEFINED_APPS
|
||||||
INSTALLED_APPS.insert(0, "jazzmin")
|
INSTALLED_APPS.insert(0, "jazzmin")
|
||||||
|
|
||||||
|
# ── Middleware ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"corsheaders.middleware.CorsMiddleware",
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
@@ -68,16 +69,20 @@ MIDDLEWARE = [
|
|||||||
|
|
||||||
ROOT_URLCONF = "config.urls"
|
ROOT_URLCONF = "config.urls"
|
||||||
|
|
||||||
|
# ── Templates ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
"DIRS": [],
|
"DIRS": [BASE_DIR / "templates"],
|
||||||
"APP_DIRS": True,
|
"APP_DIRS": True,
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"context_processors": [
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
"django.template.context_processors.request",
|
"django.template.context_processors.request",
|
||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
"src.dashboard.context_processors.site_stats",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -85,9 +90,9 @@ TEMPLATES = [
|
|||||||
|
|
||||||
WSGI_APPLICATION = "config.wsgi.application"
|
WSGI_APPLICATION = "config.wsgi.application"
|
||||||
|
|
||||||
|
# ── Authentication ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Password validation
|
AUTH_USER_MODEL = "accounts.User"
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
@@ -104,59 +109,59 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
LOGIN_URL = "login"
|
||||||
|
LOGIN_REDIRECT_URL = "dashboard"
|
||||||
|
|
||||||
# Internationalization
|
# ── Internationalisation ──────────────────────────────────────────────────────
|
||||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = "en-us"
|
LANGUAGE_CODE = "en-us"
|
||||||
TIME_ZONE = "UTC"
|
TIME_ZONE = "Asia/Kathmandu"
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# Static files and media files
|
# ── Static & Media Files ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||||
|
STATICFILES_DIRS = [BASE_DIR / "static/"]
|
||||||
|
|
||||||
MEDIA_URL = "/media/"
|
MEDIA_URL = "/media/"
|
||||||
MEDIA_ROOT = BASE_DIR / "media"
|
MEDIA_ROOT = BASE_DIR / "media"
|
||||||
|
|
||||||
STORAGES = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
|
||||||
},
|
|
||||||
"staticfiles": {
|
|
||||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
# REST Framework Settings
|
# ── Tailwind ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TAILWIND_CLI_USE_DAISY_UI = False
|
||||||
|
TAILWIND_CLI_SRC_CSS = "static/src/tailwind.css"
|
||||||
|
|
||||||
|
# ── REST Framework ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
"DEFAULT_RENDERER_CLASSES": [
|
|
||||||
"rest_framework.renderers.JSONRenderer",
|
|
||||||
],
|
|
||||||
"DEFAULT_PARSER_CLASSES": [
|
|
||||||
"rest_framework.parsers.JSONParser",
|
|
||||||
],
|
|
||||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||||
"rest_framework.authentication.SessionAuthentication",
|
"rest_framework.authentication.SessionAuthentication",
|
||||||
),
|
),
|
||||||
"DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema",
|
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
|
||||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||||
"PAGE_SIZE": 20,
|
"PAGE_SIZE": 50,
|
||||||
|
"DEFAULT_FILTER_BACKENDS": (
|
||||||
|
"django_filters.rest_framework.DjangoFilterBackend",
|
||||||
|
"rest_framework.filters.SearchFilter",
|
||||||
|
"rest_framework.filters.OrderingFilter",
|
||||||
|
),
|
||||||
|
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||||
}
|
}
|
||||||
|
|
||||||
# JWT Settings
|
|
||||||
SIMPLE_JWT = {
|
SIMPLE_JWT = {
|
||||||
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
|
"ACCESS_TOKEN_LIFETIME": timedelta(hours=1),
|
||||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=3),
|
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
|
||||||
"ROTATE_REFRESH_TOKENS": True,
|
"ROTATE_REFRESH_TOKENS": True,
|
||||||
"BLACKLIST_AFTER_ROTATION": True,
|
"AUTH_HEADER_TYPES": ("Bearer",),
|
||||||
}
|
}
|
||||||
|
|
||||||
# CORS Settings
|
# ── CORS ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
CORS_ALLOW_METHODS = [
|
CORS_ALLOW_METHODS = [
|
||||||
"DELETE",
|
"DELETE",
|
||||||
"GET",
|
"GET",
|
||||||
@@ -179,42 +184,35 @@ CORS_ALLOW_HEADERS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
|
CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
|
||||||
# Security Settings
|
|
||||||
|
# ── Security (shared baseline) ────────────────────────────────────────────────
|
||||||
|
|
||||||
SECURE_BROWSER_XSS_FILTER = True
|
SECURE_BROWSER_XSS_FILTER = True
|
||||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
X_FRAME_OPTIONS = "DENY"
|
X_FRAME_OPTIONS = "DENY"
|
||||||
|
|
||||||
# for tailwind
|
# ── Site ──────────────────────────────────────────────────────────────────────
|
||||||
STATICFILES_DIRS = [BASE_DIR / "static/"]
|
|
||||||
TAILWIND_CLI_VERSION = "2.7.8"
|
BASE_URL = env("BASE_URL", default="http://localhost:8000")
|
||||||
TAILWIND_CLI_USE_DAISY_UI = True
|
SITE_NAME = env("SITE_NAME", default="Project Management")
|
||||||
TAILWIND_CLI_CONFIG = {
|
|
||||||
"theme": {
|
# ── API Docs ──────────────────────────────────────────────────────────────────
|
||||||
"extend": {
|
|
||||||
"colors": {
|
SPECTACULAR_SETTINGS = {
|
||||||
"primary": "#DC2626",
|
"TITLE": "Project Management API",
|
||||||
"secondary": "#EF4444",
|
"DESCRIPTION": "Django port of Project Management",
|
||||||
"accent": "#FCFECC",
|
"VERSION": "1.0.0",
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
# ── Django Q2 ─────────────────────────────────────────────────────────────────
|
||||||
"daisyui": {
|
# timeout must be set and less than retry to avoid tasks being re-triggered early
|
||||||
"themes": [
|
|
||||||
{
|
Q_CLUSTER = {
|
||||||
"light": {
|
"name": "project_management",
|
||||||
"primary": "#DC2626",
|
"workers": 2,
|
||||||
"secondary": "#EF4444",
|
"timeout": 60, # seconds before a task is killed
|
||||||
"accent": "#FCFECC",
|
"retry": 120, # seconds before a failed task is retried (must be > timeout)
|
||||||
"neutral": "#1a1a2e",
|
"queue_limit": 50,
|
||||||
"base-100": "#ffffff",
|
"bulk": 10,
|
||||||
"base-200": "#f8fafc",
|
"orm": "default",
|
||||||
"base-300": "#e2e8f0",
|
|
||||||
"info": "#0E4D80",
|
|
||||||
"success": "#16a34a",
|
|
||||||
"warning": "#f59e0b",
|
|
||||||
"error": "#DC2626",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
"""generated with djinit"""
|
"""Development settings — SQLite, console email, CORS open, debug on."""
|
||||||
|
|
||||||
from .base import * # noqa: F401, F403
|
from .base import * # noqa: F401, F403
|
||||||
|
|
||||||
|
# ── Core ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
SECRET_KEY = "u4lMI*X9ArV9+CUWTOdYdh_9FcAx%$VVzlK&Kb(%1WA@C4Dkqi"
|
SECRET_KEY = "u4lMI*X9ArV9+CUWTOdYdh_9FcAx%$VVzlK&Kb(%1WA@C4Dkqi"
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
# Database
|
# ── Database (SQLite) ─────────────────────────────────────────────────────────
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
@@ -30,13 +28,39 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Email backend for development
|
# ── Static Files ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
# Use simple storage in dev — no compression/manifest needed
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Tailwind ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TAILWIND_CLI_VERSION = "4.0.0"
|
||||||
|
|
||||||
|
# ── Email ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
|
|
||||||
# CORS settings for development
|
# ── CORS ──────────────────────────────────────────────────────────────────────
|
||||||
# Allow all origins in development (convenient but not secure)
|
|
||||||
CORS_ALLOW_ALL_ORIGINS = True
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
CORS_ALLOW_CREDENTIALS = True
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
|
||||||
# Disable HTTPS redirect in development
|
# ── Security ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
SECURE_SSL_REDIRECT = False
|
SECURE_SSL_REDIRECT = False
|
||||||
|
|
||||||
|
# ── Google OAuth ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
GOOGLE_CLIENT_ID = env("GOOGLE_CLIENT_ID", default="")
|
||||||
|
GOOGLE_CLIENT_SECRET = env("GOOGLE_CLIENT_SECRET", default="")
|
||||||
|
GOOGLE_REDIRECT_URI = env(
|
||||||
|
"GOOGLE_REDIRECT_URI", default="http://localhost:8000/api/auth/google/callback/"
|
||||||
|
)
|
||||||
@@ -1,57 +1,74 @@
|
|||||||
"""generated with djinit"""
|
"""Production settings — MySQL, SMTP email, strict CORS, HTTPS enforced."""
|
||||||
|
|
||||||
from .base import * # noqa: F401, F403
|
from .base import * # noqa: F401, F403
|
||||||
|
|
||||||
DEBUG = False
|
# ── Core ──────────────────────────────────────────────────────────────────────
|
||||||
SECRET_KEY = env('SECRET_KEY')
|
|
||||||
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
|
DEBUG = False
|
||||||
|
SECRET_KEY = env("SECRET_KEY")
|
||||||
|
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[])
|
||||||
|
|
||||||
|
# ── Database (MySQL) ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Database
|
|
||||||
# Using individual database parameters
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
"ENGINE": "django.db.backends.mysql",
|
||||||
'PORT': env('DB_PORT', default='3306'),
|
"NAME": env("DB_NAME"),
|
||||||
'NAME': env('DB_NAME'),
|
"USER": env("DB_USER"),
|
||||||
'USER': env('DB_USER'),
|
"PASSWORD": env("DB_PASSWORD"),
|
||||||
'PASSWORD': env('DB_PASSWORD'),
|
"HOST": env("DB_HOST"),
|
||||||
'HOST': env('DB_HOST'),
|
"PORT": env("DB_PORT", default="3306"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# CORS settings for production
|
# ── Static Files ──────────────────────────────────────────────────────────────
|
||||||
# Filter out empty strings from FRONTEND_URL
|
|
||||||
CORS_ALLOWED_ORIGINS = env.list('FRONTEND_URL', default=[])
|
|
||||||
CORS_ALLOW_CREDENTIALS = True
|
|
||||||
|
|
||||||
# Additional CORS security for production
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
# Compressed + hashed filenames for long-term caching in production
|
||||||
|
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Tailwind ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TAILWIND_CLI_VERSION = "4.1.3"
|
||||||
|
TAILWIND_CLI_AUTOMATIC_DOWNLOAD = False # use pre-installed CLI in CI/CD
|
||||||
|
TAILWIND_CLI_DIST_CSS = "static/css/tailwind.min.css"
|
||||||
|
|
||||||
|
# ── Email (SMTP) ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||||
|
EMAIL_HOST = env("EMAIL_HOST", default="smtp.gmail.com")
|
||||||
|
EMAIL_PORT = env.int("EMAIL_PORT", default=587)
|
||||||
|
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", default=True)
|
||||||
|
EMAIL_HOST_USER = env("EMAIL_HOST_USER", default="")
|
||||||
|
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD", default="")
|
||||||
|
DEFAULT_FROM_EMAIL = env("EMAIL_HOST_USER", default="")
|
||||||
|
SERVER_EMAIL = env("EMAIL_HOST_USER", default="")
|
||||||
|
|
||||||
|
# ── CORS ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CORS_ALLOWED_ORIGINS = env.list("FRONTEND_URL", default=[])
|
||||||
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
CORS_ALLOW_PRIVATE_NETWORK = False
|
CORS_ALLOW_PRIVATE_NETWORK = False
|
||||||
|
|
||||||
# Email settings
|
# ── Security ──────────────────────────────────────────────────────────────────
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
|
||||||
EMAIL_HOST = env('EMAIL_HOST', default="smtp.gmail.com")
|
|
||||||
EMAIL_PORT = env.int('EMAIL_PORT', default=587)
|
|
||||||
EMAIL_USE_TLS = env.bool('EMAIL_USE_TLS', default=True)
|
|
||||||
EMAIL_HOST_USER = env('EMAIL_HOST_USER', default="")
|
|
||||||
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD', default="")
|
|
||||||
DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER', default="")
|
|
||||||
SERVER_EMAIL = env('EMAIL_HOST_USER', default="")
|
|
||||||
|
|
||||||
# Security settings for production
|
|
||||||
SECURE_SSL_REDIRECT = True
|
SECURE_SSL_REDIRECT = True
|
||||||
SECURE_HSTS_SECONDS = 31536000
|
SECURE_HSTS_SECONDS = 31536000
|
||||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||||
SECURE_HSTS_PRELOAD = True
|
SECURE_HSTS_PRELOAD = True
|
||||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
|
||||||
# Session settings
|
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
|
|
||||||
# Static files
|
# ── Google OAuth ──────────────────────────────────────────────────────────────
|
||||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
|
||||||
|
|
||||||
## Tailwind
|
GOOGLE_CLIENT_ID = env("GOOGLE_CLIENT_ID")
|
||||||
TAILWIND_CLI_VERSION = "4.1.3" # Pin version
|
GOOGLE_CLIENT_SECRET = env("GOOGLE_CLIENT_SECRET")
|
||||||
TAILWIND_CLI_AUTOMATIC_DOWNLOAD = False # Use pre-installed CLI
|
GOOGLE_REDIRECT_URI = env("GOOGLE_REDIRECT_URI")
|
||||||
TAILWIND_CLI_DIST_CSS = "static/css/tailwind.min.css"
|
|
||||||
+473
-47
@@ -1,58 +1,484 @@
|
|||||||
"""generated with djinit"""
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.urls import path, re_path
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.views.static import serve
|
from django.conf.urls.static import static
|
||||||
from django.urls import include
|
from django.contrib import admin
|
||||||
from drf_yasg.views import get_schema_view
|
from django.contrib.auth import views as auth_views
|
||||||
from drf_yasg import openapi
|
from django.contrib.auth.decorators import login_required
|
||||||
from rest_framework_simplejwt.views import (
|
from django.urls import include, path
|
||||||
TokenObtainPairView,
|
from django.views.generic import RedirectView, TemplateView
|
||||||
TokenRefreshView,
|
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||||
TokenBlacklistView,
|
|
||||||
|
from src.dashboard import board_views, list_views, page_views, views
|
||||||
|
|
||||||
|
api_patterns = [
|
||||||
|
path("auth/", include("src.accounts.urls")),
|
||||||
|
path("projects/", include("src.projects.urls")),
|
||||||
|
path("tickets/", include("src.tickets.urls")),
|
||||||
|
path("epics/", include("src.epics.urls")),
|
||||||
|
path("notes/", include("src.notes.urls")),
|
||||||
|
path("notifications/", include("src.notifications.urls")),
|
||||||
|
path("external/", include("src.external.urls")),
|
||||||
|
]
|
||||||
|
|
||||||
|
login_req = login_required
|
||||||
|
|
||||||
|
# --- Core/Dashboard App ---
|
||||||
|
core_dashboard_patterns = [
|
||||||
|
path("", login_req(views.dashboard), name="dashboard"),
|
||||||
|
path("stats-cards/", login_req(views.stats_cards), name="stats-cards"),
|
||||||
|
path(
|
||||||
|
"charts/tickets-per-project/",
|
||||||
|
login_req(views.tickets_per_project_chart),
|
||||||
|
name="chart-tickets-per-project",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"charts/user-statistics/",
|
||||||
|
login_req(views.user_statistics_chart),
|
||||||
|
name="chart-user-statistics",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"charts/user-statistics-data/",
|
||||||
|
login_req(views.user_statistics_data),
|
||||||
|
name="chart-user-statistics-data",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"charts/monthly-trend/",
|
||||||
|
login_req(views.monthly_trend_chart),
|
||||||
|
name="chart-monthly-trend",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"recent-activity/all/",
|
||||||
|
login_req(views.recent_activity_all),
|
||||||
|
name="recent-activity-all",
|
||||||
|
),
|
||||||
|
path("recent-activity/", login_req(views.recent_activity), name="recent-activity"),
|
||||||
|
path(
|
||||||
|
"project-timeline-widget/",
|
||||||
|
login_req(views.project_timeline_widget),
|
||||||
|
name="project-timeline-widget",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Notifications App ---
|
||||||
|
notifications_dashboard_patterns = [
|
||||||
|
path(
|
||||||
|
"notifications/unread-count/",
|
||||||
|
login_req(list_views.notifications_unread_count),
|
||||||
|
name="notif-unread-count",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"notifications/dropdown/",
|
||||||
|
login_req(page_views.notifications_dropdown),
|
||||||
|
name="notifications-dropdown",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"notifications/",
|
||||||
|
login_req(page_views.notifications_list),
|
||||||
|
name="notifications-list",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"notifications/mark-all-read/",
|
||||||
|
login_req(page_views.notification_mark_all_read),
|
||||||
|
name="notifications-mark-all-read",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"notifications/bulk/mark-read/",
|
||||||
|
login_req(page_views.notification_mark_bulk_read),
|
||||||
|
name="notifications-mark-bulk-read",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"notifications/<int:notification_id>/mark-read/",
|
||||||
|
login_req(page_views.notification_mark_read),
|
||||||
|
name="notification-mark-read",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Projects App ---
|
||||||
|
projects_dashboard_patterns = [
|
||||||
|
path("projects/", login_req(list_views.projects_list), name="projects-list"),
|
||||||
|
path(
|
||||||
|
"projects/create/", login_req(page_views.project_create), name="project-create"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"projects/<int:project_id>/",
|
||||||
|
login_req(page_views.project_detail),
|
||||||
|
name="project-detail",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"projects/<int:project_id>/edit/",
|
||||||
|
login_req(page_views.project_edit),
|
||||||
|
name="project-edit",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"projects/<int:project_id>/delete/",
|
||||||
|
login_req(page_views.project_delete),
|
||||||
|
name="project-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"projects/<int:project_id>/pin/",
|
||||||
|
login_req(page_views.project_toggle_pin),
|
||||||
|
name="project-toggle-pin",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"project-timeline/",
|
||||||
|
login_req(page_views.project_timeline),
|
||||||
|
name="project-timeline",
|
||||||
|
),
|
||||||
|
path("project-board/", login_req(board_views.project_board), name="project-board"),
|
||||||
|
path(
|
||||||
|
"project-board/<int:project_id>/",
|
||||||
|
login_req(board_views.project_board),
|
||||||
|
name="project-board-project",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"board/<int:project_id>/data/",
|
||||||
|
login_req(board_views.board_data),
|
||||||
|
name="board-data",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"board/<int:project_id>/move/<int:ticket_id>/",
|
||||||
|
login_req(board_views.move_ticket),
|
||||||
|
name="board-move-ticket",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"board/<int:project_id>/create/",
|
||||||
|
login_req(board_views.board_create_ticket),
|
||||||
|
name="board-create-ticket",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"board/<int:project_id>/export/",
|
||||||
|
login_req(board_views.export_board_tickets),
|
||||||
|
name="board-export-tickets",
|
||||||
|
),
|
||||||
|
# Project Notes
|
||||||
|
path(
|
||||||
|
"project-notes/create/",
|
||||||
|
login_req(page_views.project_note_create),
|
||||||
|
name="project-note-create",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"project-notes/<int:note_id>/edit/",
|
||||||
|
login_req(page_views.project_note_edit),
|
||||||
|
name="project-note-edit",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Tickets App ---
|
||||||
|
tickets_dashboard_patterns = [
|
||||||
|
path("tickets/", login_req(list_views.tickets_list), name="tickets-list"),
|
||||||
|
path("tickets/create/", login_req(page_views.ticket_create), name="ticket-create"),
|
||||||
|
path(
|
||||||
|
"tickets/<int:ticket_id>/",
|
||||||
|
login_req(page_views.ticket_detail),
|
||||||
|
name="ticket-detail",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/<int:ticket_id>/edit/",
|
||||||
|
login_req(page_views.ticket_edit),
|
||||||
|
name="ticket-edit",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/<int:ticket_id>/delete/",
|
||||||
|
login_req(page_views.ticket_delete),
|
||||||
|
name="ticket-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/<int:ticket_id>/description/",
|
||||||
|
login_req(page_views.ticket_update_description),
|
||||||
|
name="ticket-update-description",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/<int:ticket_id>/field/",
|
||||||
|
login_req(page_views.ticket_update_field),
|
||||||
|
name="ticket-update-field",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/<int:ticket_id>/comments/add/",
|
||||||
|
login_req(page_views.ticket_comment_add),
|
||||||
|
name="ticket-comment-add",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/<int:ticket_id>/comments/<int:comment_id>/delete/",
|
||||||
|
login_req(page_views.ticket_comment_delete),
|
||||||
|
name="ticket-comment-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/export/", login_req(page_views.tickets_export), name="tickets-export"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/import/", login_req(page_views.tickets_import), name="tickets-import"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"ticket-timeline/",
|
||||||
|
login_req(page_views.ticket_timeline),
|
||||||
|
name="ticket-timeline",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"ticket-timeline/<int:project_id>/",
|
||||||
|
login_req(page_views.ticket_timeline),
|
||||||
|
name="ticket-timeline-project",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Ticket Statuses (Part of Tickets/Projects) ---
|
||||||
|
ticket_statuses_dashboard_patterns = [
|
||||||
|
path(
|
||||||
|
"ticket-statuses/create/",
|
||||||
|
login_req(page_views.ticket_status_create),
|
||||||
|
name="ticket-status-create",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"ticket-statuses/<int:status_id>/edit/",
|
||||||
|
login_req(page_views.ticket_status_edit),
|
||||||
|
name="ticket-status-edit",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"ticket-statuses/<int:status_id>/delete/",
|
||||||
|
login_req(page_views.ticket_status_delete),
|
||||||
|
name="ticket-status-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"ticket-statuses/bulk/delete/",
|
||||||
|
login_req(page_views.ticket_statuses_bulk_delete),
|
||||||
|
name="ticket-statuses-bulk-delete",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Project Members App/Component ---
|
||||||
|
project_members_dashboard_patterns = [
|
||||||
|
path(
|
||||||
|
"project-members/create/",
|
||||||
|
login_req(page_views.project_member_create),
|
||||||
|
name="project-member-create",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"project-members/<int:member_id>/edit/",
|
||||||
|
login_req(page_views.project_member_edit),
|
||||||
|
name="project-member-edit",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"project-members/<int:member_id>/delete/",
|
||||||
|
login_req(page_views.project_member_delete),
|
||||||
|
name="project-member-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"project-members/bulk/delete/",
|
||||||
|
login_req(page_views.user_project_members_bulk_delete),
|
||||||
|
name="project-member-bulk-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"user-projects/attach/",
|
||||||
|
login_req(page_views.user_project_attach),
|
||||||
|
name="user-project-attach",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Epics App ---
|
||||||
|
epics_dashboard_patterns = [
|
||||||
|
path("epics/", login_req(page_views.epics_overview), name="epics-overview"),
|
||||||
|
path(
|
||||||
|
"epics/<int:project_id>/",
|
||||||
|
login_req(page_views.epics_overview),
|
||||||
|
name="epics-overview-project",
|
||||||
|
),
|
||||||
|
path("epics/create/", login_req(page_views.epic_create), name="epic-create"),
|
||||||
|
path(
|
||||||
|
"epics/create/<int:project_id>/",
|
||||||
|
login_req(page_views.epic_create),
|
||||||
|
name="epic-create-project",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"epics/<int:epic_id>/edit/", login_req(page_views.epic_edit), name="epic-edit"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"epics/<int:epic_id>/delete/",
|
||||||
|
login_req(page_views.epic_delete),
|
||||||
|
name="epic-delete",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Analytics / Leaderboard ---
|
||||||
|
analytics_dashboard_patterns = [
|
||||||
|
path("leaderboard/", login_req(page_views.leaderboard), name="leaderboard"),
|
||||||
|
path(
|
||||||
|
"contributions/", login_req(page_views.user_contributions), name="contributions"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Ticket Priorities App/Component ---
|
||||||
|
ticket_priorities_dashboard_patterns = [
|
||||||
|
path(
|
||||||
|
"ticket-priorities/",
|
||||||
|
login_req(page_views.ticket_priorities),
|
||||||
|
name="ticket-priorities",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"ticket-priorities/add/",
|
||||||
|
login_req(page_views.ticket_priority_add),
|
||||||
|
name="ticket-priority-add",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"ticket-priorities/<int:priority_id>/edit/",
|
||||||
|
login_req(page_views.ticket_priority_edit),
|
||||||
|
name="ticket-priority-edit",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"ticket-priorities/<int:priority_id>/delete/",
|
||||||
|
login_req(page_views.ticket_priority_delete),
|
||||||
|
name="ticket-priority-delete",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Users / Accounts Administration ---
|
||||||
|
users_dashboard_patterns = [
|
||||||
|
path("users/", login_req(page_views.users_list), name="users-list"),
|
||||||
|
path("users/create/", login_req(page_views.user_create), name="user-create"),
|
||||||
|
path("users/<int:user_id>/", login_req(page_views.user_detail), name="user-detail"),
|
||||||
|
path(
|
||||||
|
"users/<int:user_id>/edit/", login_req(page_views.user_edit), name="user-edit"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Settings App/Component ---
|
||||||
|
settings_dashboard_patterns = [
|
||||||
|
path("settings/ui/", login_req(page_views.ui_settings), name="ui-settings"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- External Access App ---
|
||||||
|
external_access_dashboard_patterns = [
|
||||||
|
path(
|
||||||
|
"external/",
|
||||||
|
login_req(page_views.external_access_list),
|
||||||
|
name="external-access-list",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"external/create/<int:project_id>/",
|
||||||
|
login_req(page_views.external_access_create),
|
||||||
|
name="external-access-create",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"external/<int:access_id>/regenerate/",
|
||||||
|
login_req(page_views.external_access_regenerate),
|
||||||
|
name="external-access-regenerate",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"external/<int:access_id>/toggle/",
|
||||||
|
login_req(page_views.external_access_toggle),
|
||||||
|
name="external-access-toggle",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"external/project/<int:project_id>/regenerate/",
|
||||||
|
login_req(page_views.external_access_regenerate_project),
|
||||||
|
name="external-access-regenerate-project",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Bulk Operations (Across Apps) ---
|
||||||
|
bulk_delete_dashboard_patterns = [
|
||||||
|
path(
|
||||||
|
"tickets/bulk/status/",
|
||||||
|
login_req(page_views.tickets_bulk_status),
|
||||||
|
name="tickets-bulk-status",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/bulk/priority/",
|
||||||
|
login_req(page_views.tickets_bulk_priority),
|
||||||
|
name="tickets-bulk-priority",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/bulk/assign/",
|
||||||
|
login_req(page_views.tickets_bulk_assign),
|
||||||
|
name="tickets-bulk-assign",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/bulk/epic/",
|
||||||
|
login_req(page_views.tickets_bulk_epic),
|
||||||
|
name="tickets-bulk-epic",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/bulk/delete/",
|
||||||
|
login_req(list_views.tickets_bulk_delete),
|
||||||
|
name="tickets-bulk-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"projects/bulk/delete/",
|
||||||
|
login_req(list_views.projects_bulk_delete),
|
||||||
|
name="projects-bulk-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/bulk/delete/",
|
||||||
|
login_req(list_views.users_bulk_delete),
|
||||||
|
name="users-bulk-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"epics/bulk/delete/",
|
||||||
|
login_req(list_views.epics_bulk_delete),
|
||||||
|
name="epics-bulk-delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"notes/bulk/delete/",
|
||||||
|
login_req(list_views.notes_bulk_delete),
|
||||||
|
name="notes-bulk-delete",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Combined Dashboard Patterns
|
||||||
|
dashboard_patterns = (
|
||||||
|
core_dashboard_patterns
|
||||||
|
+ notifications_dashboard_patterns
|
||||||
|
+ projects_dashboard_patterns
|
||||||
|
+ tickets_dashboard_patterns
|
||||||
|
+ ticket_statuses_dashboard_patterns
|
||||||
|
+ project_members_dashboard_patterns
|
||||||
|
+ epics_dashboard_patterns
|
||||||
|
+ analytics_dashboard_patterns
|
||||||
|
+ ticket_priorities_dashboard_patterns
|
||||||
|
+ users_dashboard_patterns
|
||||||
|
+ settings_dashboard_patterns
|
||||||
|
+ external_access_dashboard_patterns
|
||||||
|
+ bulk_delete_dashboard_patterns
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("", RedirectView.as_view(url="/dashboard/", permanent=True)),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
# JWT tokens
|
path(
|
||||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
"sw.js",
|
||||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
TemplateView.as_view(
|
||||||
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
|
template_name="components/sw.js", content_type="application/javascript"
|
||||||
# App URLs
|
|
||||||
path("", include("src.epics.urls")),
|
|
||||||
path("", include("src.notes.urls")),
|
|
||||||
path("", include("src.projects.urls")),
|
|
||||||
path("", include("src.accounts.urls")),
|
|
||||||
path("", include("src.external.urls")),
|
|
||||||
path("", include("src.notifications.urls")),
|
|
||||||
path("", include("src.tickets.urls")),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Don't show schema in production
|
|
||||||
if settings.DEBUG:
|
|
||||||
schema_view = get_schema_view(
|
|
||||||
openapi.Info(
|
|
||||||
title="config API",
|
|
||||||
default_version="v1",
|
|
||||||
),
|
),
|
||||||
public=True,
|
name="sw.js",
|
||||||
)
|
),
|
||||||
urlpatterns += [
|
path("api/", include(api_patterns)),
|
||||||
path("docs/", schema_view.with_ui("swagger", cache_timeout=0)),
|
path("dashboard/", include(dashboard_patterns)),
|
||||||
path("schema/", schema_view.with_ui("redoc", cache_timeout=0)),
|
path(
|
||||||
]
|
"login/",
|
||||||
|
auth_views.LoginView.as_view(
|
||||||
|
template_name="auth/login.html",
|
||||||
|
redirect_authenticated_user=True,
|
||||||
|
),
|
||||||
|
name="login",
|
||||||
|
),
|
||||||
|
path("logout/", auth_views.LogoutView.as_view(next_page="login"), name="logout"),
|
||||||
|
path(
|
||||||
|
"external/<str:token>/",
|
||||||
|
page_views.external_access_view,
|
||||||
|
name="external-dashboard",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"external/<str:token>/logout/",
|
||||||
|
page_views.external_access_logout,
|
||||||
|
name="external-logout",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
# django-browser-reload for tailwind live reload (dev only)
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path("__reload__/", include("django_browser_reload.urls")),
|
path("__reload__/", include("django_browser_reload.urls")),
|
||||||
|
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||||
|
path(
|
||||||
|
"api/docs/",
|
||||||
|
SpectacularSwaggerView.as_view(url_name="schema"),
|
||||||
|
name="swagger-ui",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += [
|
|
||||||
re_path(
|
|
||||||
r"^media/(?P<path>.*)$",
|
|
||||||
serve,
|
|
||||||
{"document_root": settings.MEDIA_ROOT},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
+6
-2
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import environ
|
import environ
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
@@ -9,6 +10,9 @@ env_path = Path(__file__).resolve().parent.parent / ".env"
|
|||||||
env = environ.Env()
|
env = environ.Env()
|
||||||
environ.Env.read_env(env_path)
|
environ.Env.read_env(env_path)
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="config.settings.production"))
|
os.environ.setdefault(
|
||||||
|
"DJANGO_SETTINGS_MODULE",
|
||||||
|
env("DJANGO_SETTINGS_MODULE", default="config.settings.production"),
|
||||||
|
)
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
Reference in new issue
Block a user