Files
octopus/myproject/tenants/management/commands/activate_registration.py
Andrey Smakotin 097d4ea304 feat: Добавить систему мультитенантности с регистрацией магазинов
Реализована полноценная система мультитенантности на базе django-tenants.
Каждый магазин получает изолированную схему БД и поддомен.

Основные компоненты:

Django-tenants интеграция:
- Модели Client (тенант) и Domain в приложении tenants/
- Разделение на SHARED_APPS и TENANT_APPS
- Public schema для общей админки
- Tenant schemas для изолированных данных магазинов

Система регистрации магазинов:
- Публичная форма регистрации на /register/
- Модель TenantRegistration для заявок со статусами (pending/approved/rejected)
- Валидация schema_name (латиница, 3-63 символа, уникальность)
- Проверка на зарезервированные имена (admin, api, www и т.д.)
- Админ-панель для модерации заявок с кнопками активации/отклонения

Система подписок:
- Модель Subscription с планами (триал 90 дней, месяц, квартал, год)
- Автоматическое создание триальной подписки при активации
- Методы is_expired() и days_left() для проверки статуса
- Цветовая индикация в админке (зеленый/оранжевый/красный)

Приложения:
- tenants/ - управление тенантами, регистрация, подписки
- shops/ - точки магазинов/самовывоза (tenant app)
- Обновлены миграции для всех приложений

Утилиты:
- switch_to_tenant.py - переключение между схемами тенантов
- Обновлены image_processor и image_service

Конфигурация:
- urls_public.py - роуты для public schema (админка + регистрация)
- urls.py - роуты для tenant schemas (магазины)
- requirements.txt - добавлены django-tenants, django-environ, phonenumber-field

Документация:
- DJANGO_TENANTS_SETUP.md - настройка мультитенантности
- TENANT_REGISTRATION_GUIDE.md - руководство по регистрации
- QUICK_START.md - быстрый старт
- START_HERE.md - общая документация

Использование:
1. Пользователь: http://localhost:8000/register/ → заполняет форму
2. Админ: http://localhost:8000/admin/ → активирует заявку
3. Результат: http://{schema_name}.localhost:8000/ - готовый магазин

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 19:13:10 +03:00

146 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Management команда для ручной активации заявки на регистрацию тенанта.
Использование:
python manage.py activate_registration <schema_name>
Пример:
python manage.py activate_registration mixflowers
"""
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from tenants.models import TenantRegistration, Client, Domain, Subscription
class Command(BaseCommand):
help = 'Активировать заявку на регистрацию тенанта вручную'
def add_arguments(self, parser):
parser.add_argument(
'schema_name',
type=str,
help='Schema name заявки для активации'
)
parser.add_argument(
'--force',
action='store_true',
help='Принудительная активация (пересоздать тенант если уже существует)'
)
def handle(self, *args, **options):
schema_name = options['schema_name']
force = options.get('force', False)
try:
# Ищем заявку
registration = TenantRegistration.objects.get(schema_name=schema_name)
except TenantRegistration.DoesNotExist:
raise CommandError(f'Заявка с schema_name "{schema_name}" не найдена')
self.stdout.write(f'Найдена заявка: {registration.shop_name} ({registration.schema_name})')
self.stdout.write(f'Статус: {registration.get_status_display()}')
self.stdout.write(f'Email: {registration.owner_email}')
self.stdout.write('')
# Проверяем статус
if registration.status == TenantRegistration.STATUS_APPROVED and not force:
self.stdout.write(self.style.WARNING('Эта заявка уже была одобрена!'))
if registration.tenant:
self.stdout.write(f'Связанный тенант: {registration.tenant.name} (ID: {registration.tenant.id})')
else:
self.stdout.write(self.style.WARNING('Но тенант не был создан. Используйте --force для пересоздания'))
if not self._confirm('Продолжить с --force?'):
return
force = True
# Проверяем существование тенанта
existing_client = Client.objects.filter(schema_name=schema_name).first()
if existing_client and not force:
raise CommandError(
f'Тенант с schema_name "{schema_name}" уже существует (ID: {existing_client.id}). '
'Используйте --force для пересоздания (ОПАСНО: удалит все данные!)'
)
# Активируем
self.stdout.write('')
self.stdout.write(self.style.WARNING('НАЧИНАЮ АКТИВАЦИЮ...'))
self.stdout.write('')
try:
with transaction.atomic():
# Если force - удаляем старый тенант
if force and existing_client:
self.stdout.write(self.style.WARNING(f'Удаление существующего тенанта {existing_client.id}...'))
# Удаляем подписку
try:
existing_client.subscription.delete()
except:
pass
# Удаляем домены
Domain.objects.filter(tenant=existing_client).delete()
# Удаляем тенант (это также удалит схему из PostgreSQL)
existing_client.delete()
self.stdout.write(self.style.SUCCESS('Старый тенант удален'))
# Создаем тенант
self.stdout.write(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
)
self.stdout.write(self.style.SUCCESS(f'✓ Тенант создан (ID: {client.id})'))
# Создаем домен
domain_name = f"{registration.schema_name}.localhost"
self.stdout.write(f'Создание домена: {domain_name}')
domain = Domain.objects.create(
domain=domain_name,
tenant=client,
is_primary=True
)
self.stdout.write(self.style.SUCCESS(f'✓ Домен создан (ID: {domain.id})'))
# Создаем триальную подписку
self.stdout.write('Создание триальной подписки на 90 дней')
subscription = Subscription.create_trial(client)
self.stdout.write(self.style.SUCCESS(
f'✓ Подписка создана (ID: {subscription.id}), '
f'истекает: {subscription.expires_at.strftime("%Y-%m-%d")}'
))
# Обновляем заявку
registration.status = TenantRegistration.STATUS_APPROVED
registration.processed_at = timezone.now()
registration.processed_by = None # Активировано через команду
registration.tenant = client
registration.save()
self.stdout.write(self.style.SUCCESS('✓ Заявка обновлена'))
self.stdout.write('')
self.stdout.write(self.style.SUCCESS('=' * 60))
self.stdout.write(self.style.SUCCESS('АКТИВАЦИЯ ЗАВЕРШЕНА УСПЕШНО!'))
self.stdout.write(self.style.SUCCESS('=' * 60))
self.stdout.write('')
self.stdout.write(f'Магазин: {client.name}')
self.stdout.write(f'Schema: {client.schema_name}')
self.stdout.write(f'Домен: http://{domain_name}:8000/')
self.stdout.write(f'Подписка до: {subscription.expires_at.strftime("%Y-%m-%d")} ({subscription.days_left()} дней)')
self.stdout.write('')
except Exception as e:
self.stdout.write(self.style.ERROR(f'ОШИБКА: {str(e)}'))
import traceback
self.stdout.write(traceback.format_exc())
raise CommandError('Активация не удалась')
def _confirm(self, question):
"""Запрос подтверждения у пользователя"""
answer = input(f'{question} (yes/no): ')
return answer.lower() in ['yes', 'y', 'да']