from rest_framework import serializers from .models import DesignSettings class DesignSettingsSerializer(serializers.ModelSerializer): """ Сериализатор для настроек дизайна пользователя """ background_image_url = serializers.SerializerMethodField() class Meta: model = DesignSettings fields = [ 'id', 'template_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', 'test-list'] 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 def validate_template_id(self, value): """ Валидация ID шаблона """ if value and len(value) > 50: raise serializers.ValidationError('ID шаблона слишком длинный') 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