init commit

This commit is contained in:
2025-05-06 20:44:33 +09:00
commit 91f0d54563
5567 changed files with 948185 additions and 0 deletions

0
backend/api/__init__.py Normal file
View File

16
backend/api/admin.py Normal file
View File

@@ -0,0 +1,16 @@
# api/admin.py
from django.contrib import admin
from .models import Link, LinkGroup
@admin.register(Link)
class LinkAdmin(admin.ModelAdmin):
list_display = ('title', 'url', 'owner', 'group', 'order')
list_filter = ('owner', 'group')
search_fields = ('title', 'url')
@admin.register(LinkGroup)
class LinkGroupAdmin(admin.ModelAdmin):
list_display = ('name', 'owner', 'order')
list_filter = ('owner',)
search_fields = ('name',)
ordering = ('owner', 'order')

6
backend/api/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.2 on 2025-05-06 04:12
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='LinkGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('order', models.PositiveIntegerField(default=0)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_link_groups', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Link',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('url', models.URLField()),
('icon', models.URLField(blank=True, null=True)),
('order', models.PositiveIntegerField(default=0)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to=settings.AUTH_USER_MODEL)),
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='links', to='api.linkgroup')),
],
),
]

View File

35
backend/api/models.py Normal file
View File

@@ -0,0 +1,35 @@
from django.db import models
from django.conf import settings
class LinkGroup(models.Model):
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='api_link_groups' # уникальное имя, чтобы не конфликтовать с links app
)
name = models.CharField(max_length=100)
order = models.PositiveIntegerField(default=0)
def __str__(self):
return f"{self.owner.username} - {self.name}"
class Link(models.Model):
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='links'
)
group = models.ForeignKey(
LinkGroup,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='links'
)
title = models.CharField(max_length=200)
url = models.URLField()
icon = models.URLField(blank=True, null=True)
order = models.PositiveIntegerField(default=0)
def __str__(self):
return self.title

View File

@@ -0,0 +1,55 @@
# api/serializers.py
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Link, LinkGroup
User = get_user_model()
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ('username', 'email', 'password')
def create(self, validated_data):
user = User(
username=validated_data['username'],
email=validated_data.get('email', '')
)
user.set_password(validated_data['password'])
user.save()
return user
# api/serializers.py
from rest_framework import serializers
from django.conf import settings
from .models import Link, LinkGroup
# сериализатор для ссылок
class LinkSerializer(serializers.ModelSerializer):
class Meta:
model = Link
fields = ['id', 'title', 'url', 'icon', 'order', 'group']
# сериализатор для групп со вложенными ссылками
class LinkGroupSerializer(serializers.ModelSerializer):
# related_name у вас в модели LinkGroup.owner = 'api_link_groups',
# а у модели Link.group = 'links', так что у группы obj.links — это QuerySet ссылок
links = LinkSerializer(many=True, read_only=True)
class Meta:
model = LinkGroup
fields = ['id', 'name', 'order', 'links']
from django.contrib.auth import get_user_model
from rest_framework import serializers
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
# поля, которые хотите отдавать на фронт:
fields = ['id', 'username', 'email', 'full_name', 'bio', 'avatar', 'last_login', 'date_joined']

3
backend/api/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

20
backend/api/urls.py Normal file
View File

@@ -0,0 +1,20 @@
from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from .views import (
RegisterView,
UserProfileView,
LinkViewSet,
LinkGroupViewSet
)
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('links', LinkViewSet, basename='link')
router.register('groups', LinkGroupViewSet, basename='group')
urlpatterns = [
path('auth/register/', RegisterView.as_view(), name='auth_register'),
path('auth/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('auth/user/', UserProfileView.as_view(), name='user-profile'), # ← новый
] + router.urls

59
backend/api/views.py Normal file
View File

@@ -0,0 +1,59 @@
# api/views.py
from rest_framework import generics, viewsets, permissions
from django.contrib.auth import get_user_model
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import RegisterSerializer, LinkSerializer, LinkGroupSerializer
from .models import Link, LinkGroup
User = get_user_model()
class RegisterView(generics.CreateAPIView):
queryset = User.objects.all()
permission_classes = (permissions.AllowAny,)
serializer_class = RegisterSerializer
class LoginView(TokenObtainPairView):
permission_classes = (permissions.AllowAny,)
class LinkGroupViewSet(viewsets.ModelViewSet):
queryset = LinkGroup.objects.all()
serializer_class = LinkGroupSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
# возвращаем только группы текущего пользователя
return self.queryset.filter(owner=self.request.user).order_by('order')
class LinkViewSet(viewsets.ModelViewSet):
serializer_class = LinkSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self):
return Link.objects.filter(owner=self.request.user).order_by('order')
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class UserLinksListView(generics.ListAPIView):
serializer_class = LinkSerializer
permission_classes = (permissions.AllowAny,)
def get_queryset(self):
username = self.kwargs['username']
return Link.objects.filter(owner__username=username).order_by('order')
from .serializers import UserSerializer # нужно завести сериализатор для пользователя
User = get_user_model()
class UserProfileView(generics.RetrieveAPIView):
"""
Возвращает данные авторизованного пользователя.
GET /api/auth/user/
"""
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
def get_object(self):
return self.request.user

