211 lines
9.1 KiB
Python
211 lines
9.1 KiB
Python
"""
|
||
Утилиты для работы с пользователями.
|
||
"""
|
||
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)
|