refactor: убран base64 для изображений, динамический API URL, ImageField в сериализаторах

This commit is contained in:
admin i 2025-12-05 10:25:24 +03:00
parent 01d5e5c77e
commit be15af8bc1
6 changed files with 89 additions and 62 deletions

View File

@ -17,8 +17,21 @@ class ApiConfig:
login_url_name: str = "auth_front:login" # куда редиректить, если токены протухли
def load_config() -> ApiConfig:
base = getattr(settings, "API_BASE_URL", "http://localhost:8000/api")
def load_config(request=None) -> ApiConfig:
"""
Загружает конфигурацию API.
Если передан request, base_url формируется динамически на основе домена запроса.
Иначе используется настройка API_BASE_URL из settings.
"""
if request:
# Динамическое формирование base_url на основе текущего домена
scheme = 'https' if request.is_secure() else 'http'
host = request.get_host()
base = f"{scheme}://{host}/api"
else:
# Fallback на статическую настройку (для фоновых задач, команд и т.д.)
base = getattr(settings, "API_BASE_URL", "http://localhost:9000/api")
return ApiConfig(
base_url=base.rstrip("/"),
token_obtain_path=getattr(settings, "API_TOKEN_OBTAIN_PATH", "/auth/token/"),
@ -57,7 +70,7 @@ class ApiClient:
def __init__(self, request, config: Optional[ApiConfig] = None):
self.request = request
self.cfg = config or load_config()
self.cfg = config or load_config(request)
self.session = requests.Session()
self.access = request.session.get(SESSION_KEY_ACCESS)
self.refresh = request.session.get(SESSION_KEY_REFRESH)

View File

@ -11,7 +11,6 @@ from employees.models import (Department, DepartmentPermission, DepartmentRole,
from eusrr_backend.auth_backends import PHONE_FIELD as DETECTED_PHONE_FIELD
from rest_framework import serializers
from ..serializers import Base64ImageField
from employees.utils import _normalize_phone
Employee = get_user_model()
@ -54,7 +53,7 @@ class RegisterSerializer(serializers.Serializer):
whatsapp = serializers.CharField(required=False, allow_blank=True, default="")
wechat = serializers.CharField(required=False, allow_blank=True, default="")
avatar = Base64ImageField(required=True)
avatar = serializers.ImageField(required=True)
patronymic = serializers.CharField(required=False, allow_blank=True, default="")
gender = serializers.ChoiceField(
@ -268,7 +267,7 @@ class EmployeeActionSerializer(serializers.ModelSerializer):
class EmployeeSerializer(serializers.ModelSerializer):
"""Полная версия сотрудника для собственных эндпоинтов /employees/."""
avatar = Base64ImageField(required=False, allow_null=True)
avatar = serializers.ImageField(required=False, allow_null=True)
actions = EmployeeActionSerializer(many=True, read_only=True)
# навыки: читаем красиво, пишем ids
@ -419,7 +418,7 @@ class EmployeeSerializer(serializers.ModelSerializer):
class EmployeeBriefSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField()
avatar = Base64ImageField(read_only=True)
avatar = serializers.ImageField(read_only=True)
full_name = serializers.SerializerMethodField()
class Meta:
@ -492,7 +491,7 @@ class EmployeeListSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField()
email = serializers.EmailField(read_only=True)
phone_number = serializers.CharField(read_only=True)
avatar = Base64ImageField(read_only=True)
avatar = serializers.ImageField(read_only=True)
position = PositionBriefSerializer(read_only=True)
departments = serializers.SerializerMethodField()
full_name = serializers.SerializerMethodField()

View File

@ -8,6 +8,7 @@ from rest_framework import serializers
from feed.constants import TYPE_COMPANY, TYPE_DEPARTMENT, TYPE_EMPLOYEE
from feed.models import Comment, Post, PostLike
from ..utils import build_media_url
Employee = get_user_model()
@ -28,14 +29,8 @@ class AuthorMiniSerializer(serializers.ModelSerializer):
def get_avatar(self, obj):
"""Возвращает полный URL для аватара"""
if not obj.avatar:
return None
request = self.context.get('request')
if request and hasattr(obj.avatar, 'url'):
return request.build_absolute_uri(obj.avatar.url)
if hasattr(obj.avatar, 'url'):
return obj.avatar.url
return str(obj.avatar)
return build_media_url(obj.avatar, request)
class CommentMiniSerializer(serializers.ModelSerializer):
@ -104,23 +99,13 @@ class PostListSerializer(serializers.ModelSerializer):
def get_image(self, obj):
"""Возвращает полный URL для изображения"""
if not obj.image:
return None
request = self.context.get('request')
if request and hasattr(obj.image, 'url'):
return request.build_absolute_uri(obj.image.url)
return obj.image.url if hasattr(obj.image, 'url') else str(obj.image)
return build_media_url(obj.image, request)
def get_attachment(self, obj):
"""Возвращает полный URL для вложения"""
if not obj.attachment:
return None
request = self.context.get('request')
if request and hasattr(obj.attachment, 'url'):
return request.build_absolute_uri(obj.attachment.url)
if hasattr(obj.attachment, 'url'):
return obj.attachment.url
return str(obj.attachment)
return build_media_url(obj.attachment, request)
def get_last_comment(self, obj):
# Берём готовую мапу из контекста, собранную во viewset.list()

64
backend/api/v1/utils.py Normal file
View File

@ -0,0 +1,64 @@
"""
Утилиты для работы с медиа URL
"""
from django.conf import settings
def get_media_base_url(request=None):
"""
Возвращает базовый URL для медиа-файлов.
Приоритет:
1. Если USE_HTTPS=true и DOMAIN установлен → используем его
2. Если есть request → используем домен из запроса
3. Fallback → /media/
"""
# Если в настройках указан предпочтительный домен
use_https = getattr(settings, 'USE_HTTPS', False)
domain = getattr(settings, 'DOMAIN', None)
if domain and domain not in ['127.0.0.1:9000', 'localhost:9000']:
# Глобальный домен из настроек
scheme = 'https' if use_https else 'http'
return f"{scheme}://{domain}/media/"
# Используем домен из запроса (для локальной разработки)
if request:
return request.build_absolute_uri('/media/')
# Fallback
return '/media/'
def build_media_url(file_field, request=None):
"""
Формирует полный URL для медиа-файла.
Args:
file_field: ImageField/FileField объект
request: HTTP запрос (опционально)
Returns:
str: Полный URL к файлу
"""
if not file_field:
return None
# Получаем относительный путь
if hasattr(file_field, 'url'):
relative_url = file_field.url
else:
relative_url = str(file_field)
# Убираем /media/ если есть
if relative_url.startswith('/media/'):
relative_url = relative_url[7:]
elif relative_url.startswith('/'):
relative_url = relative_url[1:]
# Формируем полный URL
base_url = get_media_base_url(request)
if not base_url.endswith('/'):
base_url += '/'
return f"{base_url}{relative_url}"

View File

@ -1,26 +0,0 @@
"""
Context processors для глобальных переменных в шаблонах
"""
def media_domain(request):
"""
Динамически определяет MEDIA_URL на основе текущего домена запроса.
Это позволяет правильно формировать ссылки на медиа как для локального,
так и для глобального домена без перезапуска сервера.
"""
host = request.get_host()
scheme = request.scheme
# Формируем базовый URL для текущего домена
base_url = f"{scheme}://{host}"
# Динамический MEDIA_URL
dynamic_media_url = f"{base_url}/media/"
return {
'MEDIA_URL_DYNAMIC': dynamic_media_url,
'CURRENT_DOMAIN': host,
'CURRENT_SCHEME': scheme,
}

View File

@ -80,7 +80,6 @@ TEMPLATES = [
"django.contrib.messages.context_processors.messages",
"communications.context_processors.chat_unread_total",
"eusrr_backend.context_processors.branding",
"core.context_processors.media_domain",
],
},
},
@ -157,13 +156,7 @@ PHONENUMBER_DEFAULT_REGION = "RU"
# Медиа файлы
MEDIA_ROOT = os.path.join(BASE_DIR, "media/")
# Используем полный URL для медиа на продакшене
USE_HTTPS = os.getenv("USE_HTTPS", "False").lower() == "true"
DOMAIN = os.getenv("DOMAIN", "127.0.0.1:9000")
if USE_HTTPS:
MEDIA_URL = f"https://{DOMAIN}/media/"
else:
MEDIA_URL = "/media/" # относительный URL для локальной разработки
MEDIA_URL = "/media/"
# -----------------------------------------------------------------------------
# ЛОГИРОВАНИЕ
@ -294,7 +287,6 @@ SIMPLE_JWT = {
# Автообновление JWT токенов: за сколько минут до истечения обновлять
JWT_REFRESH_THRESHOLD_MINUTES = int(os.getenv("JWT_REFRESH_THRESHOLD_MIN", "5"))
API_BASE_URL = os.getenv("API_BASE_URL", "http://127.0.0.1:9000/api")
# -----------------------------------------------------------------------------
# АУТЕНТИФИКАЦИОННЫЕ БЭКЕНДЫ