Corp/backend/common/external_sync_mixin.py
igor_nadein 59b8f56c5a refactor(employees): simplify create() using ExternalSystemSyncMixin
Replaced 150+ lines of manual transaction management with mixin:

Before:
- Manual transaction.atomic()
- Manual LDAP sync calls
- Duplicate code for LDAP/non-LDAP modes
- Manual error handling and rollback
- ~150 lines of boilerplate

After:
- create() delegates to super() - миксин управляет всем
- perform_create() - только БД логика (30 lines)
- handle_external_sync_result() - обработка LDAP результата
- Миксин автоматически управляет транзакциями и откатами

Changes:
1. create(): simplified to validation + super().create()
2. perform_create(): creates Employee in DB, sets password/avatar/skills
3. prepare_external_data(): builds DTO with avatar_bytes, group_cns
4. handle_external_sync_result(): saves DN/GUID to LdapSyncState,
   sets employeeNumber, assigns position

5. ExternalSystemSyncMixin: added handle_external_sync_result() hook
   for custom post-sync actions

Code reduction: 150 lines -> 80 lines
Benefits: cleaner, DRYer, automatic error handling

manage.py check: no issues
2026-02-16 17:07:15 +03:00

262 lines
11 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.

"""
Универсальный миксин для синхронизации DB операций с внешними системами (LDAP, AD, etc).
Использование:
class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
external_sync_service = DirectoryService()
def get_external_sync_method(self, action):
return {
'create': 'create_user_ldap',
'update': 'update_user_ldap',
'destroy': 'delete_user_ldap',
}.get(action)
def prepare_external_data(self, instance, action):
if action == 'create':
return {'user': instance}
return {'user_id': instance.id}
"""
import logging
from functools import wraps
from django.db import transaction
from rest_framework import status
from rest_framework.response import Response
logger = logging.getLogger(__name__)
class ExternalSystemSyncMixin:
"""
Миксин для автоматической синхронизации операций ViewSet с внешними системами.
Паттерн:
1. Выполняет DB операцию в транзакции
2. При успехе - вызывает метод внешней системы
3. При ошибке внешней системы - откатывает DB транзакцию
Attributes:
external_sync_service: Сервис для работы с внешней системой (например, DirectoryService)
external_sync_enabled: Флаг включения синхронизации (по умолчанию True)
external_sync_async: Использовать асинхронную синхронизацию через Celery
"""
external_sync_service = None
external_sync_enabled = True
external_sync_async = False
def get_external_sync_method(self, action):
"""
Возвращает имя метода внешнего сервиса для данного action.
Args:
action: 'create', 'update', 'partial_update', 'destroy'
Returns:
str: имя метода сервиса или None если синхронизация не нужна
"""
raise NotImplementedError("Переопределите get_external_sync_method()")
def prepare_external_data(self, instance, action):
"""
Подготавливает данные для передачи во внешний сервис.
Args:
instance: Инстанс модели Django
action: Тип операции
Returns:
dict: данные для метода внешнего сервиса
"""
return {'instance': instance}
def handle_external_sync_error(self, error, instance, action):
"""
Обработка ошибки синхронизации с внешней системой.
Args:
error: Exception от внешней системы
instance: Инстанс модели
action: Тип операции
Returns:
Response или None (если None - будет поднята ошибка)
"""
logger.error(
f"External sync failed for {action} on {instance}: {error}")
return Response(
{'detail': f'Ошибка синхронизации с внешней системой: {str(error)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def perform_create(self, serializer):
"""Переопределение create с синхронизацией."""
return self._perform_with_sync(serializer, 'create', super().perform_create)
def perform_update(self, serializer):
"""Переопределение update с синхронизацией."""
action = 'partial_update' if self.request.method == 'PATCH' else 'update'
return self._perform_with_sync(serializer, action, super().perform_update)
def perform_destroy(self, instance):
"""Переопределение destroy с синхронизацией."""
return self._perform_with_sync_instance(instance, 'destroy', super().perform_destroy)
def _perform_with_sync(self, serializer, action, db_operation):
"""
Выполняет DB операцию и синхронизацию с внешней системой.
Args:
serializer: DRF serializer с валидированными данными
action: Тип операции
db_operation: Функция для выполнения DB операции
"""
if not self.external_sync_enabled:
return db_operation(serializer)
sync_method_name = self.get_external_sync_method(action)
if not sync_method_name or not self.external_sync_service:
return db_operation(serializer)
# Атомарная транзакция: сохраняем в БД
with transaction.atomic():
db_operation(serializer)
instance = serializer.instance
# Подготавливаем данные для внешней системы
external_data = self.prepare_external_data(instance, action)
try:
# Вызываем метод внешней системы
sync_method = getattr(
self.external_sync_service, sync_method_name)
result = sync_method(**external_data)
# Если внешняя система вернула данные для обновления БД
if isinstance(result, dict) and result.get('update_db'):
for field, value in result['update_db'].items():
setattr(instance, field, value)
instance.save(update_fields=list(
result['update_db'].keys()))
# Вызываем кастомный обработчик результата если определен
if hasattr(self, 'handle_external_sync_result'):
self.handle_external_sync_result(instance, result, action)
logger.info(
f"External sync success for {action} on {instance}")
except Exception as e:
# При ошибке внешней системы - откатываем транзакцию
logger.error(f"External sync failed, rolling back: {e}")
transaction.set_rollback(True)
# Вызываем обработчик ошибок
error_response = self.handle_external_sync_error(
e, instance, action)
if error_response:
raise ExternalSyncError(error_response)
raise
def _perform_with_sync_instance(self, instance, action, db_operation):
"""
Выполняет DB операцию на инстансе с синхронизацией.
Args:
instance: Инстанс модели
action: Тип операции
db_operation: Функция для выполнения DB операции
"""
if not self.external_sync_enabled:
return db_operation(instance)
sync_method_name = self.get_external_sync_method(action)
if not sync_method_name or not self.external_sync_service:
return db_operation(instance)
# Сохраняем данные перед удалением
external_data = self.prepare_external_data(instance, action)
# Атомарная транзакция: удаляем из БД
with transaction.atomic():
db_operation(instance)
try:
# Вызываем метод внешней системы
sync_method = getattr(
self.external_sync_service, sync_method_name)
sync_method(**external_data)
logger.info(
f"External sync success for {action} on {instance}")
except Exception as e:
# При ошибке внешней системы - откатываем транзакцию
logger.error(f"External sync failed, rolling back: {e}")
transaction.set_rollback(True)
error_response = self.handle_external_sync_error(
e, instance, action)
if error_response:
raise ExternalSyncError(error_response)
raise
class ExternalSyncError(Exception):
"""Исключение с Response для возврата клиенту."""
def __init__(self, response):
self.response = response
super().__init__()
def with_external_sync(external_service_attr='external_sync_service', method_map=None):
"""
Декоратор для методов ViewSet, добавляющий синхронизацию с внешней системой.
Usage:
@with_external_sync(method_map={'create': 'create_user_ldap'})
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
Args:
external_service_attr: Имя атрибута ViewSet с сервисом
method_map: Маппинг action -> метод внешнего сервиса
"""
def decorator(func):
@wraps(func)
def wrapper(self, request, *args, **kwargs):
# Выполняем оригинальный метод
with transaction.atomic():
response = func(self, request, *args, **kwargs)
# Получаем сервис и метод для синхронизации
service = getattr(self, external_service_attr, None)
if not service or not method_map:
return response
action = self.action
sync_method_name = method_map.get(action)
if not sync_method_name:
return response
try:
# Получаем созданный/обновленный инстанс
instance = getattr(response, 'data', {}).get('id')
if isinstance(response.data, dict):
instance = self.get_queryset().get(
id=response.data['id'])
# Вызываем метод внешней системы
sync_method = getattr(service, sync_method_name)
sync_method(instance=instance)
except Exception as e:
transaction.set_rollback(True)
logger.error(f"External sync failed: {e}")
raise
return response
return wrapper
return decorator