Валидация ФИО: запрет цифр, обязательность аватара, улучшения JS live validation, фиксы паттернов для отчества

This commit is contained in:
admin i 2025-12-02 14:02:10 +03:00
parent 2fbf0e29fc
commit d370ef678d
6 changed files with 736 additions and 51 deletions

View File

@ -1,4 +1,5 @@
from collections import defaultdict
import re
from typing import Any, Dict, Optional
from django.contrib.auth import get_user_model
@ -53,18 +54,73 @@ class RegisterSerializer(serializers.Serializer):
whatsapp = serializers.CharField(required=False, allow_blank=True, default="")
wechat = serializers.CharField(required=False, allow_blank=True, default="")
avatar = Base64ImageField(required=False, allow_null=True)
avatar = Base64ImageField(required=True)
patronymic = serializers.CharField(required=False, allow_blank=True, default="")
gender = serializers.ChoiceField(
required=False,
allow_null=True,
choices=((0, "Не указан"), (1, "Мужской"), (2, "Женский")),
required=True,
choices=((1, "Мужской"), (2, "Женский")),
error_messages={
'required': 'Поле "Пол" обязательно для заполнения.',
'invalid_choice': 'Укажите пол: 1 - Мужской, 2 - Женский.'
}
)
position = serializers.IntegerField(required=False, allow_null=True)
skills = serializers.ListField(child=serializers.IntegerField(), required=False)
def validate_first_name(self, value: str) -> str:
"""Валидация имени: только буквы, пробелы и дефисы."""
value = value.strip()
if not value:
raise serializers.ValidationError(
"Имя обязательно для заполнения."
)
if re.search(r'\d', value):
raise serializers.ValidationError(
"Имя не должно содержать цифры."
)
if not re.match(r'^[\w\s\-\']+$', value, re.UNICODE):
raise serializers.ValidationError(
"Имя может содержать только буквы, пробелы, "
"дефисы и апострофы."
)
return value
def validate_last_name(self, value: str) -> str:
"""Валидация фамилии: только буквы, пробелы и дефисы."""
value = value.strip()
if not value:
raise serializers.ValidationError(
"Фамилия обязательна для заполнения."
)
if re.search(r'\d', value):
raise serializers.ValidationError(
"Фамилия не должна содержать цифры."
)
if not re.match(r'^[\w\s\-\']+$', value, re.UNICODE):
raise serializers.ValidationError(
"Фамилия может содержать только буквы, пробелы, "
"дефисы и апострофы."
)
return value
def validate_patronymic(self, value: str) -> str:
"""Валидация отчества: только буквы, пробелы и дефисы."""
value = value.strip()
if not value:
return value # Отчество опциональное
if re.search(r'\d', value):
raise serializers.ValidationError(
"Отчество не должно содержать цифры."
)
if not re.match(r'^[\w\s\-\']+$', value, re.UNICODE):
raise serializers.ValidationError(
"Отчество может содержать только буквы, пробелы, "
"дефисы и апострофы."
)
return value
def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]:
"""Глобальная валидация: контакты + телефон (alias) + нормализация E.164.

View File

@ -439,11 +439,10 @@ class RegisterAPIView(APIView):
emp.whatsapp = v.get("whatsapp", "")
emp.wechat = v.get("wechat", "")
emp.birth_date = v["birth_date"]
emp.gender = v["gender"] # обязательное поле
if v.get("patronymic"):
emp.patronymic = v["patronymic"]
if v.get("gender") is not None:
emp.gender = v["gender"]
pos_id = v.get("position")
if pos_id and Position.objects.filter(pk=pos_id).exists():

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import base64
import re
from typing import Any, Dict, List, Optional
from django import forms
@ -50,10 +51,8 @@ class RegistrationForm(forms.Form):
widget=forms.TextInput(attrs={
"type": "tel",
"inputmode": "numeric",
"pattern": r"^\+?\d{10,15}$",
"autocomplete": "tel",
"data-phone-only": "1",
"title": "Введите номер в формате +79991234567 (10–15 цифр)",
"title": "Введите номер в формате +79991234567",
})
)
email = forms.EmailField(label="Email")
@ -72,11 +71,66 @@ class RegistrationForm(forms.Form):
# Опциональные поля модели
patronymic = forms.CharField(required=False, label="Отчество")
gender = forms.IntegerField(required=False, label="Пол (0/1/2)")
gender = forms.ChoiceField(
required=True,
label="Пол",
choices=[(1, "Мужской"), (2, "Женский")],
widget=forms.RadioSelect,
error_messages={
'required': 'Необходимо указать пол.',
'invalid_choice': 'Выберите Мужской или Женский.',
}
)
position = forms.IntegerField(required=False, label="ID должности")
# skills — список ID через запятую
skills = forms.CharField(required=False, label="Навыки (ID через запятую)")
avatar = forms.ImageField(required=False, label="Аватар")
avatar = forms.ImageField(required=True, label="Аватар")
def clean_first_name(self) -> str:
"""Валидация имени: только буквы, пробелы и дефисы."""
value = self.cleaned_data.get("first_name", "").strip()
if not value:
raise forms.ValidationError("Имя обязательно для заполнения.")
if re.search(r'\d', value):
raise forms.ValidationError(
"Имя не должно содержать цифры."
)
# Разрешаем буквы (любых алфавитов), пробелы, дефисы, апострофы
if not re.match(r'^[\w\s\-\']+$', value, re.UNICODE):
raise forms.ValidationError(
"Имя может содержать только буквы, пробелы, дефисы и апострофы."
)
return value
def clean_last_name(self) -> str:
"""Валидация фамилии: только буквы, пробелы и дефисы."""
value = self.cleaned_data.get("last_name", "").strip()
if not value:
raise forms.ValidationError("Фамилия обязательна для заполнения.")
if re.search(r'\d', value):
raise forms.ValidationError(
"Фамилия не должна содержать цифры."
)
if not re.match(r'^[\w\s\-\']+$', value, re.UNICODE):
raise forms.ValidationError(
"Фамилия может содержать только буквы, пробелы, дефисы и апострофы."
)
return value
def clean_patronymic(self) -> str:
"""Валидация отчества: только буквы, пробелы и дефисы."""
value = self.cleaned_data.get("patronymic", "").strip()
if not value:
return value # Отчество опциональное
if re.search(r'\d', value):
raise forms.ValidationError(
"Отчество не должно содержать цифры."
)
if not re.match(r'^[\w\s\-\']+$', value, re.UNICODE):
raise forms.ValidationError(
"Отчество может содержать только буквы, пробелы, дефисы и апострофы."
)
return value
def clean(self) -> Dict[str, Any]:
"""Глобальная валидация формы.
@ -232,6 +286,7 @@ class RegistrationForm(forms.Form):
"email": cd["email"],
"birth_date": cd["birth_date"].isoformat(),
"password": cd["password1"],
"gender": int(cd["gender"]), # обязательное поле
"telegram": cd.get("telegram") or "",
"whatsapp": cd.get("whatsapp") or "",
"wechat": cd.get("wechat") or "",
@ -240,8 +295,6 @@ class RegistrationForm(forms.Form):
# Доп. поля
if cd.get("patronymic"):
payload["patronymic"] = cd["patronymic"]
if cd.get("gender") is not None:
payload["gender"] = cd["gender"]
if cd.get("position") is not None:
payload["position"] = cd["position"]

