uchill/backend/apps/users/utils.py

211 lines
9.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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