refactor(db): консолидация миграций и рефакторинг кода

Объединены изменения из промежуточных миграций в начальные миграции для упрощения истории базы данных.
Удалены миграции: accounts/0002, discounts/0002, orders/0003-0004, products/0002-0005, user_roles/0002, system_settings/0001-0002, integrations/0001-0002.
Добавлена автоматическая creation пользователя при установке пароля.
Обновлен UI страницы установки пароля с кастомным стилем.
Добавлен conditional rendering для кнопки синхронизации Recommerce.
Исправлены редиректы с 'index' на '/' в accounts views.
Добавлена проверка request.tenant в navbar и authenticate метод в auth backend.
This commit is contained in:
2026-01-14 16:30:28 +03:00
parent e7672588c6
commit caeb3f80bd
31 changed files with 238 additions and 558 deletions

View File

@@ -1,6 +1,5 @@
# Generated by Django 5.0.10 on 2026-01-08 15:58
# Generated by Django 5.0.10 on 2026-01-14 07:04
import django.contrib.auth.validators
import django.utils.timezone
import uuid
from django.db import migrations, models
@@ -11,7 +10,6 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
@@ -21,21 +19,16 @@ class Migration(migrations.Migration):
('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')),
('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')),
('email', models.EmailField(max_length=254, unique=True)),
('name', models.CharField(max_length=100)),
('is_active', models.BooleanField(default=True)),
('is_staff', models.BooleanField(default=False)),
('is_superuser', models.BooleanField(default=False)),
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
('is_email_confirmed', models.BooleanField(default=False)),
('email_confirmation_token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('email_confirmed_at', models.DateTimeField(blank=True, null=True)),
('password_reset_token', models.UUIDField(blank=True, editable=False, null=True, unique=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='custom_user_set', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='custom_user_set', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'Пользователь магазина',

View File

@@ -1,54 +0,0 @@
# Generated by Django 5.0.10 on 2026-01-09 21:04
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='customuser',
name='first_name',
),
migrations.RemoveField(
model_name='customuser',
name='groups',
),
migrations.RemoveField(
model_name='customuser',
name='last_name',
),
migrations.RemoveField(
model_name='customuser',
name='user_permissions',
),
migrations.RemoveField(
model_name='customuser',
name='username',
),
migrations.AlterField(
model_name='customuser',
name='date_joined',
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AlterField(
model_name='customuser',
name='is_active',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='customuser',
name='is_staff',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='customuser',
name='is_superuser',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,35 +1,108 @@
{% extends 'base.html' %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Установка пароля</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
padding: 40px;
max-width: 400px;
width: 100%;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 24px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
color: #333;
font-weight: 500;
margin-bottom: 8px;
}
input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
input[type="password"]:focus {
outline: none;
border-color: #667eea;
}
.btn {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
}
.btn:hover {
opacity: 0.9;
}
.messages {
margin-bottom: 20px;
}
.messages .error {
background: #fee;
color: #c33;
padding: 10px;
border-radius: 6px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>Установка пароля</h1>
<p class="subtitle">для {{ tenant.name }}</p>
{% block title %}Установка пароля{% endblock %}
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% block content %}
<div class="container">
<div class="form-container">
<h2 class="text-center mb-4">Установка пароля</h2>
<!-- Приветственное сообщение -->
<div class="alert alert-info">
<strong>Добро пожаловать!</strong> Ваш магазин <strong>{{ tenant.name }}</strong> активирован.
<br>Установите пароль для входа в систему.
</div>
<div class="tab-content">
<div class="tab-pane fade show active" id="setup-password">
<form method="post">
{% csrf_token %}
{% include 'accounts/password_input.html' with field_name='password1' field_label='Пароль' required=True %}
{% include 'accounts/password_input.html' with field_name='password2' field_label='Подтверждение пароля' required=True %}
<button type="submit" class="btn btn-primary w-100">Установить пароль и войти</button>
</form>
<!-- Информация -->
<div class="text-center mt-3">
<small class="text-muted">
После установки пароля вы автоматически войдете в свой магазин.
</small>
</div>
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="id_password1">Пароль</label>
<input type="password" name="password1" id="id_password1" required>
</div>
</div>
<div class="form-group">
<label for="id_password2">Подтвердите пароль</label>
<input type="password" name="password2" id="id_password2" required>
</div>
<button type="submit" class="btn">Установить пароль</button>
</form>
</div>
</div>
{% endblock %}
</body>
</html>

View File

@@ -61,7 +61,7 @@ def login_view(request):
def logout_view(request):
logout(request)
return redirect('index')
return redirect('/')
@login_required
@@ -142,7 +142,7 @@ def password_reset_confirm(request, token):
user = CustomUser.objects.get(password_reset_token=token)
except CustomUser.DoesNotExist:
messages.error(request, 'Ссылка для восстановления пароля недействительна.')
return redirect('index')
return redirect('/')
if request.method == 'POST':
password1 = request.POST.get('password1')
@@ -178,7 +178,7 @@ def password_setup_confirm(request, token):
)
except TenantRegistration.DoesNotExist:
messages.error(request, 'Ссылка для настройки пароля недействительна.')
return redirect('index')
return redirect('/')
# Проверить истечение токена (7 дней)
if registration.password_setup_token_created_at:
@@ -188,24 +188,30 @@ def password_setup_confirm(request, token):
request,
'Ссылка для настройки пароля истекла. Пожалуйста, свяжитесь с поддержкой.'
)
return redirect('index')
return redirect('/')
# Получить тенант и пользователя-владельца
from django.db import connection
tenant = registration.tenant
if not tenant:
messages.error(request, 'Тенант не найден.')
return redirect('index')
return redirect('/')
# Переключиться на схему тенанта чтобы найти владельца
connection.set_tenant(tenant)
User = get_user_model()
try:
owner = User.objects.get(email=registration.owner_email)
except User.DoesNotExist:
connection.set_schema_to_public()
messages.error(request, 'Пользователь не найден.')
return redirect('index')
from accounts.models import CustomUser
# Создаём пользователя если он не существует (для случаев когда активация прошла без создания пользователя)
owner, created = CustomUser.objects.get_or_create(
email=registration.owner_email,
defaults={
'name': registration.owner_name,
'is_active': False,
}
)
if created:
owner.is_email_confirmed = True
owner.save()
# Обработать POST - установить пароль
if request.method == 'POST':