View File

@ -0,0 +1,475 @@
/**
* @module liveValidationHandler
* @description Валидация формы в реальном времени с подсветкой ошибок.
* Проверяет поля по мере ввода и показывает feedback сразу.
*
* Использование:
* import { initLiveValidation } from './liveValidationHandler.js';
*
* initLiveValidation({
* formSelector: '#registrationForm',
* rules: {
* email: [
* { type: 'required', message: 'Email обязателен' },
* { type: 'email', message: 'Введите корректный email' }
* ],
* phone_number: [
* { type: 'required', message: 'Телефон обязателен' },
* { type: 'pattern', pattern: /^\+?\d{10,15}$/, message: 'Неверный формат телефона' }
* ],
* password1: [
* { type: 'required', message: 'Пароль обязателен' },
* { type: 'minLength', value: 6, message: 'Минимум 6 символов' }
* ],
* password2: [
* { type: 'required', message: 'Подтвердите пароль' },
* { type: 'match', field: 'password1', message: 'Пароли не совпадают' }
* ],
* gender: [
* { type: 'required', message: 'Укажите пол' }
* ]
* }
* });
*/
/**
* Санитизация телефонного номера: оставляет только цифры и +.
* @param {string} value - Исходное значение
* @returns {string} Очищенное значение
*/
function sanitizePhone(value) {
value = String(value || '');
let output = '';
for (let i = 0; i < value.length; i++) {
const char = value[i];
// Цифры пропускаем всегда
if (char >= '0' && char <= '9') {
output += char;
continue;
}
// Плюс только в начале
if (char === '+' && output.length === 0) {
output = '+';
}
}
return output;
}
/**
* Валидаторы для различных типов проверок
*/
const validators = {
required: (value, input) => {
// Специальная обработка для file input
if (input && input.type === 'file') {
return input.files && input.files.length > 0;
}
if (typeof value === 'string') {
return value.trim().length > 0;
}
return value !== null && value !== undefined && value !== '';
},
email: (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
},
pattern: (value, pattern) => {
return pattern.test(value);
},
minLength: (value, minLen) => {
return value.length >= minLen;
},
maxLength: (value, maxLen) => {
return value.length <= maxLen;
},
match: (value, formData, fieldName) => {
return value === formData[fieldName];
},
// Специальная валидация для контактов (хотя бы один)
anyContact: (value, formData, fieldName, fields) => {
return fields.some(field => {
const val = formData[field];
return val && val.trim().length > 0;
});
}
};
/**
* Проверяет поле на соответствие правилам
* @param {HTMLInputElement|HTMLSelectElement} input - Элемент поля
* @param {Array} rules - Массив правил валидации
* @param {Object} formData - Данные всей формы
* @returns {Object} { valid: boolean, message: string }
*/
function validateField(input, rules, formData) {
if (!rules || rules.length === 0) {
return { valid: true, message: '' };
}
const value = input.value;
const fieldName = input.name;
for (const rule of rules) {
let isValid = true;
switch (rule.type) {
case 'required':
isValid = validators.required(value, input);
break;
case 'email':
// Email проверяем только если поле не пустое
if (value.trim().length > 0) {
isValid = validators.email(value);
}
break;
case 'pattern':
// Pattern проверяем только если поле не пустое
if (value.trim().length > 0) {
isValid = validators.pattern(value, rule.pattern);
} else if (rule.required === false) {
// Если поле опциональное и пустое - валидно
isValid = true;
} else {
// Если поле обязательное, но пустое - будет проверено правилом required
isValid = true;
}
break;
case 'minLength':
// MinLength проверяем только если поле не пустое
if (value.trim().length > 0) {
isValid = validators.minLength(value, rule.value);
}
break;
case 'maxLength':
isValid = validators.maxLength(value, rule.value);
break;
case 'match':
isValid = validators.match(value, formData, rule.field);
break;
case 'anyContact':
isValid = validators.anyContact(value, formData, fieldName, rule.fields);
break;
default:
// Кастомная функция валидации
if (typeof rule.validate === 'function') {
isValid = rule.validate(value, formData, input);
}
}
if (!isValid) {
return { valid: false, message: rule.message };
}
}
return { valid: true, message: '' };
}
/**
* Отображает feedback для поля
* @param {HTMLInputElement|HTMLSelectElement} input - Элемент поля
* @param {boolean} valid - Валидно ли поле
* @param {string} message - Сообщение об ошибке
*/
function showFieldFeedback(input, valid, message) {
// Очищаем HTML5 custom validity чтобы не конфликтовать
input.setCustomValidity('');
// Удаляем старые классы
input.classList.remove('is-valid', 'is-invalid');
// Ищем существующий feedback или создаём новый
let feedbackEl = input.parentElement.querySelector('.invalid-feedback');
if (!feedbackEl) {
feedbackEl = document.createElement('div');
feedbackEl.className = 'invalid-feedback';
input.parentElement.appendChild(feedbackEl);
}
if (valid) {
input.classList.add('is-valid');
feedbackEl.textContent = '';
feedbackEl.style.display = 'none';
} else {
input.classList.add('is-invalid');
feedbackEl.textContent = message;
feedbackEl.style.display = 'block';
}
}
/**
* Очищает feedback для поля
* @param {HTMLInputElement|HTMLSelectElement} input - Элемент поля
*/
function clearFieldFeedback(input) {
input.classList.remove('is-valid', 'is-invalid');
const feedbackEl = input.parentElement.querySelector('.invalid-feedback');
if (feedbackEl) {
feedbackEl.textContent = '';
feedbackEl.style.display = 'none';
}
}
/**
* Собирает данные формы в объект
* @param {HTMLFormElement} form - Элемент формы
* @returns {Object} Данные формы
*/
function getFormData(form) {
const formData = {};
const elements = form.elements;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element.name) {
if (element.type === 'radio') {
if (element.checked) {
formData[element.name] = element.value;
}
} else if (element.type === 'checkbox') {
formData[element.name] = element.checked;
} else {
formData[element.name] = element.value;
}
}
}
return formData;
}
/**
* Инициализирует live validation для формы
* @param {Object} options - Настройки
* @param {string} options.formSelector - CSS селектор формы
* @param {Object} options.rules - Правила валидации для полей
* @param {number} [options.debounce=300] - Задержка перед валидацией (мс)
* @param {boolean} [options.validateOnBlur=true] - Валидировать при потере фокуса
* @param {boolean} [options.validateOnInput=true] - Валидировать при вводе
* @param {boolean} [options.validateOnSubmit=true] - Валидировать при отправке
* @returns {Object} API для управления валидацией
*/
export function initLiveValidation(options = {}) {
const {
formSelector,
rules = {},
debounce = 300,
validateOnBlur = true,
validateOnInput = true,
validateOnSubmit = true
} = options;
const form = document.querySelector(formSelector);
if (!form) {
console.error(`Form not found: ${formSelector}`);
return null;
}
const validationTimers = {};
const touchedFields = new Set();
/**
* Валидирует конкретное поле
* @param {HTMLInputElement|HTMLSelectElement} input
*/
const validateInput = (input) => {
const fieldName = input.name;
const fieldRules = rules[fieldName];
if (!fieldRules) return;
const formData = getFormData(form);
const result = validateField(input, fieldRules, formData);
showFieldFeedback(input, result.valid, result.message);
};
/**
* Валидирует поле с задержкой (debounce)
* @param {HTMLInputElement|HTMLSelectElement} input
*/
const validateInputDebounced = (input) => {
const fieldName = input.name;
// Очищаем предыдущий таймер
if (validationTimers[fieldName]) {
clearTimeout(validationTimers[fieldName]);
}
// Устанавливаем новый таймер
validationTimers[fieldName] = setTimeout(() => {
validateInput(input);
}, debounce);
};
/**
* Обработчик ввода в поле
*/
const handleInput = (e) => {
const input = e.target;
const fieldName = input.name;
// Санитизация телефонных номеров
if (input.type === 'tel') {
const previousValue = input.value;
const sanitized = sanitizePhone(previousValue);
if (previousValue !== sanitized) {
const cursorPosition = input.selectionStart || sanitized.length;
const diff = previousValue.length - sanitized.length;
input.value = sanitized;
try {
const newPosition = Math.max(0, cursorPosition - diff);
input.setSelectionRange(newPosition, newPosition);
} catch (err) {
// Игнорируем ошибки setSelectionRange
}
}
}
if (!validateOnInput) return;
// Валидируем только если поле уже было "тронуто"
if (touchedFields.has(fieldName)) {
validateInputDebounced(input);
}
};
/**
* Обработчик потери фокуса
*/
const handleBlur = (e) => {
const input = e.target;
const fieldName = input.name;
// Помечаем поле как "тронутое"
touchedFields.add(fieldName);
if (validateOnBlur && rules[fieldName]) {
validateInput(input);
}
};
/**
* Обработчик отправки формы
*/
const handleSubmit = (e) => {
if (!validateOnSubmit) return;
let isFormValid = true;
const formData = getFormData(form);
// Валидируем все поля с правилами
for (const fieldName in rules) {
const input = form.elements[fieldName];
if (!input) continue;
const result = validateField(input, rules[fieldName], formData);
showFieldFeedback(input, result.valid, result.message);
if (!result.valid) {
isFormValid = false;
}
}
// Прерываем отправку если есть ошибки
if (!isFormValid) {
e.preventDefault();
// Фокусируем первое невалидное поле
const firstInvalid = form.querySelector('.is-invalid');
if (firstInvalid) {
firstInvalid.focus();
}
}
};
// Навешиваем обработчики
form.addEventListener('input', handleInput);
form.addEventListener('blur', handleBlur, true); // true для capture phase
form.addEventListener('submit', handleSubmit);
/**
* API для управления валидацией
*/
return {
/**
* Валидирует конкретное поле вручную
*/
validateField: (fieldName) => {
const input = form.elements[fieldName];
if (input) {
touchedFields.add(fieldName);
validateInput(input);
}
},
/**
* Валидирует всю форму
*/
validateForm: () => {
let isValid = true;
const formData = getFormData(form);
for (const fieldName in rules) {
const input = form.elements[fieldName];
if (!input) continue;
const result = validateField(input, rules[fieldName], formData);
showFieldFeedback(input, result.valid, result.message);
if (!result.valid) {
isValid = false;
}
}
return isValid;
},
/**
* Очищает валидацию для всех полей
*/
clearValidation: () => {
for (const fieldName in rules) {
const input = form.elements[fieldName];
if (input) {
clearFieldFeedback(input);
}
}
touchedFields.clear();
},
/**
* Удаляет обработчики
*/
destroy: () => {
form.removeEventListener('input', handleInput);
form.removeEventListener('blur', handleBlur, true);
form.removeEventListener('submit', handleSubmit);
}
};
}
// Экспорт для совместимости с неModular кодом
if (typeof window !== 'undefined') {
window.initLiveValidation = initLiveValidation;
}

