Добавлена валидация уникальности email и phone для клиентов
Изменения: - Добавлено ограничение unique=True для поля email в модели Customer - Email теперь поддерживает NULL значения (несколько клиентов могут быть без email) - Добавлены методы clean_email() и clean_phone() в CustomerForm для валидации - Переписан API endpoint api_create_customer для использования формы вместо прямого создания - Создано две миграции: сначала разрешение NULL, затем добавление unique constraint - В текущей БД преобразованы пустые строки email в NULL (4 записи) Это исправляет: - Возможность создания дубликатов клиентов с одинаковыми email/phone - Работает как в веб-форме, так и в модальном окне создания заказа - Устранена уязвимость race condition через использование DB constraints 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from phonenumber_field.formfields import PhoneNumberField
|
from phonenumber_field.formfields import PhoneNumberField
|
||||||
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||||
from .models import Customer
|
from .models import Customer
|
||||||
@@ -6,6 +7,7 @@ from .models import Customer
|
|||||||
class CustomerForm(forms.ModelForm):
|
class CustomerForm(forms.ModelForm):
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
region='BY',
|
region='BY',
|
||||||
|
required=False,
|
||||||
help_text='Формат: +375XXXXXXXXX или 80XXXXXXXXX',
|
help_text='Формат: +375XXXXXXXXX или 80XXXXXXXXX',
|
||||||
widget=forms.TextInput(attrs={'placeholder': '+375XXXXXXXXX'})
|
widget=forms.TextInput(attrs={'placeholder': '+375XXXXXXXXX'})
|
||||||
)
|
)
|
||||||
@@ -36,3 +38,43 @@ class CustomerForm(forms.ModelForm):
|
|||||||
else:
|
else:
|
||||||
# Regular input fields get form-control class
|
# Regular input fields get form-control class
|
||||||
field.widget.attrs.update({'class': 'form-control'})
|
field.widget.attrs.update({'class': 'form-control'})
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
"""Проверяет уникальность email при создании/редактировании"""
|
||||||
|
email = self.cleaned_data.get('email')
|
||||||
|
|
||||||
|
# Если email пустой, это нормально (blank=True)
|
||||||
|
if not email:
|
||||||
|
return email
|
||||||
|
|
||||||
|
# Проверяем уникальность
|
||||||
|
queryset = Customer.objects.filter(email=email)
|
||||||
|
|
||||||
|
# При редактировании исключаем текущий экземпляр
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
queryset = queryset.exclude(pk=self.instance.pk)
|
||||||
|
|
||||||
|
if queryset.exists():
|
||||||
|
raise ValidationError('Клиент с таким email уже существует.')
|
||||||
|
|
||||||
|
return email
|
||||||
|
|
||||||
|
def clean_phone(self):
|
||||||
|
"""Проверяет уникальность телефона при создании/редактировании"""
|
||||||
|
phone = self.cleaned_data.get('phone')
|
||||||
|
|
||||||
|
# Если телефон пустой, это нормально (blank=True)
|
||||||
|
if not phone:
|
||||||
|
return phone
|
||||||
|
|
||||||
|
# Проверяем уникальность
|
||||||
|
queryset = Customer.objects.filter(phone=phone)
|
||||||
|
|
||||||
|
# При редактировании исключаем текущий экземпляр
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
queryset = queryset.exclude(pk=self.instance.pk)
|
||||||
|
|
||||||
|
if queryset.exists():
|
||||||
|
raise ValidationError('Клиент с таким номером телефона уже существует.')
|
||||||
|
|
||||||
|
return phone
|
||||||
18
myproject/customers/migrations/0003_alter_customer_email.py
Normal file
18
myproject/customers/migrations/0003_alter_customer_email.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2025-11-11 14:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('customers', '0002_remove_address_model'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customer',
|
||||||
|
name='email',
|
||||||
|
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
myproject/customers/migrations/0004_alter_customer_email.py
Normal file
18
myproject/customers/migrations/0004_alter_customer_email.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2025-11-11 14:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('customers', '0003_alter_customer_email'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customer',
|
||||||
|
name='email',
|
||||||
|
field=models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='Email'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -12,7 +12,7 @@ class Customer(models.Model):
|
|||||||
# Name field that is not required to be unique
|
# Name field that is not required to be unique
|
||||||
name = models.CharField(max_length=200, blank=True, verbose_name="Имя")
|
name = models.CharField(max_length=200, blank=True, verbose_name="Имя")
|
||||||
|
|
||||||
email = models.EmailField(blank=True, verbose_name="Email")
|
email = models.EmailField(blank=True, null=True, unique=True, verbose_name="Email")
|
||||||
|
|
||||||
# Phone with validation using django-phonenumber-field
|
# Phone with validation using django-phonenumber-field
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
|
|||||||
@@ -380,67 +380,54 @@ def api_create_customer(request):
|
|||||||
try:
|
try:
|
||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
|
|
||||||
name = data.get('name', '')
|
# Нормализуем данные
|
||||||
phone = data.get('phone')
|
name = data.get('name', '').strip() if data.get('name') else ''
|
||||||
email = data.get('email', '')
|
phone = data.get('phone', '').strip() if data.get('phone') else ''
|
||||||
|
email = data.get('email', '').strip() if data.get('email') else ''
|
||||||
|
|
||||||
# Нормализуем строки (может быть None из JavaScript)
|
# Подготавливаем данные для формы
|
||||||
name = name.strip() if name else ''
|
form_data = {
|
||||||
phone = phone.strip() if phone else ''
|
'name': name,
|
||||||
email = email.strip() if email else ''
|
'phone': phone if phone else None,
|
||||||
|
'email': email if email else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Используем форму для валидации и создания
|
||||||
|
form = CustomerForm(data=form_data)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
# Сохраняем клиента через форму (автоматически вызывает все валидации)
|
||||||
|
customer = form.save()
|
||||||
|
|
||||||
|
phone_display = str(customer.phone) if customer.phone else ''
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'id': customer.pk,
|
||||||
|
'name': customer.name,
|
||||||
|
'phone': phone_display,
|
||||||
|
'email': customer.email if customer.email else '',
|
||||||
|
}, status=201)
|
||||||
|
else:
|
||||||
|
# Собираем ошибки валидации
|
||||||
|
errors = []
|
||||||
|
for field, field_errors in form.errors.items():
|
||||||
|
for error in field_errors:
|
||||||
|
errors.append(error)
|
||||||
|
|
||||||
|
# Возвращаем первую ошибку
|
||||||
|
error_message = errors[0] if errors else 'Ошибка валидации данных'
|
||||||
|
|
||||||
# Валидация: имя обязательно
|
|
||||||
if not name:
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': 'Имя клиента обязательно'
|
'error': error_message
|
||||||
}, status=400)
|
}, status=400)
|
||||||
|
|
||||||
# Нормализуем телефон если он указан
|
|
||||||
if phone:
|
|
||||||
phone = normalize_query_phone(phone)
|
|
||||||
|
|
||||||
# Проверяем, не существует ли уже клиент с таким телефоном
|
|
||||||
if phone and Customer.objects.filter(phone=phone).exists():
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Клиент с таким номером телефона уже существует'
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
# Проверяем, не существует ли уже клиент с таким email
|
|
||||||
if email and Customer.objects.filter(email=email).exists():
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Клиент с таким email уже существует'
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
# Создаем нового клиента
|
|
||||||
customer = Customer.objects.create(
|
|
||||||
name=name,
|
|
||||||
phone=phone if phone else None,
|
|
||||||
email=email if email else None
|
|
||||||
)
|
|
||||||
|
|
||||||
phone_display = str(customer.phone) if customer.phone else ''
|
|
||||||
|
|
||||||
return JsonResponse({
|
|
||||||
'success': True,
|
|
||||||
'id': customer.pk,
|
|
||||||
'name': customer.name,
|
|
||||||
'phone': phone_display,
|
|
||||||
'email': customer.email,
|
|
||||||
}, status=201)
|
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': 'Некорректный JSON'
|
'error': 'Некорректный JSON'
|
||||||
}, status=400)
|
}, status=400)
|
||||||
except ValidationError as e:
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}, status=400)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': False,
|
'success': False,
|
||||||
|
|||||||
Reference in New Issue
Block a user