# -*- coding: utf-8 -*- from django.contrib import admin from django.contrib import messages from django.utils import timezone from django.utils.html import format_html from django.urls import reverse from django.db import transaction from .models import Client, Domain, TenantRegistration, Subscription class DomainInline(admin.TabularInline): """ Inline для управления доменами тенанта. """ model = Domain extra = 1 fields = ['domain', 'is_primary'] @admin.register(Client) class ClientAdmin(admin.ModelAdmin): """ Админ-панель для управления тенантами (магазинами). ВАЖНО: Эта админка доступна только в Public схеме! """ list_display = [ 'name', 'schema_name', 'owner_email', 'is_active', 'created_at', 'subscription_status', ] list_filter = [ 'is_active', 'created_at', ] search_fields = [ 'name', 'schema_name', 'owner_email', 'owner_name', ] # Убираем schema_name из readonly при создании нового тенанта def get_readonly_fields(self, request, obj=None): if obj: # Редактирование существующего тенанта return ['schema_name', 'created_at'] else: # Создание нового тенанта return ['created_at'] fieldsets = ( ('Информация о магазине', { 'fields': ('name', 'schema_name', 'is_active') }), ('Владелец', { 'fields': ('owner_name', 'owner_email', 'phone') }), ('Дополнительно', { 'fields': ('created_at', 'notes'), 'classes': ('collapse',) }), ) inlines = [DomainInline] def subscription_status(self, obj): """Отображение статуса подписки""" try: sub = obj.subscription if sub.is_expired(): return format_html('Истекла {}', sub.expires_at.date()) elif sub.days_left() <= 7: return format_html('Осталось {} дн.', sub.days_left()) else: return format_html('{} до {}', sub.get_plan_display(), sub.expires_at.date()) except Subscription.DoesNotExist: return format_html('Нет подписки') subscription_status.short_description = "Подписка" def has_delete_permission(self, request, obj=None): """ Запрещаем удаление тенантов через админку (для безопасности). Удаление должно происходить через специальную команду. """ return False @admin.register(Domain) class DomainAdmin(admin.ModelAdmin): """ Админ-панель для управления доменами. """ list_display = [ 'domain', 'tenant', 'is_primary', ] list_filter = [ 'is_primary', ] search_fields = [ 'domain', 'tenant__name', 'tenant__schema_name', ] @admin.register(TenantRegistration) class TenantRegistrationAdmin(admin.ModelAdmin): """ Админка для управления заявками на регистрацию """ list_display = ("shop_name", "schema_name", "owner_email", "status", "created_at", "actions_column") list_filter = ("status", "created_at") search_fields = ("shop_name", "schema_name", "owner_email", "owner_name") readonly_fields = ("created_at", "processed_at", "processed_by", "tenant") fieldsets = ( ("Информация о магазине", { "fields": ("shop_name", "schema_name") }), ("Владелец", { "fields": ("owner_name", "owner_email", "phone") }), ("Статус", { "fields": ("status", "created_at", "processed_at", "processed_by", "rejection_reason") }), ("Результат", { "fields": ("tenant",) }), ) actions = ['approve_registrations', 'reject_registrations'] def actions_column(self, obj): """Кнопки действий для каждой заявки""" if obj.status == TenantRegistration.STATUS_PENDING: approve_url = reverse('admin:tenants_tenantregistration_change', args=[obj.pk]) return format_html( 'Активировать ' 'Отклонить', approve_url, approve_url ) elif obj.status == TenantRegistration.STATUS_APPROVED: if obj.tenant: tenant_url = reverse('admin:tenants_client_change', args=[obj.tenant.pk]) return format_html('Перейти к тенанту →', tenant_url) return "Одобрено" else: return "Отклонено" actions_column.short_description = "Действия" def approve_registrations(self, request, queryset): """Массовая активация заявок""" approved_count = 0 for registration in queryset.filter(status=TenantRegistration.STATUS_PENDING): try: self._approve_registration(registration, request.user) approved_count += 1 except Exception as e: messages.error(request, f"Ошибка при активации '{registration.shop_name}': {str(e)}") if approved_count > 0: messages.success(request, f"Успешно активировано заявок: {approved_count}") approve_registrations.short_description = "✓ Активировать выбранные заявки" def reject_registrations(self, request, queryset): """Массовое отклонение заявок""" rejected_count = queryset.filter(status=TenantRegistration.STATUS_PENDING).update( status=TenantRegistration.STATUS_REJECTED, processed_at=timezone.now(), processed_by=request.user, rejection_reason="Отклонено массовым действием" ) if rejected_count > 0: messages.success(request, f"Отклонено заявок: {rejected_count}") reject_registrations.short_description = "✗ Отклонить выбранные заявки" def changeform_view(self, request, object_id=None, form_url='', extra_context=None): """ Обработка действий активации/отклонения через GET параметры """ if object_id and request.method == 'GET': registration = TenantRegistration.objects.get(pk=object_id) if 'approve' in request.GET and registration.status == TenantRegistration.STATUS_PENDING: try: self._approve_registration(registration, request.user) messages.success(request, f"Заявка '{registration.shop_name}' успешно активирована!") return self._redirect_to_changelist() except Exception as e: messages.error(request, f"Ошибка при активации: {str(e)}") elif 'reject' in request.GET and registration.status == TenantRegistration.STATUS_PENDING: # Обрабатываем отклонение registration.status = TenantRegistration.STATUS_REJECTED registration.processed_at = timezone.now() registration.processed_by = request.user registration.rejection_reason = "Отклонено администратором" registration.save() messages.warning(request, f"Заявка '{registration.shop_name}' отклонена.") return self._redirect_to_changelist() return super().changeform_view(request, object_id, form_url, extra_context) @transaction.atomic def _approve_registration(self, registration, admin_user): """ Активация заявки: создание тенанта, домена и триальной подписки """ import logging logger = logging.getLogger(__name__) try: # Проверяем, не создан ли уже тенант if Client.objects.filter(schema_name=registration.schema_name).exists(): raise ValueError(f"Тенант с schema_name '{registration.schema_name}' уже существует!") # Создаем тенант logger.info(f"Создание тенанта: {registration.schema_name}") client = Client.objects.create( schema_name=registration.schema_name, name=registration.shop_name, owner_email=registration.owner_email, owner_name=registration.owner_name, phone=registration.phone, is_active=True ) logger.info(f"Тенант создан: {client.id}") # Создаем домен (для локальной разработки используем localhost) domain_name = f"{registration.schema_name}.localhost" logger.info(f"Создание домена: {domain_name}") domain = Domain.objects.create( domain=domain_name, tenant=client, is_primary=True ) logger.info(f"Домен создан: {domain.id}") # Применяем миграции для нового тенанта logger.info(f"Применение миграций для тенанта: {registration.schema_name}") from django.core.management import call_command try: call_command('migrate_schemas', schema_name=registration.schema_name, verbosity=1) logger.info(f"Миграции успешно применены для тенанта: {registration.schema_name}") except Exception as e: logger.error(f"Ошибка при применении миграций: {str(e)}", exc_info=True) raise # Создаем триальную подписку на 90 дней logger.info(f"Создание триальной подписки для тенанта: {client.id}") subscription = Subscription.create_trial(client) logger.info(f"Подписка создана: {subscription.id}, истекает: {subscription.expires_at}") # Автоматически создаем суперпользователя для тенанта logger.info(f"Создание суперпользователя для тенанта: {client.id}") from django.db import connection from django.contrib.auth import get_user_model from django.conf import settings # Переключаемся на схему тенанта connection.set_tenant(client) User = get_user_model() # Проверяем, не существует ли уже пользователь с таким email if not User.objects.filter(email=settings.TENANT_ADMIN_EMAIL).exists(): superuser = User.objects.create_superuser( email=settings.TENANT_ADMIN_EMAIL, name=settings.TENANT_ADMIN_NAME, password=settings.TENANT_ADMIN_PASSWORD ) logger.info(f"Суперпользователь создан: {superuser.id} ({superuser.email})") else: logger.warning(f"Пользователь с email {settings.TENANT_ADMIN_EMAIL} уже существует в тенанте") # Возвращаемся в public схему connection.set_schema_to_public() # Обновляем статус заявки registration.status = TenantRegistration.STATUS_APPROVED registration.processed_at = timezone.now() registration.processed_by = admin_user registration.tenant = client registration.save() logger.info(f"Заявка {registration.id} успешно активирована") return client except Exception as e: logger.error(f"Ошибка при активации заявки {registration.id}: {str(e)}", exc_info=True) raise # Перебрасываем исключение для отображения в админке def _redirect_to_changelist(self): """Редирект на список заявок""" from django.shortcuts import redirect return redirect('admin:tenants_tenantregistration_changelist') @admin.register(Subscription) class SubscriptionAdmin(admin.ModelAdmin): """ Админка для управления подписками """ list_display = ("client", "plan", "started_at", "expires_at", "is_active", "days_left_display", "is_expired_display") list_filter = ("plan", "is_active", "auto_renew") search_fields = ("client__name", "client__schema_name") readonly_fields = ("created_at", "updated_at", "days_left_display", "is_expired_display") fieldsets = ( ("Основная информация", { "fields": ("client", "plan", "is_active", "auto_renew") }), ("Период действия", { "fields": ("started_at", "expires_at", "days_left_display", "is_expired_display") }), ("Служебная информация", { "fields": ("created_at", "updated_at") }), ) def days_left_display(self, obj): """Отображение оставшихся дней""" days = obj.days_left() if days == 0: return format_html('Истекла') elif days <= 7: return format_html('{} дн.', days) else: return format_html('{} дн.', days) days_left_display.short_description = "Осталось" def is_expired_display(self, obj): """Отображение статуса истечения""" if obj.is_expired(): return format_html('Да') else: return format_html('Нет') is_expired_display.short_description = "Истекла"