View File

16
backend/backend/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for backend project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
application = get_asgi_application()

162
backend/backend/settings.py Normal file
View File

@@ -0,0 +1,162 @@
"""
Django settings for backend project.
Generated by 'django-admin startproject' using Django 5.2.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
from dotenv import load_dotenv
import os
# Load environment variables from .env file
load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '127.0.0.1').split(',')
CORS_ALLOWED_ORIGINS = [
"http://127.0.0.1:3001",
"http://localhost:3001",
]
# Application definition
INSTALLED_APPS = [
"corsheaders",
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'users',
'links',
'customization',
'api',
'rest_framework',
'rest_framework_simplejwt',
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'backend.wsgi.application'
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
),
}
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'AUTH_HEADER_TYPES': ('Bearer',),
}
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': os.getenv('DATABASE_ENGINE', 'django.db.backends.postgresql'),
'NAME': os.getenv('DATABASE_NAME'),
'USER': os.getenv('DATABASE_USER'),
'PASSWORD': os.getenv('DATABASE_PASSWORD'),
'HOST': os.getenv('DATABASE_HOST'),
'PORT': os.getenv('DATABASE_PORT'),
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_USER_MODEL = 'users.User'
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'ru-ru'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
MEDIA_URL = '/storage/'
MEDIA_ROOT = BASE_DIR / 'storage'

59
backend/backend/urls.py Normal file
View File

@@ -0,0 +1,59 @@
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')), # API endpoints
path('users/', include('users.urls')), # User management app
path('links/', include('links.urls')), # Link management app
path('customization/', include('customization.urls')), # Design customization app
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Summary of API Endpoints:
# POST /api/auth/register/ - Register new user
# POST /api/auth/login/ - Obtain JWT tokens (access & refresh)
# GET /api/users/<username>/links/ - Public list of user's links
# GET /api/links/ - List authenticated user's links
# POST /api/links/ - Create a new link
# GET /api/links/{id}/ - Retrieve a specific link
# PUT /api/links/{id}/ - Update a specific link
# PATCH /api/links/{id}/ - Partially update a link
# DELETE /api/links/{id}/ - Delete a specific link
# GET /api/groups/ - List authenticated user's link groups
# POST /api/groups/ - Create a new link group
# GET /api/groups/{id}/ - Retrieve a specific link group
# PUT /api/groups/{id}/ - Update a link group
# PATCH /api/groups/{id}/ - Partially update a link group
# DELETE /api/groups/{id}/ - Delete a specific link group
# To avoid URL configuration errors, ensure the following placeholder URLConfs exist:
# users/urls.py
# ----------------
# from django.urls import path, include
#
# urlpatterns = [
# # Define user-management endpoints here (e.g., profile, settings)
# ]
# links/urls.py
# ----------------
# from django.urls import path, include
#
# urlpatterns = [
# # Define additional link-management endpoints here if needed
# ]
# customization/urls.py
# ----------------
# from django.urls import path, include
#
# urlpatterns = [
# # Define design customization endpoints here
# ]

16
backend/backend/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for backend project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
application = get_wsgi_application()

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

View File

@@ -0,0 +1,11 @@
# backend/apps/customization/admin.py
from django.contrib import admin
from .models import DesignSettings
@admin.register(DesignSettings)
class DesignSettingsAdmin(admin.ModelAdmin):
list_display = ('user', 'theme_color', 'font_family', 'updated_at')
list_filter = ('theme_color',)
search_fields = ('user__username',)
readonly_fields = ('updated_at',)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CustomizationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'customization'

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.2 on 2025-05-06 01:51
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='DesignSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('theme_color', models.CharField(default='#ffffff', help_text='Основной цвет темы (hex)', max_length=7)),
('background_image', models.ImageField(blank=True, help_text='Фоновое изображение', null=True, upload_to='backgrounds/')),
('font_family', models.CharField(default='sans-serif', help_text='Название шрифта', max_length=100)),
('custom_css', models.TextField(blank=True, help_text='Дополнительный CSS')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения')),
('user', models.OneToOneField(help_text='Пользователь, которому принадлежат настройки', on_delete=django.db.models.deletion.CASCADE, related_name='design', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Настройки дизайна',
'verbose_name_plural': 'Настройки дизайна',
'ordering': ['user'],
'unique_together': {('user',)},
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2 on 2025-05-06 09:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customization', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='designsettings',
name='background_image',
field=models.ImageField(blank=True, help_text='Фоновое изображение', null=True, upload_to='customization/'),
),
]

View File

@@ -0,0 +1,110 @@
# customization/models.py
from django.db import models
from django.conf import settings
from django.utils import timezone
class DesignSettings(models.Model):
"""
Настройки дизайна для публичной страницы пользователя.
"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='design',
help_text='Пользователь, которому принадлежат настройки'
)
theme_color = models.CharField(
max_length=7,
default='#ffffff',
help_text='Основной цвет темы (hex)'
)
background_image = models.ImageField(
upload_to='customization/',
null=True,
blank=True,
help_text='Фоновое изображение'
)
font_family = models.CharField(
max_length=100,
default='sans-serif',
help_text='Название шрифта'
)
custom_css = models.TextField(
blank=True,
help_text='Дополнительный CSS'
)
updated_at = models.DateTimeField(
auto_now=True,
help_text='Дата и время последнего изменения'
)
class Meta:
verbose_name = 'Настройки дизайна'
verbose_name_plural = 'Настройки дизайна'
ordering = ['user']
unique_together = ('user',)
def __str__(self):
return f"Design for {self.user.username}"
def get_background_image_url(self):
"""
Возвращает URL фонового изображения.
"""
return self.background_image.url if self.background_image else None
def get_custom_css(self):
"""
Возвращает пользовательский CSS.
"""
return self.custom_css or ""
def get_design_settings(self):
"""
Возвращает словарь с настройками дизайна.
"""
return {
'theme_color': self.theme_color,
'background_image': self.get_background_image_url(),
'font_family': self.font_family,
'custom_css': self.get_custom_css()
}
def save(self, *args, **kwargs):
"""
Переопределяем метод save, чтобы автоматически обновлять дату изменения.
"""
self.updated_at = timezone.now()
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
"""
Переопределяем метод delete, чтобы удалять связанные изображения.
"""
if self.background_image:
self.background_image.delete(save=False)
super().delete(*args, **kwargs)
# Вспомогательные методы для доступа к данным пользователя
def get_user(self):
return self.user
def get_user_id(self):
return self.user.id
def get_user_username(self):
return self.user.username
def get_user_email(self):
return self.user.email or "Нет email"
def get_user_full_name(self):
return self.user.get_full_name() or "Нет полного имени"
def get_user_bio(self):
return self.user.bio or "Нет биографии"
def get_user_avatar(self):
return self.user.avatar.url if self.user.avatar else None

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,5 @@
from django.urls import path
urlpatterns = [
# дополнительные эндпоинты по ссылкам
]

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