View File

@ -29,11 +29,13 @@
{% endif %}
<div class="mb-4 form-floating">
<input type="tel"
<input type="text"
name="username"
class="form-control {% if form.username.errors %}is-invalid{% endif %}"
id="id_username"
placeholder="Номер телефона или email"
value="{{ form.username.value|default:'' }}"
autocomplete="username"
required autofocus>
<label for="id_username">Номер телефона или email</label>
{% if form.username.errors %}
@ -43,21 +45,27 @@
{% endif %}
</div>
<div class="mb-4 form-floating position-relative">
<input type="password"
name="password"
class="form-control {% if form.password.errors %}is-invalid{% endif %}"
id="id_password"
placeholder="Пароль"
required>
<label for="id_password">Пароль</label>
<i class="bi bi-eye-slash password-toggle position-absolute top-50 end-0 translate-middle-y me-3"
style="cursor: pointer; z-index: 5"></i>
{% if form.password.errors %}
<div class="invalid-feedback">
{{ form.password.errors|striptags }}
<div class="mb-4 position-relative">
<div class="form-floating">
<input type="password"
name="password"
class="form-control {% if form.password.errors %}is-invalid{% endif %} pe-5"
id="id_password"
placeholder="Пароль"
autocomplete="current-password"
required>
<label for="id_password">Пароль</label>
{% if form.password.errors %}
<div class="invalid-feedback">
{{ form.password.errors|striptags }}
</div>
{% endif %}
</div>
{% endif %}
<span class="password-toggle position-absolute top-50 end-0 translate-middle-y me-3"
data-target="#id_password"
style="cursor: pointer; z-index: 10; margin-top: -0.25rem;">
<i class="bi bi-eye-slash"></i>
</span>
</div>
<div class="d-grid gap-2">

