From 33e34bbe3f16dca8721655717792a36035c0da16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 3 Dec 2025 07:30:39 +0300 Subject: [PATCH] =?UTF-8?q?feat(requests):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=81=D0=BA=D0=B0=D1=8F=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B7=D0=B0?= =?UTF-8?q?=D1=8F=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RecipientPicker.validate() проверяет наличие получателей/отделов - RecipientPicker.setError()/clearError() для визуальной индикации ошибок - Валидация в requestCrudHandler для create и edit - Проверка: автор не может быть получателем - Автоматическая очистка ошибки при изменении получателей - Удалено временное логирование из сериализатора --- backend/api/v1/requests_app/serializers.py | 9 ++-- .../static/js/components/recipientPicker.js | 46 +++++++++++++++++++ .../js/components/requestCrudHandler.js | 34 ++++++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/backend/api/v1/requests_app/serializers.py b/backend/api/v1/requests_app/serializers.py index 770a59d5..f9417337 100644 --- a/backend/api/v1/requests_app/serializers.py +++ b/backend/api/v1/requests_app/serializers.py @@ -245,8 +245,9 @@ class RequestWriteSerializer(serializers.ModelSerializer): def to_internal_value(self, data: Mapping[str, Any]) -> dict[str, Any]: """Предвалидационная очистка входа. - Для обычного пользователя удаляет `employee`, `status`, `approver` до полевой валидации, - чтобы не получить 400 из-за, например, несуществующего PK в `employee`. + Для обычного пользователя удаляет `employee`, `status`, `approver` + до полевой валидации, чтобы не получить 400 из-за, например, + несуществующего PK в `employee`. Args: data (Mapping[str, Any]): Входные данные запроса. @@ -260,6 +261,7 @@ class RequestWriteSerializer(serializers.ModelSerializer): data.pop("employee", None) data.pop("status", None) data.pop("approver", None) + return super().to_internal_value(data) def validate(self, attrs): @@ -316,7 +318,8 @@ class RequestWriteSerializer(serializers.ModelSerializer): def create(self, validated_data: dict[str, Any]) -> Request: """Создание заявки. - Обычным пользователям проставляет текущего пользователя; статус берётся из default модели. + Обычным пользователям проставляет текущего пользователя; + статус берётся из default модели. Args: validated_data (dict[str, Any]): Провалидированные данные. diff --git a/backend/static/js/components/recipientPicker.js b/backend/static/js/components/recipientPicker.js index 54011557..27299484 100644 --- a/backend/static/js/components/recipientPicker.js +++ b/backend/static/js/components/recipientPicker.js @@ -491,6 +491,9 @@ export class RecipientPicker { } triggerChange() { + // Очищаем ошибку при любом изменении + this.clearError(); + if (this.options.onChange) { this.options.onChange(this.getValues()); } @@ -505,6 +508,49 @@ export class RecipientPicker { }; } + /** + * Проверяет, заполнены ли получатели + * @returns {boolean} true если есть получатели или отделы + */ + validate() { + const hasDepartments = this.state.departments.length > 0; + const hasRecipients = this.state.recipients.length > 0; + const isValid = hasDepartments || hasRecipients; + + if (!isValid) { + this.setError('Укажите хотя бы одного получателя или отдел'); + } else { + this.clearError(); + } + + return isValid; + } + + /** + * Устанавливает сообщение об ошибке + * @param {string} message - Текст ошибки + */ + setError(message) { + this.clearError(); + const errorDiv = document.createElement('div'); + errorDiv.className = 'invalid-feedback d-block'; + errorDiv.textContent = message; + errorDiv.id = 'recipientPickerError'; + this.container.appendChild(errorDiv); + this.container.classList.add('is-invalid'); + } + + /** + * Убирает сообщение об ошибке + */ + clearError() { + const errorDiv = this.container.querySelector('#recipientPickerError'); + if (errorDiv) { + errorDiv.remove(); + } + this.container.classList.remove('is-invalid'); + } + setValues(data) { if (data.department_ids) { this.state.departments = Array.isArray(data.department_ids) diff --git a/backend/static/js/components/requestCrudHandler.js b/backend/static/js/components/requestCrudHandler.js index 1452ceb1..3f007c96 100644 --- a/backend/static/js/components/requestCrudHandler.js +++ b/backend/static/js/components/requestCrudHandler.js @@ -65,6 +65,23 @@ export function initRequestCrudHandler(options) { // Добавляем данные получателей из RecipientPicker if (createRecipientPicker) { + // Валидация: для обычной отправки должны быть получатели или отделы + if (saveAs === 'submit') { + if (!createRecipientPicker.validate()) { + return; + } + + // Проверка: автор не может быть получателем + const recipients = createRecipientPicker.getValues(); + if (recipients.recipient_ids && window.currentUserId) { + if (recipients.recipient_ids.includes(window.currentUserId)) { + alert('Вы не можете отправить заявление самому себе'); + createRecipientPicker.setError('Вы не можете быть получателем'); + return; + } + } + } + const recipients = createRecipientPicker.getValues(); // Добавляем department_ids @@ -146,6 +163,23 @@ export function initRequestCrudHandler(options) { // Добавляем данные получателей из RecipientPicker if (editRecipientPicker) { + // Валидация: для обычной отправки должны быть получатели или отделы + if (saveAs === 'submit') { + if (!editRecipientPicker.validate()) { + return; + } + + // Проверка: автор не может быть получателем + const recipients = editRecipientPicker.getValues(); + if (recipients.recipient_ids && window.currentUserId) { + if (recipients.recipient_ids.includes(window.currentUserId)) { + alert('Вы не можете отправить заявление самому себе'); + editRecipientPicker.setError('Вы не можете быть получателем'); + return; + } + } + } + const recipients = editRecipientPicker.getValues(); // Добавляем department_ids