BIN
backend/db.sqlite3 Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

18
backend/links/admin.py Normal file
View File

@@ -0,0 +1,18 @@
# backend/apps/links/admin.py
from django.contrib import admin
from .models import LinkGroup, Link
@admin.register(LinkGroup)
class LinkGroupAdmin(admin.ModelAdmin):
list_display = ('title', 'owner', 'order')
list_filter = ('owner',)
search_fields = ('title', 'owner__username')
ordering = ('owner', 'order')
@admin.register(Link)
class LinkAdmin(admin.ModelAdmin):
list_display = ('title', 'group', 'is_active', 'order', 'created_at')
list_filter = ('group', 'is_active')
search_fields = ('title', 'url', 'group__title', 'group__owner__username')
ordering = ('group', 'order')

6
backend/links/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class LinksConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'links'

View File

@@ -0,0 +1,126 @@
# Generated by Django 5.2 on 2025-05-06 01:44
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='LinkGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(help_text='Название группы', max_length=100)),
('order', models.PositiveIntegerField(default=0, help_text='Порядок сортировки')),
('image', models.ImageField(blank=True, help_text='Изображение группы ссылок', null=True, upload_to='link_groups/')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего обновления')),
('is_active', models.BooleanField(default=True, help_text='Активность группы ссылок')),
('description', models.TextField(blank=True, help_text='Описание группы ссылок')),
('is_public', models.BooleanField(default=False, help_text='Публичная группа ссылок')),
('is_featured', models.BooleanField(default=False, help_text='Выделенная группа ссылок')),
('is_private', models.BooleanField(default=False, help_text='Закрытая группа ссылок')),
('is_password_protected', models.BooleanField(default=False, help_text='Группа ссылок защищена паролем')),
('password', models.CharField(blank=True, help_text='Пароль для доступа к группе ссылок', max_length=128)),
('is_approved', models.BooleanField(default=False, help_text='Группа ссылок одобрена администратором')),
('is_rejected', models.BooleanField(default=False, help_text='Группа ссылок отклонена администратором')),
('is_deleted', models.BooleanField(default=False, help_text='Группа ссылок удалена')),
('is_archived', models.BooleanField(default=False, help_text='Группа ссылок архивирована')),
('is_scheduled', models.BooleanField(default=False, help_text='Группа ссылок запланирована на публикацию')),
('scheduled_at', models.DateTimeField(blank=True, help_text='Дата и время запланированной публикации', null=True)),
('is_expired', models.BooleanField(default=False, help_text='Группа ссылок истекла')),
('expiration_date', models.DateTimeField(blank=True, help_text='Дата и время истечения срока действия группы ссылок', null=True)),
('is_featured_on_homepage', models.BooleanField(default=False, help_text='Группа ссылок выделена на главной странице')),
('is_featured_on_profile', models.BooleanField(default=False, help_text='Группа ссылок выделена на профиле пользователя')),
('is_featured_on_category', models.BooleanField(default=False, help_text='Группа ссылок выделена в категории')),
('is_featured_on_tag', models.BooleanField(default=False, help_text='Группа ссылок выделена по тегу')),
('is_featured_on_search', models.BooleanField(default=False, help_text='Группа ссылок выделена в результатах поиска')),
('is_featured_on_related', models.BooleanField(default=False, help_text='Группа ссылок выделена в связанных ссылках')),
('is_featured_on_recommended', models.BooleanField(default=False, help_text='Группа ссылок выделена в рекомендуемых ссылках')),
('is_featured_on_popular', models.BooleanField(default=False, help_text='Группа ссылок выделена в популярных ссылках')),
('is_featured_on_trending', models.BooleanField(default=False, help_text='Группа ссылок выделена в трендовых ссылках')),
('is_featured_on_new', models.BooleanField(default=False, help_text='Группа ссылок выделена в новых ссылках')),
('is_featured_on_top', models.BooleanField(default=False, help_text='Группа ссылок выделена в верхних ссылках')),
('is_featured_on_bottom', models.BooleanField(default=False, help_text='Группа ссылок выделена в нижних ссылках')),
('is_featured_on_sidebar', models.BooleanField(default=False, help_text='Группа ссылок выделена в боковой панели')),
('is_featured_on_footer', models.BooleanField(default=False, help_text='Группа ссылок выделена в нижнем колонтитуле')),
('is_featured_on_header', models.BooleanField(default=False, help_text='Группа ссылок выделена в верхнем колонтитуле')),
('is_featured_on_banner', models.BooleanField(default=False, help_text='Группа ссылок выделена в баннере')),
('is_featured_on_popup', models.BooleanField(default=False, help_text='Группа ссылок выделена в всплывающем окне')),
('is_featured_on_modal', models.BooleanField(default=False, help_text='Группа ссылок выделена в модальном окне')),
('is_featured_on_notification', models.BooleanField(default=False, help_text='Группа ссылок выделена в уведомлении')),
('is_featured_on_alert', models.BooleanField(default=False, help_text='Группа ссылок выделена в алерте')),
('is_featured_on_toast', models.BooleanField(default=False, help_text='Группа ссылок выделена в тосте')),
('is_featured_on_snackbar', models.BooleanField(default=False, help_text='Группа ссылок выделена в снэкбаре')),
('is_featured_on_tooltip', models.BooleanField(default=False, help_text='Группа ссылок выделена в тултипе')),
('owner', models.ForeignKey(help_text='Владелец группы ссылок', on_delete=django.db.models.deletion.CASCADE, related_name='link_groups', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['order'],
'unique_together': {('owner', 'title')},
},
),
migrations.CreateModel(
name='Link',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(help_text='Текст ссылки', max_length=200)),
('url', models.URLField(help_text='URL-адрес')),
('order', models.PositiveIntegerField(default=0, help_text='Порядок сортировки внутри группы')),
('is_active', models.BooleanField(default=True, help_text='Активность ссылки')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего обновления')),
('image', models.ImageField(blank=True, help_text='Изображение ссылки', null=True, upload_to='links/')),
('description', models.TextField(blank=True, help_text='Описание ссылки')),
('is_public', models.BooleanField(default=False, help_text='Публичная ссылка')),
('is_featured', models.BooleanField(default=False, help_text='Выделенная ссылка')),
('is_private', models.BooleanField(default=False, help_text='Закрытая ссылка')),
('is_password_protected', models.BooleanField(default=False, help_text='Ссылка защищена паролем')),
('password', models.CharField(blank=True, help_text='Пароль для доступа к ссылке', max_length=128)),
('is_approved', models.BooleanField(default=False, help_text='Ссылка одобрена администратором')),
('is_rejected', models.BooleanField(default=False, help_text='Ссылка отклонена администратором')),
('is_deleted', models.BooleanField(default=False, help_text='Ссылка удалена')),
('is_archived', models.BooleanField(default=False, help_text='Ссылка архивирована')),
('is_scheduled', models.BooleanField(default=False, help_text='Ссылка запланирована на публикацию')),
('scheduled_at', models.DateTimeField(blank=True, help_text='Дата и время запланированной публикации', null=True)),
('is_expired', models.BooleanField(default=False, help_text='Ссылка истекла')),
('expiration_date', models.DateTimeField(blank=True, help_text='Дата и время истечения срока действия ссылки', null=True)),
('is_featured_on_homepage', models.BooleanField(default=False, help_text='Ссылка выделена на главной странице')),
('is_featured_on_profile', models.BooleanField(default=False, help_text='Ссылка выделена на профиле пользователя')),
('is_featured_on_category', models.BooleanField(default=False, help_text='Ссылка выделена в категории')),
('is_featured_on_tag', models.BooleanField(default=False, help_text='Ссылка выделена по тегу')),
('is_featured_on_search', models.BooleanField(default=False, help_text='Ссылка выделена в результатах поиска')),
('is_featured_on_related', models.BooleanField(default=False, help_text='Ссылка выделена в связанных ссылках')),
('is_featured_on_recommended', models.BooleanField(default=False, help_text='Ссылка выделена в рекомендуемых ссылках')),
('is_featured_on_popular', models.BooleanField(default=False, help_text='Ссылка выделена в популярных ссылках')),
('is_featured_on_trending', models.BooleanField(default=False, help_text='Ссылка выделена в трендовых ссылках')),
('is_featured_on_new', models.BooleanField(default=False, help_text='Ссылка выделена в новых ссылках')),
('is_featured_on_top', models.BooleanField(default=False, help_text='Ссылка выделена в верхних ссылках')),
('is_featured_on_bottom', models.BooleanField(default=False, help_text='Ссылка выделена в нижних ссылках')),
('is_featured_on_sidebar', models.BooleanField(default=False, help_text='Ссылка выделена в боковой панели')),
('is_featured_on_footer', models.BooleanField(default=False, help_text='Ссылка выделена в нижнем колонтитуле')),
('is_featured_on_header', models.BooleanField(default=False, help_text='Ссылка выделена в верхнем колонтитуле')),
('is_featured_on_banner', models.BooleanField(default=False, help_text='Ссылка выделена в баннере')),
('is_featured_on_popup', models.BooleanField(default=False, help_text='Ссылка выделена в всплывающем окне')),
('is_featured_on_modal', models.BooleanField(default=False, help_text='Ссылка выделена в модальном окне')),
('is_featured_on_notification', models.BooleanField(default=False, help_text='Ссылка выделена в уведомлении')),
('is_featured_on_alert', models.BooleanField(default=False, help_text='Ссылка выделена в алерте')),
('is_featured_on_toast', models.BooleanField(default=False, help_text='Ссылка выделена в тосте')),
('is_featured_on_snackbar', models.BooleanField(default=False, help_text='Ссылка выделена в снэкбаре')),
('is_featured_on_tooltip', models.BooleanField(default=False, help_text='Ссылка выделена в тултипе')),
('is_featured_on_qr_code', models.BooleanField(default=False, help_text='Ссылка выделена в QR-коде')),
('group', models.ForeignKey(help_text='Группа, к которой относится ссылка', on_delete=django.db.models.deletion.CASCADE, related_name='links', to='links.linkgroup')),
],
options={
'ordering': ['order'],
},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2 on 2025-05-06 04:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('links', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='link',
name='image',
field=models.ImageField(blank=True, help_text='Изображение ссылки', null=True, upload_to='storage/images/links/'),
),
migrations.AlterField(
model_name='linkgroup',
name='image',
field=models.ImageField(blank=True, help_text='Изображение группы ссылок', null=True, upload_to='storage/images/link_groups/'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2 on 2025-05-06 09:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('links', '0002_alter_link_image_alter_linkgroup_image'),
]
operations = [
migrations.AlterField(
model_name='link',
name='image',
field=models.ImageField(blank=True, help_text='Изображение ссылки', null=True, upload_to='links/'),
),
migrations.AlterField(
model_name='linkgroup',
name='image',
field=models.ImageField(blank=True, help_text='Изображение группы ссылок', null=True, upload_to='link_groups/'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2 on 2025-05-06 10:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('links', '0003_alter_link_image_alter_linkgroup_image'),
]
operations = [
migrations.AlterField(
model_name='linkgroup',
name='image',
field=models.ImageField(blank=True, help_text='Изображение группы ссылок', null=True, upload_to='images/link_groups/'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2 on 2025-05-06 10:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('links', '0004_alter_linkgroup_image'),
]
operations = [
migrations.AlterField(
model_name='link',
name='image',
field=models.ImageField(blank=True, help_text='Изображение ссылки', null=True, upload_to='images/links/'),
),
]

View File

428
backend/links/models.py Normal file
View File

@@ -0,0 +1,428 @@
# links/models.py
from django.db import models
from django.conf import settings
class LinkGroup(models.Model):
"""
Группа ссылок, принадлежащая пользователю.
"""
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='link_groups', # чтобы не конфликтовать с auth.User.groups
help_text='Владелец группы ссылок'
)
title = models.CharField(
max_length=100,
help_text='Название группы'
)
order = models.PositiveIntegerField(
default=0,
help_text='Порядок сортировки'
)
image = models.ImageField(
upload_to='images/link_groups/',
null=True,
blank=True,
help_text='Изображение группы ссылок'
)
created_at = models.DateTimeField(
auto_now_add=True,
help_text='Дата и время создания'
)
updated_at = models.DateTimeField(
auto_now=True,
help_text='Дата и время последнего обновления'
)
is_active = models.BooleanField(
default=True,
help_text='Активность группы ссылок'
)
description = models.TextField(
blank=True,
help_text='Описание группы ссылок'
)
is_public = models.BooleanField(
default=False,
help_text='Публичная группа ссылок'
)
is_featured = models.BooleanField(
default=False,
help_text='Выделенная группа ссылок'
)
is_private = models.BooleanField(
default=False,
help_text='Закрытая группа ссылок'
)
is_password_protected = models.BooleanField(
default=False,
help_text='Группа ссылок защищена паролем'
)
password = models.CharField(
max_length=128,
blank=True,
help_text='Пароль для доступа к группе ссылок'
)
is_approved = models.BooleanField(
default=False,
help_text='Группа ссылок одобрена администратором'
)
is_rejected = models.BooleanField(
default=False,
help_text='Группа ссылок отклонена администратором'
)
is_deleted = models.BooleanField(
default=False,
help_text='Группа ссылок удалена'
)
is_archived = models.BooleanField(
default=False,
help_text='Группа ссылок архивирована'
)
is_scheduled = models.BooleanField(
default=False,
help_text='Группа ссылок запланирована на публикацию'
)
scheduled_at = models.DateTimeField(
null=True,
blank=True,
help_text='Дата и время запланированной публикации'
)
is_expired = models.BooleanField(
default=False,
help_text='Группа ссылок истекла'
)
expiration_date = models.DateTimeField(
null=True,
blank=True,
help_text='Дата и время истечения срока действия группы ссылок'
)
is_featured_on_homepage = models.BooleanField(
default=False,
help_text='Группа ссылок выделена на главной странице'
)
is_featured_on_profile = models.BooleanField(
default=False,
help_text='Группа ссылок выделена на профиле пользователя'
)
is_featured_on_category = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в категории'
)
is_featured_on_tag = models.BooleanField(
default=False,
help_text='Группа ссылок выделена по тегу'
)
is_featured_on_search = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в результатах поиска'
)
is_featured_on_related = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в связанных ссылках'
)
is_featured_on_recommended = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в рекомендуемых ссылках'
)
is_featured_on_popular = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в популярных ссылках'
)
is_featured_on_trending = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в трендовых ссылках'
)
is_featured_on_new = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в новых ссылках'
)
is_featured_on_top = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в верхних ссылках'
)
is_featured_on_bottom = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в нижних ссылках'
)
is_featured_on_sidebar = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в боковой панели'
)
is_featured_on_footer = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в нижнем колонтитуле'
)
is_featured_on_header = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в верхнем колонтитуле'
)
is_featured_on_banner = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в баннере'
)
is_featured_on_popup = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в всплывающем окне'
)
is_featured_on_modal = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в модальном окне'
)
is_featured_on_notification = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в уведомлении'
)
is_featured_on_alert = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в алерте'
)
is_featured_on_toast = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в тосте'
)
is_featured_on_snackbar = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в снэкбаре'
)
is_featured_on_tooltip = models.BooleanField(
default=False,
help_text='Группа ссылок выделена в тултипе'
)
class Meta:
ordering = ['order']
unique_together = ('owner', 'title')
def __str__(self):
return f"{self.owner.username} {self.title}"
class Link(models.Model):
"""
Отдельная ссылка внутри группы.
"""
group = models.ForeignKey(
LinkGroup,
on_delete=models.CASCADE,
related_name='links',
help_text='Группа, к которой относится ссылка'
)
title = models.CharField(
max_length=200,
help_text='Текст ссылки'
)
url = models.URLField(
help_text='URL-адрес'
)
order = models.PositiveIntegerField(
default=0,
help_text='Порядок сортировки внутри группы'
)
is_active = models.BooleanField(
default=True,
help_text='Активность ссылки'
)
created_at = models.DateTimeField(
auto_now_add=True,
help_text='Дата и время создания'
)
updated_at = models.DateTimeField(
auto_now=True,
help_text='Дата и время последнего обновления'
)
image = models.ImageField(
upload_to='images/links/',
null=True,
blank=True,
help_text='Изображение ссылки'
)
description = models.TextField(
blank=True,
help_text='Описание ссылки'
)
is_public = models.BooleanField(
default=False,
help_text='Публичная ссылка'
)
is_featured = models.BooleanField(
default=False,
help_text='Выделенная ссылка'
)
is_private = models.BooleanField(
default=False,
help_text='Закрытая ссылка'
)
is_password_protected = models.BooleanField(
default=False,
help_text='Ссылка защищена паролем'
)
password = models.CharField(
max_length=128,
blank=True,
help_text='Пароль для доступа к ссылке'
)
is_approved = models.BooleanField(
default=False,
help_text='Ссылка одобрена администратором'
)
is_rejected = models.BooleanField(
default=False,
help_text='Ссылка отклонена администратором'
)
is_deleted = models.BooleanField(
default=False,
help_text='Ссылка удалена'
)
is_archived = models.BooleanField(
default=False,
help_text='Ссылка архивирована'
)
is_scheduled = models.BooleanField(
default=False,
help_text='Ссылка запланирована на публикацию'
)
scheduled_at = models.DateTimeField(
null=True,
blank=True,
help_text='Дата и время запланированной публикации'
)
is_expired = models.BooleanField(
default=False,
help_text='Ссылка истекла'
)
expiration_date = models.DateTimeField(
null=True,
blank=True,
help_text='Дата и время истечения срока действия ссылки'
)
is_featured_on_homepage = models.BooleanField(
default=False,
help_text='Ссылка выделена на главной странице'
)
is_featured_on_profile = models.BooleanField(
default=False,
help_text='Ссылка выделена на профиле пользователя'
)
is_featured_on_category = models.BooleanField(
default=False,
help_text='Ссылка выделена в категории'
)
is_featured_on_tag = models.BooleanField(
default=False,
help_text='Ссылка выделена по тегу'
)
is_featured_on_search = models.BooleanField(
default=False,
help_text='Ссылка выделена в результатах поиска'
)
is_featured_on_related = models.BooleanField(
default=False,
help_text='Ссылка выделена в связанных ссылках'
)
is_featured_on_recommended = models.BooleanField(
default=False,
help_text='Ссылка выделена в рекомендуемых ссылках'
)
is_featured_on_popular = models.BooleanField(
default=False,
help_text='Ссылка выделена в популярных ссылках'
)
is_featured_on_trending = models.BooleanField(
default=False,
help_text='Ссылка выделена в трендовых ссылках'
)
is_featured_on_new = models.BooleanField(
default=False,
help_text='Ссылка выделена в новых ссылках'
)
is_featured_on_top = models.BooleanField(
default=False,
help_text='Ссылка выделена в верхних ссылках'
)
is_featured_on_bottom = models.BooleanField(
default=False,
help_text='Ссылка выделена в нижних ссылках'
)
is_featured_on_sidebar = models.BooleanField(
default=False,
help_text='Ссылка выделена в боковой панели'
)
is_featured_on_footer = models.BooleanField(
default=False,
help_text='Ссылка выделена в нижнем колонтитуле'
)
is_featured_on_header = models.BooleanField(
default=False,
help_text='Ссылка выделена в верхнем колонтитуле'
)
is_featured_on_banner = models.BooleanField(
default=False,
help_text='Ссылка выделена в баннере'
)
is_featured_on_popup = models.BooleanField(
default=False,
help_text='Ссылка выделена в всплывающем окне'
)
is_featured_on_modal = models.BooleanField(
default=False,
help_text='Ссылка выделена в модальном окне'
)
is_featured_on_notification = models.BooleanField(
default=False,
help_text='Ссылка выделена в уведомлении'
)
is_featured_on_alert = models.BooleanField(
default=False,
help_text='Ссылка выделена в алерте'
)
is_featured_on_toast = models.BooleanField(
default=False,
help_text='Ссылка выделена в тосте'
)
is_featured_on_snackbar = models.BooleanField(
default=False,
help_text='Ссылка выделена в снэкбаре'
)
is_featured_on_tooltip = models.BooleanField(
default=False,
help_text='Ссылка выделена в тултипе'
)
is_featured_on_qr_code = models.BooleanField(
default=False,
help_text='Ссылка выделена в QR-коде'
)
class Meta:
ordering = ['order']
def __str__(self):
return self.title
def get_absolute_url(self):
"""
Возвращает абсолютный URL для доступа к ссылке.
"""
return self.url
def get_group(self):
"""
Возвращает группу, к которой принадлежит ссылка.
"""
return self.group
def get_owner(self):
"""
Возвращает владельца ссылки.
"""
return self.group.owner
def get_link_info(self):
"""
Возвращает словарь с информацией о ссылке.
"""
return {
'title': self.title,
'url': self.url,
'is_active': self.is_active,
'created_at': self.created_at,
'updated_at': self.updated_at
}

