""" Base settings for PX360 project. This file contains settings common to all environments. """ import os from pathlib import Path import environ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent.parent # Initialize environment variables env = environ.Env( DEBUG=(bool, False), ALLOWED_HOSTS=(list, []), ) # Read .env file if it exists environ.Env.read_env(os.path.join(BASE_DIR, ".env")) # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = env("SECRET_KEY", default="django-insecure-change-this-in-production") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env("DEBUG") ALLOWED_HOSTS = env("ALLOWED_HOSTS") # Application definition DJANGO_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", ] THIRD_PARTY_APPS = [ "rest_framework", "rest_framework_simplejwt", "django_filters", "drf_spectacular", "django_celery_beat", ] LOCAL_APPS = [ "apps.core", "apps.accounts", "apps.organizations", "apps.journeys", "apps.surveys", "apps.complaints", "apps.feedback", "apps.callcenter", "apps.social", "apps.px_action_center", "apps.analytics", "apps.physicians", "apps.projects", "apps.integrations", "apps.notifications", "apps.ai_engine", "apps.dashboard", "apps.appreciation", "apps.observations", "apps.px_sources", "apps.references", "apps.standards", "apps.simulator", "apps.reports", ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", # i18n support "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "apps.core.middleware.TenantMiddleware", # Multi-tenancy support "apps.px_sources.middleware.SourceUserRestrictionMiddleware", # STRICT source user restrictions "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "config.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", "django.template.context_processors.i18n", "apps.core.context_processors.sidebar_counts", "apps.core.context_processors.hospital_context", "apps.accounts.context_processors.acknowledgement_counts", ], }, }, ] WSGI_APPLICATION = "config.wsgi.application" # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases # DATABASES = { # 'default': env.db('DATABASE_URL', default='postgresql://px360:px360@localhost:5432/px360') # } DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } } # Password validation # https://docs.djangoproject.com/en/5.0/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", }, ] # Custom User Model AUTH_USER_MODEL = "accounts.User" # Authentication URLs LOGIN_URL = "/accounts/login/" LOGIN_REDIRECT_URL = "/" LOGOUT_REDIRECT_URL = "/accounts/login/" # Internationalization # https://docs.djangoproject.com/en/5.0/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "Asia/Riyadh" USE_I18N = True USE_TZ = True # Languages supported (Arabic and English) LANGUAGES = [ ("en", "English"), ("ar", "Arabic"), ] # Locale paths for translation files LOCALE_PATHS = [ BASE_DIR / "locale", ] # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ STATIC_URL = "/static/" STATIC_ROOT = BASE_DIR / "staticfiles" STATICFILES_DIRS = [ BASE_DIR / "static", ] # Media files MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" # Data upload settings # Increased limit to support bulk patient imports from HIS DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000 # WhiteNoise configuration STORAGES = { "default": { "BACKEND": "django.core.files.storage.FileSystemStorage", }, "staticfiles": { "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", }, } # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # Django REST Framework REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework_simplejwt.authentication.JWTAuthentication", "rest_framework.authentication.SessionAuthentication", ], "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticated", ], "DEFAULT_FILTER_BACKENDS": [ "django_filters.rest_framework.DjangoFilterBackend", "rest_framework.filters.SearchFilter", "rest_framework.filters.OrderingFilter", ], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 50, "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } # JWT Settings from datetime import timedelta SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(hours=1), "REFRESH_TOKEN_LIFETIME": timedelta(days=7), "ROTATE_REFRESH_TOKENS": True, "BLACKLIST_AFTER_ROTATION": True, "UPDATE_LAST_LOGIN": True, } # DRF Spectacular (OpenAPI/Swagger) SPECTACULAR_SETTINGS = { "TITLE": "PX360 API", "DESCRIPTION": "Patient Experience 360 Management System API", "VERSION": "1.0.0", "SERVE_INCLUDE_SCHEMA": False, } # Celery Configuration CELERY_BROKER_URL = env("CELERY_BROKER_URL", default="redis://localhost:6379/0") CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND", default="redis://localhost:6379/0") CELERY_ACCEPT_CONTENT = ["json"] CELERY_TASK_SERIALIZER = "json" CELERY_RESULT_SERIALIZER = "json" CELERY_TIMEZONE = TIME_ZONE CELERY_TASK_TRACK_STARTED = True CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes # Celery Beat Schedule CELERY_BEAT_SCHEDULER = "config.celery_scheduler:PatchedDatabaseScheduler" # Logging Configuration LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "verbose": { "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", "style": "{", }, "simple": { "format": "{levelname} {message}", "style": "{", }, }, "filters": { "require_debug_true": { "()": "django.utils.log.RequireDebugTrue", }, }, "handlers": { "console": {"level": "INFO", "class": "logging.StreamHandler", "formatter": "verbose"}, "file": { "level": "INFO", "class": "logging.handlers.RotatingFileHandler", "filename": BASE_DIR / "logs" / "px360.log", "maxBytes": 1024 * 1024 * 15, # 15MB "backupCount": 10, "formatter": "verbose", }, "integration_file": { "level": "INFO", "class": "logging.handlers.RotatingFileHandler", "filename": BASE_DIR / "logs" / "integrations.log", "maxBytes": 1024 * 1024 * 15, # 15MB "backupCount": 10, "formatter": "verbose", }, }, "loggers": { "django": { "handlers": ["console", "file"], "level": "INFO", "propagate": False, }, "apps": { "handlers": ["console", "file"], "level": "INFO", "propagate": False, }, "apps.integrations": { "handlers": ["console", "integration_file"], "level": "INFO", "propagate": False, }, }, } # Create logs directory if it doesn't exist LOGS_DIR = BASE_DIR / "logs" LOGS_DIR.mkdir(exist_ok=True) # PX360 Business Configuration # These will be overridden by database configurations but provide defaults # Survey Configuration SURVEY_NEGATIVE_THRESHOLD = 3 # Scores below this trigger actions (out of 5) SURVEY_TOKEN_EXPIRY_DAYS = 30 # SLA Configuration (in hours) SLA_DEFAULTS = { "complaint": { "low": 72, "medium": 48, "high": 24, "critical": 12, }, "action": { "low": 120, "medium": 72, "high": 48, "critical": 24, }, } COMPLAINT_LINK_EXPIRY_DAYS = env.int("COMPLAINT_LINK_EXPIRY_DAYS", default=7) # AI Configuration (LiteLLM with OpenRouter) OPENROUTER_API_KEY = env('OPENROUTER_API_KEY', default='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') AI_MODEL = env('AI_MODEL', default='z-ai/glm-4.5-air:free') AI_TEMPERATURE = env.float('AI_TEMPERATURE', default=0.3) AI_MAX_TOKENS = env.int('AI_MAX_TOKENS', default=500) # Notification Configuration NOTIFICATION_CHANNELS = { "sms": { "enabled": env.bool("SMS_ENABLED", default=False), "provider": env("SMS_PROVIDER", default="console"), }, "whatsapp": { "enabled": env.bool("WHATSAPP_ENABLED", default=False), "provider": env("WHATSAPP_PROVIDER", default="console"), }, "email": { "enabled": env.bool("EMAIL_ENABLED", default=True), "provider": env("EMAIL_PROVIDER", default="console"), }, } # Twilio Configuration TWILIO_ACCOUNT_SID = env("TWILIO_ACCOUNT_SID", default="") TWILIO_AUTH_TOKEN = env("TWILIO_AUTH_TOKEN", default="") TWILIO_PHONE_NUMBER = env("TWILIO_PHONE_NUMBER", default="") TWILIO_MESSAGING_SERVICE_SID = env("TWILIO_MESSAGING_SERVICE_SID", default="") # Mshastra SMS Configuration MSHASTRA_USERNAME = env("MSHASTRA_USERNAME", default="") MSHASTRA_PASSWORD = env("MSHASTRA_PASSWORD", default="") MSHASTRA_SENDER_ID = env("MSHASTRA_SENDER_ID", default="") # External API Notification Configuration EXTERNAL_NOTIFICATION_API = { "email": { "enabled": env.bool("EMAIL_API_ENABLED", default=False), "url": env("EMAIL_API_URL", default=""), "api_key": env("EMAIL_API_KEY", default=""), "auth_method": env("EMAIL_API_AUTH_METHOD", default="bearer"), "method": env("EMAIL_API_METHOD", default="POST"), "timeout": env.int("EMAIL_API_TIMEOUT", default=10), "max_retries": env.int("EMAIL_API_MAX_RETRIES", default=3), "retry_delay": env.int("EMAIL_API_RETRY_DELAY", default=2), }, "sms": { "enabled": env.bool("SMS_API_ENABLED", default=False), "url": env("SMS_API_URL", default=""), "api_key": env("SMS_API_KEY", default=""), "auth_method": env("SMS_API_AUTH_METHOD", default="bearer"), "method": env("SMS_API_METHOD", default="POST"), "timeout": env.int("SMS_API_TIMEOUT", default=10), "max_retries": env.int("SMS_API_MAX_RETRIES", default=3), "retry_delay": env.int("SMS_API_RETRY_DELAY", default=2), }, } # Email Configuration EMAIL_BACKEND = env("EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend") EMAIL_HOST = env("EMAIL_HOST", default="localhost") EMAIL_PORT = env.int("EMAIL_PORT", default=2525) EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", default=False) EMAIL_HOST_USER = env("EMAIL_HOST_USER", default="") EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD", default="") DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL", default="noreply@px360.sa") # Security Settings SECURE_BROWSER_XSS_FILTER = True SECURE_CONTENT_TYPE_NOSNIFF = True X_FRAME_OPTIONS = "DENY" SECURE_SSL_REDIRECT = env.bool("SECURE_SSL_REDIRECT", default=False) SESSION_COOKIE_SECURE = env.bool("SESSION_COOKIE_SECURE", default=False) CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE", default=False) SESSION_COOKIE_HTTPONLY = True CSRF_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = "Lax" CSRF_COOKIE_SAMESITE = "Lax" # Password Policy Settings PASSWORD_MIN_LENGTH = 8 PASSWORD_COMPLEXITY = True # Login Security - Rate Limiting # Login attempts rate limiting (Django Axes would be recommended for production) MAX_LOGIN_ATTEMPTS = 5 LOGIN_ATTEMPT_TIMEOUT_MINUTES = 30 # Session Security SESSION_COOKIE_AGE = 120 * 60 # 2 hours SESSION_EXPIRE_AT_BROWSER_CLOSE = env.bool("SESSION_EXPIRE_AT_BROWSER_CLOSE", default=True) SESSION_SAVE_EVERY_REQUEST = True # Multi-Tenancy Settings TENANCY_ENABLED = True TENANT_MODEL = "organizations.Hospital" TENANT_FIELD = "hospital" # Tenant isolation level # 'strict' - Complete isolation (users only see their hospital) # 'relaxed' - PX admins can see all hospitals TENANT_ISOLATION_LEVEL = "strict" # Social Media API Configuration YOUTUBE_API_KEY = env('YOUTUBE_API_KEY', default='AIzaSyAem20etP6GkRNMmCyI1pRJF7v8U_xDyMM') YOUTUBE_CHANNEL_ID = env('YOUTUBE_CHANNEL_ID', default='UCKoEfCXsm4_cQMtqJTvZUVQ') FACEBOOK_PAGE_ID = env('FACEBOOK_PAGE_ID', default='938104059393026') FACEBOOK_ACCESS_TOKEN = env('FACEBOOK_ACCESS_TOKEN', default='EAATrDf0UAS8BQWSKbljCUDMbluZBbxZCSWLJkZBGIviBtK8IQ7FDHfGQZBHHm7lsgLhZBL2trT3ZBGPtsWRjntFWQovhkhx726ZBexRZCKitEMhxAiZBmls7uX946432k963Myl6aYBzJzwLhSyygZAFOGP7iIIZANVf6GtLlvAnWn0NXRwZAYR0CNNUwCEEsZAAc') INSTAGRAM_ACCOUNT_ID = env('INSTAGRAM_ACCOUNT_ID', default='17841431861985364') INSTAGRAM_ACCESS_TOKEN = env('INSTAGRAM_ACCESS_TOKEN', default='EAATrDf0UAS8BQWSKbljCUDMbluZBbxZCSWLJkZBGIviBtK8IQ7FDHfGQZBHHm7lsgLhZBL2trT3ZBGPtsWRjntFWQovhkhx726ZBexRZCKitEMhxAiZBmls7uX946432k963Myl6aYBzJzwLhSyygZAFOGP7iIIZANVf6GtLlvAnWn0NXRwZAYR0CNNUwCEEsZAAc') # Twitter/X Configuration TWITTER_BEARER_TOKEN = env('TWITTER_BEARER_TOKEN', default=None) TWITTER_USERNAME = env('TWITTER_USERNAME', default=None) # LinkedIn Configuration LINKEDIN_ACCESS_TOKEN = env('LINKEDIN_ACCESS_TOKEN', default=None) LINKEDIN_ORGANIZATION_ID = env('LINKEDIN_ORGANIZATION_ID', default=None) # Google Reviews Configuration GOOGLE_CREDENTIALS_FILE = env('GOOGLE_CREDENTIALS_FILE', default='client_secret.json') GOOGLE_TOKEN_FILE = env('GOOGLE_TOKEN_FILE', default='token.json') GOOGLE_LOCATIONS = env.list('GOOGLE_LOCATIONS', default=[]) # OpenRouter Configuration for AI Comment Analysis OPENROUTER_API_KEY = env('OPENROUTER_API_KEY', default='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') OPENROUTER_MODEL = env('OPENROUTER_MODEL', default='google/gemma-3-27b-it:free') ANALYSIS_BATCH_SIZE = env.int('ANALYSIS_BATCH_SIZE', default=2) ANALYSIS_ENABLED = env.bool('ANALYSIS_ENABLED', default=True)