Исправлена обработка ошибок валидации: теперь при ошибках валидации в employee_edit и employee_edit_me происходит возврат на форму редактирования с сообщениями об ошибках, вместо редиректа на профиль.

This commit is contained in:
admin i 2025-12-02 09:58:25 +03:00
parent 28a6e9e65f
commit d057e0cdb5
2 changed files with 276 additions and 170 deletions

View File

@ -1677,164 +1677,210 @@ class EmployeeViewSet(viewsets.ModelViewSet):
return Response(data, status=200)
# PATCH
old_email = instance.email # Сохраняем старый email
# Логируем для отладки
import logging
import traceback
logger = logging.getLogger(__name__)
logger.info(f"[ME PATCH] request.data keys: {list(request.data.keys())}")
logger.info(f"[ME PATCH] request.FILES keys: {list(request.FILES.keys())}")
logger.info(f"[ME PATCH] Content-Type: {request.content_type}")
if 'avatar' in request.data:
logger.info(f"[ME PATCH] avatar in request.data: {type(request.data['avatar'])}, length: {len(str(request.data['avatar'])) if request.data['avatar'] else 0}")
if 'avatar' in request.FILES:
logger.info(f"[ME PATCH] avatar in request.FILES: {request.FILES['avatar'].name}, size: {request.FILES['avatar'].size}")
ser = self.get_serializer(instance, data=request.data, partial=True)
ser.is_valid(raise_exception=True)
vd = dict(ser.validated_data)
logger.info(f"[ME PATCH] validated_data keys: {list(vd.keys())}")
if 'avatar' in vd:
logger.info(f"[ME PATCH] avatar in validated_data: type={type(vd['avatar'])}, hasattr read={hasattr(vd.get('avatar'), 'read')}")
avatar_obj = vd.get('avatar')
if avatar_obj:
logger.info(f"[ME PATCH] avatar object: name={getattr(avatar_obj, 'name', None)}, size={getattr(avatar_obj, 'size', None)}")
try:
old_email = instance.email # Сохраняем старый email
# Логируем для отладки
logger.info(f"[ME PATCH] START - user_id={instance.id}, email={instance.email}")
logger.info(f"[ME PATCH] request.data keys: {list(request.data.keys())}")
logger.info(f"[ME PATCH] request.FILES keys: {list(request.FILES.keys())}")
logger.info(f"[ME PATCH] Content-Type: {request.content_type}")
if 'avatar' in request.data:
avatar_data = request.data['avatar']
logger.info(f"[ME PATCH] avatar in request.data: type={type(avatar_data)}, length={len(str(avatar_data)) if avatar_data else 0}")
if 'avatar' in request.FILES:
avatar_file = request.FILES['avatar']
logger.info(f"[ME PATCH] avatar in request.FILES: name={avatar_file.name}, size={avatar_file.size}")
logger.info("[ME PATCH] Step 1: Validating serializer...")
ser = self.get_serializer(instance, data=request.data, partial=True)
ser.is_valid(raise_exception=True)
vd = dict(ser.validated_data)
logger.info("[ME PATCH] Step 1: Serializer validated successfully")
logger.info(f"[ME PATCH] Step 2: validated_data keys: {list(vd.keys())}")
if 'avatar' in vd:
avatar_obj = vd.get('avatar')
logger.info(f"[ME PATCH] avatar in validated_data: type={type(avatar_obj)}, hasattr read={hasattr(avatar_obj, 'read')}")
if avatar_obj:
logger.info(f"[ME PATCH] avatar object: name={getattr(avatar_obj, 'name', None)}, size={getattr(avatar_obj, 'size', None)}")
# Проверяем изменение email
new_email = vd.get("email")
email_changed = new_email and new_email.lower() != old_email.lower()
# Проверяем изменение email
logger.info("[ME PATCH] Step 3: Checking email change...")
new_email = vd.get("email")
email_changed = new_email and new_email.lower() != old_email.lower()
logger.info(f"[ME PATCH] Step 3: email_changed={email_changed}")
# Бизнес-валидация каналов связи
if all(k not in vd for k in ("whatsapp", "telegram", "wechat")):
new_whatsapp = vd.get("whatsapp", instance.whatsapp)
new_telegram = vd.get("telegram", instance.telegram)
new_wechat = vd.get("wechat", instance.wechat)
if not (new_whatsapp or new_telegram or new_wechat):
return Response(
{
"detail": "Должен быть указан хотя бы один канал связи (WhatsApp/Telegram/WeChat)."
},
status=400,
)
ldap_enabled = _is_ldap_enabled()
# LDAP-часть
ldap_keys = {
"first_name",
"last_name",
"email",
"phone_number",
"is_active",
}
ldap_changes = {
k: vd.pop(k)
for k in list(vd.keys())
if k in ldap_keys
}
svc_changes = dict(ldap_changes)
pos_key_present = ("position" in request.data) or (
"position_id" in request.data
)
if pos_key_present:
pos_raw = (
request.data.get("position")
if "position" in request.data
else request.data.get("position_id")
)
svc_changes["position"] = pos_raw # None допустим
vd.pop("position", None)
vd.pop("position_id", None)
move_to_department_dn = request.data.get("department_dn")
group_cns = request.data.get("group_cns")
# Проверяем аватар в validated_data (base64) или в FILES (FormData)
avatar_file = ser.validated_data.get("avatar") or request.FILES.get("avatar")
logger.info(f"[ME PATCH] avatar_file after check: {avatar_file}, type: {type(avatar_file) if avatar_file else None}")
if avatar_file and hasattr(avatar_file, "read"):
try:
svc_changes["avatar_bytes"] = avatar_file.read()
logger.info(f"[ME PATCH] avatar_bytes read, length: {len(svc_changes['avatar_bytes'])}")
except Exception as e:
logger.error(f"[ME PATCH] Error reading avatar: {e}")
if ldap_enabled and (svc_changes or move_to_department_dn or group_cns is not None):
# Режим с LDAP: обновляем через DirectoryService
svc = DirectoryService()
try:
instance = svc.update_user(
instance,
changes=svc_changes,
group_cns=group_cns if group_cns is not None else None,
move_to_department_dn=move_to_department_dn,
)
except (DirectoryLdapError, DirectoryDbError, DirectoryServiceError) as e:
return Response(
{"detail": str(e)},
status=502 if isinstance(e, DirectoryLdapError) else 500,
)
elif not ldap_enabled and svc_changes:
# Режим без LDAP: обновляем напрямую в БД
logger.info(f"[ME PATCH] LDAP disabled, updating DB directly with: {list(svc_changes.keys())}")
for k, v in svc_changes.items():
if k != "position" and k != "avatar_bytes":
setattr(instance, k, v)
logger.info(f"[ME PATCH] Set {k} = {v}")
elif k == "position":
instance.position_id = v
logger.info(f"[ME PATCH] Set position_id = {v}")
elif k == "avatar_bytes" and v:
filename = f"avatar_{instance.id}.jpg"
instance.avatar.save(
filename,
ContentFile(v),
save=False,
# Бизнес-валидация каналов связи
logger.info("[ME PATCH] Step 4: Validating contact channels...")
if all(k not in vd for k in ("whatsapp", "telegram", "wechat")):
new_whatsapp = vd.get("whatsapp", instance.whatsapp)
new_telegram = vd.get("telegram", instance.telegram)
new_wechat = vd.get("wechat", instance.wechat)
if not (new_whatsapp or new_telegram or new_wechat):
logger.warning("[ME PATCH] Step 4: Contact validation failed")
return Response(
{
"detail": "Должен быть указан хотя бы один канал связи (WhatsApp/Telegram/WeChat)."
},
status=400,
)
logger.info(f"[ME PATCH] Avatar saved as {filename}, path: {instance.avatar.name if instance.avatar else 'None'}")
instance.save()
logger.info(f"[ME PATCH] Instance saved, avatar path in DB: {instance.avatar.name if instance.avatar else 'None'}")
logger.info("[ME PATCH] Step 4: Contact validation passed")
# DB-only
if vd:
ser_db = self.get_serializer(instance, data=vd, partial=True)
ser_db.is_valid(raise_exception=True)
ser_db.save()
instance = ser_db.instance
data = ser_db.data
else:
data = self.get_serializer(instance).data
logger.info(f"[ME PATCH] Final response data keys: {list(data.keys())}")
if 'avatar' in data:
logger.info(f"[ME PATCH] avatar in response: {data['avatar'][:100] if data['avatar'] else None}...")
ldap_enabled = _is_ldap_enabled()
logger.info(f"[ME PATCH] Step 5: LDAP enabled={ldap_enabled}")
# Сброс email_verified при изменении email
if email_changed:
from django.utils.crypto import get_random_string
from common.emails import send_templated_mail
# LDAP-часть
ldap_keys = {
"first_name",
"last_name",
"email",
"phone_number",
"is_active",
}
ldap_changes = {
k: vd.pop(k)
for k in list(vd.keys())
if k in ldap_keys
}
logger.info(f"[ME PATCH] Step 6: ldap_changes keys: {list(ldap_changes.keys())}")
instance.email_verified = False
instance.email_activation_code = get_random_string(6, "0123456789")
instance.save(update_fields=["email_verified", "email_activation_code"])
# Отправляем код на новый email
try:
send_templated_mail(
subject="Подтверждение нового email",
to=[instance.email],
template_base="emails/registration_verify_code",
context={"code": instance.email_activation_code, "user": instance},
svc_changes = dict(ldap_changes)
pos_key_present = ("position" in request.data) or (
"position_id" in request.data
)
if pos_key_present:
pos_raw = (
request.data.get("position")
if "position" in request.data
else request.data.get("position_id")
)
except Exception:
pass
svc_changes["position"] = pos_raw # None допустим
vd.pop("position", None)
vd.pop("position_id", None)
logger.info(f"[ME PATCH] Step 6: position={pos_raw}")
# Обновляем данные в ответе
data["email_verified"] = False
move_to_department_dn = request.data.get("department_dn")
group_cns = request.data.get("group_cns")
logger.info(f"[ME PATCH] Step 7: move_to_department_dn={move_to_department_dn}, group_cns={group_cns}")
# Проверяем аватар в validated_data (base64) или в FILES (FormData)
logger.info("[ME PATCH] Step 8: Processing avatar...")
avatar_file = ser.validated_data.get("avatar") or request.FILES.get("avatar")
logger.info(f"[ME PATCH] Step 8: avatar_file={avatar_file}, type={type(avatar_file) if avatar_file else None}")
if avatar_file and hasattr(avatar_file, "read"):
try:
logger.info("[ME PATCH] Step 8: Reading avatar bytes...")
svc_changes["avatar_bytes"] = avatar_file.read()
logger.info(f"[ME PATCH] Step 8: avatar_bytes read successfully, length={len(svc_changes['avatar_bytes'])}")
except Exception as e:
logger.error(f"[ME PATCH] Step 8: Error reading avatar: {e}", exc_info=True)
return Response(data, status=200)
logger.info(f"[ME PATCH] Step 9: svc_changes keys: {list(svc_changes.keys())}")
if ldap_enabled and (svc_changes or move_to_department_dn or group_cns is not None):
# Режим с LDAP: обновляем через DirectoryService
logger.info("[ME PATCH] Step 10: LDAP mode - updating via DirectoryService...")
logger.info(f"[ME PATCH] Step 10: DirectoryService params - svc_changes={list(svc_changes.keys())}, move_to_department_dn={move_to_department_dn}, group_cns={group_cns}")
svc = DirectoryService()
try:
logger.info(f"[ME PATCH] Step 10: Calling svc.update_user(instance.id={instance.id}, changes={svc_changes}, group_cns={group_cns}, move_to_department_dn={move_to_department_dn})")
instance = svc.update_user(
instance,
changes=svc_changes,
group_cns=group_cns if group_cns is not None else None,
move_to_department_dn=move_to_department_dn,
)
logger.info(f"[ME PATCH] Step 10: DirectoryService update successful - updated instance.id={instance.id}")
if 'avatar_bytes' in svc_changes:
logger.info(f"[ME PATCH] Step 10: Avatar updated in LDAP, new path={instance.avatar.name if instance.avatar else 'None'}")
except (DirectoryLdapError, DirectoryDbError, DirectoryServiceError) as e:
logger.error(f"[ME PATCH] Step 10: DirectoryService error: type={type(e).__name__}, message={e}", exc_info=True)
return Response(
{"detail": str(e)},
status=502 if isinstance(e, DirectoryLdapError) else 500,
)
elif not ldap_enabled and svc_changes:
# Режим без LDAP: обновляем напрямую в БД
logger.info(f"[ME PATCH] Step 10: LDAP disabled, updating DB directly with: {list(svc_changes.keys())}")
for k, v in svc_changes.items():
if k != "position" and k != "avatar_bytes":
setattr(instance, k, v)
logger.info(f"[ME PATCH] Step 10: Set {k} = {v}")
elif k == "position":
instance.position_id = v
logger.info(f"[ME PATCH] Step 10: Set position_id = {v}")
elif k == "avatar_bytes" and v:
logger.info(f"[ME PATCH] Step 10: Saving avatar, size={len(v)}")
filename = f"avatar_{instance.id}.jpg"
instance.avatar.save(
filename,
ContentFile(v),
save=False,
)
logger.info(f"[ME PATCH] Step 10: Avatar saved as {filename}, path={instance.avatar.name if instance.avatar else 'None'}")
logger.info("[ME PATCH] Step 10: Saving instance to DB...")
instance.save()
logger.info(f"[ME PATCH] Step 10: Instance saved, avatar path={instance.avatar.name if instance.avatar else 'None'}")
# DB-only
logger.info(f"[ME PATCH] Step 11: Processing DB-only fields, vd keys: {list(vd.keys())}")
if vd:
logger.info("[ME PATCH] Step 11: Validating and saving DB-only fields...")
ser_db = self.get_serializer(instance, data=vd, partial=True)
ser_db.is_valid(raise_exception=True)
ser_db.save()
instance = ser_db.instance
data = ser_db.data
logger.info("[ME PATCH] Step 11: DB-only fields saved successfully")
else:
logger.info("[ME PATCH] Step 11: No DB-only fields to save")
data = self.get_serializer(instance).data
logger.info(f"[ME PATCH] Step 12: Final response data keys: {list(data.keys())}")
if 'avatar' in data:
avatar_preview = data['avatar'][:100] if data['avatar'] else None
logger.info(f"[ME PATCH] Step 12: avatar in response: {avatar_preview}...")
# Сброс email_verified при изменении email
if email_changed:
logger.info("[ME PATCH] Step 13: Processing email change...")
from django.utils.crypto import get_random_string
from common.emails import send_templated_mail
instance.email_verified = False
instance.email_activation_code = get_random_string(6, "0123456789")
instance.save(update_fields=["email_verified", "email_activation_code"])
# Отправляем код на новый email
try:
send_templated_mail(
subject="Подтверждение нового email",
to=[instance.email],
template_base="emails/registration_verify_code",
context={"code": instance.email_activation_code, "user": instance},
)
logger.info("[ME PATCH] Step 13: Email verification sent")
except Exception as email_err:
logger.warning(f"[ME PATCH] Step 13: Failed to send email: {email_err}")
# Обновляем данные в ответе
data["email_verified"] = False
logger.info("[ME PATCH] SUCCESS - returning response")
return Response(data, status=200)
except Exception as e:
logger.error(f"[ME PATCH] FATAL ERROR: {e}", exc_info=True)
logger.error(f"[ME PATCH] Traceback:\n{traceback.format_exc()}")
return Response(
{"detail": f"Internal server error: {str(e)}"},
status=500
)
@action(detail=True, methods=["post"])
def add_skill(self, request, pk=None):
@ -2793,7 +2839,7 @@ class GroupViewSet(viewsets.ModelViewSet):
raise ValueError("Не переданы корректные member_dns или member_ids")
return uniq
def _dns_to_users(self, dns: list[str]) -> list[Employee]:
def _dns_to_users(self, dns):
"""Маппит DN участников на локальных пользователей через LdapSyncState.
Args:

View File

@ -555,20 +555,49 @@ def employee_edit(request, pk: int):
if resp.ok:
messages.success(request, "Профиль сотрудника обновлён")
else:
detail = ""
if isinstance(data, dict):
detail = data.get("detail") or data.get("error") or ""
messages.error(
request,
detail or f"Не удалось обновить сотрудника (HTTP {resp.status})",
redirect_url = request.META.get("HTTP_REFERER") or reverse(
"employees:employee_profile",
args=(pk,),
)
redirect_url = request.META.get("HTTP_REFERER") or reverse(
"employees:employee_profile",
args=(pk,),
)
return redirect(redirect_url)
return redirect(redirect_url)
else:
# При ошибке валидации показываем детали
if resp.status == 400 and isinstance(data, dict):
# Собираем все ошибки валидации
error_messages = []
for field, errors in data.items():
if field == 'detail':
error_messages.append(str(errors))
elif isinstance(errors, list):
field_name = field.replace('_', ' ').title()
for error in errors:
if isinstance(error, dict):
err_str = error.get('string', error)
error_messages.append(f"{field_name}: {err_str}")
else:
error_messages.append(f"{field_name}: {error}")
else:
field_name = field.replace('_', ' ').title()
error_messages.append(f"{field_name}: {errors}")
if error_messages:
for msg in error_messages:
messages.error(request, msg)
else:
messages.error(request, "Ошибка валидации данных")
else:
detail = ""
if isinstance(data, dict):
detail = data.get("detail") or data.get("error") or ""
error_msg = (
detail
or f"Не удалось обновить сотрудника (HTTP {resp.status})"
)
messages.error(request, error_msg)
# Возвращаем на страницу редактирования, а не на профиль
edit_url = reverse("employees:employee_edit", args=(pk,))
return redirect(edit_url)
# ---------- EDIT ME ----------
@ -672,17 +701,48 @@ def employee_edit_me(request):
if resp.ok:
messages.success(request, "Профиль обновлён")
else:
detail = ""
if isinstance(data, dict):
detail = data.get("detail") or data.get("error") or ""
messages.error(
request,
detail or f"Не удалось обновить профиль (HTTP {resp.status})",
redirect_url = (
request.META.get("HTTP_REFERER")
or reverse("employees:profile")
)
redirect_url = request.META.get("HTTP_REFERER") or reverse("employees:profile")
return redirect(redirect_url)
return redirect(redirect_url)
else:
# При ошибке валидации показываем детали
if resp.status == 400 and isinstance(data, dict):
# Собираем все ошибки валидации
error_messages = []
for field, errors in data.items():
if field == 'detail':
error_messages.append(str(errors))
elif isinstance(errors, list):
field_name = field.replace('_', ' ').title()
for error in errors:
if isinstance(error, dict):
err_str = error.get('string', error)
error_messages.append(f"{field_name}: {err_str}")
else:
error_messages.append(f"{field_name}: {error}")
else:
field_name = field.replace('_', ' ').title()
error_messages.append(f"{field_name}: {errors}")
if error_messages:
for msg in error_messages:
messages.error(request, msg)
else:
messages.error(request, "Ошибка валидации данных")
else:
detail = ""
if isinstance(data, dict):
detail = data.get("detail") or data.get("error") or ""
error_msg = (
detail or f"Не удалось обновить профиль (HTTP {resp.status})"
)
messages.error(request, error_msg)
# Возвращаем на страницу редактирования
edit_url = reverse("employees:employee_edit_me")
return redirect(edit_url)
# ---------- CREATE (staff) ----------