5d06d4d3ce
Co-authored-by: Cursor <cursoragent@cursor.com>
230 lines
8.0 KiB
Python
230 lines
8.0 KiB
Python
import re, ipaddress
|
|
from django.core.exceptions import ValidationError
|
|
from datetime import date
|
|
from django.core.validators import EmailValidator
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
PHONE_PLACEHOLDER = '+380 (__) ___-__-__'
|
|
UA_PHONE_CANONICAL_RE = re.compile(r'^\+380\d{9}$')
|
|
|
|
|
|
def phone_input_attrs(**extra):
|
|
"""Единые атрибуты полей телефона в портале и админке."""
|
|
attrs = {
|
|
'class': 'input js-phone',
|
|
'placeholder': PHONE_PLACEHOLDER,
|
|
'inputmode': 'tel',
|
|
'autocomplete': 'tel',
|
|
}
|
|
extra_class = extra.pop('class', None)
|
|
attrs.update(extra)
|
|
if extra_class:
|
|
attrs['class'] = extra_class
|
|
return attrs
|
|
|
|
|
|
def normalize_ukrainian_phone(value) -> str:
|
|
"""Приводит украинский номер к канону +380XXXXXXXXX.
|
|
|
|
Понимает 050…, 380…, +38 (050) …, 9 цифр без кода. Если это не номер —
|
|
возвращает исходную строку (валидатор уже покажет ошибку).
|
|
"""
|
|
if value is None:
|
|
return ''
|
|
text = str(value).strip()
|
|
if not text:
|
|
return ''
|
|
digits = re.sub(r'\D', '', text)
|
|
if not digits:
|
|
return text
|
|
if digits.startswith('380'):
|
|
ua = digits[:12]
|
|
elif digits.startswith('80'):
|
|
ua = ('3' + digits)[:12]
|
|
elif digits.startswith('0'):
|
|
ua = ('38' + digits)[:12]
|
|
elif len(digits) <= 9:
|
|
ua = ('380' + digits)[:12]
|
|
else:
|
|
return text
|
|
if len(ua) != 12 or not ua.startswith('380'):
|
|
return text
|
|
return f'+{ua}'
|
|
|
|
|
|
def validate_ukrainian_phone(value: str) -> None:
|
|
"""Валидация украинских номеров телефона"""
|
|
if value in (None, ''):
|
|
return
|
|
canonical = normalize_ukrainian_phone(value)
|
|
if not UA_PHONE_CANONICAL_RE.match(canonical):
|
|
raise ValidationError(
|
|
_('Номер телефона должен быть в формате +380XXXXXXXXX'),
|
|
code='invalid_phone'
|
|
)
|
|
|
|
def validate_international_phone(value: str) -> None:
|
|
"""Валидация международных номеров телефона"""
|
|
pattern = r'^\+\d{1,15}$'
|
|
if not re.match(pattern, value):
|
|
raise ValidationError(
|
|
_('Номер телефона должен быть в международном формате +XXXXXXXXXXX'),
|
|
code='invalid_phone'
|
|
)
|
|
if len(value) < 8 or len(value) > 16:
|
|
raise ValidationError(
|
|
_('Номер телефона должен содержать от 8 до 16 цифр включая код страны'),
|
|
code='invalid_phone_length'
|
|
)
|
|
|
|
# Класс-валидатор с параметрами
|
|
class PhoneValidator:
|
|
def __init__(self, country_code='UA'):
|
|
self.country_code = country_code
|
|
|
|
def __call__(self, value):
|
|
if self.country_code == 'UA':
|
|
return validate_ukrainian_phone(value)
|
|
else:
|
|
return validate_international_phone(value)
|
|
|
|
def validate_birth_date(value: date) -> None:
|
|
"""Валидация даты рождения"""
|
|
if value > date.today():
|
|
raise ValidationError(
|
|
_('Дата рождения не может быть в будущем'),
|
|
code='invalid_birth_date'
|
|
)
|
|
|
|
# Проверка минимального возраста (18 лет)
|
|
min_date = date.today().replace(year=date.today().year - 18)
|
|
if value > min_date:
|
|
raise ValidationError(
|
|
_('Возраст должен быть не менее 18 лет'),
|
|
code='underage'
|
|
)
|
|
|
|
def get_email_validator(banned_domains=None):
|
|
def validator(value):
|
|
validate_email_domain(value, banned_domains)
|
|
return validator
|
|
|
|
def validate_email_domain(value: str, banned_domains: list = None) -> None:
|
|
"""
|
|
Проверка домена email с возможностью кастомизации
|
|
|
|
Args:
|
|
value: Email для проверки
|
|
banned_domains: Список запрещенных доменов
|
|
"""
|
|
EmailValidator()(value)
|
|
|
|
domain = value.split('@')[-1].lower()
|
|
|
|
# Дефолтный список запрещенных доменов
|
|
default_banned = ['temp-mail.org', '10minutemail.com', 'example.com',
|
|
'yopmail.com', 'mailinator.com']
|
|
|
|
banned_list = banned_domains if banned_domains is not None else default_banned
|
|
|
|
if domain in banned_list:
|
|
raise ValidationError(
|
|
_('Использование временных email запрещено'),
|
|
code='invalid_email_domain'
|
|
)
|
|
|
|
|
|
def validate_ip_address(value: str) -> None:
|
|
|
|
try:
|
|
ipaddress.ip_address(value)
|
|
except ValueError:
|
|
raise ValidationError(
|
|
_('Недопустимый IP адрес: %(value)s') % {'value': value},
|
|
code='invalid_ip'
|
|
)
|
|
|
|
def validate_mac_address(value: str) -> None:
|
|
pattern = r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$'
|
|
|
|
if not re.match(pattern, value):
|
|
raise ValidationError(
|
|
_('MAC адрес должен быть в формате XX:XX:XX:XX:XX:XX или XX-XX-XX-XX-XX-XX'),
|
|
code='invalid_mac'
|
|
)
|
|
|
|
|
|
def validate_ukrainian_tax_number(value: str) -> None:
|
|
"""Валидация украинского идентификационного кода (налогового номера)"""
|
|
pattern = r'^\d{10}$'
|
|
|
|
if not re.match(pattern, value):
|
|
raise ValidationError(
|
|
_('Украинский налоговый номер должен содержать 10 цифр'),
|
|
code='invalid_tax_number_length'
|
|
)
|
|
|
|
# Коэффициенты для украинского алгоритма
|
|
coefficients = [-1, 5, 7, 9, 4, 6, 10, 5, 7]
|
|
total = sum(int(value[i]) * coefficients[i] for i in range(9))
|
|
|
|
remainder = total % 11
|
|
if remainder < 2:
|
|
checksum = 0
|
|
elif remainder >= 10:
|
|
checksum = remainder - 10
|
|
else:
|
|
checksum = remainder
|
|
|
|
if int(value[9]) != checksum:
|
|
raise ValidationError(
|
|
_('Неверная контрольная сумма украинского налогового номера'),
|
|
code='invalid_tax_number_checksum'
|
|
)
|
|
|
|
def validate_inn(value: str) -> None:
|
|
"""Валидация ИНН (10 или 12 цифр)"""
|
|
pattern = r'^\d{10,12}$'
|
|
|
|
if not re.match(pattern, value):
|
|
raise ValidationError(
|
|
_('ИНН должен содержать 10 или 12 цифр'),
|
|
code='invalid_inn_length'
|
|
)
|
|
|
|
# Проверка контрольных сумм
|
|
if len(value) == 10:
|
|
_validate_inn_10(value)
|
|
elif len(value) == 12:
|
|
_validate_inn_12(value)
|
|
|
|
def _validate_inn_10(value: str) -> None:
|
|
"""Проверка контрольной цифры для 10-значного ИНН"""
|
|
coefficients = [2, 4, 10, 3, 5, 9, 4, 6, 8]
|
|
total = sum(int(value[i]) * coefficients[i] for i in range(9))
|
|
checksum = total % 11 % 10
|
|
if int(value[9]) != checksum:
|
|
raise ValidationError(
|
|
_('Неверная контрольная сумма ИНН'),
|
|
code='invalid_inn_checksum'
|
|
)
|
|
|
|
def _validate_inn_12(value: str) -> None:
|
|
"""Проверка контрольных цифр для 12-значного ИНН"""
|
|
coefficients1 = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
|
|
coefficients2 = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
|
|
|
|
total1 = sum(int(value[i]) * coefficients1[i] for i in range(10))
|
|
checksum1 = total1 % 11 % 10
|
|
|
|
total2 = sum(int(value[i]) * coefficients2[i] for i in range(11))
|
|
checksum2 = total2 % 11 % 10
|
|
|
|
if int(value[10]) != checksum1 or int(value[11]) != checksum2:
|
|
raise ValidationError(
|
|
_('Неверная контрольная сумма ИНН'),
|
|
code='invalid_inn_checksum'
|
|
)
|
|
|
|
|