huge updates

This commit is contained in:
sankalp committed 2025-04-22 03:47:31 +05:45
1 parent b5a43ac0ad
commit 6cc74a355f
40 files changed
+6878 -2378

No files matched your search

+258
View File
@@ -0,0 +1,258 @@
"""
Configuration management system for the Student Management System.
This module provides a centralized configuration system with:
- Environment variable validation
- Error handling for missing configurations
- Fallback mechanisms
- Configuration documentation
- Configuration versioning
"""
import os
from pathlib import Path
from typing import Any, Dict, Optional, Type, Union
from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator
from dotenv import load_dotenv
import logging
from functools import lru_cache
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Configuration version
CONFIG_VERSION = "1.0.0"
def parse_optional_int(value: Any) -> Optional[int]:
"""Parse a value into an optional integer."""
if value is None or value == '':
return None
try:
return int(value)
except (ValueError, TypeError):
return None
class DatabaseConfig(BaseModel):
"""Database configuration settings."""
engine: str = Field(default="django.db.backends.sqlite3")
name: str = Field(default="db.sqlite3")
timeout: int = Field(default=20)
host: Optional[str] = None
port: Optional[int] = None
user: Optional[str] = None
password: Optional[str] = None
@model_validator(mode='before')
@classmethod
def validate_optional_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
if 'port' in values:
values['port'] = parse_optional_int(values['port'])
return values
class CacheConfig(BaseModel):
"""Cache configuration settings."""
use_redis: bool = Field(default=False)
redis_url: str = Field(default="redis://localhost:6379/1")
timeout: int = Field(default=300)
key_prefix: str = Field(default="sms")
max_connections: int = Field(default=1000)
class EmailConfig(BaseModel):
"""Email configuration settings."""
backend: str = Field(default="django.core.mail.backends.console.EmailBackend")
default_from_email: str = Field(default="noreply@studentmanagementsystem.com")
host: Optional[str] = None
port: Optional[int] = None
username: Optional[str] = None
password: Optional[str] = None
use_tls: bool = Field(default=True)
@model_validator(mode='before')
@classmethod
def validate_optional_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
if 'port' in values:
values['port'] = parse_optional_int(values['port'])
return values
class FirebaseConfig(BaseModel):
"""Firebase configuration settings."""
api_key: str
auth_domain: str
project_id: str
storage_bucket: str
messaging_sender_id: str
app_id: str
measurement_id: str
class SMSConfig(BaseModel):
"""SMS configuration settings."""
api_key: str
sender_id: str = Field(default="SMSSYS")
class GunicornConfig(BaseModel):
"""Gunicorn configuration settings."""
workers: int = Field(default=4)
threads: int = Field(default=2)
timeout: int = Field(default=30)
keepalive: int = Field(default=2)
class Config(BaseModel):
"""Main configuration model."""
version: str = Field(default=CONFIG_VERSION)
debug: bool = Field(default=False)
secret_key: str
allowed_hosts: list[str] = Field(default=["*"])
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
cache: CacheConfig = Field(default_factory=CacheConfig)
email: EmailConfig = Field(default_factory=EmailConfig)
firebase: FirebaseConfig
sms: SMSConfig
gunicorn: GunicornConfig = Field(default_factory=GunicornConfig)
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
@lru_cache()
def get_config() -> Config:
"""
Get the application configuration.
Returns:
Config: The validated configuration object.
Raises:
ValidationError: If required environment variables are missing or invalid.
"""
# Load environment variables
load_dotenv()
try:
config = Config(
debug=os.getenv("DJANGO_DEBUG", "False").lower() == "true",
secret_key=os.getenv("DJANGO_SECRET_KEY", ""),
allowed_hosts=os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(","),
database=DatabaseConfig(
engine=os.getenv("DB_ENGINE", "django.db.backends.sqlite3"),
name=os.getenv("DB_NAME", "db.sqlite3"),
timeout=int(os.getenv("DB_TIMEOUT", "20")),
host=os.getenv("DB_HOST"),
port=os.getenv("DB_PORT"),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD"),
),
cache=CacheConfig(
use_redis=os.getenv("USE_REDIS_CACHE", "False").lower() == "true",
redis_url=os.getenv("REDIS_URL", "redis://localhost:6379/1"),
timeout=int(os.getenv("CACHE_TIMEOUT", "300")),
key_prefix=os.getenv("CACHE_KEY_PREFIX", "sms"),
max_connections=int(os.getenv("CACHE_MAX_CONNECTIONS", "1000")),
),
email=EmailConfig(
backend=os.getenv("EMAIL_BACKEND", "django.core.mail.backends.console.EmailBackend"),
default_from_email=os.getenv("DEFAULT_FROM_EMAIL", "noreply@studentmanagementsystem.com"),
host=os.getenv("EMAIL_HOST"),
port=os.getenv("EMAIL_PORT"),
username=os.getenv("EMAIL_USERNAME"),
password=os.getenv("EMAIL_PASSWORD"),
use_tls=os.getenv("EMAIL_USE_TLS", "True").lower() == "true",
),
firebase=FirebaseConfig(
api_key=os.getenv("FIREBASE_API_KEY", ""),
auth_domain=os.getenv("FIREBASE_AUTH_DOMAIN", ""),
project_id=os.getenv("FIREBASE_PROJECT_ID", ""),
storage_bucket=os.getenv("FIREBASE_STORAGE_BUCKET", ""),
messaging_sender_id=os.getenv("FIREBASE_MESSAGING_SENDER_ID", ""),
app_id=os.getenv("FIREBASE_APP_ID", ""),
measurement_id=os.getenv("FIREBASE_MEASUREMENT_ID", ""),
),
sms=SMSConfig(
api_key=os.getenv("SMS_API_KEY", ""),
sender_id=os.getenv("SMS_SENDER_ID", "SMSSYS"),
),
gunicorn=GunicornConfig(
workers=int(os.getenv("GUNICORN_WORKERS", "4")),
threads=int(os.getenv("GUNICORN_THREADS", "2")),
timeout=int(os.getenv("GUNICORN_TIMEOUT", "30")),
keepalive=int(os.getenv("GUNICORN_KEEPALIVE", "2")),
),
)
# Validate required fields
if not config.secret_key:
raise ValidationError("DJANGO_SECRET_KEY is required")
if not config.firebase.api_key:
raise ValidationError("FIREBASE_API_KEY is required")
if not config.sms.api_key:
raise ValidationError("SMS_API_KEY is required")
return config
except ValidationError as e:
logger.error(f"Configuration validation error: {e}")
raise
except Exception as e:
logger.error(f"Error loading configuration: {e}")
raise
def get_django_settings() -> Dict[str, Any]:
"""
Convert the configuration to Django settings format.
Returns:
Dict[str, Any]: Dictionary of Django settings.
"""
config = get_config()
return {
"DEBUG": config.debug,
"SECRET_KEY": config.secret_key,
"ALLOWED_HOSTS": config.allowed_hosts,
"DATABASES": {
"default": {
"ENGINE": config.database.engine,
"NAME": config.database.name,
"HOST": config.database.host,
"PORT": config.database.port,
"USER": config.database.user,
"PASSWORD": config.database.password,
"OPTIONS": {
"timeout": config.database.timeout,
}
}
},
"CACHES": {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache" if config.cache.use_redis
else "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": config.cache.redis_url if config.cache.use_redis else "unique-snowflake",
"OPTIONS": {
"retry_on_timeout": True,
"max_connections": config.cache.max_connections
},
"KEY_PREFIX": config.cache.key_prefix,
"TIMEOUT": config.cache.timeout,
}
},
"EMAIL_BACKEND": config.email.backend,
"DEFAULT_FROM_EMAIL": config.email.default_from_email,
"EMAIL_HOST": config.email.host,
"EMAIL_PORT": config.email.port,
"EMAIL_HOST_USER": config.email.username,
"EMAIL_HOST_PASSWORD": config.email.password,
"EMAIL_USE_TLS": config.email.use_tls,
"FIREBASE_API_KEY": config.firebase.api_key,
"FIREBASE_AUTH_DOMAIN": config.firebase.auth_domain,
"FIREBASE_PROJECT_ID": config.firebase.project_id,
"FIREBASE_STORAGE_BUCKET": config.firebase.storage_bucket,
"FIREBASE_MESSAGING_SENDER_ID": config.firebase.messaging_sender_id,
"FIREBASE_APP_ID": config.firebase.app_id,
"FIREBASE_MEASUREMENT_ID": config.firebase.measurement_id,
"SMS_API_KEY": config.sms.api_key,
"SMS_SENDER_ID": config.sms.sender_id,
"GUNICORN_WORKERS": config.gunicorn.workers,
"GUNICORN_THREADS": config.gunicorn.threads,
"GUNICORN_TIMEOUT": config.gunicorn.timeout,
"GUNICORN_KEEPALIVE": config.gunicorn.keepalive,
}
+102 -41
View File
@@ -12,11 +12,11 @@ https://docs.djangoproject.com/en/5.1/ref/settings/
import os
from pathlib import Path
from dotenv import load_dotenv
from student_management_system.jazzmin import JAZZMIN_SETTINGS, JAZZMIN_UI_TWEAKS
from student_management_system.config import get_django_settings
# Load environment variables
load_dotenv()
# Get configuration
config = get_django_settings()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -25,12 +25,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")
SECRET_KEY = config["SECRET_KEY"]
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv("DJANGO_DEBUG", "False") == "True"
DEBUG = config["DEBUG"]
ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(",")
ALLOWED_HOSTS = config["ALLOWED_HOSTS"]
# Application definition
@@ -50,13 +50,15 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.middleware.cache.UpdateCacheMiddleware",
"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",
"app.middleware.HTTP505Middleware", # Custom middleware for handling 505 errors
"app.middleware.HTTP505Middleware",
"django.middleware.cache.FetchFromCacheMiddleware",
]
AUTHENTICATION_BACKENDS = [
@@ -95,13 +97,18 @@ WSGI_APPLICATION = "student_management_system.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
DATABASES = config["DATABASES"]
# Database optimization settings
DATABASE_ROUTERS = ['app.routers.DatabaseRouter']
# Query optimization
DATABASE_OPTIMIZATION = {
'DEFAULT_QUERY_TIMEOUT': 30,
'MAX_QUERY_SIZE': 1000,
'ENABLE_QUERY_CACHE': True,
'QUERY_CACHE_TIMEOUT': 300
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@@ -109,9 +116,16 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
"OPTIONS": {
"user_attributes": ("username", "email", "first_name", "last_name"),
"max_similarity": 0.7
}
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {
"min_length": 12,
}
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
@@ -164,40 +178,87 @@ JAZZMIN_SETTINGS = JAZZMIN_SETTINGS
JAZZMIN_UI_TWEAKS = JAZZMIN_UI_TWEAKS
# Login redirect URL
LOGIN_REDIRECT_URL = "/app/dashboard/"
LOGIN_URL = "/login/"
LOGIN_REDIRECT_URL = '/app/dashboard/'
LOGIN_URL = '/login/'
# Email Configuration
EMAIL_BACKEND = os.getenv(
"EMAIL_BACKEND", "django.core.mail.backends.console.EmailBackend"
)
DEFAULT_FROM_EMAIL = os.getenv(
"DEFAULT_FROM_EMAIL", "noreply@studentmanagementsystem.com"
)
EMAIL_BACKEND = config["EMAIL_BACKEND"]
DEFAULT_FROM_EMAIL = config["DEFAULT_FROM_EMAIL"]
EMAIL_HOST = config["EMAIL_HOST"]
EMAIL_PORT = config["EMAIL_PORT"]
EMAIL_HOST_USER = config["EMAIL_HOST_USER"]
EMAIL_HOST_PASSWORD = config["EMAIL_HOST_PASSWORD"]
EMAIL_USE_TLS = config["EMAIL_USE_TLS"]
# Cache configuration for OTP storage
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": os.getenv("CACHE_LOCATION", "unique-snowflake"),
}
}
# Cache configuration
CACHES = config["CACHES"]
# Cache settings
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 300
CACHE_MIDDLEWARE_KEY_PREFIX = 'sms'
# SMS API configuration
SMS_API_KEY = os.getenv("SMS_API_KEY", "")
SMS_SENDER_ID = os.getenv("SMS_SENDER_ID", "SMSSYS")
SMS_API_KEY = config["SMS_API_KEY"]
SMS_SENDER_ID = config["SMS_SENDER_ID"]
# CSRF settings
# CSRF_TRUSTED_ORIGINS = [
# "https://*.ngrok-free.app",
# "https://*.ngrok.io"
# ]
CSRF_TRUSTED_ORIGINS = [
"https://*.ngrok-free.app",
"https://*.ngrok.io"
]
# Firebase settings
FIREBASE_API_KEY = os.getenv("FIREBASE_API_KEY", "")
FIREBASE_AUTH_DOMAIN = os.getenv("FIREBASE_AUTH_DOMAIN", "")
FIREBASE_PROJECT_ID = os.getenv("FIREBASE_PROJECT_ID", "")
FIREBASE_STORAGE_BUCKET = os.getenv("FIREBASE_STORAGE_BUCKET", "")
FIREBASE_MESSAGING_SENDER_ID = os.getenv("FIREBASE_MESSAGING_SENDER_ID", "")
FIREBASE_APP_ID = os.getenv("FIREBASE_APP_ID", "")
FIREBASE_MEASUREMENT_ID = os.getenv("FIREBASE_MEASUREMENT_ID", "")
FIREBASE_API_KEY = config["FIREBASE_API_KEY"]
FIREBASE_AUTH_DOMAIN = config["FIREBASE_AUTH_DOMAIN"]
FIREBASE_PROJECT_ID = config["FIREBASE_PROJECT_ID"]
FIREBASE_STORAGE_BUCKET = config["FIREBASE_STORAGE_BUCKET"]
FIREBASE_MESSAGING_SENDER_ID = config["FIREBASE_MESSAGING_SENDER_ID"]
FIREBASE_APP_ID = config["FIREBASE_APP_ID"]
FIREBASE_MEASUREMENT_ID = config["FIREBASE_MEASUREMENT_ID"]
# Load balancing settings
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Gunicorn settings
GUNICORN_WORKERS = config["GUNICORN_WORKERS"]
GUNICORN_THREADS = config["GUNICORN_THREADS"]
GUNICORN_TIMEOUT = config["GUNICORN_TIMEOUT"]
GUNICORN_KEEPALIVE = config["GUNICORN_KEEPALIVE"]
# Static files serving optimization
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WHITENOISE_MAX_AGE = 31536000 # 1 year
WHITENOISE_USE_FINDERS = True
WHITENOISE_AUTOREFRESH = True
# Security settings
SECURE_SSL_REDIRECT = not DEBUG
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Session settings
SESSION_COOKIE_AGE = 3600 # 1 hour
SESSION_SAVE_EVERY_REQUEST = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
# CSRF settings
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Strict'
CSRF_USE_SESSIONS = True
# Session settings
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Strict'
# Background Tasks Configuration
BACKGROUND_TASK_RUN_ASYNC = True
MAX_ATTEMPTS = 3 # Maximum number of retries for failed tasks
MAX_RUN_TIME = 3600 # Maximum time a task can run (in seconds)
+3 -3
View File
@@ -29,9 +29,9 @@ from app import auth
urlpatterns = [
# Application URLs ------------------------------------------------
path("app/", include("app.urls")),
path("admin/", custom_admin_site.urls),
# path("", custom_admin_site.urls, name="admin"),
# Root Redirect --------------------------------------------------
path("", RedirectView.as_view(url="/app/dashboard/", permanent=False)),
# path("", RedirectView.as_view(url="/app/dashboard/", permanent=False)),
# Authentication -------------------------------------------------
path("login/", auth.custom_login, name="login"),
path("logout/", auth_views.LogoutView.as_view(next_page="/login/"), name="logout"),
@@ -41,6 +41,7 @@ urlpatterns = [
path("firebase-messaging-sw.js", views.serve_firebase_sw),
]
urlpatterns.append(path("", custom_admin_site.urls, name="admin"))
# Static and Media Files (Development Only) --------------------------
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
@@ -52,4 +53,3 @@ handler403 = "app.error_handlers.handler403"
handler404 = "app.error_handlers.handler404"
handler500 = "app.error_handlers.handler500"
handler505 = "app.error_handlers.handler505"