Tutorial baseado em https://django-tenants.readthedocs.io/en/latest/index.html
- Clone esse repositório.
- Crie um virtualenv com Python 3.
- Ative o virtualenv.
- Instale as dependências.
- Rode as migrações.
git clone https://github.com/rg3915/django-tenants-tutorial.git
cd django-tenants-tutorial
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python contrib/env_gen.py
docker-compose up -d # O objetivo é rodar o PostgreSQL
python manage.py migrate
python manage.py test
python manage.py createsuperuser --username="admin" --email=""
Opcionalmente você pode rodar o Portainer
docker run -d \
--name myportainer \
-v /opt/portainer:/data \
portainer/portainer
- Atualização dos Tipos de Produtos em todos os tentants.
https://django-tenants.readthedocs.io/en/latest/use.html#running-code-across-every-tenant
python manage.py update_product_type_on_tentants
https://django-tenants.readthedocs.io/en/latest/index.html
https://github.com/django-tenants/django-tenants
https://blog.4linux.com.br/schemas-e-namespaces-postgresql-com-django/
git clone https://github.com/rg3915/django-tenants-tutorial.git
cd django-tenants-tutorial
python -m venv .venv
source .venv/bin/activate
python contrib/env_gen.py
docker-compose up -d # O objetivo é rodar o PostgreSQL
git checkout base
pip install -U pip
pip install Django==4.0.7 django-tenants==3.4.3 django-extensions psycopg2-binary python-decouple
Obs: Na verdade já foi criado na branch.
django-admin startproject backend .
Configura o settings.py com o básico (já está pronto!).
# settings.py
from pathlib import Path
from decouple import Csv, config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv())
# ...
LANGUAGE_CODE = 'pt-br'
TIME_ZONE = 'America/Sao_Paulo'
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR.joinpath('staticfiles')Daqui pra baixo precisar implementar...
python contrib/env_gen.py
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django_tenants.postgresql_backend',
'NAME': config('POSTGRES_DB', 'db'), # postgres
'USER': config('POSTGRES_USER', 'postgres'),
'PASSWORD': config('POSTGRES_PASSWORD', 'postgres'),
# 'db' caso exista um serviço com esse nome.
'HOST': config('DB_HOST', '127.0.0.1'),
'PORT': 5433,
}
}E acompanhar pelo pgAdmin.
docker container ls
docker container exec -it db psql
CREATE DATABASE test;
\l
\c test
CREATE SCHEMA my_schema01;
\dn
CREATE TABLE my_schema01.cities (id SERIAL PRIMARY KEY, city VARCHAR(50), uf VARCHAR(2));
\dt my_schema01.*
INSERT INTO my_schema01.cities (city, uf) VALUES ('São Paulo', 'SP');
SELECT * FROM my_schema01.cities;
CREATE SCHEMA my_schema02;
\dn
CREATE TABLE my_schema02.cities (id SERIAL PRIMARY KEY, city VARCHAR(50), uf VARCHAR(2));
\dt my_schema02.*
INSERT INTO my_schema02.cities (city, uf) VALUES ('Bahia', 'BA');
SELECT * FROM my_schema02.cities;
DROP TABLE my_schema01.cities;
DROP TABLE my_schema02.cities;
DROP SCHEMA my_schema01;
DROP SCHEMA my_schema02;
\dn
# Saia do postgres
docker container stop pgadmin
docker container exec -it db psql
\c postgres
DROP DATABASE test;
\l
cd backend
python ../manage.py startapp tenant
cd ..
# tenant/apps.py
...
name = 'backend.tenant'# tenant/models.py
from django.db import models
from django_tenants.models import DomainMixin, TenantMixin
class Client(TenantMixin):
name = models.CharField(max_length=100)
on_trial = models.BooleanField(default=True)
created_on = models.DateField(auto_now_add=True)
# default true, schema will be automatically created and synced when it is saved
auto_create_schema = True
class Domain(DomainMixin):
...# tenant/admin.py
from django.apps import apps
from django.contrib import admin
from django_tenants.utils import get_public_schema_name
class TenantsAdmin(admin.ModelAdmin):
'''
Hides public models from tenants
https://stackoverflow.com/a/66898816
'''
def has_view_permission(self, request, view=None):
try:
if request.tenant.schema_name == get_public_schema_name():
return True
return False
except AttributeError:
return True
except Exception as e:
raise e
def has_add_permission(self, request, view=None):
return False
def has_change_permission(self, request, view=None):
return False
def has_delete_permission(self, request, view=None):
return False
def has_view_or_change_permission(self, request, view=None):
try:
if request.tenant.schema_name == get_public_schema_name():
return True
return False
except AttributeError:
return True
except Exception as e:
raise e
app = apps.get_app_config('tenant')
for model_name, model in app.models.items():
admin.site.register(model, TenantsAdmin)Deletando algumas coisas
rm -f backend/tenant/tests.py
rm -f backend/tenant/views.py
# settings.py
SHARED_APPS = (
'django_tenants', # mandatory
'backend.tenant', # you must list the app where your tenant model resides in
# 'backend.company',
# everything below here is optional
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# others apps
'django_extensions',
)
TENANT_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# your tenant-specific apps
# 'backend.core',
# 'backend.crm',
# 'backend.sale',
)
INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS]
TENANT_MODEL = "tenant.Client" # app.Model
TENANT_DOMAIN_MODEL = "tenant.Domain" # app.Model
MIDDLEWARE = [
'django_tenants.middleware.main.TenantMainMiddleware', # <<<
'django.middleware.security.SecurityMiddleware',
...
]
DATABASE_ROUTERS = (
'django_tenants.routers.TenantSyncRouter',
)python manage.py makemigrations # caso tenha alterações nos models
python manage.py migrate_schemas --shared
python manage.py create_tenant # Cria um novo tenant
python manage.py create_tenant_superuser # Cria um novo super user para o tenant escolhido
python manage.py migrate_schemas
schema name: acme
name: Acme Corp.
on trial: True
domain: acme.localhost
is_primary: True
schema name: stark
name: Stark Industries
on trial: True
domain: stark.localhost
is_primary: True
Já podemos acessar
acme.localhost:8000/admin
stark.localhost:8000/admin
Crie esses dois usuários.
stark: Howard Stark, Tony Stark
Se você acessar localhost:8000/admin vai dar erro, então
cp backend/urls.py backend/urls_public.py
# settings.py
PUBLIC_SCHEMA_URLCONF = 'backend.urls_public'
SHOW_PUBLIC_IF_NO_TENANT_FOUND = Truepython manage.py tenant_command shell_plus --schema=stark
>>> User.objects.all()
<QuerySet [<User: admin>, <User: howard>, <User: tony>]>python manage.py tenant_command shell_plus --schema=acme
Crie o usuário
acme: stewart
>>> User.objects.all()
<QuerySet [<User: admin>, <User: stewart>]>Company deve ser global.
cd backend
python ../manage.py startapp company
cd ..
# company/apps.py
...
name = 'backend.company'# company/models.py
from django.db import models
from backend.tenant.models import Client
class Company(models.Model):
name = models.CharField('nome', max_length=100, unique=True)
cnpj = models.CharField('CNPJ', max_length=14, unique=True, null=True, blank=True)
client = models.OneToOneField(
Client,
on_delete=models.CASCADE,
verbose_name='cliente',
related_name='companies',
)
class Meta:
ordering = ('name',)
verbose_name = 'empresa'
verbose_name_plural = 'empresas'
def __str__(self):
return f'{self.name}'# tenant/admin.py
...
class CompanyAdmin(admin.ModelAdmin):
'''
Hides public models from tenants
https://stackoverflow.com/a/66898816
'''
readonly_fields = ('client',)
def has_view_permission(self, request, view=None):
try:
if request.tenant.schema_name == get_public_schema_name():
return True
return False
except AttributeError:
return True
except Exception as e:
raise e
def has_add_permission(self, request, view=None):
return False
def has_change_permission(self, request, view=None):
try:
if request.tenant.schema_name == get_public_schema_name():
return True
return False
except AttributeError:
return True
except Exception as e:
raise e
def has_delete_permission(self, request, view=None):
return False
def has_view_or_change_permission(self, request, view=None):
try:
if request.tenant.schema_name == get_public_schema_name():
return True
return False
except AttributeError:
return True
except Exception as e:
raise e
...
app = apps.get_app_config('company')
for model_name, model in app.models.items():
admin.site.register(model, CompanyAdmin)Deletando algumas coisas
rm -f backend/company/admin.py
rm -f backend/company/tests.py
rm -f backend/company/views.py
SHARED_APPS = (
'django_tenants', # mandatory
'backend.tenant', # you must list the app where your tenant model resides in
'backend.company',
...
)python manage.py makemigrations
python manage.py migrate
Repare que temos Empresa no Admin público, mas não no tenant.
python manage.py shell_plus
acme = Client.objects.get(schema_name='acme')
Company.objects.create(name='Acme Corp.', cnpj='48085893000141', client=acme)
stark = Client.objects.get(schema_name='stark')
Company.objects.create(name='Stark Industries', cnpj='77863660000120', client=stark)cd backend
python ../manage.py startapp crm
cd ..
rm -f backend/crm/tests.py
# settings.py
TENANT_APPS = (
...
'backend.crm',
)# crm/apps.py
...
name = 'backend.crm'# crm/models.py
from django.contrib.auth.models import User
from django.db import models
class Employee(models.Model):
occupation = models.CharField('cargo', max_length=30, null=True, blank=True)
cpf = models.CharField('CPF', max_length=11, null=True, blank=True)
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
verbose_name='usuário',
related_name='employees',
)
class Meta:
ordering = ('user__first_name',)
verbose_name = 'funcionário'
verbose_name_plural = 'funcionários'
def __str__(self):
return f'{self.user.get_full_name()}'# crm/admin.py
from django.contrib import admin
from .models import Employee
@admin.register(Employee)
class EmployeeAdmin(admin.ModelAdmin):
list_display = ('__str__', 'cpf', 'occupation')
search_fields = ('user__first_name', 'user__last_name', 'user__email')Veja no Admin que o crm aparece nos tenants, mas não no público.
# crm/models.py
class Customer(models.Model):
name = models.CharField('nome', max_length=100, unique=True)
class Meta:
ordering = ('name',)
verbose_name = 'cliente'
verbose_name_plural = 'clientes'
def __str__(self):
return f'{self.name}'# crm/admin.py
from .models import Customer, Employee
@admin.register(Customer)
class CustomerAdmin(admin.ModelAdmin):
list_display = ('__str__',)
search_fields = ('name',)mkdir -p backend/crm/templates/crm
touch backend/crm/templates/crm/employee_form.html
<!-- employee_form.html -->
<form action="." method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Salvar</button>
</form>touch backend/crm/forms.py
# crm/forms.py
from django import forms
from .models import Employee
class EmployeeForm(forms.ModelForm):
required_css_class = 'required'
username = forms.CharField(
label='Usuário',
max_length=150,
)
first_name = forms.CharField(
label='Nome',
max_length=150,
)
last_name = forms.CharField(
label='Sobrenome',
max_length=150,
required=False,
)
email = forms.EmailField(
label='E-mail',
required=False,
)
class Meta:
model = Employee
fields = (
'username',
'first_name',
'last_name',
'email',
'occupation',
'cpf',
)# crm/views.py
from django.contrib.auth.models import User
from django.http import HttpResponse
from django.shortcuts import redirect, render
from .forms import EmployeeForm
from .models import Employee
def create_new_user(form):
# Cria o Usuário.
user = User.objects.create(
username=form.cleaned_data['username'],
first_name=form.cleaned_data['first_name'],
last_name=form.cleaned_data['last_name'],
email=form.cleaned_data['email'],
)
return user
def create_new_employee(form, user):
# Cria o Funcionário.
Employee.objects.create(
occupation=form.cleaned_data['occupation'],
cpf=form.cleaned_data['cpf'],
user=user,
)
def employee_create(request):
template_name = 'crm/employee_form.html'
form = EmployeeForm(request.POST or None)
if request.method == 'POST':
if form.is_valid():
form.save(commit=False)
user = create_new_user(form)
create_new_employee(form, user)
# return redirect('crm:employee_list')
return HttpResponse('OK')
context = {'form': form}
return render(request, template_name, context)touch backend/crm/urls.py
# crm/urls.py
from django.urls import path
from backend.crm import views as v
app_name = 'crm'
urlpatterns = [
path('employee/create/', v.employee_create, name='employee_create'),
]# urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('crm/', include('backend.crm.urls')),
path('admin/', admin.site.urls),
]Acesse http://stark.localhost:8000/crm/employee/create/
cd backend
python ../manage.py startapp sale
cd ..
rm -f backend/sale/tests.py
# settings.py
TENANT_APPS = (
...
'backend.crm',
'backend.sale',
)# sale/apps.py
...
name = 'backend.sale'# sale/models.py
from django.db import models
from backend.crm.models import Customer, Employee
class Sale(models.Model):
title = models.CharField('título', max_length=30)
customer = models.ForeignKey(
Customer,
on_delete=models.SET_NULL,
verbose_name='cliente',
related_name='customer_sales',
null=True,
blank=True
)
employee = models.ForeignKey(
Employee,
on_delete=models.SET_NULL,
verbose_name='vendedor',
related_name='employees_sales',
null=True,
blank=True
)
created = models.DateTimeField(
'criado em',
auto_now_add=True,
auto_now=False
)
modified = models.DateTimeField(
'modificado em',
auto_now_add=False,
auto_now=True
)
class Meta:
ordering = ('title',)
verbose_name = 'venda'
verbose_name_plural = 'vendas'
def __str__(self):
return f'{self.title}'# sale/admin.py
from django.contrib import admin
from backend.sale.models import Sale
@admin.register(Sale)
class SaleAdmin(admin.ModelAdmin):
list_display = ('__str__', 'customer', 'employee', 'created')
search_fields = ('title',)Acesse as vendas pelo Admin.
lista: clientes, funcionários e vendas
cd backend
python ../manage.py startapp core
cd ..
rm -f backend/core/admin.py
rm -f backend/core/models.py
rm -f backend/core/tests.py
touch backend/core/urls.py
# settings.py
TENANT_APPS = (
...
'backend.core',
'backend.crm',
'backend.sale',
)# core/apps.py
...
name = 'backend.core'# core/views.py
from django.shortcuts import render
from backend.crm.models import Customer, Employee
from backend.sale.models import Sale
def index(request):
template_name = 'index.html'
customers = Customer.objects.all()
employees = Employee.objects.all()
sales = Sale.objects.all()
context = {
'customers': customers,
'employees': employees,
'sales': sales,
}
return render(request, template_name, context)# core/urls.py
from django.urls import path
from .views import index
app_name = 'core'
urlpatterns = [
path('', index, name='index'),
]# urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('', include('backend.core.urls')),
path('crm/', include('backend.crm.urls')),
# path('sale/', include('backend.sale.urls')),
path('admin/', admin.site.urls),
]mkdir backend/core/templates
touch backend/core/templates/base.html
touch backend/core/templates/index.html
<!-- base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<link rel="shortcut icon" href="http://html5-training-in-hyderabad.blogspot.com.br/favicon.ico">
<link rel="shortcut icon" href="https://www.djangoproject.com/favicon.ico">
<title>Django Tenant</title>
<!-- Bulma -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
</head>
<body>
<div class="container is-fluid">
<div class="notification is-primary">
<h1 class="title"><b>{{ request.tenant.name }}</b></h1>
</div>
{% block content %}{% endblock content %}
</div>
</body>
</html><!-- index.html -->
{% extends "base.html" %}
{% block content %}
<div class="columns">
<div class="column">
<h1 class="title">
<a href="{% url 'crm:customer_list' %}">Clientes</a>
</h1>
<ul>
{% for customer in customers %}
<li>{{ customer }}</li>
{% endfor %}
</ul>
</div>
<div class="column">
<h1 class="title">
<a href="{% url 'crm:employee_list' %}">Funcionários</a>
</h1>
<ul>
{% for employee in employees %}
<li>{{ employee }}</li>
{% endfor %}
</ul>
</div>
<div class="column">
<h1 class="title">
<a href="{% url 'sale:sale_list' %}">Vendas</a>
</h1>
<ul>
{% for sale in sales %}
<li>{{ sale }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock content %}Acesse http://stark.localhost:8000/
touch backend/crm/templates/crm/customer_list.html
touch backend/crm/templates/crm/employee_list.html
<!-- customer_list.html -->
{% extends "base.html" %}
{% block content %}
<h1 class="title">Clientes
<a href="{% url 'core:index' %}">
<small>Voltar</small>
</a>
</h1>
<table class="table">
<thead>
<tr>
<th>Nome</th>
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr>
<td>{{ object }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}<!-- employee_list.html -->
{% extends "base.html" %}
{% block content %}
<h1 class="title">Funcionários
<a href="{% url 'core:index' %}">
<small>Voltar</small>
</a>
</h1>
<table class="table">
<thead>
<tr>
<th>Nome</th>
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr>
<td>{{ object }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}# crm/urls.py
urlpatterns = [
path('customer/', v.customer_list, name='customer_list'),
path('employee/', v.employee_list, name='employee_list'),
path('employee/create/', v.employee_create, name='employee_create'),
]# crm/views.py
from .models import Customer, Employee
...
def customer_list(request):
template_name = 'crm/customer_list.html'
object_list = Customer.objects.all()
context = {'object_list': object_list}
return render(request, template_name, context)
def employee_list(request):
template_name = 'crm/employee_list.html'
object_list = Employee.objects.all()
context = {'object_list': object_list}
return render(request, template_name, context)Arrume os links de index.html.
touch backend/sale/urls.py
mkdir -p backend/sale/templates/sale
touch backend/sale/templates/sale/sale_list.html
<!-- sale/sale_list.html -->
{% extends "base.html" %}
{% block content %}
<h1 class="title">Vendas
<a href="{% url 'core:index' %}">
<small>Voltar</small>
</a>
</h1>
<table class="table">
<thead>
<tr>
<th>Título</th>
<th>Data</th>
<th>Cliente</th>
<th>Funcionário</th>
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr>
<td>{{ object.title }}</td>
<td>{{ object.created|date:"d/m/Y" }}</td>
<td>{{ object.customer }}</td>
<td>{{ object.employee }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}# sale/urls.py
from django.urls import path
from backend.sale import views as v
app_name = 'sale'
urlpatterns = [
path('', v.sale_list, name='sale_list'),
]# sale/views.py
from django.shortcuts import render
from .models import Sale
def sale_list(request):
template_name = 'sale/sale_list.html'
object_list = Sale.objects.all()
context = {'object_list': object_list}
return render(request, template_name, context)# urls.py
urlpatterns = [
...
path('sale/', include('backend.sale.urls')),
path('admin/', admin.site.urls),
]<a href="{% url 'core:index' %}">
<small>Voltar</small>
</a>
<a class="button is-primary" href="{% url 'crm:employee_create' %}">Adicionar</a>{% extends "base.html" %}
{% block content %}
<div class="columns is-mobile is-centered">
<div class="column is-half">
<form action="." method="POST">
{% csrf_token %}
{{ form.as_p }}
<button class="button is-primary mt-3" type="submit">Salvar</button>
</form>
</div>
</div>
{% endblock content %}class EmployeeForm(forms.ModelForm):
...
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field_name, field in self.fields.items():
field.widget.attrs['class'] = 'input mb-3'def employee_create(request):
...
# return HttpResponse('OK')
return redirect('crm:employee_list')

