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
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:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
19
src/apps/user/migrations/0006_alter_user_groups.py
Normal file
19
src/apps/user/migrations/0006_alter_user_groups.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
19
src/apps/user/migrations/0007_alter_user_groups.py
Normal file
19
src/apps/user/migrations/0007_alter_user_groups.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
24
src/apps/user/migrations/0008_auto_20260328_1630.py
Normal file
24
src/apps/user/migrations/0008_auto_20260328_1630.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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="Пароль")
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user