""" Утилиты для работы с пользователями. """ import re from django.utils import timezone import pytz def normalize_phone(value): """ Нормализует номер телефона к формату +999999999 (до 15 цифр). Принимает ввод с пробелами, скобками, дефисами и т.д. """ if not value or not isinstance(value, str): return '' digits = re.sub(r'\D', '', value) if not digits: return '' # Россия: 8XXXXXXXXXX -> +7..., 7XXXXXXXXXX -> +7..., 9XXXXXXXXX -> +79... if len(digits) == 11 and digits[0] == '8': digits = '7' + digits[1:] elif len(digits) == 10 and digits[0] == '9': digits = '7' + digits elif len(digits) == 11 and digits[0] == '7': pass return '+' + digits[:15] def convert_to_user_timezone(dt, user_timezone='UTC'): """ Конвертирует datetime из UTC в часовой пояс пользователя. Args: dt: datetime объект (должен быть aware в UTC) user_timezone: строка с названием часового пояса (например, 'Europe/Moscow' или 'UTC+10') Returns: datetime объект в часовом поясе пользователя """ if dt is None: return None try: # Нормализуем часовой пояс # Если передан формат UTC+X или UTC-X, конвертируем в Etc/GMT±X normalized_tz = (user_timezone or 'UTC').strip() # Обрабатываем формат UTC+X или UTC-X if normalized_tz.upper().startswith('UTC'): # Обрабатываем UTC+X или UTC-X # Убираем "UTC" (регистронезависимо) offset_str = normalized_tz[3:].strip() # Убираем "UTC" if offset_str.startswith('+'): # UTC+10 -> Etc/GMT-10 (обратите внимание на минус! В Etc/GMT знак инвертирован) try: offset_hours = int(offset_str[1:]) normalized_tz = f'Etc/GMT-{offset_hours}' except (ValueError, IndexError): normalized_tz = 'UTC' elif offset_str.startswith('-'): # UTC-5 -> Etc/GMT+5 (обратите внимание на плюс! В Etc/GMT знак инвертирован) try: offset_hours = int(offset_str[1:]) normalized_tz = f'Etc/GMT+{offset_hours}' except (ValueError, IndexError): normalized_tz = 'UTC' elif offset_str == '': # UTC -> UTC normalized_tz = 'UTC' else: # Если формат не распознан (например, UTC+10:00 или другой формат), используем UTC normalized_tz = 'UTC' # Получаем часовой пояс пользователя # Если normalized_tz все еще 'UTC+10' (не был обработан), пробуем еще раз if normalized_tz.startswith('UTC') and normalized_tz != 'UTC': # Если дошли сюда, значит формат не был распознан выше # Пробуем конвертировать еще раз offset_str = normalized_tz[3:].strip() if offset_str.startswith('+'): try: offset_hours = int(offset_str[1:]) normalized_tz = f'Etc/GMT-{offset_hours}' except (ValueError, IndexError): normalized_tz = 'UTC' elif offset_str.startswith('-'): try: offset_hours = int(offset_str[1:]) normalized_tz = f'Etc/GMT+{offset_hours}' except (ValueError, IndexError): normalized_tz = 'UTC' # Получаем часовой пояс пользователя tz = pytz.timezone(normalized_tz) # Если datetime не aware, делаем его aware в UTC if timezone.is_naive(dt): dt = timezone.make_aware(dt, pytz.UTC) # Конвертируем в часовой пояс пользователя return dt.astimezone(tz) except (pytz.exceptions.UnknownTimeZoneError, ValueError, Exception) as e: # Если часовой пояс неизвестен или произошла ошибка, возвращаем как есть # Логируем ошибку для отладки import logging logger = logging.getLogger(__name__) logger.warning(f"Error converting timezone {user_timezone}: {e}") return dt def get_user_timezone(user_timezone='UTC'): """ Получить объект timezone для часового пояса пользователя. Обрабатывает формат UTC+X и UTC-X. Args: user_timezone: строка с названием часового пояса (например, 'Europe/Moscow' или 'UTC+10') Returns: pytz timezone объект """ if not user_timezone: return pytz.UTC normalized_tz = user_timezone.strip() # Обрабатываем формат UTC+X или UTC-X if normalized_tz.upper().startswith('UTC'): offset_str = normalized_tz[3:].strip() if offset_str.startswith('+'): try: offset_hours = int(offset_str[1:]) normalized_tz = f'Etc/GMT-{offset_hours}' except (ValueError, IndexError): normalized_tz = 'UTC' elif offset_str.startswith('-'): try: offset_hours = int(offset_str[1:]) normalized_tz = f'Etc/GMT+{offset_hours}' except (ValueError, IndexError): normalized_tz = 'UTC' elif offset_str == '': normalized_tz = 'UTC' else: normalized_tz = 'UTC' try: return pytz.timezone(normalized_tz) except pytz.exceptions.UnknownTimeZoneError: import logging logger = logging.getLogger(__name__) logger.warning(f"Unknown timezone {user_timezone}, using UTC") return pytz.UTC def format_datetime_for_user(dt, user_timezone='UTC', format_str='isoformat'): """ Форматирует datetime в часовой пояс пользователя и возвращает строку. Args: dt: datetime объект (должен быть aware в UTC) user_timezone: строка с названием часового пояса format_str: формат вывода ('isoformat' для ISO строки, или strftime формат) Returns: строка с датой и временем в часовом поясе пользователя """ if dt is None: return None local_dt = convert_to_user_timezone(dt, user_timezone) if format_str == 'isoformat': # Убеждаемся, что isoformat() возвращает строку с timezone offset # Для pytz timezone isoformat() должен автоматически включать offset # Но для Etc/GMT timezone может быть проблема, поэтому добавляем offset вручную if local_dt.tzinfo: # Получаем offset в секундах offset = local_dt.utcoffset() if offset: # Формируем ISO строку с offset вручную для гарантии правильного формата # Формат: YYYY-MM-DDTHH:MM:SS+HH:MM или YYYY-MM-DDTHH:MM:SS-HH:MM total_seconds = int(offset.total_seconds()) hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 sign = '+' if hours >= 0 else '-' # Форматируем дату и время без timezone dt_str = local_dt.strftime('%Y-%m-%dT%H:%M:%S') # Добавляем миллисекунды, если есть if local_dt.microsecond: dt_str += f".{local_dt.microsecond // 1000:03d}" # Добавляем offset iso_str = f"{dt_str}{sign}{abs(hours):02d}:{abs(minutes):02d}" else: # Если offset равен 0, используем Z для UTC iso_str = local_dt.strftime('%Y-%m-%dT%H:%M:%S') if local_dt.microsecond: iso_str += f".{local_dt.microsecond // 1000:03d}" iso_str += 'Z' else: # Если нет timezone, просто возвращаем ISO строку iso_str = local_dt.isoformat() return iso_str else: return local_dt.strftime(format_str)