""" 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', ] 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 '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', ], }, }, ] 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' # 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 = 'django_celery_beat.schedulers:DatabaseScheduler' # 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, }, } # AI Configuration (LiteLLM with OpenRouter) OPENROUTER_API_KEY = env('OPENROUTER_API_KEY', default='') AI_MODEL = env('AI_MODEL', default='xiaomi/mimo-v2-flash: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'), }, } # Email Configuration EMAIL_BACKEND = env('EMAIL_BACKEND', default='django.core.mail.backends.console.EmailBackend') EMAIL_HOST = env('EMAIL_HOST', default='localhost') 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('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)