diff --git a/config/asgi.py b/config/asgi.py index edc05e7..b368d1c 100644 --- a/config/asgi.py +++ b/config/asgi.py @@ -2,6 +2,7 @@ import os from pathlib import Path + import environ from django.core.asgi import get_asgi_application @@ -9,5 +10,8 @@ env_path = Path(__file__).resolve().parent.parent / ".env" env = environ.Env() environ.Env.read_env(env_path) -os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="config.settings.production")) -application = get_asgi_application() \ No newline at end of file +os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", + env("DJANGO_SETTINGS_MODULE", default="config.settings.production"), +) +application = get_asgi_application() diff --git a/config/settings/base.py b/config/settings/base.py index 11cf99c..20f126b 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -1,9 +1,9 @@ """ -generated with djinit -Common settings shared between development and production environment +Common settings shared between development and production environments. +Environment-specific settings (DEBUG, SECRET_KEY, DATABASES, etc.) +live in development.py and production.py respectively. """ -import os from datetime import timedelta from pathlib import Path @@ -16,19 +16,18 @@ environ.Env.read_env(env_path) # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent.parent -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ +# ── Installed Apps ──────────────────────────────────────────────────────────── - -# Application definition THIRD_PARTY_APPS = [ "rest_framework", "rest_framework_simplejwt", - "rest_framework_simplejwt.token_blacklist", "corsheaders", - "drf_yasg", "django_tailwind_cli", "django_htmx", + "django_filters", + "import_export", + "drf_spectacular", + "django_q", ] USER_DEFINED_APPS = [ @@ -53,6 +52,8 @@ BUILT_IN_APPS = [ INSTALLED_APPS = BUILT_IN_APPS + THIRD_PARTY_APPS + USER_DEFINED_APPS INSTALLED_APPS.insert(0, "jazzmin") +# ── Middleware ──────────────────────────────────────────────────────────────── + MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", @@ -68,16 +69,20 @@ MIDDLEWARE = [ ROOT_URLCONF = "config.urls" +# ── Templates ───────────────────────────────────────────────────────────────── + TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], + "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", + "src.dashboard.context_processors.site_stats", ], }, }, @@ -85,9 +90,9 @@ TEMPLATES = [ WSGI_APPLICATION = "config.wsgi.application" +# ── Authentication ──────────────────────────────────────────────────────────── -# Password validation -# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators +AUTH_USER_MODEL = "accounts.User" AUTH_PASSWORD_VALIDATORS = [ { @@ -104,59 +109,59 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +LOGIN_URL = "login" +LOGIN_REDIRECT_URL = "dashboard" -# Internationalization -# https://docs.djangoproject.com/en/5.2/topics/i18n/ +# ── Internationalisation ────────────────────────────────────────────────────── LANGUAGE_CODE = "en-us" -TIME_ZONE = "UTC" +TIME_ZONE = "Asia/Kathmandu" USE_I18N = True USE_TZ = True -# Static files and media files +# ── Static & Media Files ────────────────────────────────────────────────────── + STATIC_URL = "/static/" STATIC_ROOT = BASE_DIR / "staticfiles" +STATICFILES_DIRS = [BASE_DIR / "static/"] MEDIA_URL = "/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" -# REST Framework Settings +# ── Tailwind ────────────────────────────────────────────────────────────────── + +TAILWIND_CLI_USE_DAISY_UI = False +TAILWIND_CLI_SRC_CSS = "static/src/tailwind.css" + +# ── REST Framework ──────────────────────────────────────────────────────────── + REST_FRAMEWORK = { - "DEFAULT_RENDERER_CLASSES": [ - "rest_framework.renderers.JSONRenderer", - ], - "DEFAULT_PARSER_CLASSES": [ - "rest_framework.parsers.JSONParser", - ], "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", "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", - "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 = { - "ACCESS_TOKEN_LIFETIME": timedelta(days=1), - "REFRESH_TOKEN_LIFETIME": timedelta(days=3), + "ACCESS_TOKEN_LIFETIME": timedelta(hours=1), + "REFRESH_TOKEN_LIFETIME": timedelta(days=30), "ROTATE_REFRESH_TOKENS": True, - "BLACKLIST_AFTER_ROTATION": True, + "AUTH_HEADER_TYPES": ("Bearer",), } -# CORS Settings +# ── CORS ────────────────────────────────────────────────────────────────────── + CORS_ALLOW_METHODS = [ "DELETE", "GET", @@ -179,42 +184,35 @@ CORS_ALLOW_HEADERS = [ ] CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours -# Security Settings + +# ── Security (shared baseline) ──────────────────────────────────────────────── + SECURE_BROWSER_XSS_FILTER = True SECURE_CONTENT_TYPE_NOSNIFF = True X_FRAME_OPTIONS = "DENY" -# for tailwind -STATICFILES_DIRS = [BASE_DIR / "static/"] -TAILWIND_CLI_VERSION = "2.7.8" -TAILWIND_CLI_USE_DAISY_UI = True -TAILWIND_CLI_CONFIG = { - "theme": { - "extend": { - "colors": { - "primary": "#DC2626", - "secondary": "#EF4444", - "accent": "#FCFECC", - } - } - }, - "daisyui": { - "themes": [ - { - "light": { - "primary": "#DC2626", - "secondary": "#EF4444", - "accent": "#FCFECC", - "neutral": "#1a1a2e", - "base-100": "#ffffff", - "base-200": "#f8fafc", - "base-300": "#e2e8f0", - "info": "#0E4D80", - "success": "#16a34a", - "warning": "#f59e0b", - "error": "#DC2626", - } - } - ] - }, +# ── Site ────────────────────────────────────────────────────────────────────── + +BASE_URL = env("BASE_URL", default="http://localhost:8000") +SITE_NAME = env("SITE_NAME", default="Project Management") + +# ── API Docs ────────────────────────────────────────────────────────────────── + +SPECTACULAR_SETTINGS = { + "TITLE": "Project Management API", + "DESCRIPTION": "Django port of Project Management", + "VERSION": "1.0.0", +} + +# ── Django Q2 ───────────────────────────────────────────────────────────────── +# timeout must be set and less than retry to avoid tasks being re-triggered early + +Q_CLUSTER = { + "name": "project_management", + "workers": 2, + "timeout": 60, # seconds before a task is killed + "retry": 120, # seconds before a failed task is retried (must be > timeout) + "queue_limit": 50, + "bulk": 10, + "orm": "default", } diff --git a/config/settings/development.py b/config/settings/development.py index fe67bef..3426a42 100644 --- a/config/settings/development.py +++ b/config/settings/development.py @@ -1,16 +1,14 @@ -"""generated with djinit""" +"""Development settings — SQLite, console email, CORS open, debug on.""" from .base import * # noqa: F401, F403 +# ── Core ────────────────────────────────────────────────────────────────────── + SECRET_KEY = "u4lMI*X9ArV9+CUWTOdYdh_9FcAx%$VVzlK&Kb(%1WA@C4Dkqi" - -# SECURITY WARNING: don't run with debug turned on in production! DEBUG = True - ALLOWED_HOSTS = ["*"] -# Database -# https://docs.djangoproject.com/en/5.2/ref/settings/#databases +# ── Database (SQLite) ───────────────────────────────────────────────────────── DATABASES = { "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" -# CORS settings for development -# Allow all origins in development (convenient but not secure) +# ── CORS ────────────────────────────────────────────────────────────────────── + CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True -# Disable HTTPS redirect in development +# ── Security ────────────────────────────────────────────────────────────────── + 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/" +) diff --git a/config/settings/production.py b/config/settings/production.py index ad54abd..3b5f5e3 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -1,57 +1,74 @@ -"""generated with djinit""" +"""Production settings — MySQL, SMTP email, strict CORS, HTTPS enforced.""" from .base import * # noqa: F401, F403 -DEBUG = False -SECRET_KEY = env('SECRET_KEY') -ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) +# ── Core ────────────────────────────────────────────────────────────────────── + +DEBUG = False +SECRET_KEY = env("SECRET_KEY") +ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[]) + +# ── Database (MySQL) ────────────────────────────────────────────────────────── -# Database -# Using individual database parameters DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'PORT': env('DB_PORT', default='3306'), - 'NAME': env('DB_NAME'), - 'USER': env('DB_USER'), - 'PASSWORD': env('DB_PASSWORD'), - 'HOST': env('DB_HOST'), + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": env("DB_NAME"), + "USER": env("DB_USER"), + "PASSWORD": env("DB_PASSWORD"), + "HOST": env("DB_HOST"), + "PORT": env("DB_PORT", default="3306"), } } -# CORS settings for production -# Filter out empty strings from FRONTEND_URL -CORS_ALLOWED_ORIGINS = env.list('FRONTEND_URL', default=[]) -CORS_ALLOW_CREDENTIALS = True +# ── Static Files ────────────────────────────────────────────────────────────── -# 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 -# Email settings -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 ────────────────────────────────────────────────────────────────── -# Security settings for production SECURE_SSL_REDIRECT = True SECURE_HSTS_SECONDS = 31536000 SECURE_HSTS_INCLUDE_SUBDOMAINS = 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 CSRF_COOKIE_SECURE = True -# Static files -STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' +# ── Google OAuth ────────────────────────────────────────────────────────────── -## Tailwind -TAILWIND_CLI_VERSION = "4.1.3" # Pin version -TAILWIND_CLI_AUTOMATIC_DOWNLOAD = False # Use pre-installed CLI -TAILWIND_CLI_DIST_CSS = "static/css/tailwind.min.css" +GOOGLE_CLIENT_ID = env("GOOGLE_CLIENT_ID") +GOOGLE_CLIENT_SECRET = env("GOOGLE_CLIENT_SECRET") +GOOGLE_REDIRECT_URI = env("GOOGLE_REDIRECT_URI") diff --git a/config/urls.py b/config/urls.py index 1047589..0df4082 100644 --- a/config/urls.py +++ b/config/urls.py @@ -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.views.static import serve -from django.urls import include -from drf_yasg.views import get_schema_view -from drf_yasg import openapi -from rest_framework_simplejwt.views import ( - TokenObtainPairView, - TokenRefreshView, - TokenBlacklistView, +from django.conf.urls.static import static +from django.contrib import admin +from django.contrib.auth import views as auth_views +from django.contrib.auth.decorators import login_required +from django.urls import include, path +from django.views.generic import RedirectView, TemplateView +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView + +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//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//", + login_req(page_views.project_detail), + name="project-detail", + ), + path( + "projects//edit/", + login_req(page_views.project_edit), + name="project-edit", + ), + path( + "projects//delete/", + login_req(page_views.project_delete), + name="project-delete", + ), + path( + "projects//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//", + login_req(board_views.project_board), + name="project-board-project", + ), + path( + "board//data/", + login_req(board_views.board_data), + name="board-data", + ), + path( + "board//move//", + login_req(board_views.move_ticket), + name="board-move-ticket", + ), + path( + "board//create/", + login_req(board_views.board_create_ticket), + name="board-create-ticket", + ), + path( + "board//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//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//", + login_req(page_views.ticket_detail), + name="ticket-detail", + ), + path( + "tickets//edit/", + login_req(page_views.ticket_edit), + name="ticket-edit", + ), + path( + "tickets//delete/", + login_req(page_views.ticket_delete), + name="ticket-delete", + ), + path( + "tickets//description/", + login_req(page_views.ticket_update_description), + name="ticket-update-description", + ), + path( + "tickets//field/", + login_req(page_views.ticket_update_field), + name="ticket-update-field", + ), + path( + "tickets//comments/add/", + login_req(page_views.ticket_comment_add), + name="ticket-comment-add", + ), + path( + "tickets//comments//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//", + 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//edit/", + login_req(page_views.ticket_status_edit), + name="ticket-status-edit", + ), + path( + "ticket-statuses//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//edit/", + login_req(page_views.project_member_edit), + name="project-member-edit", + ), + path( + "project-members//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//", + 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//", + login_req(page_views.epic_create), + name="epic-create-project", + ), + path( + "epics//edit/", login_req(page_views.epic_edit), name="epic-edit" + ), + path( + "epics//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//edit/", + login_req(page_views.ticket_priority_edit), + name="ticket-priority-edit", + ), + path( + "ticket-priorities//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//", login_req(page_views.user_detail), name="user-detail"), + path( + "users//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//", + login_req(page_views.external_access_create), + name="external-access-create", + ), + path( + "external//regenerate/", + login_req(page_views.external_access_regenerate), + name="external-access-regenerate", + ), + path( + "external//toggle/", + login_req(page_views.external_access_toggle), + name="external-access-toggle", + ), + path( + "external/project//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 = [ + path("", RedirectView.as_view(url="/dashboard/", permanent=True)), path("admin/", admin.site.urls), - # JWT tokens - path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), - path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), - path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"), - # 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", + path( + "sw.js", + TemplateView.as_view( + template_name="components/sw.js", content_type="application/javascript" ), - public=True, - ) - urlpatterns += [ - path("docs/", schema_view.with_ui("swagger", cache_timeout=0)), - path("schema/", schema_view.with_ui("redoc", cache_timeout=0)), - ] + name="sw.js", + ), + path("api/", include(api_patterns)), + path("dashboard/", include(dashboard_patterns)), + 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//", + page_views.external_access_view, + name="external-dashboard", + ), + path( + "external//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: urlpatterns += [ 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.*)$", - serve, - {"document_root": settings.MEDIA_ROOT}, - ), -] \ No newline at end of file diff --git a/config/wsgi.py b/config/wsgi.py index edcfc80..5ce106c 100644 --- a/config/wsgi.py +++ b/config/wsgi.py @@ -2,6 +2,7 @@ import os from pathlib import Path + import environ from django.core.wsgi import get_wsgi_application @@ -9,6 +10,9 @@ env_path = Path(__file__).resolve().parent.parent / ".env" env = environ.Env() 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() \ No newline at end of file +application = get_wsgi_application()