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

+ Наведен порядок в файлах проекта
+ Наведен порядок в документации
+ Настроены скрипты установки, развертки и так далее, расширен 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,33 @@
# Generated by Django 5.2.7 on 2025-10-31 09:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0005_alter_link_icon_alter_linkgroup_is_favorite_and_more'),
]
operations = [
migrations.AddField(
model_name='linkgroup',
name='background_image',
field=models.ImageField(blank=True, help_text='Фоновое изображение группы', null=True, upload_to='group_backgrounds/'),
),
migrations.AddField(
model_name='linkgroup',
name='display_style',
field=models.CharField(choices=[('grid', 'Сетка'), ('list', 'Список'), ('cards', 'Карточки')], default='list', help_text='Стиль отображения ссылок в группе', max_length=20),
),
migrations.AddField(
model_name='linkgroup',
name='header_color',
field=models.CharField(default='#ffffff', help_text='Цвет заголовка группы (hex)', max_length=7),
),
migrations.AddField(
model_name='linkgroup',
name='is_expanded',
field=models.BooleanField(default=True, help_text='Развернута ли группа по умолчанию'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-10-31 11:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0006_linkgroup_background_image_linkgroup_display_style_and_more'),
]
operations = [
migrations.AddField(
model_name='link',
name='description',
field=models.TextField(blank=True, null=True),
),
]

View File

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

View File

@@ -0,0 +1,13 @@
# Generated by Django 5.2.7 on 2025-11-01 08:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('api', '0008_alter_linkgroup_display_style'),
]
operations = [
]

View File

@@ -18,6 +18,39 @@ class LinkGroup(models.Model):
blank=True,
help_text='Иконка группы ссылок'
)
# Новые поля для кастомизации
header_color = models.CharField(
max_length=7,
default='#ffffff',
help_text='Цвет заголовка группы (hex)'
)
background_image = models.ImageField(
upload_to='group_backgrounds/',
null=True,
blank=True,
help_text='Фоновое изображение группы'
)
is_expanded = models.BooleanField(
default=True,
help_text='Развернута ли группа по умолчанию'
)
display_style = models.CharField(
max_length=20,
choices=[
('list', 'Список'),
('grid', 'Сетка'),
('cards', 'Карточки'),
('compact', 'Компактный'),
('sidebar', 'Боковая панель'),
('masonry', 'Кладка'),
('timeline', 'Лента времени'),
('magazine', 'Журнальный'),
],
default='list',
help_text='Стиль отображения ссылок в группе'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_public = models.BooleanField(default=False)
@@ -42,6 +75,7 @@ class Link(models.Model):
)
title = models.CharField(max_length=200)
url = models.URLField()
description = models.TextField(blank=True, null=True)
icon = models.ImageField(
upload_to='links/',
null=True,

View File

@@ -26,23 +26,110 @@ class RegisterSerializer(serializers.ModelSerializer):
# сериализатор для ссылок
class UserSerializer(serializers.ModelSerializer):
avatar_url = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['id', 'username', 'email', 'full_name', 'bio', 'avatar', 'last_login', 'date_joined']
fields = ['id', 'username', 'email', 'full_name', 'bio', 'avatar', 'avatar_url', 'last_login', 'date_joined']
read_only_fields = ['avatar_url']
def get_avatar_url(self, obj):
"""Возвращает полный URL аватара"""
if obj.avatar:
request = self.context.get('request')
if request:
absolute_uri = request.build_absolute_uri(obj.avatar.url)
# Заменяем различные варианты внутренних Docker URL
absolute_uri = absolute_uri.replace('http://web:8000', 'http://localhost:8000')
absolute_uri = absolute_uri.replace('http://links-web-1:8000', 'http://localhost:8000')
absolute_uri = absolute_uri.replace('http://backend:8000', 'http://localhost:8000')
return absolute_uri
return f'http://localhost:8000{obj.avatar.url}'
return None
class LinkGroupSerializer(serializers.ModelSerializer):
icon = serializers.ImageField(required=False, allow_null=True)
background_image = serializers.ImageField(required=False, allow_null=True)
icon_url = serializers.SerializerMethodField()
background_image_url = serializers.SerializerMethodField()
class Meta:
model = LinkGroup
fields = ['id', 'name', 'description', 'icon', 'order', 'is_public', 'is_favorite',
'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at']
fields = [
'id', 'name', 'description', 'icon', 'icon_url', 'order',
'is_public', 'is_favorite', 'created_at', 'updated_at',
# Новые поля кастомизации
'header_color', 'background_image', 'background_image_url',
'is_expanded', 'display_style'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'icon_url', 'background_image_url']
def get_icon_url(self, obj):
"""Возвращает полный URL иконки"""
if obj.icon:
request = self.context.get('request')
if request:
absolute_uri = request.build_absolute_uri(obj.icon.url)
# Заменяем различные варианты внутренних Docker URL
absolute_uri = absolute_uri.replace('http://web:8000', 'http://localhost:8000')
absolute_uri = absolute_uri.replace('http://links-web-1:8000', 'http://localhost:8000')
absolute_uri = absolute_uri.replace('http://backend:8000', 'http://localhost:8000')
return absolute_uri
return f'http://localhost:8000{obj.icon.url}'
return None
def get_background_image_url(self, obj):
"""Возвращает полный URL фонового изображения"""
if obj.background_image:
request = self.context.get('request')
if request:
absolute_uri = request.build_absolute_uri(obj.background_image.url)
# Заменяем различные варианты внутренних Docker URL
absolute_uri = absolute_uri.replace('http://web:8000', 'http://localhost:8000')
absolute_uri = absolute_uri.replace('http://links-web-1:8000', 'http://localhost:8000')
absolute_uri = absolute_uri.replace('http://backend:8000', 'http://localhost:8000')
return absolute_uri
return f'http://localhost:8000{obj.background_image.url}'
return None
def validate_header_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_display_style(self, value):
"""Валидация стиля отображения"""
valid_styles = ['list', 'grid', 'cards', 'compact', 'sidebar', 'masonry', 'timeline', 'magazine']
if value not in valid_styles:
raise serializers.ValidationError(f'Стиль должен быть одним из: {", ".join(valid_styles)}')
return value
class LinkSerializer(serializers.ModelSerializer):
icon = serializers.ImageField(required=False, allow_null=True)
icon_url = serializers.SerializerMethodField()
class Meta:
model = Link
fields = ['id', 'title', 'url', 'icon', 'group', 'order']
read_only_fields = ['id']
fields = ['id', 'title', 'url', 'icon', 'icon_url', 'group', 'order']
read_only_fields = ['id', 'icon_url']
def get_icon_url(self, obj):
"""Возвращает полный URL иконки ссылки"""
if obj.icon:
request = self.context.get('request')
if request:
absolute_uri = request.build_absolute_uri(obj.icon.url)
# Заменяем различные варианты внутренних Docker URL
absolute_uri = absolute_uri.replace('http://web:8000', 'http://localhost:8000')
absolute_uri = absolute_uri.replace('http://links-web-1:8000', 'http://localhost:8000')
absolute_uri = absolute_uri.replace('http://backend:8000', 'http://localhost:8000')
return absolute_uri
return f'http://localhost:8000{obj.icon.url}'
return None

View File

@@ -1,4 +1,4 @@
from django.urls import path
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from .views import (
RegisterView,
@@ -32,6 +32,8 @@ urlpatterns = [
PublicUserGroupsView.as_view(),
name='public-user-groups'
),
# Добавляем кастомизацию
path('customization/', include('customization.urls')),
# схема OpenAPI
path('schema/', SpectacularAPIView.as_view(), name='schema'),
# Swagger UI, берёт шаблон из drf_spectacular_sidecar

View File

@@ -83,38 +83,127 @@ class PublicUserGroupsView(APIView):
# 1. Ищем пользователя
user = get_object_or_404(User, username=username)
# 2. Берём его группы со ссылками
groups_qs = LinkGroup.objects.filter(owner=user).prefetch_related('links')
# 2. Получаем настройки дизайна пользователя
from customization.models import DesignSettings
try:
design_settings = DesignSettings.objects.get(user=user)
# Заменяем Docker URL на localhost для клиента
background_image_url = None
if design_settings.background_image:
background_image_url = request.build_absolute_uri(design_settings.background_image.url)
# Заменяем различные варианты внутренних Docker URL
background_image_url = background_image_url.replace('http://web:8000', 'http://localhost:8000')
background_image_url = background_image_url.replace('http://links-web-1:8000', 'http://localhost:8000')
background_image_url = background_image_url.replace('http://backend:8000', 'http://localhost:8000')
design_data = {
'theme_color': design_settings.theme_color,
'background_image': background_image_url,
'dashboard_layout': design_settings.dashboard_layout,
'groups_default_expanded': design_settings.groups_default_expanded,
'show_group_icons': design_settings.show_group_icons,
'show_link_icons': design_settings.show_link_icons,
'dashboard_background_color': design_settings.dashboard_background_color,
'font_family': design_settings.font_family,
'header_text_color': getattr(design_settings, 'header_text_color', '#000000'),
'group_text_color': getattr(design_settings, 'group_text_color', '#333333'),
'link_text_color': getattr(design_settings, 'link_text_color', '#666666'),
'cover_overlay_enabled': getattr(design_settings, 'cover_overlay_enabled', False),
'cover_overlay_color': getattr(design_settings, 'cover_overlay_color', '#000000'),
'cover_overlay_opacity': getattr(design_settings, 'cover_overlay_opacity', 0.5),
}
except DesignSettings.DoesNotExist:
# Настройки по умолчанию
design_data = {
'theme_color': '#ffffff',
'background_image': None,
'dashboard_layout': 'list',
'groups_default_expanded': True,
'show_group_icons': True,
'show_link_icons': True,
'dashboard_background_color': '#f8f9fa',
'font_family': 'sans-serif',
}
# 3. Берём только публичные группы со ссылками
groups_qs = LinkGroup.objects.filter(
owner=user,
is_public=True # Показываем только публичные группы
).prefetch_related('links')
# Формируем URL аватара и обложки с заменой Docker URL
avatar_url = None
if user.avatar:
avatar_url = request.build_absolute_uri(user.avatar.url)
# Заменяем различные варианты внутренних Docker URL
avatar_url = avatar_url.replace('http://web:8000', 'http://localhost:8000')
avatar_url = avatar_url.replace('http://links-web-1:8000', 'http://localhost:8000')
avatar_url = avatar_url.replace('http://backend:8000', 'http://localhost:8000')
cover_url = None
if user.cover:
cover_url = request.build_absolute_uri(user.cover.url)
# Заменяем различные варианты внутренних Docker URL
cover_url = cover_url.replace('http://web:8000', 'http://localhost:8000')
cover_url = cover_url.replace('http://links-web-1:8000', 'http://localhost:8000')
cover_url = cover_url.replace('http://backend:8000', 'http://localhost:8000')
result = {
"username": user.username,
"full_name": user.full_name,
"bio": user.bio,
"avatar": avatar_url,
"cover": cover_url,
"design_settings": design_data,
"groups": []
}
for grp in groups_qs:
# icon у группы (абсолютный URL)
# icon у группы (абсолютный URL с заменой Docker URL)
grp_icon_url = None
if grp.icon:
grp_icon_url = request.build_absolute_uri(grp.icon.url)
# Заменяем различные варианты внутренних Docker URL
grp_icon_url = grp_icon_url.replace('http://web:8000', 'http://localhost:8000')
grp_icon_url = grp_icon_url.replace('http://links-web-1:8000', 'http://localhost:8000')
grp_icon_url = grp_icon_url.replace('http://backend:8000', 'http://localhost:8000')
# background_image у группы
grp_bg_url = None
if grp.background_image:
grp_bg_url = request.build_absolute_uri(grp.background_image.url)
# Заменяем различные варианты внутренних Docker URL
grp_bg_url = grp_bg_url.replace('http://web:8000', 'http://localhost:8000')
grp_bg_url = grp_bg_url.replace('http://links-web-1:8000', 'http://localhost:8000')
grp_bg_url = grp_bg_url.replace('http://backend:8000', 'http://localhost:8000')
grp_data = {
"id": grp.id,
"name": grp.name,
"icon": grp_icon_url,
"description": grp.description,
"icon_url": grp_icon_url, # Используем icon_url для консистентности с API
"background_image": grp_bg_url,
"header_color": grp.header_color,
"is_favorite": grp.is_favorite,
"links": [],
}
for ln in grp.links.all():
# icon у ссылки
# icon у ссылки с заменой Docker URL
ln_icon_url = None
if ln.icon:
ln_icon_url = request.build_absolute_uri(ln.icon.url)
# Заменяем различные варианты внутренних Docker URL
ln_icon_url = ln_icon_url.replace('http://web:8000', 'http://localhost:8000')
ln_icon_url = ln_icon_url.replace('http://links-web-1:8000', 'http://localhost:8000')
ln_icon_url = ln_icon_url.replace('http://backend:8000', 'http://localhost:8000')
grp_data["links"].append({
"id": ln.id,
"title": ln.title,
"url": ln.url,
"icon": ln_icon_url,
"icon_url": ln_icon_url, # Используем icon_url для консистентности
"description": ln.description,
})
result["groups"].append(grp_data)