View File

@ -26,7 +26,6 @@
<div class="row g-3 mb-4">
<div class="col-md-6">
{% bootstrap_field form.email show_label=False placeholder="E-mail" %}
<small class="text-muted">Пример: ivanov@company.com</small>
</div>
<div class="col-md-6">
{% bootstrap_field form.phone_number show_label=False placeholder="Телефон" %}
@ -77,21 +76,8 @@
{% bootstrap_field form.birth_date show_label=False placeholder="Дата рождения" %}
</div>
<div class="col-md-8">
<label class="form-label d-block mb-2">Пол</label>
<div class="d-flex flex-wrap gap-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="gender" id="gender-m" value="1">
<label class="form-check-label" for="gender-m">Мужской</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="gender" id="gender-f" value="2">
<label class="form-check-label" for="gender-f">Женский</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="gender" id="gender-u" value="0" checked>
<label class="form-check-label" for="gender-u">Не указан</label>
</div>
</div>
<label class="form-label d-block mb-2">Пол <span class="text-danger">*</span></label>
{% bootstrap_field form.gender show_label=False %}
</div>
</div>
@ -99,14 +85,18 @@
<div class="row g-3 mb-4">
<div class="col-md-6 position-relative">
{% bootstrap_field form.password1 show_label=False placeholder="Пароль" %}
<span class="position-absolute top-50 end-0 translate-middle-y me-3" style="cursor:pointer">
<i class="bi bi-eye-slash password-toggle" data-target="#{{ form.password1.auto_id }}"></i>
<span class="password-toggle position-absolute top-50 end-0 translate-middle-y me-3 pe-5"
data-target="#{{ form.password1.auto_id }}"
style="cursor:pointer; z-index:10;">
<i class="bi bi-eye-slash"></i>
</span>
</div>
<div class="col-md-6 position-relative">
{% bootstrap_field form.password2 show_label=False placeholder="Повторите пароль" %}
<span class="position-absolute top-50 end-0 translate-middle-y me-3" style="cursor:pointer">
<i class="bi bi-eye-slash password-toggle" data-target="#{{ form.password2.auto_id }}"></i>
<span class="password-toggle position-absolute top-50 end-0 translate-middle-y me-3 pe-5"
data-target="#{{ form.password2.auto_id }}"
style="cursor:pointer; z-index:10;">
<i class="bi bi-eye-slash"></i>
</span>
</div>
</div>
@ -146,7 +136,7 @@
<script type="module">
import { initPasswordToggle } from "{% static 'js/components/passwordToggleHandler.js' %}";
import { initAvatarPreview } from "{% static 'js/components/avatarPreviewHandler.js' %}";
import { initPhoneValidation } from "{% static 'js/components/phoneValidationHandler.js' %}";
import { initLiveValidation } from "{% static 'js/components/liveValidationHandler.js' %}";
document.addEventListener('DOMContentLoaded', function () {
// Переключение видимости пароля
@ -159,8 +149,112 @@
placeholderId: 'avatarPlaceholder'
});
// Валидация телефонных номеров
initPhoneValidation();
// Live validation - валидация в реальном времени
const validator = initLiveValidation({
formSelector: 'form',
debounce: 500, // задержка 500мс после ввода
rules: {
email: [
{ type: 'required', message: 'Email обязателен для заполнения' },
{ type: 'email', message: 'Введите корректный email (example@company.com)' }
],
phone_number: [
{ type: 'required', message: 'Номер телефона обязателен' },
{
type: 'pattern',
pattern: /^\+\d{11,14}$/,
message: 'Введите номер в формате +79991234567 (11-14 цифр с плюсом)'
}
],
first_name: [
{ type: 'required', message: 'Имя обязательно для заполнения' },
{ type: 'minLength', value: 2, message: 'Минимум 2 символа' },
{
type: 'pattern',
pattern: /^[^\d]+$/,
message: 'Имя не должно содержать цифры'
}
],
last_name: [
{ type: 'required', message: 'Фамилия обязательна для заполнения' },
{ type: 'minLength', value: 2, message: 'Минимум 2 символа' },
{
type: 'pattern',
pattern: /^[^\d]+$/,
message: 'Фамилия не должна содержать цифры'
}
],
patronymic: [
{
type: 'pattern',
pattern: /^[^\d]*$/,
message: 'Отчество не должно содержать цифры',
required: false // Поле опциональное
}
],
birth_date: [
{ type: 'required', message: 'Дата рождения обязательна' },
{
type: 'custom',
validate: (value) => {
const date = new Date(value);
const today = new Date();
const age = today.getFullYear() - date.getFullYear();
return age >= 18 && age <= 100;
},
message: 'Возраст должен быть от 18 до 100 лет'
}
],
gender: [
{ type: 'required', message: 'Необходимо указать пол' }
],
avatar: [
{ type: 'required', message: 'Необходимо загрузить фотографию' }
],
password1: [
{ type: 'required', message: 'Пароль обязателен' },
{ type: 'minLength', value: 6, message: 'Пароль должен содержать минимум 6 символов' }
],
password2: [
{ type: 'required', message: 'Подтвердите пароль' },
{
type: 'match',
field: 'password1',
message: 'Пароли не совпадают'
}
],
// Специальная проверка контактов (хотя бы один)
telegram: [
{
type: 'anyContact',
fields: ['telegram', 'whatsapp', 'wechat'],
message: 'Заполните хотя бы один мессенджер (Telegram, WhatsApp или WeChat)'
}
],
whatsapp: [
{
type: 'anyContact',
fields: ['telegram', 'whatsapp', 'wechat'],
message: 'Заполните хотя бы один мессенджер (Telegram, WhatsApp или WeChat)'
}
],
wechat: [
{
type: 'anyContact',
fields: ['telegram', 'whatsapp', 'wechat'],
message: 'Заполните хотя бы один мессенджер (Telegram, WhatsApp или WeChat)'
}
]
}
});
// При изменении password1 - ревалидируем password2
document.querySelector('[name="password1"]')?.addEventListener('input', () => {
const password2 = document.querySelector('[name="password2"]');
if (password2 && password2.value) {
validator.validateField('password2');
}
});
});
</script>
{% endblock %}