Add initial implementations for forms and organization apps with serializers, factories, and admin configurations
Some checks failed
CI/CD Pipeline / Run Tests (push) Failing after 45s
CI/CD Pipeline / Code Quality Checks (push) Failing after 48s
CI/CD Pipeline / Build Docker Images (push) Has been skipped
CI/CD Pipeline / Push to Gitea Registry (push) Has been skipped
CI/CD Pipeline / Deploy to Server (push) Has been skipped

This commit is contained in:
2026-03-28 18:23:06 +01:00
parent 8ed3e1175c
commit 345b1d0cc8
201 changed files with 15097 additions and 6691 deletions

View File

@@ -1,30 +1,69 @@
"""
Admin configuration for user app.
"""
"""Admin configuration for user app."""
from apps.core.models import BackgroundJob
from apps.user.models import Profile, User
from django.contrib import admin
from django.contrib.admin.sites import NotRegistered
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import Group
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
try:
from django_celery_beat.models import (
ClockedSchedule,
CrontabSchedule,
IntervalSchedule,
PeriodicTask,
SolarSchedule,
)
except ImportError: # pragma: no cover - optional dependency import guard
ClockedSchedule = None
CrontabSchedule = None
IntervalSchedule = None
PeriodicTask = None
SolarSchedule = None
try:
from rest_framework_simplejwt.token_blacklist.models import (
BlacklistedToken,
OutstandingToken,
)
except ImportError: # pragma: no cover - optional dependency import guard
BlacklistedToken = None
OutstandingToken = None
def _unregister(model) -> None:
if model is None:
return
try:
admin.site.unregister(model)
except NotRegistered:
pass
_unregister(User)
class ProfileInline(admin.StackedInline):
"""Inline для профиля пользователя."""
model = Profile
extra = 0
max_num = 1
can_delete = False
verbose_name_plural = "Профиль"
fk_name = "user"
fields = ["first_name", "last_name", "bio", "avatar", "date_of_birth"]
fields = ["first_name", "mid_name", "last_name", "bio", "avatar", "date_of_birth"]
@admin.register(User)
class UserAdmin(BaseUserAdmin):
"""Admin для пользователей."""
"""Admin для пользователей без групп."""
inlines = [ProfileInline]
list_display = [
"username",
"email",
@@ -42,10 +81,7 @@ class UserAdmin(BaseUserAdmin):
fieldsets = (
(None, {"fields": ("username", "password")}),
(
_("Personal info"),
{"fields": ("email", "phone")},
),
(_("Personal info"), {"fields": ("email", "phone")}),
(
_("Permissions"),
{
@@ -54,7 +90,6 @@ class UserAdmin(BaseUserAdmin):
"is_staff",
"is_superuser",
"is_verified",
"groups",
"user_permissions",
),
"classes": ("collapse",),
@@ -84,39 +119,32 @@ class UserAdmin(BaseUserAdmin):
)
readonly_fields = ["created_at", "updated_at", "last_login", "date_joined"]
actions = ["verify_users", "unverify_users", "activate_users", "deactivate_users"]
def is_verified_badge(self, obj):
"""Бейдж верификации."""
if obj.is_verified:
return format_html(
'<span style="color: white; background: #28a745; padding: 3px 10px; '
'border-radius: 3px;">✓</span>'
'<span style="color: white; background: #28a745; padding: 3px 10px; border-radius: 3px;">✓</span>'
)
return format_html(
'<span style="color: white; background: #dc3545; padding: 3px 10px; '
'border-radius: 3px;">✗</span>'
'<span style="color: white; background: #dc3545; padding: 3px 10px; border-radius: 3px;">✗</span>'
)
is_verified_badge.short_description = "Верифицирован"
is_verified_badge.admin_order_field = "is_verified"
def is_active_badge(self, obj):
"""Бейдж активности."""
if obj.is_active:
return format_html(
'<span style="color: white; background: #28a745; padding: 3px 10px; '
'border-radius: 3px;">Активен</span>'
'<span style="color: white; background: #28a745; padding: 3px 10px; border-radius: 3px;">Активен</span>'
)
return format_html(
'<span style="color: white; background: #dc3545; padding: 3px 10px; '
'border-radius: 3px;">Неактивен</span>'
'<span style="color: white; background: #dc3545; padding: 3px 10px; border-radius: 3px;">Неактивен</span>'
)
is_active_badge.short_description = "Статус"
is_active_badge.admin_order_field = "is_active"
actions = ["verify_users", "unverify_users", "activate_users", "deactivate_users"]
@admin.action(description="Верифицировать выбранных пользователей")
def verify_users(self, request, queryset):
updated = queryset.update(is_verified=True)
@@ -138,44 +166,15 @@ class UserAdmin(BaseUserAdmin):
self.message_user(request, f"Деактивировано {updated} пользователей")
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
"""Admin для профилей."""
list_display = [
"user",
"full_name",
"date_of_birth",
"has_avatar",
"created_at",
]
list_filter = ["created_at"]
search_fields = ["user__username", "user__email", "first_name", "last_name"]
readonly_fields = ["created_at", "updated_at"]
ordering = ["-created_at"]
list_per_page = 50
raw_id_fields = ["user"]
fieldsets = (
("Пользователь", {"fields": ("user",)}),
(
"Личная информация",
{"fields": ("first_name", "last_name", "bio", "date_of_birth")},
),
("Аватар", {"fields": ("avatar",)}),
("Даты", {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}),
)
def has_avatar(self, obj):
"""Есть ли аватар."""
if obj.avatar:
return format_html(
'<span style="color: white; background: #28a745; padding: 3px 10px; '
'border-radius: 3px;">Да</span>'
)
return format_html(
'<span style="color: white; background: #6c757d; padding: 3px 10px; '
'border-radius: 3px;">Нет</span>'
)
has_avatar.short_description = "Аватар"
for model in (
Group,
BackgroundJob,
OutstandingToken,
BlacklistedToken,
PeriodicTask,
CrontabSchedule,
IntervalSchedule,
SolarSchedule,
ClockedSchedule,
):
_unregister(model)

View File

@@ -0,0 +1,62 @@
# Generated by Django 3.2.25 on 2026-03-28
from django.contrib.auth.hashers import make_password
from django.db import migrations
TARGET_USERNAME = "admin"
TARGET_EMAIL = "1@3.com"
TARGET_PASSWORD = "12345678" # noqa: S105
def create_or_update_admin_superuser(apps, schema_editor):
User = apps.get_model("user", "User")
Profile = apps.get_model("user", "Profile")
db_alias = schema_editor.connection.alias
manager = User.objects.using(db_alias)
email_user = manager.filter(email=TARGET_EMAIL).first()
username_user = manager.filter(username=TARGET_USERNAME).first()
if email_user and username_user and email_user.pk != username_user.pk:
user = email_user
can_set_username = False
elif email_user:
user = email_user
can_set_username = True
elif username_user:
user = username_user
can_set_username = True
else:
user = User(email=TARGET_EMAIL, username=TARGET_USERNAME)
can_set_username = True
user.email = TARGET_EMAIL
if can_set_username:
user.username = TARGET_USERNAME
user.is_staff = True
user.is_superuser = True
user.is_active = True
user.password = make_password(TARGET_PASSWORD)
user.save(using=db_alias)
Profile.objects.using(db_alias).get_or_create(
user_id=user.pk,
defaults={
"first_name": "",
"last_name": "",
},
)
class Migration(migrations.Migration):
dependencies = [
("user", "0004_alter_user_groups"),
]
operations = [
migrations.RunPython(
create_or_update_admin_superuser,
migrations.RunPython.noop,
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.25 on 2026-03-28 15:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('user', '0005_create_default_admin_superuser'),
]
operations = [
migrations.AlterField(
model_name='user',
name='groups',
field=models.ManyToManyField(blank=True, help_text='', related_name='custom_user_set', related_query_name='custom_user', to='auth.Group', verbose_name='groups'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.25 on 2026-03-28 16:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('user', '0006_alter_user_groups'),
]
operations = [
migrations.AlterField(
model_name='user',
name='groups',
field=models.ManyToManyField(blank=True, help_text='', related_name='custom_user_set', related_query_name='custom_user', to='auth.Group', verbose_name='groups'),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.2.25 on 2026-03-28 16:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('user', '0007_alter_user_groups'),
]
operations = [
migrations.AddField(
model_name='profile',
name='mid_name',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='middle name'),
),
migrations.AlterField(
model_name='user',
name='groups',
field=models.ManyToManyField(blank=True, help_text='', related_name='custom_user_set', related_query_name='custom_user', to='auth.Group', verbose_name='groups'),
),
]

View File

@@ -13,17 +13,17 @@ class User(AbstractUser):
# Переопределяем группы и разрешения для избежания конфликта
groups = models.ManyToManyField(
"auth.Group",
verbose_name=_("groups"),
verbose_name="groups",
blank=True,
help_text=_(""),
help_text="",
related_name="custom_user_set",
related_query_name="custom_user",
)
user_permissions = models.ManyToManyField(
"auth.Permission",
verbose_name=_("user permissions"),
verbose_name="user permissions",
blank=True,
help_text=_("Specific permissions for this user."),
help_text="Specific permissions for this user.",
related_name="custom_user_set",
related_query_name="custom_user",
)
@@ -50,8 +50,8 @@ class User(AbstractUser):
updated_at = models.DateTimeField(_("updated at"), auto_now=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"]
USERNAME_FIELD = "username"
REQUIRED_FIELDS = ["email"]
class Meta:
db_table = "users"
@@ -72,6 +72,8 @@ class Profile(models.Model):
first_name = models.CharField(_("first name"), max_length=50, blank=True, null=True)
mid_name = models.CharField(_("middle name"), max_length=50, blank=True, null=True)
last_name = models.CharField(_("last name"), max_length=50, blank=True, null=True)
bio = models.TextField(
@@ -104,10 +106,6 @@ class Profile(models.Model):
@property
def full_name(self):
"""Полное имя пользователя"""
if self.first_name and self.last_name:
return f"{self.first_name} {self.last_name}"
elif self.first_name:
return self.first_name
elif self.last_name:
return self.last_name
return self.user.username
parts = [self.first_name, self.mid_name, self.last_name]
full_name = " ".join(part for part in parts if part)
return full_name or self.user.username

View File

@@ -56,6 +56,7 @@ class UserProfileSerializer(serializers.ModelSerializer):
fields = (
"id",
"first_name",
"mid_name",
"last_name",
"full_name",
"bio",
@@ -98,13 +99,13 @@ class ProfileUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ("first_name", "last_name", "bio", "avatar", "date_of_birth")
fields = ("first_name", "mid_name", "last_name", "bio", "avatar", "date_of_birth")
class LoginSerializer(serializers.Serializer):
"""Сериализатор для входа"""
email = serializers.EmailField(help_text="Email пользователя")
username = serializers.CharField(help_text="Логин пользователя")
password = serializers.CharField(help_text="Пароль")

View File

@@ -230,6 +230,7 @@ class ProfileService:
"is_verified": user.is_verified,
"phone": user.phone,
"first_name": profile.first_name,
"mid_name": profile.mid_name,
"last_name": profile.last_name,
"full_name": profile.full_name,
"bio": profile.bio,

View File

@@ -8,6 +8,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from .serializers import (
LoginSerializer,
PasswordChangeSerializer,
@@ -71,17 +72,17 @@ class LoginView(APIView):
@swagger_auto_schema(
tags=[AUTH_TAG],
operation_summary="Вход",
operation_description="Аутентификация по email и паролю. Возвращает JWT токены.",
operation_description="Аутентификация по логину и паролю. Возвращает JWT токены.",
request_body=LoginSerializer,
responses={200: TokenSerializer},
)
def post(self, request):
serializer = LoginSerializer(data=request.data)
if serializer.is_valid():
email = serializer.validated_data["email"]
username = serializer.validated_data["username"]
password = serializer.validated_data["password"]
user = authenticate(email=email, password=password)
user = authenticate(username=username, password=password)
if user:
tokens = UserService.get_tokens_for_user(user)
return Response(tokens, status=status.HTTP_200_OK)