+ Приведены все функции приложения в рабочий вид

+ Наведен порядок в файлах проекта
+ Наведен порядок в документации
+ Настроены скрипты установки, развертки и так далее, расширен MakeFile
This commit is contained in:
2025-11-02 06:09:55 +09:00
parent 367e1c932e
commit 2e535513b5
6103 changed files with 7040 additions and 1027861 deletions

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.2.7 on 2025-10-31 09:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customization', '0002_alter_designsettings_background_image'),
]
operations = [
migrations.AddField(
model_name='designsettings',
name='dashboard_background_color',
field=models.CharField(default='#f8f9fa', help_text='Цвет фона дашборда (hex)', max_length=7),
),
migrations.AddField(
model_name='designsettings',
name='dashboard_layout',
field=models.CharField(choices=[('sidebar', 'Боковая панель'), ('grid', 'Сетка'), ('list', 'Список')], default='list', help_text='Стиль отображения дашборда', max_length=20),
),
migrations.AddField(
model_name='designsettings',
name='groups_default_expanded',
field=models.BooleanField(default=True, help_text='Развернуты ли группы по умолчанию'),
),
migrations.AddField(
model_name='designsettings',
name='show_group_icons',
field=models.BooleanField(default=True, help_text='Показывать иконки групп'),
),
migrations.AddField(
model_name='designsettings',
name='show_link_icons',
field=models.BooleanField(default=True, help_text='Показывать иконки ссылок'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2025-10-31 11:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customization', '0003_designsettings_dashboard_background_color_and_more'),
]
operations = [
migrations.AddField(
model_name='designsettings',
name='group_text_color',
field=models.CharField(default='#333333', help_text='Цвет названий групп (hex)', max_length=7),
),
migrations.AddField(
model_name='designsettings',
name='header_text_color',
field=models.CharField(default='#000000', help_text='Цвет заголовков (hex)', max_length=7),
),
migrations.AddField(
model_name='designsettings',
name='link_text_color',
field=models.CharField(default='#666666', help_text='Цвет названий ссылок (hex)', max_length=7),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-01 09:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customization', '0004_designsettings_group_text_color_and_more'),
]
operations = [
migrations.AlterField(
model_name='designsettings',
name='dashboard_layout',
field=models.CharField(choices=[('sidebar', 'Боковая панель'), ('grid', 'Сетка'), ('list', 'Список'), ('cards', 'Карточки'), ('compact', 'Компактный'), ('masonry', 'Кладка'), ('timeline', 'Временная линия'), ('magazine', 'Журнальный')], default='list', help_text='Стиль отображения дашборда', max_length=20),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2025-11-01 10:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customization', '0005_alter_designsettings_dashboard_layout'),
]
operations = [
migrations.AddField(
model_name='designsettings',
name='cover_overlay_color',
field=models.CharField(default='#000000', help_text='Цвет перекрытия обложки (hex)', max_length=7),
),
migrations.AddField(
model_name='designsettings',
name='cover_overlay_enabled',
field=models.BooleanField(default=False, help_text='Включить цветовое перекрытие обложки'),
),
migrations.AddField(
model_name='designsettings',
name='cover_overlay_opacity',
field=models.FloatField(default=0.5, help_text='Прозрачность перекрытия (0.0 - 1.0)'),
),
]

View File

@@ -25,6 +25,41 @@ class DesignSettings(models.Model):
blank=True,
help_text='Фоновое изображение'
)
# Новые поля для дашборда
dashboard_layout = models.CharField(
max_length=20,
choices=[
('sidebar', 'Боковая панель'),
('grid', 'Сетка'),
('list', 'Список'),
('cards', 'Карточки'),
('compact', 'Компактный'),
('masonry', 'Кладка'),
('timeline', 'Временная линия'),
('magazine', 'Журнальный'),
],
default='list',
help_text='Стиль отображения дашборда'
)
groups_default_expanded = models.BooleanField(
default=True,
help_text='Развернуты ли группы по умолчанию'
)
show_group_icons = models.BooleanField(
default=True,
help_text='Показывать иконки групп'
)
show_link_icons = models.BooleanField(
default=True,
help_text='Показывать иконки ссылок'
)
dashboard_background_color = models.CharField(
max_length=7,
default='#f8f9fa',
help_text='Цвет фона дашборда (hex)'
)
font_family = models.CharField(
max_length=100,
default='sans-serif',
@@ -34,6 +69,39 @@ class DesignSettings(models.Model):
blank=True,
help_text='Дополнительный CSS'
)
# Новые поля для цветов текста
header_text_color = models.CharField(
max_length=7,
default='#000000',
help_text='Цвет заголовков (hex)'
)
group_text_color = models.CharField(
max_length=7,
default='#333333',
help_text='Цвет названий групп (hex)'
)
link_text_color = models.CharField(
max_length=7,
default='#666666',
help_text='Цвет названий ссылок (hex)'
)
# Поля для настройки обложки
cover_overlay_enabled = models.BooleanField(
default=False,
help_text='Включить цветовое перекрытие обложки'
)
cover_overlay_color = models.CharField(
max_length=7,
default='#000000',
help_text='Цвет перекрытия обложки (hex)'
)
cover_overlay_opacity = models.FloatField(
default=0.5,
help_text='Прозрачность перекрытия (0.0 - 1.0)'
)
updated_at = models.DateTimeField(
auto_now=True,
help_text='Дата и время последнего изменения'
@@ -84,7 +152,7 @@ class DesignSettings(models.Model):
"""
if self.background_image:
self.background_image.delete(save=False)
super().delete(*args, **kwargs)
return super().delete(*args, **kwargs)
# Вспомогательные методы для доступа к данным пользователя

View File

@@ -0,0 +1,225 @@
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',
'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
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

View File

@@ -1,5 +1,7 @@
from django.urls import path
from .views import DesignSettingsView, get_user_design_settings
urlpatterns = [
# дополнительные эндпоинты по ссылкам
path('settings/', DesignSettingsView.as_view(), name='design-settings'),
path('settings/<str:username>/', get_user_design_settings, name='user-design-settings'),
]

View File

@@ -1,3 +1,66 @@
from django.shortcuts import render
from rest_framework import generics, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from django.shortcuts import get_object_or_404
from .models import DesignSettings
from .serializers import DesignSettingsSerializer, PublicDesignSettingsSerializer
# Create your views here.
class DesignSettingsView(generics.RetrieveUpdateAPIView):
"""
API для получения и обновления настроек дизайна пользователя
"""
serializer_class = DesignSettingsSerializer
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser, JSONParser] # Поддержка файлов и JSON
def get_object(self):
# Получаем или создаем настройки для текущего пользователя
settings, created = DesignSettings.objects.get_or_create(
user=self.request.user
)
return settings
def perform_update(self, serializer):
serializer.save(user=self.request.user)
@api_view(['GET'])
def get_user_design_settings(request, username=None):
"""
Получение публичных настроек дизайна пользователя для отображения его страницы
"""
if username:
from django.contrib.auth import get_user_model
User = get_user_model()
try:
user = User.objects.get(username=username)
try:
settings = DesignSettings.objects.get(user=user)
# Возвращаем только публичные настройки
serializer = PublicDesignSettingsSerializer(settings, context={'request': request})
return Response(serializer.data)
except DesignSettings.DoesNotExist:
# Возвращаем настройки по умолчанию, если пользователь еще не настроил дизайн
default_settings = {
'theme_color': '#ffffff',
'dashboard_layout': 'list',
'groups_default_expanded': True,
'show_group_icons': True,
'show_link_icons': True,
'dashboard_background_color': '#f8f9fa',
'font_family': 'sans-serif'
}
return Response(default_settings)
except User.DoesNotExist:
return Response({'error': 'Пользователь не найден'}, status=404)
else:
# Для получения настроек текущего пользователя требуется авторизация
if not request.user.is_authenticated:
return Response({'error': 'Требуется авторизация'}, status=401)
# Возвращаем настройки текущего пользователя
settings, created = DesignSettings.objects.get_or_create(user=request.user)
serializer = DesignSettingsSerializer(settings, context={'request': request})
return Response(serializer.data)