refactor: убран base64 для изображений, динамический API URL, ImageField в сериализаторах
This commit is contained in:
parent
dfd4462d20
commit
ebaf02362d
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
64
backend/api/v1/utils.py
Normal 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}"
|
||||
@ -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,
|
||||
}
|
||||
@ -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")
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# АУТЕНТИФИКАЦИОННЫЕ БЭКЕНДЫ
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user