refactor(employees): minimize ViewSet overrides - let mixin do the work
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
This commit is contained in:
parent
518fb6e790
commit
4b56711636
@ -96,7 +96,7 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||||
password = self.request.data.get('password')
|
||||
avatar_file = self.request.data.get('avatar')
|
||||
avatar_bytes = None
|
||||
|
||||
|
||||
if avatar_file and hasattr(avatar_file, 'read'):
|
||||
try:
|
||||
if hasattr(avatar_file, 'seek'):
|
||||
@ -104,13 +104,14 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||||
avatar_bytes = avatar_file.read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
dto = DirectoryUserDTO(
|
||||
username=instance.username if instance.username else None,
|
||||
first_name=instance.first_name,
|
||||
last_name=instance.last_name,
|
||||
email=instance.email,
|
||||
phone_e164=str(instance.phone_number) if instance.phone_number else '',
|
||||
phone_e164=str(
|
||||
instance.phone_number) if instance.phone_number else '',
|
||||
department_dn=self.request.data.get('department_dn'),
|
||||
group_cns=self.request.data.get('group_cns', []) or [],
|
||||
initial_password=password,
|
||||
@ -123,13 +124,14 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||||
# При обновлении передаем инстанс и изменения
|
||||
changes = {}
|
||||
request_data = dict(self.request.data)
|
||||
|
||||
|
||||
# Поля которые идут в LDAP
|
||||
ldap_fields = {'first_name', 'last_name', 'email', 'phone_number', 'is_active', 'password'}
|
||||
ldap_fields = {'first_name', 'last_name', 'email',
|
||||
'phone_number', 'is_active', 'password'}
|
||||
for field in ldap_fields:
|
||||
if field in request_data:
|
||||
changes[field] = request_data[field]
|
||||
|
||||
|
||||
# Аватар -> avatar_bytes
|
||||
avatar_file = request_data.get('avatar')
|
||||
if avatar_file and hasattr(avatar_file, 'read'):
|
||||
@ -139,15 +141,16 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||||
changes['avatar_bytes'] = avatar_file.read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Должность
|
||||
if 'position' in request_data or 'position_id' in request_data:
|
||||
changes['position'] = request_data.get('position') or request_data.get('position_id')
|
||||
|
||||
changes['position'] = request_data.get(
|
||||
'position') or request_data.get('position_id')
|
||||
|
||||
# Отдел (для перемещения)
|
||||
move_to_department_dn = request_data.get('department_dn')
|
||||
group_cns = request_data.get('group_cns')
|
||||
|
||||
|
||||
return {
|
||||
'instance': instance,
|
||||
'changes': changes,
|
||||
@ -180,21 +183,21 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||||
{'detail': f'Ошибка внешней системы: {str(error)}'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
def handle_external_sync_result(self, instance, result, action):
|
||||
"""Обработка успешного результата синхронизации с LDAP.
|
||||
|
||||
|
||||
Вызывается миксином после успешной синхронизации.
|
||||
"""
|
||||
if action != 'create' or not isinstance(result, dict):
|
||||
return
|
||||
|
||||
|
||||
dn = result.get('dn')
|
||||
guid = result.get('guid')
|
||||
|
||||
|
||||
if not dn:
|
||||
return
|
||||
|
||||
|
||||
# Сохраняем sync state
|
||||
st, _ = LdapSyncState.objects.get_or_create(
|
||||
model="employee", object_pk=str(instance.pk)
|
||||
@ -205,13 +208,15 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||||
sync_dir="ldap",
|
||||
last_django_modify_ts=timezone.now(),
|
||||
)
|
||||
|
||||
|
||||
# Записываем Django PK в LDAP employeeNumber для обратной связки
|
||||
try:
|
||||
from employees.ldap.infrastructure.connections import _ldap
|
||||
from employees.ldap.repositories.ldap_repository import modify_user_attrs
|
||||
|
||||
employee_id_attr = getattr(settings, "LDAP_EMPLOYEE_ID_ATTR", "employeeNumber")
|
||||
from employees.ldap.repositories.ldap_repository import \
|
||||
modify_user_attrs
|
||||
|
||||
employee_id_attr = getattr(
|
||||
settings, "LDAP_EMPLOYEE_ID_ATTR", "employeeNumber")
|
||||
with _ldap() as conn:
|
||||
modify_user_attrs(
|
||||
conn, dn,
|
||||
@ -220,7 +225,7 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to set employeeNumber in LDAP: {e}")
|
||||
|
||||
|
||||
# Назначаем должность если указана
|
||||
if instance.position_id:
|
||||
try:
|
||||
@ -366,57 +371,14 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||||
return EmployeeListSerializer
|
||||
return EmployeeSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Создание пользователя админом.
|
||||
|
||||
Использует ExternalSystemSyncMixin для автоматической синхронизации с LDAP.
|
||||
Миксин управляет транзакциями и откатывает БД если LDAP упал.
|
||||
"""
|
||||
if not (request.user.is_staff or request.user.is_superuser):
|
||||
return Response({"detail": "Only staff can create users."}, status=403)
|
||||
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Валидация обязательных полей
|
||||
if not serializer.validated_data.get("email"):
|
||||
return Response({"detail": "email обязателен"}, status=400)
|
||||
if not serializer.validated_data.get("phone_number"):
|
||||
return Response({"detail": "phone_number обязателен"}, status=400)
|
||||
if not request.data.get("password"):
|
||||
return Response({"detail": "password обязателен"}, status=400)
|
||||
|
||||
# Проверка контактов
|
||||
vd = serializer.validated_data
|
||||
if not (vd.get("whatsapp") or vd.get("telegram") or vd.get("wechat")):
|
||||
return Response(
|
||||
{"detail": "Заполните хотя бы одно из полей: WhatsApp, WeChat или Telegram"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Миксин автоматически вызовет perform_create() -> LDAP sync
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Переопределяем для кастомной логики создания.
|
||||
|
||||
ExternalSystemSyncMixin автоматически обернет это в транзакцию
|
||||
и вызовет LDAP синхронизацию после успешного сохранения.
|
||||
"""Кастомная логика создания: unusable password для LDAP пользователей.
|
||||
|
||||
Миксин автоматически обернет в транзакцию и вызовет LDAP sync.
|
||||
"""
|
||||
password = self.request.data.get("password")
|
||||
avatar_bytes = None
|
||||
avatar_file = serializer.validated_data.get("avatar")
|
||||
|
||||
if avatar_file and hasattr(avatar_file, "read"):
|
||||
try:
|
||||
avatar_bytes = avatar_file.read()
|
||||
if hasattr(avatar_file, "seek"):
|
||||
avatar_file.seek(0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ldap_enabled = _is_ldap_enabled()
|
||||
|
||||
|
||||
# Создаем пользователя в БД
|
||||
if ldap_enabled:
|
||||
# LDAP-managed пользователь с unusable password
|
||||
@ -429,22 +391,41 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||||
instance = serializer.save(is_ldap_managed=False)
|
||||
instance.set_password(password)
|
||||
instance.save(update_fields=["password"])
|
||||
|
||||
# Сохраняем аватар если есть
|
||||
if avatar_bytes:
|
||||
instance.avatar.save(
|
||||
f"avatar_{instance.id}.jpg",
|
||||
ContentFile(avatar_bytes),
|
||||
save=False,
|
||||
)
|
||||
instance.save(update_fields=["avatar"])
|
||||
|
||||
# Навыки
|
||||
|
||||
# Навыки (если есть)
|
||||
skills_ids = self.request.data.get("skills_ids", [])
|
||||
if skills_ids:
|
||||
instance.skills.set(skills_ids)
|
||||
|
||||
# Миксин вызовет LDAP синхронизацию автоматически после этого
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""Обработка изменения email для верификации.
|
||||
|
||||
Миксин автоматически обернет в транзакцию и вызовет LDAP sync.
|
||||
"""
|
||||
instance = serializer.instance
|
||||
old_email = instance.email
|
||||
|
||||
# Стандартное сохранение (миксин добавит LDAP sync)
|
||||
serializer.save()
|
||||
|
||||
# Если email изменился - сбрасываем верификацию
|
||||
new_email = instance.email
|
||||
if new_email and new_email.lower() != old_email.lower():
|
||||
instance.email_verified = False
|
||||
instance.email_activation_code = get_random_string(6, "0123456789")
|
||||
instance.save(
|
||||
update_fields=["email_verified", "email_activation_code"])
|
||||
|
||||
try:
|
||||
send_templated_mail(
|
||||
subject="Подтверждение нового email",
|
||||
to=[instance.email],
|
||||
template_base="emails/registration_verify_code",
|
||||
context={"code": instance.email_activation_code,
|
||||
"user": instance},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@action(detail=False, methods=["get", "patch"])
|
||||
def me(self, request):
|
||||
@ -475,8 +456,9 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||||
data = self.get_serializer(instance, context=ctx).data
|
||||
return Response(data, status=200)
|
||||
|
||||
# PATCH — используем стандартный механизм partial_update
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=True)
|
||||
# PATCH — используем стандартный механизм (миксин управляет LDAP sync)
|
||||
serializer = self.get_serializer(
|
||||
instance, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
return Response(serializer.data, status=200)
|
||||
@ -729,69 +711,3 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||||
ctx["include_actions"] = True
|
||||
ctx["include_action_history"] = True
|
||||
return ctx
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
"""Частичное обновление.
|
||||
|
||||
Миксин автоматически управляет транзакциями и LDAP синхронизацией.
|
||||
"""
|
||||
# Обработка изменения email для отправки верификации
|
||||
instance = self.get_object()
|
||||
old_email = instance.email
|
||||
|
||||
# Вызываем стандартный механизм обновления через миксин
|
||||
response = super().partial_update(request, *args, **kwargs)
|
||||
|
||||
# После успешного обновления проверяем изменение email
|
||||
if response.status_code == 200:
|
||||
instance.refresh_from_db()
|
||||
new_email = instance.email
|
||||
|
||||
if new_email and new_email.lower() != old_email.lower():
|
||||
# Email изменился - сбрасываем верификацию
|
||||
instance.email_verified = False
|
||||
instance.email_activation_code = get_random_string(6, "0123456789")
|
||||
instance.save(update_fields=["email_verified", "email_activation_code"])
|
||||
|
||||
try:
|
||||
send_templated_mail(
|
||||
subject="Подтверждение нового email",
|
||||
to=[instance.email],
|
||||
template_base="emails/registration_verify_code",
|
||||
context={"code": instance.email_activation_code, "user": instance},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Обновляем response data
|
||||
if isinstance(response.data, dict):
|
||||
response.data["email_verified"] = False
|
||||
|
||||
return response
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""Переопределяем для обработки аватара и специальных полей.
|
||||
|
||||
Миксин автоматически обернет в транзакцию и вызовет LDAP sync.
|
||||
"""
|
||||
# Обработка аватара
|
||||
avatar_file = serializer.validated_data.get("avatar")
|
||||
if avatar_file and hasattr(avatar_file, "read"):
|
||||
try:
|
||||
if hasattr(avatar_file, "seek"):
|
||||
avatar_file.seek(0)
|
||||
avatar_bytes = avatar_file.read()
|
||||
|
||||
instance = serializer.instance
|
||||
instance.avatar.save(
|
||||
avatar_file.name,
|
||||
ContentFile(avatar_bytes),
|
||||
save=False,
|
||||
)
|
||||
# Убираем avatar из validated_data чтобы serializer не пытался его сохранить
|
||||
serializer.validated_data.pop("avatar", None)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save avatar: {e}")
|
||||
|
||||
# Стандартное сохранение
|
||||
serializer.save()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user