3
backend/links/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

5
backend/links/urls.py Normal file
View File

@@ -0,0 +1,5 @@
from django.urls import path
urlpatterns = [
# дополнительные эндпоинты по ссылкам
]

3
backend/links/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

22
backend/manage.py Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

0
backend/storage/.gitignore vendored Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

0
backend/storage/images/.gitignore vendored Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

19
backend/users/admin.py Normal file
View File

@@ -0,0 +1,19 @@
# backend/apps/users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User
@admin.register(User)
class UserAdmin(BaseUserAdmin):
list_display = ('username', 'email', 'full_name', 'is_staff', 'is_active')
list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups')
search_fields = ('username', 'email', 'full_name')
fieldsets = (
(None, {'fields': ('username', 'password')}),
('Персональное', {'fields': ('full_name', 'bio', 'avatar')}),
('Контакты', {'fields': ('email',)}),
('Права доступа', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
('Важные даты', {'fields': ('last_login', 'date_joined')}),
)

6
backend/users/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'users'

View File

@@ -0,0 +1,47 @@
# Generated by Django 5.2 on 2025-05-06 01:24
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('full_name', models.CharField(blank=True, help_text='Полное имя пользователя', max_length=150)),
('bio', models.TextField(blank=True, help_text='Краткая биография пользователя')),
('avatar', models.ImageField(blank=True, help_text='Аватар пользователя', null=True, upload_to='avatars/')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2 on 2025-05-06 09:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='user',
name='avatar',
field=models.ImageField(blank=True, help_text='Аватар пользователя', null=True, upload_to='frontend/assets/img/avatars/'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2 on 2025-05-06 10:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_alter_user_avatar'),
]
operations = [
migrations.AddField(
model_name='user',
name='cover',
field=models.ImageField(blank=True, null=True, upload_to='avatars/covers/'),
),
migrations.AlterField(
model_name='user',
name='avatar',
field=models.ImageField(blank=True, help_text='Аватар пользователя', null=True, upload_to='avatars/'),
),
]

View File

101
backend/users/models.py Normal file
View File

@@ -0,0 +1,101 @@
# users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
"""
Пользовательская модель, расширяющая стандартную AbstractUser.
"""
full_name = models.CharField(
max_length=150,
blank=True,
help_text='Полное имя пользователя'
)
bio = models.TextField(
blank=True,
help_text='Краткая биография пользователя'
)
avatar = models.ImageField(
upload_to='avatars/',
null=True,
blank=True,
help_text='Аватар пользователя'
)
cover = models.ImageField(
upload_to='avatars/covers/',
null=True,
blank=True,
)
def __str__(self):
return self.username
def get_full_name(self):
"""
Возвращает полное имя пользователя.
"""
return self.full_name if self.full_name else self.username
def get_short_name(self):
"""
Возвращает короткое имя пользователя.
"""
return self.username
def get_bio(self):
"""
Возвращает биографию пользователя.
"""
return self.bio if self.bio else "Нет информации о пользователе"
def get_avatar(self):
"""
Возвращает URL аватара пользователя.
"""
return self.avatar.url if self.avatar else None
def get_user_info(self):
"""
Возвращает словарь с информацией о пользователе.
"""
return {
'username': self.username,
'full_name': self.get_full_name(),
'bio': self.get_bio(),
'avatar': self.get_avatar()
}
def get_user_links(self):
"""
Возвращает список ссылок пользователя.
"""
from links.models import Link
return Link.objects.filter(group__owner=self).values('title', 'url')
def get_user_design(self):
"""
Возвращает настройки дизайна пользователя.
"""
from customization.models import DesignSettings
try:
design = DesignSettings.objects.get(user=self)
return {
'theme_color': design.theme_color,
'background_image': design.background_image.url if design.background_image else None,
'font_family': design.font_family,
'custom_css': design.custom_css
}
except DesignSettings.DoesNotExist:
return None
def get_user_groups(self):
"""
Возвращает группы пользователя.
"""
return list(self.link_groups.values('title', 'order'))
def get_user_permissions(self):
"""
Возвращает разрешения пользователя.
"""
return list(self.user_permissions.values_list('codename', flat=True))

3
backend/users/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

5
backend/users/urls.py Normal file
View File

@@ -0,0 +1,5 @@
from django.urls import path
urlpatterns = [
# дополнительные эндпоинты по ссылкам
]

3
backend/users/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.