Изменения: - Добавлено ограничение 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>
80 lines
3.2 KiB
Python
80 lines
3.2 KiB
Python
from django import forms
|
||
from django.core.exceptions import ValidationError
|
||
from phonenumber_field.formfields import PhoneNumberField
|
||
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||
from .models import Customer
|
||
|
||
class CustomerForm(forms.ModelForm):
|
||
phone = PhoneNumberField(
|
||
region='BY',
|
||
required=False,
|
||
help_text='Формат: +375XXXXXXXXX или 80XXXXXXXXX',
|
||
widget=forms.TextInput(attrs={'placeholder': '+375XXXXXXXXX'})
|
||
)
|
||
|
||
class Meta:
|
||
model = Customer
|
||
fields = ['name', 'email', 'phone', 'loyalty_tier', 'notes']
|
||
widgets = {
|
||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
# Ensure phone displays in E.164 format
|
||
if self.instance and self.instance.phone:
|
||
self.initial['phone'] = str(self.instance.phone)
|
||
|
||
for field_name, field in self.fields.items():
|
||
if field_name == 'notes':
|
||
# Textarea already has rows=3 from widget, just add class
|
||
field.widget.attrs.update({'class': 'form-control'})
|
||
elif field_name == 'loyalty_tier':
|
||
# Select fields need form-select class
|
||
field.widget.attrs.update({'class': 'form-select'})
|
||
elif field_name == 'phone':
|
||
# Phone field gets form-control class
|
||
field.widget.attrs.update({'class': 'form-control'})
|
||
else:
|
||
# Regular input fields get form-control class
|
||
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 |