from pathlib import Path from django.urls import reverse_lazy import environ BASE_DIR = Path(__file__).resolve().parent.parent.parent env = environ.Env() environ.Env.read_env(BASE_DIR / '.env') SECRET_KEY = env('SECRET_KEY') ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) # Ключ для шифрования секретных полей в БД (EncryptedCharField: пароли/токены # коннекторов, access-токены Mattermost). Задавайте ОТДЕЛЬНО от SECRET_KEY — # тогда утечка SECRET_KEY не раскрывает секреты at-rest. # Пустой default: production требует явный ключ; local может fallback на SECRET_KEY. FIELD_ENCRYPTION_KEY = env('FIELD_ENCRYPTION_KEY', default='') # E016 — ложноположительный для proxy-моделей над Directory (django-fast-treenode # динамически добавляет индекс по _path, который proxy «наследует»). На реальных # таблицах индексы корректны; для proxy это лишь предупреждение проверки. SILENCED_SYSTEM_CHECKS = ['models.E016'] # Базовый URL портала — используется для абсолютных ссылок в уведомлениях # (например, Mattermost-бот шлёт ссылку на задачу) PORTAL_BASE_URL = env('PORTAL_BASE_URL', default='http://localhost:8000') # Токен слэш-команды Mattermost (System Console/Team → Integrations → Slash Commands). # Пустая строка по умолчанию — запросы без совпадающего токена всегда отклоняются (fail-closed), # пока команда реально не создана в Mattermost и токен не прописан в .env. MATTERMOST_SLASH_COMMAND_TOKEN = env('MATTERMOST_SLASH_COMMAND_TOKEN', default='') # Токен интерактивных кнопок Mattermost (attachment actions). Fail-closed: # пустой токен → task_done_action отклоняет все запросы. Можно совпадать со # slash-токеном или задать отдельный секрет. MATTERMOST_INTERACTIVE_TOKEN = env( 'MATTERMOST_INTERACTIVE_TOKEN', default=env('MATTERMOST_SLASH_COMMAND_TOKEN', default=''), ) INSTALLED_APPS = [ # Admin "unfold", # before django.contrib.admin "unfold.contrib.filters", # optional, if special filters are needed "unfold.contrib.forms", # optional, if special form elements are needed "unfold.contrib.inlines", # optional, if special inlines are needed "unfold.contrib.import_export", # optional, if django-import-export package is used "unfold.contrib.guardian", # optional, if django-guardian package is used "unfold.contrib.simple_history", # optional, if django-simple-history package is used "unfold.contrib.location_field", # optional, if django-location-field package is used "unfold.contrib.constance", # optional, if django-constance package is used # Django 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Third-party 'treenode', 'location_field', 'import_export', 'rest_framework', # Core 'core', # Apps 'apps.locations', 'apps.employees', 'apps.visit_manager', 'tinymce', 'apps.it_manager', 'apps.feed', 'apps.tasks', 'apps.hiring', 'apps.utilities', 'apps.rro', 'apps.ad_connector', 'apps.mail_connector', 'apps.mailparser', 'apps.mattermost_connector', 'apps.portal_connector', 'apps.nextcloud_connector', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', # выбор языка (после Session, до Common) 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'apps.employees.middleware.TwoFactorMiddleware', # обязательная 2FA для staff (после Auth) 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'core.middleware.UpdateLastSeenMiddleware', ] ROOT_URLCONF = 'acore.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', 'django.template.context_processors.i18n', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'acore.wsgi.application' ASGI_APPLICATION = 'acore.asgi.application' DATABASES = { 'default': env.db('DATABASE_URL') } AUTH_USER_MODEL = 'employees.Employee' 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'}, ] LANGUAGE_CODE = 'ru' # базовый язык интерфейса (исходные строки msgid) LANGUAGES = [ ('ru', 'Русский'), ('uk', 'Українська'), ('en', 'English'), ] LOCALE_PATHS = [BASE_DIR / 'locale'] TIME_ZONE = 'Europe/Kiev' USE_I18N = True USE_TZ = True STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_DIRS = [BASE_DIR / 'static'] MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' LOGIN_URL = '/accounts/login/' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # Required for OSM tile usage policy: send origin as Referer on cross-origin requests. SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin' # --- Security --- SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Lax' CSRF_COOKIE_HTTPONLY = True CSRF_COOKIE_SAMESITE = 'Lax' X_FRAME_OPTIONS = 'DENY' # File upload limits DATA_UPLOAD_MAX_MEMORY_SIZE = 15 * 1024 * 1024 # 15 MB FILE_UPLOAD_MAX_MEMORY_SIZE = 15 * 1024 * 1024 # 15 MB DATA_UPLOAD_MAX_NUMBER_FIELDS = 500 UNFOLD = { "SITE_TITLE": "A-CORE", "SITE_HEADER": "A-CORE", "SITE_SUBHEADER": "Управление аптечной сетью", "SITE_URL": "/", "COLORS": { "base": { "50": "249, 250, 251", "100": "243, 244, 246", "200": "229, 231, 235", "300": "209, 213, 219", "400": "156, 163, 175", "500": "107, 114, 128", "600": "75, 85, 99", "700": "55, 65, 81", "800": "31, 41, 55", "900": "17, 24, 39", "950": "3, 7, 18", }, "primary": { "50": "255, 241, 242", "100": "255, 228, 230", "200": "254, 205, 211", "300": "253, 164, 175", "400": "251, 113, 133", "500": "244, 63, 94", "600": "227, 6, 19", "700": "190, 18, 60", "800": "159, 18, 57", "900": "136, 19, 55", "950": "76, 5, 25", }, "font": { "subtle-light": "var(--color-base-500)", "subtle-dark": "var(--color-base-400)", "default-light": "var(--color-base-600)", "default-dark": "var(--color-base-300)", "important-light": "var(--color-base-900)", "important-dark": "var(--color-base-100)", }, }, "SIDEBAR": { "show_search": True, "show_all_applications": False, "navigation": [ { "title": "Сотрудники", "separator": False, "collapsible": False, "items": [ { "title": "Сотрудники", "icon": "people", "link": reverse_lazy("admin:employees_employee_changelist"), }, { "title": "Группы доступа", "icon": "shield_person", "link": reverse_lazy("admin:auth_group_changelist"), }, ], }, { "title": "Структура компании", "separator": True, "collapsible": True, "items": [ { "title": "Аптеки", "icon": "local_pharmacy", "link": reverse_lazy("admin:locations_pharmacy_changelist"), }, { "title": "Локации (офисы, склады)", "icon": "location_on", "link": reverse_lazy("admin:locations_location_changelist"), }, { "title": "Справочники", "icon": "menu_book", "link": reverse_lazy("admin:core_directory_changelist"), }, { "title": "Типы справочников", "icon": "category", "link": reverse_lazy("admin:core_directorytype_changelist"), }, ], }, { "title": "Учёт посещений", "separator": True, "collapsible": True, "items": [ { "title": "Записи посещений", "icon": "how_to_reg", "link": reverse_lazy("admin:visit_manager_attendancelog_changelist"), }, { "title": "Рабочие смены (факт)", "icon": "work_history", "link": reverse_lazy("admin:visit_manager_workshift_changelist"), }, { "title": "Шаблоны смен", "icon": "schedule", "link": reverse_lazy("admin:visit_manager_shift_changelist"), }, { "title": "Расписания", "icon": "event_note", "link": reverse_lazy("admin:visit_manager_scheduletemplate_changelist"), }, { "title": "Рабочие расписания", "icon": "calendar_month", "link": reverse_lazy("admin:visit_manager_workschedule_changelist"), }, { "title": "Праздники", "icon": "celebration", "link": reverse_lazy("admin:visit_manager_holiday_changelist"), }, ], }, { "title": "IT-отдел", "separator": True, "collapsible": True, "items": [ { "title": "Оборудование", "icon": "computer", "link": reverse_lazy("admin:it_manager_equipment_changelist"), }, { "title": "Расходники", "icon": "ink_eraser", "link": reverse_lazy("admin:it_manager_consumable_changelist"), }, { "title": "Заявки", "icon": "confirmation_number", "link": reverse_lazy("admin:it_manager_ticket_changelist"), }, { "title": "Журнал работ", "icon": "build", "link": reverse_lazy("admin:it_manager_worklog_changelist"), }, { "title": "Обращения", "icon": "support_agent", "link": reverse_lazy("admin:it_manager_servicecall_changelist"), }, { "title": "Провайдеры", "icon": "router", "link": reverse_lazy("admin:it_manager_internetprovider_changelist"), }, { "title": "Договоры интернет", "icon": "cable", "link": reverse_lazy("admin:it_manager_internetcontract_changelist"), }, ], }, { "title": "Mattermost", "separator": True, "collapsible": True, "items": [ { "title": "Пользователи", "icon": "chat", "link": reverse_lazy("admin:mattermost_connector_mattermostuser_changelist"), }, { "title": "Команды", "icon": "group", "link": reverse_lazy("admin:mattermost_connector_mattermostteam_changelist"), }, { "title": "Каналы", "icon": "tag", "link": reverse_lazy("admin:mattermost_connector_mattermostchannel_changelist"), }, { "title": "Синхронизация команд", "icon": "sync", "link": reverse_lazy("admin:mattermost_connector_teamsyncsettings_changelist"), }, { "title": "Синхронизация каналов", "icon": "sync_alt", "link": reverse_lazy("admin:mattermost_connector_channelsyncsettings_changelist"), }, ], }, { "title": "Настройки", "separator": True, "collapsible": True, "items": [ { "title": "Конфиги коннекторов", "icon": "settings", "link": reverse_lazy("admin:core_connectorconfig_changelist"), }, ], }, ], }, } LOCATION_FIELD = { 'map.provider': 'openstreetmap', 'map.zoom': 14, 'search.provider': 'nominatim', 'search.suffix': '', 'provider.openstreetmap.max_zoom': 19, } REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.SessionAuthentication', # BasicAuthentication убран: передаёт credentials в открытом виде ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 20, } LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', 'style': '{', }, 'simple': { 'format': '{levelname} {message}', 'style': '{', }, }, 'handlers': { 'file': { 'level': 'ERROR', 'class': 'logging.FileHandler', 'filename': BASE_DIR / 'logs' / 'django.log', 'formatter': 'verbose', }, 'console': { 'level': 'INFO', 'class': 'logging.StreamHandler', 'formatter': 'simple', }, }, 'root': { 'handlers': ['console', 'file'], 'level': 'INFO', }, 'loggers': { 'apps.mattermost_connector': { 'handlers': ['file'], 'level': 'INFO', 'propagate': False, }, 'apps.ad_connector': { 'handlers': ['file'], 'level': 'INFO', 'propagate': False, }, 'apps.mail_connector': { 'handlers': ['file'], 'level': 'INFO', 'propagate': False, }, 'apps.nextcloud_connector': { 'handlers': ['file'], 'level': 'INFO', 'propagate': False, }, 'apps.visit_manager': { 'handlers': ['file'], 'level': 'INFO', 'propagate': False, }, }, } # --- Celery --- CELERY_BROKER_URL = env('CELERY_BROKER_URL', default='redis://localhost:6379/0') CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND', default=CELERY_BROKER_URL) CELERY_TIMEZONE = TIME_ZONE # Без брокера (локальная разработка) задачи выполняются синхронно CELERY_TASK_ALWAYS_EAGER = env.bool('CELERY_TASK_ALWAYS_EAGER', default=False) # --- TinyMCE (редактор постов ленты) --- TINYMCE_DEFAULT_CONFIG = { 'height': 360, 'menubar': False, 'plugins': 'lists link table autolink image charmap', 'toolbar': ( 'undo redo | blocks | bold italic underline strikethrough | ' 'forecolor | alignleft aligncenter alignright | ' 'bullist numlist | link table | removeformat' ), 'language': 'uk', 'license_key': 'gpl', 'promotion': False, 'branding': False, 'convert_urls': False, }