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' )