- Add group overlay color and opacity settings - Add font customization (body and heading fonts) - Add group description text color control - Add option to hide 'Groups' title - Update frontend DesignSettings interface - Update CustomizationPanel with new UI controls - Update Django model with new fields - Create migration for new customization options - Update DRF serializer with validation
280 lines
11 KiB
Python
280 lines
11 KiB
Python
from rest_framework import serializers
|
|
from .models import DesignSettings
|
|
|
|
|
|
class DesignSettingsSerializer(serializers.ModelSerializer):
|
|
"""
|
|
Сериализатор для настроек дизайна пользователя
|
|
"""
|
|
background_image_url = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = DesignSettings
|
|
fields = [
|
|
'id',
|
|
'theme_color',
|
|
'background_image',
|
|
'background_image_url',
|
|
'dashboard_layout',
|
|
'groups_default_expanded',
|
|
'show_group_icons',
|
|
'show_link_icons',
|
|
'dashboard_background_color',
|
|
'font_family',
|
|
'custom_css',
|
|
'header_text_color',
|
|
'group_text_color',
|
|
'link_text_color',
|
|
'cover_overlay_enabled',
|
|
'cover_overlay_color',
|
|
'cover_overlay_opacity',
|
|
'group_overlay_enabled',
|
|
'group_overlay_color',
|
|
'group_overlay_opacity',
|
|
'show_groups_title',
|
|
'group_description_text_color',
|
|
'body_font_family',
|
|
'heading_font_family',
|
|
'updated_at'
|
|
]
|
|
read_only_fields = ['id', 'updated_at', 'background_image_url']
|
|
# Делаем background_image необязательным
|
|
extra_kwargs = {
|
|
'background_image': {'required': False}
|
|
}
|
|
|
|
def get_background_image_url(self, obj):
|
|
"""
|
|
Возвращает полный URL фонового изображения
|
|
"""
|
|
if obj.background_image:
|
|
request = self.context.get('request')
|
|
if request:
|
|
return request.build_absolute_uri(obj.background_image.url)
|
|
return obj.background_image.url
|
|
return None
|
|
|
|
def update(self, instance, validated_data):
|
|
"""
|
|
Переопределяем метод update для обработки случая, когда background_image не передается
|
|
"""
|
|
# Специально обрабатываем background_image
|
|
background_image = validated_data.pop('background_image', None)
|
|
|
|
# Обновляем остальные поля
|
|
for attr, value in validated_data.items():
|
|
setattr(instance, attr, value)
|
|
|
|
# Обновляем background_image только если передан новый файл
|
|
if background_image is not None:
|
|
instance.background_image = background_image
|
|
|
|
instance.save()
|
|
return instance
|
|
|
|
def validate_background_image(self, value):
|
|
"""
|
|
Валидация загружаемого изображения
|
|
"""
|
|
if value:
|
|
# Проверяем размер файла (максимум 10MB)
|
|
if value.size > 10 * 1024 * 1024:
|
|
raise serializers.ValidationError('Размер файла не должен превышать 10MB')
|
|
|
|
# Проверяем тип файла
|
|
if not value.content_type.startswith('image/'):
|
|
raise serializers.ValidationError('Файл должен быть изображением')
|
|
|
|
# Допустимые форматы
|
|
allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']
|
|
if value.content_type not in allowed_types:
|
|
raise serializers.ValidationError('Поддерживаются только форматы: JPEG, PNG, GIF, WebP')
|
|
|
|
return value
|
|
|
|
def validate_theme_color(self, value):
|
|
"""
|
|
Валидация цвета темы (должен быть в формате hex)
|
|
"""
|
|
if not value.startswith('#') or len(value) != 7:
|
|
raise serializers.ValidationError('Цвет должен быть в формате #RRGGBB')
|
|
try:
|
|
int(value[1:], 16)
|
|
except ValueError:
|
|
raise serializers.ValidationError('Некорректный hex цвет')
|
|
return value
|
|
|
|
def validate_dashboard_background_color(self, value):
|
|
"""
|
|
Валидация цвета фона дашборда
|
|
"""
|
|
if not value.startswith('#') or len(value) != 7:
|
|
raise serializers.ValidationError('Цвет должен быть в формате #RRGGBB')
|
|
try:
|
|
int(value[1:], 16)
|
|
except ValueError:
|
|
raise serializers.ValidationError('Некорректный hex цвет')
|
|
return value
|
|
|
|
def validate_dashboard_layout(self, value):
|
|
"""
|
|
Валидация типа макета дашборда
|
|
"""
|
|
valid_layouts = ['sidebar', 'grid', 'list', 'cards', 'compact', 'masonry', 'timeline', 'magazine']
|
|
if value not in valid_layouts:
|
|
raise serializers.ValidationError(f'Макет должен быть одним из: {", ".join(valid_layouts)}')
|
|
return value
|
|
|
|
def validate_font_family(self, value):
|
|
"""
|
|
Валидация шрифта
|
|
"""
|
|
if len(value) > 100:
|
|
raise serializers.ValidationError('Название шрифта слишком длинное')
|
|
return value
|
|
|
|
def validate_custom_css(self, value):
|
|
"""
|
|
Базовая валидация пользовательского CSS
|
|
"""
|
|
if len(value) > 10000: # Ограничение на размер CSS
|
|
raise serializers.ValidationError('CSS код слишком длинный (максимум 10000 символов)')
|
|
|
|
# Простая проверка на потенциально опасные директивы
|
|
dangerous_patterns = ['@import', 'javascript:', 'expression(']
|
|
for pattern in dangerous_patterns:
|
|
if pattern in value.lower():
|
|
raise serializers.ValidationError(f'Обнаружена потенциально опасная директива: {pattern}')
|
|
|
|
return value
|
|
|
|
def validate_header_text_color(self, value):
|
|
"""
|
|
Валидация цвета заголовков
|
|
"""
|
|
if not value.startswith('#') or len(value) != 7:
|
|
raise serializers.ValidationError('Цвет должен быть в формате #RRGGBB')
|
|
try:
|
|
int(value[1:], 16)
|
|
except ValueError:
|
|
raise serializers.ValidationError('Некорректный hex цвет')
|
|
return value
|
|
|
|
def validate_group_text_color(self, value):
|
|
"""
|
|
Валидация цвета названий групп
|
|
"""
|
|
if not value.startswith('#') or len(value) != 7:
|
|
raise serializers.ValidationError('Цвет должен быть в формате #RRGGBB')
|
|
try:
|
|
int(value[1:], 16)
|
|
except ValueError:
|
|
raise serializers.ValidationError('Некорректный hex цвет')
|
|
return value
|
|
|
|
def validate_link_text_color(self, value):
|
|
"""
|
|
Валидация цвета названий ссылок
|
|
"""
|
|
if not value.startswith('#') or len(value) != 7:
|
|
raise serializers.ValidationError('Цвет должен быть в формате #RRGGBB')
|
|
try:
|
|
int(value[1:], 16)
|
|
except ValueError:
|
|
raise serializers.ValidationError('Некорректный hex цвет')
|
|
return value
|
|
|
|
def validate_cover_overlay_color(self, value):
|
|
"""
|
|
Валидация цвета перекрытия обложки
|
|
"""
|
|
if not value.startswith('#') or len(value) != 7:
|
|
raise serializers.ValidationError('Цвет должен быть в формате #RRGGBB')
|
|
try:
|
|
int(value[1:], 16)
|
|
except ValueError:
|
|
raise serializers.ValidationError('Некорректный hex цвет')
|
|
return value
|
|
|
|
def validate_cover_overlay_opacity(self, value):
|
|
"""
|
|
Валидация прозрачности перекрытия обложки
|
|
"""
|
|
if not 0.0 <= value <= 1.0:
|
|
raise serializers.ValidationError('Прозрачность должна быть между 0.0 и 1.0')
|
|
return value
|
|
|
|
def validate_group_overlay_color(self, value):
|
|
"""
|
|
Валидация цвета перекрытия групп
|
|
"""
|
|
if not value.startswith('#') or len(value) != 7:
|
|
raise serializers.ValidationError('Цвет должен быть в формате #RRGGBB')
|
|
try:
|
|
int(value[1:], 16)
|
|
except ValueError:
|
|
raise serializers.ValidationError('Некорректный hex цвет')
|
|
return value
|
|
|
|
def validate_group_overlay_opacity(self, value):
|
|
"""
|
|
Валидация прозрачности перекрытия групп
|
|
"""
|
|
if not 0.0 <= value <= 1.0:
|
|
raise serializers.ValidationError('Прозрачность должна быть между 0.0 и 1.0')
|
|
return value
|
|
|
|
def validate_group_description_text_color(self, value):
|
|
"""
|
|
Валидация цвета описаний групп
|
|
"""
|
|
if not value.startswith('#') or len(value) != 7:
|
|
raise serializers.ValidationError('Цвет должен быть в формате #RRGGBB')
|
|
try:
|
|
int(value[1:], 16)
|
|
except ValueError:
|
|
raise serializers.ValidationError('Некорректный hex цвет')
|
|
return value
|
|
|
|
def validate_body_font_family(self, value):
|
|
"""
|
|
Валидация шрифта основного текста
|
|
"""
|
|
if value and len(value) > 100:
|
|
raise serializers.ValidationError('Название шрифта слишком длинное')
|
|
return value
|
|
|
|
def validate_heading_font_family(self, value):
|
|
"""
|
|
Валидация шрифта заголовков
|
|
"""
|
|
if value and len(value) > 100:
|
|
raise serializers.ValidationError('Название шрифта слишком длинное')
|
|
return value
|
|
|
|
|
|
class PublicDesignSettingsSerializer(serializers.ModelSerializer):
|
|
"""
|
|
Публичный сериализатор для настроек дизайна (только для отображения)
|
|
"""
|
|
background_image_url = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = DesignSettings
|
|
fields = [
|
|
'theme_color',
|
|
'background_image_url',
|
|
'font_family',
|
|
'custom_css'
|
|
]
|
|
|
|
def get_background_image_url(self, obj):
|
|
"""
|
|
Возвращает полный URL фонового изображения
|
|
"""
|
|
if obj.background_image:
|
|
request = self.context.get('request')
|
|
if request:
|
|
return request.build_absolute_uri(obj.background_image.url)
|
|
return obj.background_image.url
|
|
return None |