Создание многопользовательского приложения с помощью Django
В этом руководстве объясняется, как реализовать веб-приложение с несколькими пользователями в Django, используя django-tenants и django-tenants-users пакеты, помогающие ответить на вопросы:
- Как создать мультитенантное веб-приложение с помощью Django?
- Как поддерживать несколько клиентов в проекте Django?
Описанное решение идеально подходит для проектов среднего и крупного размера "Программное обеспечение как услуга" (SaaS).
Содержимое
- Цели
- Мультитенантные подходы
- Что такое django-tenants?
- Что такое django-tenant-users?
- Введение в проект
- джанго-арендаторы
- django-клиент-пользователи
- Заключение
Цели
К концу этого урока вы сможете:
- Объясните различные стратегии архитектуры с несколькими арендаторами
- Преобразуйте однопользовательский проект Django в многопользовательский
- Установка и конфигурирование пакета django-tenants и django-tenant-users
- Управление арендаторами и пользователями-арендаторами (предоставление арендаторов, удаление арендаторов, назначение пользователей и т.д.)
- Работа с мультитенантными проектами Django
Мультитенантные подходы
Мультитенантные приложения предназначены для использования несколькими клиентами. Каждый пользователь называется клиентом и обычно представляет группу пользователей, например организацию.
Вообще говоря, существует три подхода к такому:
- Изолированный подход: у каждого клиента есть своя собственная база данных и собственные экземпляры приложений
- Полуизолированный подход: экземпляры базы данных и приложения являются общими для всех клиентов; ограничения устанавливаются на уровне базы данных - например, с помощью схем базы данных
- Общий подход: экземпляры базы данных и приложения являются общими для всех клиентов; нет ограничений на уровне базы данных; предоставление в аренду осуществляется с помощью внешних ключей
Изолированный подход
Изолированный подход имеет смысл, если вам нужна высокая степень изолированности между арендаторами и вы не планируете привлекать многих из них. Для большинства проектов такой подход является излишним, поскольку довольно сложно обеспечить и отменить предоставление услуг арендаторам. Кроме того, стоимость облачных ресурсов, как правило, выше по сравнению с альтернативными вариантами.
Если вы выберете этот подход, вам не нужно будет изменять свой код на Django. Вместо этого большая часть работы будет перенесена на настройку вашей инфраструктуры. Каждый раз, когда вы подключаете клиента, вам нужно создавать новую базу данных и экземпляр приложения. В большинстве изолированных установок используется виртуализация или контейнеризация.
Общий подход
Общий подход подходит для небольших проектов. Тем не менее, он может значительно усложнить работу, поскольку для каждой модели требуются внешние ключи. Вам также необходимо убедиться, что вы случайно не допустили утечки данных других клиентов.
С каждой моделью, которая может принадлежать арендатору, вам нужно будет сделать что-то вроде этого:
class Project(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
# an extra field that determines to which tenant the model instance belongs
tenant = models.ForeignKey(to="tenants.Tenant", on_delete=models.CASCADE)
Оптимально, если вы захотите создать абстрактную модель для этого, например, TenantOwnableModel
, и переопределить модель менеджера get_queryset()
для автоматической фильтрации в зависимости от клиента.
Для этого подхода вы можете использовать пакет django-multitenant. В качестве альтернативы, если вы хотите реализовать функциональность клиента самостоятельно и просто избежать утечки данных других клиентов, есть django-scopes.
Полуизолированный подход
В большинстве случаев вам лучше всего использовать полуизолированный подход, что мы и делаем в этом руководстве. Этот подход обеспечивает баланс между изолированным и общим подходами. Django изначально не поддерживает это, но вы можете использовать такие пакеты, как django-tenants и django-tenant-users, чтобы заставить его работать.
Давайте сначала объясним, как работают эти два пакета, а затем перейдем к практическому руководству.
Что такое django-tenants?
django-tenants - это простой в использовании пакет, который позволяет проектам Django поддерживать несколько клиентов. Это позволяет преобразовать любой однопользовательский проект в многопользовательский без существенного изменения кода. По своей сути, он использует Схемы PostgreSQL.
Схемы PostgreSQL - это логические контейнеры, в которых хранятся объекты базы данных, такие как таблицы, представления, функции и индексы. Иерархически схемы находятся ниже баз данных, что означает, что база данных может содержать несколько схем.
В django-tenants каждому клиенту присваивается своя схема и доменное имя (обычно это поддомен). Например, demo1.duplxey.com
указывает на схему demo1
, demo2.duplxey.com
указывает на demo2
и так далее.
По мере поступления запросов, TenantMainMiddleware
проверяет доменное имя, сопоставляет его с именем арендатора и вводит имя арендатора в запрос. Это также переключает активную схему PostgreSQL, что означает, что возвращаются только запрошенные данные клиента.
Кроме того, пакет предоставляет общедоступную схему, которую можно использовать для кросс-клиентских приложений и данных, таких как локализация, аутентификация и биллинговые системы.
Что такое django-tenant-users?
django-tenant-users - это пакет, который создается поверх django-tenants. Он предоставляет мультитенантным проектам Django:
- Управление пользователями клиента
- Глобальная аутентификация с разрешениями, специфичными для клиента
- Отдельные разрешения для схемы
Объединение обоих пакетов позволяет создать готовый к работе мультитенантный проект.
Введение в проект
В этом руководстве мы будем работать над веб-приложением для управления проектами, похожим на Jira. Каждый клиент сможет создавать свои проекты и управлять ими, а также добавлять к ним задачи. Кроме того, веб-приложение будет поддерживать общедоступный блог в маркетинговых целях.
Чтобы сэкономить ваше время, я собрал примерный проект, который мы будем использовать в качестве отправной точки. В настоящее время он поддерживает только одного клиента, однако к концу этого руководства мы преобразуем его в мультитенантный проект.
Начните с клонирования base
ветви django-multi-tenant
репозитория:
$ git clone https://github.com/duplxey/django-multi-tenant.git \
--single-branch --branch base && cd django-multi-tenant
Далее создайте новую виртуальную среду и активируйте ее:
$ python3 -m venv venv && source venv/bin/activate
Установите требования:
(venv)$ pip install -r requirements.txt
Прежде чем двигаться дальше, пожалуйста, уделите несколько минут краткому просмотру исходного кода.
Затем используйте локально установленный PostgreSQL или запустите его с помощью Docker:
$ docker run --name sprinty-postgres -p 5432:5432 \
-e POSTGRES_USER=sprinty -e POSTGRES_PASSWORD=complexpassword123 \
-e POSTGRES_DB=sprinty -d postgres
Перенос базы данных:
(venv)$ python manage.py migrate
Наконец, запустите сервер разработки:
(venv)$ python manage.py runserver
Ваше веб-приложение должно быть доступно по адресу http://localhost:8000/api/. Вы увидите три маршрута API: /api/blog/
, /api/projects/
, и /api/tasks/
. Каждый из них поддерживает базовые операции CRUD.
django-tenants
В этом разделе мы установим и настроим django-tenants.
Пользовательское приложение
Перед установкой django-tenants создайте новое приложение Django с именем tenants
:
(venv)$ python manage.py startapp tenants
Это приложение будет содержать наши пользовательские модели, связанные с пользователями и арендаторами, которые мы создадим позже.
Далее зарегистрируйте его в core/settings.py:
# core/settings.py
INSTALLED_APPS = [
# ...
"blog.apps.BlogConfig",
"tasks.apps.TasksConfig",
"tenants.apps.TenantsConfig", # new
]
Установка
Начните с установки django-tenants через pip:
(venv)$ pip install django-tenants==3.7.0
Добавить django-tenants
в начало INSTALLED_APPS
:
# core/settings.py
INSTALLED_APPS = [
"django_tenants", # new, must be first
"django.contrib.admin",
"django.contrib.auth",
# ...
]
Если вы не добавите пакет в начало списка, позже у вас могут возникнуть проблемы. Например, Раскраска сайта администратора Django не будет работать должным образом.
Затем добавьте TenantMainMiddleware
в начало списка MIDDLEWARE
:
# core/settings.py
MIDDLEWARE = [
"django_tenants.middleware.main.TenantMainMiddleware", # new, must be first
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
# ...
]
Это промежуточное программное обеспечение гарантирует, что правильная схема базы данных будет выбрана на основе узла запроса. Кроме того, он вводит экземпляр tenant
в request
, так что вы можете получить к нему доступ непосредственно в своих представлениях.
Переключите компонент database engine и установите параметр DATABASE_ROUTERS
:
# core/settings.py
DATABASES = {
"default": {
"ENGINE": "django_tenants.postgresql_backend", # changed
"NAME": "sprinty",
"USER": "sprinty",
"PASSWORD": "complexpassword123",
"HOST": "localhost",
"PORT": "5432",
}
}
# new
DATABASE_ROUTERS = [
"django_tenants.routers.TenantSyncRouter",
]
Позже, когда мы разделим наш INSTALLED_APPS
на TENANT_APPS
и SHARED_APPS
, этот маршрутизатор обеспечит правильную синхронизацию общедоступных приложений и приложений, доступных только для клиентов, с базой данных.
Наконец, определите параметры BASE_DOMAIN
и PUBLIC_SCHEMA_NAME
:
# core/settings.py
BASE_DOMAIN = "localhost"
PUBLIC_SCHEMA_NAME = "public"
Примечания:
BASE_DOMAIN
это пользовательская настройка, которую мы будем использовать для заполнения базы данных.PUBLIC_SCHEMA_NAME
определяет имя общедоступной схемы PostgreSQL.
Пользователь, клиент и домен
Продвигаясь вперед, давайте определим пользовательские модели, связанные с арендаторами:
# tenants/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from django_tenants.models import TenantMixin, DomainMixin
from core.models import TimeStampedModel
class User(AbstractUser):
pass
class Tenant(TenantMixin, TimeStampedModel):
name = models.CharField(max_length=100)
class Domain(DomainMixin, TimeStampedModel):
pass
Примечания:
User
это пользовательская модель пользователя, которая наследуется от AbstractUser в Django. Определение пользовательского пользователя не является обязательным. Тем не менее, это хорошая идея, поскольку большинство мультитенантных проектов Django требуют модификации пользовательской модели.Tenant
должен наследоваться от TenantMixin, который предоставляет методы управления арендаторами.Domain
должен наследоваться от DomainMixin. Эта модель будет иметь отношение1:1
к вашей модели арендатора. Это предоставит арендаторам их доменные имена.
Зарегистрируйте их в tenants/admin.py вот так:
# tenants/admin.py
from django.contrib import admin
from django_tenants.admin import TenantAdminMixin
from tenants.models import Tenant, Domain, User
class TenantAdmin(TenantAdminMixin, admin.ModelAdmin):
list_display = ["schema_name", "name", "created_at", "updated_at"]
class DomainAdmin(admin.ModelAdmin):
list_display = ["domain", "tenant", "is_primary", "created_at", "updated_at"]
class UserAdmin(admin.ModelAdmin):
list_display = ["id", "email", "is_active"]
list_display_links = ["id", "email"]
search_fields = ["email"]
fieldsets = [
(
None,
{
"fields": [
"email",
"password",
],
},
),
(
"Administrative",
{
"fields": [
"tenants",
"last_login",
"is_active",
"is_verified",
],
},
),
]
admin.site.register(Tenant, TenantAdmin)
admin.site.register(Domain, DomainAdmin)
admin.site.register(User, UserAdmin)
Наконец, укажите пути к только что созданным моделям в core/settings.py:
# core/settings.py
AUTH_USER_MODEL = "tenants.User"
TENANT_MODEL = "tenants.Tenant"
TENANT_DOMAIN_MODEL = "tenants.Domain"
Общие и клиентские приложения
django-tenants предоставляет две настройки, связанные с приложением:
SHARED_APPS
это список приложений, общих для всех клиентов. Их SQL-таблицы создаются в схемеpublic
.TENANT_APPS
это список приложений, выделенных для каждого клиента. Их SQL-таблицы создаются в схеме каждого клиента (например,demo1
,demo2
).
Давайте разделим INSTALLED_APPS
соответственно:
# core/settings.py
SHARED_APPS = [
"django_tenants",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"django_filters",
"tenants.apps.TenantsConfig",
"blog.apps.BlogConfig",
]
TENANT_APPS = [
"tasks.apps.TasksConfig",
]
INSTALLED_APPS = list(SHARED_APPS) + [
app for app in TENANT_APPS if app not in SHARED_APPS
]
В приведенной выше конфигурации мы указываем, что все приложения, кроме tasks
, должны быть синхронизированы с общедоступной схемой. Это означает, что данные блога будут доступны всем арендаторам, в то время как tasks
будут оставаться изолированными внутри каждого арендатора.
Общедоступные URL-адреса и URL-адреса клиентов
В настоящее время мы используем один и тот же urls.py файл как для общедоступных, так и для клиентских схем.
Это нехорошо, потому что приложение tasks
больше не зарегистрировано в схеме public
, а это означает, что мы столкнемся с ошибками при нажатии, например, http://localhost:8000/api/tasks/.
# core/urls.py
# ...
from blog.views import ArticleViewSet
from core.views import index_view
from tasks.views import ProjectViewSet, TaskViewSet
router = DefaultRouter()
router.register("blog", ArticleViewSet)
router.register("projects", ProjectViewSet)
router.register("tasks", TaskViewSet) # <- this line will fail, because
# `tasks` DB tables aren't created
# on the `public` schema
# ...
Чтобы исправить это, мы можем отделить общедоступные URL-адреса от URL-адресов клиентов.
Создайте файл с именем urls_public.py в основном приложении:
# core/urls_public.py
"""
URLs for the 'public' schema.
"""
from django.contrib import admin
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from blog.views import ArticleViewSet
from core.views import index_view
router = DefaultRouter()
router.register("blog", ArticleViewSet)
urlpatterns = [
path("", index_view, name="index"),
path("api/", include(router.urls)),
path("admin/", admin.site.urls),
]
Затем зарегистрируйте конфигурацию публичного URL-адреса, указанную ниже ROOT_URLCONF
:
# core/settings.py
ROOT_URLCONF = "core.urls"
PUBLIC_SCHEMA_URLCONF = "core.urls_public"
Предоставление арендаторам
Существует несколько способов предоставления услуг арендаторам:
- С помощью Команд управления django-tenants.
- Использование оболочки Django и запуск пользовательского кода.
- С помощью пользовательской команды populate_db.py .
Если вам нужно создать одного арендатора, то, как правило, лучше всего использовать первые два варианта. Однако, поскольку мы хотим создать несколько арендаторов, мы создадим команду "заполнить".
Давайте создадим трех арендаторов: public
(общедоступный), demo1
, и demo2
.
Сначала создайте следующую структуру непосредственно в вашем приложении для арендаторов:
tenants/
├── data/
│ └── tenants.json
├── management/
│ ├── __init__.py
│ └── commands/
│ ├── __init__.py
│ └── populate_db.py
└── ...
Поместите следующее содержимое в файл tenants.json:
// tenants/data/tenants.json
[
{
"id": 1,
"name": "Public Tenant",
"schema_name": "public",
"subdomain": "",
"owner": {
"username": "admin@localhost",
"email": "admin@localhost",
"password": "password"
}
},
{
"id": 2,
"name": "Demo1 Tenant",
"schema_name": "demo1",
"subdomain": "demo1",
"owner": {
"username": "[email protected]",
"email": "[email protected]",
"password": "password"
}
},
{
"id": 3,
"name": "Demo2 Tenant",
"schema_name": "demo2",
"subdomain": "demo2",
"owner": {
"username": "[email protected]",
"email": "[email protected]",
"password": "password"
}
}
]
Затем добавьте к следующееpopulate_db.py:
# tenants/management/commands/populate_db.py
import json
from psycopg2 import connect, sql
from django.core.management import BaseCommand, call_command
from core import settings
from tenants.models import Tenant, Domain, User
class Command(BaseCommand):
help = "Creates a public tenant and two demo tenants"
tenants_data_file = "tenants/data/tenants.json"
def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
super().__init__(stdout, stderr, no_color, force_color)
# Load the tenant data from JSON
self.tenants_data = []
with open(self.tenants_data_file, "r") as file:
self.tenants_data = json.load(file)
def handle(self, *args, **kwargs):
self.drop_and_recreate_db()
call_command("migrate")
self.create_tenants()
self.stdout.write(
self.style.SUCCESS("Yay, database has been populated successfully.")
)
def drop_and_recreate_db(self):
db = settings.DATABASES["default"]
db_name = db["NAME"]
# Create a connection to the database
conn = connect(
dbname="postgres",
user=db["USER"],
password=db["PASSWORD"],
host=db["HOST"],
port=db["PORT"],
)
conn.autocommit = True
cur = conn.cursor()
# Terminate all connections to the database except the current one
cur.execute(
"""
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = %s
AND pid <> pg_backend_pid();
""",
[db_name],
)
# Drop the database if it exists and create a new one
cur.execute(
sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(db_name))
)
cur.execute(
sql.SQL("CREATE DATABASE {}").format(sql.Identifier(db_name))
)
cur.close()
conn.close()
def create_tenants(self):
for tenant_data in self.tenants_data:
# Create the tenant
tenant = Tenant(
id=tenant_data["id"],
name=tenant_data["name"],
schema_name=tenant_data["schema_name"],
)
tenant.save()
# Build the full domain name
domain_str = settings.BASE_DOMAIN
if tenant_data["subdomain"]:
domain_str = f"{tenant_data['subdomain']}.{settings.BASE_DOMAIN}"
# Create the domain
domain = Domain(
domain=domain_str,
is_primary=tenant_data["schema_name"] == settings.PUBLIC_SCHEMA_NAME,
tenant=tenant,
)
domain.save()
# Create the tenant owner
tenant_owner = User.objects.create_superuser(
username=tenant_data["owner"]["username"],
email=tenant_data["owner"]["email"],
password=tenant_data["owner"]["password"],
)
Примечания:
- Эта команда сначала загружает данные tenants.json.
- Затем он удаляет, воссоздает и переносит базу данных.
- После этого он создает экземпляр
Tenant
и экземплярDomain
для каждого из арендаторов. - Наконец, для каждого арендатора создается "владелец арендатора".
Наконец, выполните перенос и заполните базу данных:
(venv)$ python manage.py makemigrations
(venv)$ python manage.py populate_db
Ого, мы успешно установили и настроили django-арендаторов.
Использование
Мы создали трех арендаторов, каждый на своем поддомене:
public
localhost:8000demo1
demo1.localhost:8000demo2
demo2.localhost:8000
Кроме того, у каждого клиента есть учетная запись суперпользователя.
Войдите в http://demo1.localhost:8000/admin , используя следующие учетные данные:
user: admin@demo1.localhost
pass: password
Чтобы войти в систему других пользователей, измените URL-адрес и имя пользователя соответствующим образом.
Вы заметите, что приложения Django теперь окрашены в разные цвета. Синий цвет используется для общедоступных приложений, а зеленый - для приложений, доступных только для клиентов.
Создайте несколько статей, один-два проекта и добавьте несколько задач.
Если вы перейдете по следующим URL-адресам, то заметите, что статьи доступны всем пользователям:
- http://localhost:8000/api/blog/
- http://demo1.localhost:8000/api/blog/
- http://demo2.localhost:8000/api/blog/
Напротив, проекты и задачи изолированы на уровне клиента:
Поскольку мы определили PUBLIC_SCHEMA_URLCONF
, следующий URL-адрес должен привести к 404:
django-tenant-users
Как упоминалось ранее, django-tenants не обеспечивает управление пользователями. Нет способа добавлять или удалять пользователей из tenants, ограничивать доступ клиентов или управлять разрешениями клиентов.
В этом разделе мы установим django-tenant-users, чтобы решить эти проблемы.
Установка
Начните с установки через pip:
(venv)$ pip install django-tenant-users==2.1.1
Затем измените ваши SHARED_APPS
и TENANT_APPS
следующим образом:
# core/settings.py
SHARED_APPS = [
"django_tenants",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"django_filters",
"tenants.apps.TenantsConfig",
"blog.apps.BlogConfig",
"tenant_users.permissions", # new
"tenant_users.tenants", # new
]
TENANT_APPS = [
"django.contrib.auth", # new
"django.contrib.contenttypes", # new
"tenant_users.permissions", # new
"tasks.apps.TasksConfig",
]
Обратите внимание, что
tenant_users.permissions
необходимо добавить как в общие, так и в клиентские приложения, поскольку разрешения обрабатываются на уровне схемы.
Затем добавьте TenantAccessMiddleware
сразу после AuthenticationMiddleware
:
# core/settings.py
MIDDLEWARE = [
# ...
"django.contrib.auth.middleware.AuthenticationMiddleware",
"tenant_users.tenants.middleware.TenantAccessMiddleware", # must be right here
# ...
]
Это промежуточное программное обеспечение гарантирует, что прошедший проверку пользователь имеет доступ к клиенту, указанному в запросе. Если у пользователя нет доступа к клиенту, возникает ошибка 404.
Наконец, установите следующие два параметра в core/settings.py:
# core/settings.py
TENANT_USERS_DOMAIN = BASE_DOMAIN
AUTHENTICATION_BACKENDS = [
"tenant_users.permissions.backend.UserBackend",
]
Примечания:
TENANT_USERS_DOMAIN
определяет, из какого домена следует выполнять подготовку пользователей. Это значение должно соответствовать домену общедоступного арендатора.AUTHENTICATION_BACKENDS
устанавливает пользовательский сервер проверки подлинности, который хорошо работает с группами и разрешениями уровня клиента.
Пользователь, клиент и домен
Продвигаясь вперед, мы должны немного изменить модели User
и Tenant
:
# tenants/models.py
class User(UserProfile):
pass
class Tenant(TenantBase, TimeStampedModel):
name = models.CharField(max_length=100)
# ...
Примечания:
User
- вместо наследования отAbstractUser
мы должны наследовать отUserProfile
.Tenant
- вместо использованияTenantMixin
, мы должны использоватьTenantBase
.
Не забудьте об импорте:
from tenant_users.tenants.models import TenantBase, UserProfile
Поскольку мы заменили пользовательскую модель Django по умолчанию, нам нужно заново выполнить миграцию. Удалите папку клиенты/миграции и затем запустите makemigrations
:
(venv)$ rm -rf tenants/migrations/0001_initial.py
(venv)$ python manage.py makemigrations
Предоставление арендаторам
Наконец, мы должны немного изменить команду populate_db
, чтобы привести ее в соответствие с новыми моделями:
# tenants/management/commands/populate_db.py
import json
from psycopg2 import connect, sql
from django.core.management import BaseCommand, call_command
from tenant_users.tenants.tasks import provision_tenant
from tenant_users.tenants.utils import create_public_tenant
from core import settings
from tenants.models import User
class Command(BaseCommand):
help = "Creates a public tenant and two demo tenants"
tenants_data_file = "tenants/data/tenants.json"
root_user = None
public_tenant = None
def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
super().__init__(stdout, stderr, no_color, force_color)
# Load the tenant data from the JSON file
self.tenants_data = []
with open(self.tenants_data_file, "r") as file:
self.tenants_data = json.load(file)
def handle(self, *args, **kwargs):
self.drop_and_recreate_db()
call_command("migrate_schemas", "--shared", "--noinput")
self.stdout.write(
self.style.SUCCESS("Database recreated & migrated successfully.")
)
self.create_public_tenant()
self.create_private_tenants()
self.stdout.write(
self.style.SUCCESS("Yay, database has been populated successfully.")
)
def drop_and_recreate_db(self):
db = settings.DATABASES["default"]
db_name = db["NAME"]
# Create a connection to the database
conn = connect(
dbname="postgres",
user=db["USER"],
password=db["PASSWORD"],
host=db["HOST"],
port=db["PORT"],
)
conn.autocommit = True
cur = conn.cursor()
# Terminate all connections to the database except the current one
cur.execute(
"""
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = %s
AND pid <> pg_backend_pid();
""",
[db_name],
)
# Drop the database if it exists and create a new one
cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(db_name)))
cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(db_name)))
cur.close()
conn.close()
def create_public_tenant(self):
self.stdout.write(f"Creating the public tenant...")
public_tenant_data = self.tenants_data[0]
# Create the public tenant and the root user
public_tenant, public_tenant_domain, root_user = create_public_tenant(
domain_url=settings.BASE_DOMAIN,
tenant_extra_data={"slug": public_tenant_data["subdomain"]},
owner_email=public_tenant_data["owner"]["email"],
is_superuser=True,
is_staff=True,
**{
"password": public_tenant_data["owner"]["password"],
"is_verified": True,
},
)
self.public_tenant = public_tenant
self.root_user = root_user
self.stdout.write(
self.style.SUCCESS(
f"Public tenant ('{public_tenant.schema_name}') has been successfully created."
)
)
def create_private_tenants(self):
private_tenant_data = self.tenants_data[1:]
for tenant_data in private_tenant_data:
self.stdout.write(f"Creating tenant {tenant_data['schema_name']}...")
# Create the tenant owner
tenant_owner = User.objects.create_user(
email=tenant_data["owner"]["email"],
password=tenant_data["owner"]["password"],
)
tenant_owner.is_verified = True
tenant_owner.save()
# Create the tenant
tenant, domain = provision_tenant(
tenant_name=tenant_data["name"],
tenant_slug=tenant_data["subdomain"],
schema_name=tenant_data["schema_name"],
owner=tenant_owner,
is_superuser=True,
is_staff=True,
)
# Add the root user to the tenant
tenant.add_user(
self.root_user,
is_superuser=True,
is_staff=True,
)
self.stdout.write(
self.style.SUCCESS(
f"Tenant '{tenant.schema_name}' has been successfully created."
)
)
Примечания:
- Команда воссоздает и переносит базу данных так же, как и раньше.
- Вместо непосредственного создания клиентов с помощью
ObjectManager
мы теперь используем специализированные команды, напримерcreate_public_tenant()
иprovision_tenant()
. - При создании арендаторов мы должны предоставить арендатору
owner
. - Чтобы добавить пользователей к арендаторам, мы используем метод
add_user()
.
Наконец, выполните команду:
(venv)$ python manage.py populate_db
Исправления администратора Django
На момент написания этой статьи при использовании Django admin для управления арендаторами и пользователями возникало несколько проблем. В частности:
- При удалении экземпляров
Tenant
иUser
возникает исключение UserAdmin
неправильно хэширует пароль (сохраняет его в виде обычного текста)UserTenantPermissions
не отражает автоматически изменения при добавлении или удалении пользователя из клиента
Если вы не планируете управлять арендаторами и пользователями через Django admin, не стесняйтесь пропустить этот раздел. Однако я все же считаю, что вам следует, по крайней мере, исправить пункт второй.
Удаление клиента и пользователя
По умолчанию django-tenants не позволяет удалять клиентов и пользователей. Это помогает избежать ошибок. Однако это может вызвать разочарование, когда вам действительно нужно удалить клиента или пользователя.
Чтобы обойти это ограничение, добавьте следующие delete_model()
методы в admin.py классы:
# tenants/admin.py
class TenantAdmin(TenantAdminMixin, admin.ModelAdmin):
# ...
def delete_model(self, request, obj):
# Force delete the tenant
obj.delete(force_drop=True)
class UserAdmin(admin.ModelAdmin):
# ...
def delete_model(self, request, obj):
# Cancel the delete if the user owns any tenant
if obj.id in Tenant.objects.values_list("owner_id", flat=True):
raise ValidationError("You cannot delete a user that is a tenant owner.")
# Cancel the delete if the user still belongs to any tenant
if obj.tenants.count() > 0:
raise ValidationError("Cannot delete a tenant owner.")
# Otherwise, delete the user
obj.delete(force_drop=True)
Не забудьте об импорте:
from django.core.exceptions import ValidationError
Теперь вы должны иметь возможность удалять арендаторов и пользователей с помощью администратора Django.
Хэширование пароля пользователя
Поскольку мы используем пользовательскую модель и пользовательского администратора, хеширование паролей больше не работает. Каждый раз, когда вы создаете или обновляете пользователя, пароль сохраняется в виде обычного текста.
Давайте снова включим хеширование и принудительно проверим надежность пароля.
Создайте forms.py в приложении tenants со следующим UserAdminForm:
# tenants/forms.py
from django import forms
from django.contrib.auth import password_validation
from django.contrib.auth.hashers import make_password
from tenants.models import User
class UserAdminForm(forms.ModelForm):
class Meta:
model = User
fields = "__all__"
def clean_password(self):
password = self.cleaned_data.get("password")
# Run the password validators
try:
password_validation.validate_password(password=password, user=self.instance)
except forms.ValidationError as e:
raise forms.ValidationError(e.messages)
# Hash the password only if it isn't hashed yet
if password and not password.startswith("pbkdf2_"):
return make_password(password)
return password
Далее настройте форму UserAdmin
:
# tenants/signals.py
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from django_tenants.utils import schema_context
from tenant_users.permissions.models import UserTenantPermissions
from tenants.models import User
@receiver(m2m_changed, sender=User.tenants.through)
def on_tenant_user_tenants_changed(
sender, instance, action, reverse, model, pk_set, **kwargs
):
# Automatically create 'UserTenantPermissions' when user is added to a tenant
if action == "post_add":
for tenant_id in pk_set:
tenant = model.objects.get(pk=tenant_id)
with schema_context(tenant.schema_name):
UserTenantPermissions.objects.get_or_create(profile=instance)
# Automatically delete 'UserTenantPermissions' when user is removed from a tenant
if action == "post_remove":
for tenant_id in pk_set:
tenant = model.objects.get(pk=tenant_id)
with schema_context(tenant.schema_name):
UserTenantPermissions.objects.filter(profile=instance).delete()
Наконец, импортируем сигналы в apps.py вот так:
# tenants/forms.py
class UserAdmin(admin.ModelAdmin):
form = UserAdminForm
# ...
Не забудьте про импорт:
from tenants.forms import UserAdminForm
Пароль пользователя теперь должен автоматически хэшироваться при сохранении.
Автоматические разрешения для арендаторов
Наконец, давайте обеспечим автоматическое создание или удаление разрешений UserTenantPermissions
каждый раз, когда пользователь назначается или удаляется из арендатора с помощью администратора Django.
Я имею в виду, в частности, сферу арендаторов:
Создайте signals.py в приложении клиента следующим образом:
# tenants/signals.py
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from django_tenants.utils import schema_context
from tenant_users.permissions.models import UserTenantPermissions
from tenants.models import User
@receiver(m2m_changed, sender=User.tenants.through)
def on_tenant_user_tenants_changed(
sender, instance, action, reverse, model, pk_set, **kwargs
):
# Automatically create 'UserTenantPermissions' when user is added to a tenant
if action == "post_add":
for tenant_id in pk_set:
tenant = model.objects.get(pk=tenant_id)
with schema_context(tenant.schema_name):
UserTenantPermissions.objects.get_or_create(profile=instance)
# Automatically delete 'UserTenantPermissions' when user is removed from a tenant
if action == "post_remove":
for tenant_id in pk_set:
tenant = model.objects.get(pk=tenant_id)
with schema_context(tenant.schema_name):
UserTenantPermissions.objects.filter(profile=instance).delete()
Наконец, импортируйте сигналы в apps.py следующим образом:
# tenants/apps.py
from django.apps import AppConfig
class TenantsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "tenants"
def ready(self):
from tenants import signals
Изменения в полях tenants
теперь должны быть отражены в базе данных.
Заключение
В этом руководстве мы рассмотрели различные подходы к многоквартирному дому. Рекомендуемый подход для большинства проектов Django - это полуизолированный подход, который может быть реализован с помощью пакета django-tenants и django-tenant-users.
Исходный код доступен на GitHub.
Если у вас возникнут какие-либо вопросы или проблемы, не стесняйтесь обращаться ко мне.
Вернуться на верх