Files
ACORE/acore/settings/base.py
T
lokses 6f299890d2 fix: таймзона Europe/Kiev → Europe/Kyiv
В свежей tzdata (Debian 13 / Python 3.13) старое имя Europe/Kiev удалено,
Django 6 отвергает его (ValueError: Incorrect timezone setting). Переименовано
во всех местах, включая дефолты Mattermost-таймзоны.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 17:11:14 +03:00

542 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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')
# CARTO raster basemaps (Leaflet): без ключа используется OpenStreetMap.
# Бесплатный ключ: https://carto.com/basemaps/apikey/
CARTO_BASEMAPS_API_KEY = env('CARTO_BASEMAPS_API_KEY', default='')
# ACORE → PNMS identity sync (portal_connector)
PNMS_BASE_URL = env('PNMS_BASE_URL', default='')
PNMS_ACORE_TOKEN = env('PNMS_ACORE_TOKEN', default='')
PNMS_SYNC_ENABLED = env.bool('PNMS_SYNC_ENABLED', default=False)
# ACORE → MSORTER: только сотрудники в группе «Почта» (админы UI — «Администратор почты»).
MSORTER_BASE_URL = env('MSORTER_BASE_URL', default='')
MSORTER_ACORE_TOKEN = env('MSORTER_ACORE_TOKEN', default='')
MSORTER_SYNC_ENABLED = env.bool('MSORTER_SYNC_ENABLED', default=False)
MSORTER_ACCESS_GROUP = env('MSORTER_ACCESS_GROUP', default='Почта')
MSORTER_ADMIN_GROUP = env('MSORTER_ADMIN_GROUP', default='Администратор почты')
# ACORE → PVMS: identity + reports. PVMS_ATTENDANCE_SOT=1 — ACORE больше не пишет AttendanceLog.
PVMS_BASE_URL = env('PVMS_BASE_URL', default='')
PVMS_ACORE_TOKEN = env('PVMS_ACORE_TOKEN', default='')
# Reports only; falls back to PVMS_ACORE_TOKEN when empty.
PVMS_REPORT_TOKEN = env('PVMS_REPORT_TOKEN', default='')
PVMS_SYNC_ENABLED = env.bool('PVMS_SYNC_ENABLED', default=False)
PVMS_ATTENDANCE_SOT = env.bool('PVMS_ATTENDANCE_SOT', default=False)
# Токен слэш-команды 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.reports',
'apps.feed',
'apps.tasks',
'apps.hiring',
'apps.utilities',
'apps.power_assets',
'apps.rro',
'apps.help',
'apps.ad_connector',
'apps.mail_connector',
'apps.mailparser',
'apps.mattermost_connector',
'apps.portal_connector.apps.PortalConnectorConfig',
'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)
'core.middleware.DevAccessPreviewMiddleware',
'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',
'core.context_processors.portal_map_config',
'core.context_processors.access_preview',
],
},
},
]
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/Kyiv'
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,
}
# Без аргументов `manage.py test` должен покрывать core + apps.*, а не только
# то, что unittest находит от корня проекта (см. acore/test_runner.py).
TEST_RUNNER = 'acore.test_runner.AcoreDiscoverRunner'
# Локально — in-process кэш. В production.py переопределяется на Redis:
# иначе троттлинг логина на нескольких воркерах не общий и сбрасывается рестартом.
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'acore-local',
}
}
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': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': BASE_DIR / 'logs' / 'django.log',
'maxBytes': 10 * 1024 * 1024,
'backupCount': 5,
'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,
},
'security.audit': {
'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,
}