Files
octopus/myproject/tenants/forms.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

116 lines
5.5 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 -*-
from django import forms
from django.core.exceptions import ValidationError
from .models import TenantRegistration, Client, RESERVED_SCHEMA_NAMES
class TenantRegistrationForm(forms.ModelForm):
"""
Форма регистрации нового тенанта
"""
class Meta:
model = TenantRegistration
fields = ['shop_name', 'schema_name', 'owner_name', 'owner_email', 'phone']
widgets = {
'shop_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Название вашего магазина',
'required': True
}),
'schema_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'myshop',
'required': True,
'pattern': '[a-z0-9][a-z0-9\-]{1,61}[a-z0-9]',
'title': 'Только латинские буквы в нижнем регистре, цифры и дефис (3-63 символа)'
}),
'owner_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Иван Иванов',
'required': True
}),
'owner_email': forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'your@email.com',
'required': True
}),
'phone': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '+375291234567',
'required': True
}),
}
labels = {
'shop_name': 'Название магазина',
'schema_name': 'Желаемый поддомен',
'owner_name': 'Ваше имя',
'owner_email': 'Email',
'phone': 'Телефон',
}
help_texts = {
'schema_name': 'Будет использоваться как поддомен: yourshop.inventory.by. '
'Только латинские буквы, цифры и дефис (3-63 символа)',
'owner_email': 'На этот email придет уведомление об активации',
}
def clean_schema_name(self):
"""
Валидация поддомена:
1. Приводим к нижнему регистру
2. Проверяем длину
3. Проверяем, что не зарезервировано
4. Проверяем уникальность
"""
schema_name = self.cleaned_data.get('schema_name', '').lower().strip()
# Проверка длины
if len(schema_name) < 3:
raise ValidationError("Поддомен должен содержать минимум 3 символа.")
if len(schema_name) > 63:
raise ValidationError("Поддомен не может быть длиннее 63 символов.")
# Проверка на зарезервированные имена
if schema_name in RESERVED_SCHEMA_NAMES:
raise ValidationError(f"Поддомен '{schema_name}' зарезервирован системой. Выберите другой.")
# Проверка уникальности в заявках
if TenantRegistration.objects.filter(schema_name=schema_name).exists():
raise ValidationError(f"Поддомен '{schema_name}' уже занят. Выберите другой.")
# Проверка уникальности в существующих тенантах
if Client.objects.filter(schema_name=schema_name).exists():
raise ValidationError(f"Поддомен '{schema_name}' уже занят. Выберите другой.")
return schema_name
def clean_owner_email(self):
"""
Валидация email: проверка на дубликаты для обычных пользователей
(супер-админ может иметь несколько тенантов)
"""
email = self.cleaned_data.get('owner_email', '').lower().strip()
# Проверяем, есть ли активные заявки с таким email
pending_registrations = TenantRegistration.objects.filter(
owner_email=email,
status=TenantRegistration.STATUS_PENDING
).count()
if pending_registrations > 0:
raise ValidationError(
f"У вас уже есть заявка на регистрацию с email {email}, ожидающая проверки. "
"Дождитесь активации или свяжитесь с поддержкой."
)
# Проверяем количество существующих тенантов с таким email
# Если больше 0 - это нормально для админов, но для обычных пользователей показываем предупреждение
existing_tenants_count = Client.objects.filter(owner_email=email).count()
if existing_tenants_count > 0:
# Добавляем предупреждение, но не блокируем (на случай если это супер-админ)
# В реальности здесь можно добавить более сложную логику
pass
return email