BREAKING CHANGES:
- Department.head автоматически очищается при увольнении руководителя
- LDAP managedBy атрибут очищается при увольнении
Fixes:
- DN не перемещался при восстановлении сотрудника (Dismissed → Users)
- DN не перемещался при увольнении сотрудника без отделов
- Ошибка при увольнении из множественных отделов (двойной modify_dn)
- Неправильные URL patterns для LDAP моделей в админке
- Двойное кодирование DN в редиректе смены пароля
Features:
- Добавлен метод _ensure_ldap_dn_location для гарантированного перемещения DN
- Добавлена проверка already_there в sync_department_member_to_ldap
- Управление паролями LDAP пользователей через админку:
* Кнопка смены пароля в карточке пользователя
* Массовое действие смены пароля
* Автосинхронизация с Django password hash
- Автоочистка Department.head и LDAP managedBy при увольнении руководителя
- Добавлены методы set_password() и change_password() в LdapUser
Changed:
- Переписан сигнал sync_department_member_to_ldap с реальными методами
- Исправлены URL patterns: ldap_* → employees_*
DRF request.data is dict for JSON but QueryDict for form data.
Use hasattr check to call .dict() only when available, otherwise
use dict() constructor for plain dicts.
dict(QueryDict) wraps every value in a list: {'first_name': ['Вадимин']}
QueryDict.dict() returns single values: {'first_name': 'Вадимин'}
This caused LDAP fields to be saved as "['Вадимин']" instead of 'Вадимин'
because the list was stringified by ldapdb before writing to AD.
- Удалён LDAP_SERVER_TYPE и все OpenLDAP ветки кода
- user_service.py: только AD (sAMAccountName, UAC, microsoft.modify_password)
- auth_backends.py: AD фильтры и атрибуты (objectCategory, objectGUID)
- Удалён server_configs.py (больше не нужен)
- Удалён OpenLDAP Docker окружение (docker-compose.ldap.yml, ldap/, QUICKSTART_LDAP.md)
- Фронтенд: мульти-шаговая регистрация, AvatarCropper 3:4, verify-email страница
- Убран CSRF для JWT API, IP-ограничения работают независимо
- Полевые ошибки регистрации с красной подсветкой на фронте
ПРОБЛЕМА:
- RegisterAPIView знал о режиме LDAP (вызывал _is_ldap_enabled())
- if ldap_enabled: create_ldap_user() else: create_db_user()
- View имел LDAP-специфичную логику выбора режима
- Нарушение принципа инкапсуляции
РЕШЕНИЕ:
Создан единый метод в LdapUserCreationMixin:
create_user(first_name, last_name, email, phone, password, ...)
- Автоматически выбирает LDAP или БД режим
- Инкапсулирует вызов _is_ldap_enabled()
- View больше не знает о существовании LDAP
ИЗМЕНЕНИЯ:
mixins.py:
+ create_user() - фасад, выбирает режим автоматически
• create_ldap_user() - остался для внутреннего использования
• create_db_user() - остался для внутреннего использования
auth.py:
- Удалён import _is_ldap_enabled
- Удалён блок if ldap_enabled: ... else: ...
- Заменён на: emp, error = self.create_user(...)
РЕЗУЛЬТАТ:
- View НЕ знает про LDAP вообще
- Вся LDAP логика инкапсулирована в миксине
- Чистое разделение ответственности
- Упрощение: -18 строк в view
БЫЛО в RegisterAPIView:
ldap_enabled = _is_ldap_enabled()
if ldap_enabled:
emp, err = self.create_ldap_user(...)
else:
emp = self.create_db_user(...)
СТАЛО:
emp, err = self.create_user(...)
Django check: успешно
ПРОБЛЕМА:
- RegisterAPIView содержал ~30 строк LDAP-специфичной логики
- DirectoryService, DirectoryUserDTO, обработка исключений - всё в основном методе
- Нарушение принципа единственной ответственности
- Сложность тестирования и переиспользования
РЕШЕНИЕ:
Создан api/v1/employees/views/mixins.py с миксинами:
1. LdapUserCreationMixin:
- create_ldap_user() - создание пользователя в LDAP через DirectoryService
- create_db_user() - создание пользователя напрямую в БД (fallback без LDAP)
- Централизованная обработка DirectoryLdapError/DirectoryDbError
- Возвращает (Employee, None) при успехе или (None, Response) при ошибке
2. LdapPasswordMixin:
- update_ldap_password() - обновление пароля в LDAP
- Подготовка для использования в PasswordChangeView и других endpoints
ИЗМЕНЕНИЯ:
- auth.py: RegisterAPIView использует LdapUserCreationMixin
- Блок if ldap_enabled заменён на вызовы self.create_ldap_user() / self.create_db_user()
- _helpers.py: удалён _is_ldap_enabled (перемещён в mixins.py)
- Уменьшено количество строк в RegisterAPIView: ~190 → ~170
ПРЕИМУЩЕСТВА:
- Чёткое разделение: общая логика регистрации vs LDAP-специфика
- Переиспользование: миксины можно применить в других views
- Тестируемость: можно мокать методы миксина
- Читаемость: основной метод фокусируется на бизнес-логике
Django check: успешно
Критические исправления:
- actions.py: заменить bulk update() на save() для EmployeeDepartment
Причина: bulk update НЕ вызывает Django signals!
Было: .update(is_active=False) - сигнал не срабатывает
Стало: for loop с save() - сигнал срабатывает для каждой записи
- _helpers.py: удалить неиспользуемую функцию _ldap_try()
Нигде не используется после рефакторинга
- _helpers.py: удалить неиспользуемые импорты
DirectoryLdapError, DirectoryServiceError, DirectoryDbError, status
- actions.py: обновить комментарий с правильным названием сигнала
sync_department_member_to_ldap вместо устаревшего sync_member
Функционал сохранен: при увольнении сотрудника теперь корректно
срабатывают сигналы для каждого EmployeeDepartment, что приводит
к корректной синхронизации с LDAP (перемещение из OU отделов).
Упрощены ViewSets:
- PositionViewSet: 146 → 137 строк (-9 строк, -6%)
- DepartmentRoleViewSet: 362 → 248 строк (-114 строк, -31%)
Изменения PositionViewSet:
- Удалены вызовы DirectoryService().reconcile_position()
- Удалены вызовы DirectoryService().delete_position_group()
- Удалены импорты: DirectoryService, _is_ldap_enabled, _ldap_try
- Actions (set_groups, add_groups, remove_groups): только работа с БД
- perform_create/update/destroy: базовые методы без LDAP
Изменения DepartmentRoleViewSet:
- create(): только создание в БД, удалены LDAP блоки
- update(): упрощен до базового метода
- destroy(): только удаление из БД
- assign(): только update_or_create RoleAssignment
- revoke(): только update is_active=False
- Удалены все if ldap_enabled блоки
- Удалены импорты: DirectoryService, DirectoryLdapError, DirectoryDbError, _is_ldap_enabled
- Удалена обработка ошибок LDAP (try/except блоки)
Итого: -123 строки, все ViewSets теперь работают только с БД
Примечание: Полноценная LDAP синхронизация для Position и DepartmentRole
требует реализации отдельных сигналов (аналогично signals_group.py).
Это может быть следующим этапом рефакторинга.
Изменения:
- EmployeeActionViewSet: 240 → 115 строк (-125 строк, -52%)
- _apply_effects(): удалена вся LDAP логика (~140 строк)
Удалено:
- Прямые вызовы DirectoryService
- Обработка LdapSyncState
- Логика перемещения в OU=Dismissed и обратно
- Удаление из департаментов в LDAP
- Все try/except блоки для LDAP ошибок
- Импорты: DirectoryService, DirectoryLdapError, DirectoryServiceError, DirectoryDbError, LdapSyncState, settings, _is_ldap_enabled
Осталось только работа с БД:
- При ACTION_DISMISSED: is_active=False + деактивация EmployeeDepartment
- При других действиях: is_active=True
LDAP синхронизация через существующие сигналы:
- sync_employee_to_ldap_on_save: обрабатывает is_active (активация/деактивация)
- sync_member (signals_department.py): обрабатывает EmployeeDepartment (удаление из отделов)
Функционал сохранен, код стал в 2 раза короче
Изменения:
- VerifyEmailAPIView: 331 → 271 строк (-60 строк)
- Удалена вся логика if ldap_enabled с DirectoryService
- Удалены проверки LdapSyncState и обработка LDAP ошибок
- Активация пользователя теперь через сигнал sync_employee_to_ldap_on_save
- Используется _ldap_changes для передачи данных в сигнал
- Удален импорт DirectoryServiceError
Логика:
- ViewSet устанавливает is_active = True и _ldap_changes
- Сигнал автоматически синхронизирует активацию в LDAP
- Обработка ldap_enabled и ошибок происходит в сигнале
- Код стал в 3 раза короче и понятнее
- Удален метод ui_context (42 строки)
- Все его данные доступны через отдельные endpoints:
- dept → GET /departments/{id}/
- roles → можно добавить /departments/{id}/roles/
- links → GET /departments/{id}/members/
- user_perms → GET /departments/{id}/user-perms/
- choices → UI логика (фронтенд)
Преимущества:
- Кешируемые отдельные endpoints
- Фронт запрашивает только нужное
- Проще тестировать
- Более RESTful
-42 строки (480 → 438)
- Удалены все @transaction.atomic из actions (set_head, set_member_role, add_member, remove_member)
- Удален импорт transaction
- Одиночные операции БД уже атомарны по умолчанию
- Django request уже в транзакции если настроен ATOMIC_REQUESTS
-5 строк (485 → 480)
Методы дублировали стандартную функциональность ModelViewSet:
- create(): serializer.save() делает то же самое
- destroy(): стандартный метод идентичен
-25 строк (510 → 485)
- Удалены импорты DirectoryService, DirectoryDepartmentDTO, LDAP errors
- Удален импорт _is_ldap_enabled
- Убраны все блоки if ldap_enabled: ... else: ...
- ViewSet теперь только работает с БД (сигналы синхронизируют LDAP)
Методы упрощены до чистой БД логики:
- partial_update: просто обновляет поля
- set_head: просто устанавливает head + head_appointed_at
- set_member_role: обновляет link.role и RoleAssignment
- add_member: создает/активирует EmployeeDepartment
- remove_member: деактивирует EmployeeDepartment
- create: просто создает Department
- destroy: просто удаляет Department
DepartmentViewSet теперь чистый CRUD: -189 строк (699 → 510)
- Убрана логика создания LDAP пользователей из сигналов
- Сигналы ТОЛЬКО синхронизируют изменения существующих LDAP пользователей
- Создание LDAP пользователей - ответственность RegisterAPIView
- Убран _ldap_password из perform_create (создание через API не создает LDAP пользователей)
- Создан LdapPasswordMixin для endpoint'ов работы с паролями
- perform_create теперь только создает Employee в БД (административное создание)
Архитектурное улучшение: разделение ответственности
- Auth views (RegisterAPIView) - создают LDAP пользователей напрямую
- Сигналы - только синхронизация изменений
- LdapPasswordMixin - для endpoint'ов смены пароля
- Удалён ExternalSystemSyncMixin из EmployeeViewSet
- ViewSet теперь использует временные атрибуты (_ldap_password, _ldap_avatar, _ldap_changes)
- Создан employees/signals_ldap.py с post_save/post_delete handlers
- Сигналы автоматически синхронизируют Employee с LDAP через DirectoryService
- Убран дублирующий блок required_perms_by_action из EmployeeViewSet
- Упрощены perform_create и perform_update (-170 строк)
Архитектурное улучшение: разделение ответственности ViewSet (HTTP) и signals (синхронизация)
- Создан frontend/src/lib/api.ts с 43 методами для всех API endpoint'ов
- Создан frontend/src/lib/permissions.ts для проверки прав доступа
- Исправлен URL в getChatMessagesAround: /messages/around/ → /messages-around/
- Исправлен параметр: message_id → around_id
- Восстановлен backend из ветки master для стабильности
- Удалены конфликтующие файлы Pages Router (src/pages, App.tsx, providers)
- Оставлена только чистая структура Next.js App Router
- Добавлена документация по миграции API v2
- Move department_id to department_dn resolution to ViewSet layer
- update_user_in_ldap_only now receives ready department_dn parameter
- LDAP service no longer queries Department.objects directly
- All three *_in_ldap_only methods are now pure LDAP (no DB access)
Removed unnecessary method overrides:
Deleted:
- create(): removed entirely - миксин работает через perform_create
- partial_update(): removed entirely - миксин работает автоматически
- perform_update() avatar handling: moved to perform_update
Simplified:
- perform_create(): only unusable password + skills (30 lines)
- perform_update(): only email verification (15 lines)
Benefits:
- Миксин работает "из коробки" без явных переопределений
- Меньше кода для поддержки
- create/update/destroy используют стандартный механизм DRF
- Только необходимая кастомная логика в perform_*
Теперь ViewSet минимален:
- Конфигурация миксина (get_external_sync_method, prepare_external_data)
- Только критичная логика (unusable password, email verification)
- Все остальное - автоматически через миксин
manage.py check: no issues
Created universal solution for ViewSet + external system synchronization:
1. ExternalSystemSyncMixin (common/external_sync_mixin.py):
- Automatically wraps CRUD operations in transactions
- Coordinates DB + LDAP in single atomic transaction
- Rolls back DB if LDAP fails
- Configurable error handling
- Works with any external system (LDAP, APIs, etc)
2. Applied to EmployeeViewSet:
- Added mixin to base classes
- Configured LDAP sync mapping (create/update/destroy)
- prepare_external_data() for DTO creation
- handle_external_sync_error() for LDAP errors
- No need to override create/update/destroy methods
3. Created pure LDAP methods in DirectoryService/UserService:
- create_user_in_ldap_only() - LDAP create, returns dn/guid
- update_user_in_ldap_only() - LDAP update, no DB access
- delete_user_in_ldap_only() - LDAP delete, no DB access
- All methods removed transaction.atomic (managed by ViewSet)
4. Documentation:
- Complete guide in docs/guides/EXTERNAL_SYNC_MIXIN_GUIDE.md
- Usage examples for all patterns
- Migration guide from old architecture
- Comparison with alternatives (Signals, Celery, Saga)
Benefits:
- Minimal code (3 methods vs overriding entire ViewSet)
- Clear separation: View owns DB, LDAP service owns LDAP
- Reusable for all ViewSets (departments, positions, etc)
- Easy to test (external_sync_enabled = False)
- Universal (works with any external system)
manage.py check: System check identified no issues
DirectoryService:
- Added public methods: assign_role, revoke_role, create_role, update_role, delete_role
- Added public method: move_user_to_base (replaces private _user_service._move_user_to_base)
- Fixed method signatures to match DepartmentService API
roles.py:
- Replaced direct DepartmentService usage with DirectoryService facade (5 methods)
- Removed manual GroupService/UserService instantiation
- All CRUD operations now go through facade layer
actions.py:
- Replaced private API svc._user_service._move_user_to_base() with public svc.move_user_to_base()
- Simplified LDAP connection handling (2 occurrences)
- Removed manual _ldap() and ensure_container_exists calls
manage.py check: System check identified no issues
- Replaced ..permissions with ...permissions in all 7 view modules
- Deleted backend/api/v1/employees/permissions.py (duplicate re-export)
- Views now import directly from api.v1.permissions (3 dots up)
manage.py check: System check identified no issues
- Split into 9 modules by ViewSet: departments, employees, positions,
roles, skills, actions, groups, auth, _helpers
- Created views/__init__.py with re-exports for backward compatibility
- Created employees/permissions.py re-export shim (views/ is now a
sub-package, so ..permissions resolves to employees.permissions)
- Fixed api/auth/serializers.py import: PHONE_FIELD from _helpers,
_normalize_phone from employees.utils
- Removed ~250+ debug print()/logger.info() statements from
departments.py and employees.py
- Removed dead _with_ldap_service() decorator from helpers
- Removed unreachable code at end of GroupViewSet.replace_members
manage.py check: System check identified no issues (0 silenced)
- Добавлено поле Employee.username для кэширования LDAP логина
- API endpoint обновлен: поддержка кэша и force_refresh параметра
- Новый дизайн: убран blur, добавлена иконка обновления
- Автоматическая загрузка при инициализации (из кэша или LDAP)
- Визуальная индикация источника данных (cached/fresh)
- Доступно всем пользователям без ограничений
- LdapRepository требует активное соединение в конструкторе
- Добавлен вызов _conn() для получения LDAP соединения
- Исправлена ошибка: TypeError: LdapRepository.__init__() missing 1 required positional argument: 'conn'
- Теперь используется: LdapRepository(_conn())
- Использована встроенная система пермишенов DRF вместо ручной проверки
- Добавлен ldap_info в required_perms_by_action с правом view_ldap_info
- Добавлен ldap_info в get_permissions() для применения IsSelfOrStaff
- Удалена избыточная ручная проверка прав внутри метода ldap_info()
- Проверка прав теперь выполняется автоматически через get_object()
- Пользователь может видеть свой LDAP логин (IsSelfOrStaff)
- Админы и пользователи с view_ldap_info могут видеть любые логины
- Добавлено поле с LDAP логином (sAMAccountName) в профиле сотрудника
- Реализован blur-эффект с раскрытием по клику для безопасности
- Создан API endpoint /api/v1/employees/{id}/ldap-info/ для получения LDAP данных
- Добавлен новый пермишен employees.view_ldap_info
- Стили: SCSS компонент _ldap-login.scss с анимациями и состояниями
- JavaScript: класс LdapLoginField с AJAX запросами и обработкой ошибок
- Доступ только для staff или пользователей с правом view_ldap_info
- Данные запрашиваются из Active Directory on-demand (не хранятся в БД)
- Изменена отправка аватаров на multipart/form-data вместо JSON+base64
- Исправлено в employee_create, employee_create_modal, employee_edit, employee_edit_me
- Исправлена ошибка 'Employee' object has no attribute 'photo' (user.photo -> user.avatar)
- Убрано сжатие изображений в _file_to_data_uri (функция deprecated)
Fixes: #400 Bad Request при загрузке больших аватаров
When LDAP is enabled:
- create_role: creates LDAP group before DB record
- update_role: renames LDAP group before DB update
- delete_role: deletes LDAP group before DB deletion
- assign_role: adds to LDAP group before creating RoleAssignment
- revoke_role: removes from LDAP group before deactivating assignment
On LDAP error: operation fails, no DB changes
On DB error after LDAP success: rollback LDAP changes
Fixes: roles could bypass LDAP when it was enabled
BREAKING CHANGES:
- Removed OU=Roles from LDAP structure, role groups now created directly in department OU
- Role groups use ROLE_<Name> naming convention (similar to DEP_, POS_)
New features:
- RoleAssignment model: allows assigning roles to ANY employee, not just department members
- New API endpoints: /assign/, /revoke/, /assignments/ on DepartmentRoleViewSet
- Permission system updated: has_dept_perm() checks RoleAssignment first
- Employee profile shows external roles with 'внешняя роль' badge
- Employee list filter includes role-only employees with department_relation field
- Role assignment modal now searches all employees via AJAX
LDAP changes:
- _ensure_role_group(): creates ROLE_<Name> group in department OU
- assign_role(), revoke_role(): manage RoleAssignment + LDAP sync
- rename_role_group(), delete_role_group(): full lifecycle support
- Removed all references to OU=Roles
Migrations:
- 0035: Create RoleAssignment model
- 0036: Data migration from EmployeeDepartment.role
Tests:
- 7 new tests for role assignment (all passing)
Docs:
- 9 detailed reports on architecture, models, API, LDAP, tests