Conceptos clave de Django
- Apuntes sobre Django
- Pycharm
- Python Hosting
- ¿Qué es Django?
- Django APPS
- Templates
- Modelos
- Panel de administrador
- Ficheros multimedia
- Relaciones (ORM)
- Internacionalización con Django
- Integrar Django + React
- Graphql con Django
- Multi-Tenant
https://data-flair.training/blogs/django-orm-tutorial/
Descarga el mejor editor de código para Python
https://www.jetbrains.com/es-es/pycharm
https://www.pythonanywhere.com/ https://heroku.com
- Django es un framework web programado en Python, gratuito y libre.
- Su nombre viene dado en alusión al guitarrista de jazz Django Reinhardt.
- Fue creado en 2003 cuando los desarrolladores web del diario Lawrence Journal-World migraron la arquitectura de su versión online a Python.
- En 2005 fue liberado al público bajo la licencia BSD y desde 2008 es la DSF (Django Software Foundation) quien se hace cargo del proyecto con la intención de apoyar el desarrollo, promover su uso, proteger su propiedad intelectual y asegurar su viabilidad a largo plazo.
- Es ámpliamente utilizado por grandes sitios web, entre los que destacan Mozilla, Pinterest, Instagram, Bitbucket, Eventbrite, el diario Washington Times y muchos otros.
- Promueve el desarrollo ágil y extensible, aplicando el principio DRY (don’t repeat yourself).
Algunas de sus características más importantes son:
- Un sistema basado en componentes reutilizables llamados “apps”.
- Un mapeador ORM para manejar registros de la base de datos como si fueran objetos, junto a una API muy robusta para su acceso.
- Un panel de administrador para gestionar esos objetos a través de formularios.
- Un potente sistema de plantillas extensibles con herencia basado en etiquetas.
- Y mil cosas más: sesiones, formularios, internacionalización, middlewares, testing, caché…
Para acabar destacar también la gran documentación oficial, un lugar perfecto para aprender, así como a su cada día más extensa comunidad de usuarios, lo que confirma su buen momento y el futuro que tiene por delante.
Consulta la documentación
https://www.djangoproject.com/start/
python -m venv env
python -m pip install Django
django-admin startproject mysite
Al crear un proyecto nuevo se creará una carpeta con el nombre de la aplicación junto a un fichero llamado manage.py. Este fichero es un script que sirve para gestionar todo nuestro proyecto desde la terminal.
La carpeta contiene los siguientes archivos,
- El fichero init.py nos indica que la carpeta es un paquete
- Settings.py es el que contiene la configuración del proyecto
- urls.py es el fichero encargado de manejar las direcciones de la web (sí esas que se escribirán en la barra del navegador)
- y por último wsgi.py, un script que contiene la información para desplegar el proyecto en producción.
Para ejecutar el servidor de desarrollo ejecutar:
python manage.py runserver
Django nos muestra DEBUG está en True variable que se encuentra en el settings.
Ahí vamos a encontrar una configuración muuuy extensa, llena de variables, listas, directorios… Por ahora quedémonos con la de DEBUG = Truem, nos indica una descripción justo encima "AVISO DE SEGURIDAD: no poner en marcha con el debug en producción".
El modo DEBUG, es un modo de ejecución especial en el que siempre que ocurra un fallo, Django nos mostrará un montón de información para analizarla y poder solucionarlo.
Podemos cambiar el idioma por defecto cambiando la variable del fichero settings: LANGUAGE_CODE a es_mx. Si queréis algo más específico consultad la documentación del enlace justo encima:
El siguiente paso que tenemos que llevar a cabo, justo después de crear un proyecto, es crear la base de datos inicial. Lo tenemos que hacer para que Django pueda gestionar un montón de cosas por nosotros, como por ejemplo las sesiones, los usuarios o los grupos. Para hacerlo vamos de vuelta al fichero settings.py y buscaremos el diccionario llamado DATABASES.
https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydatabase',
'USER': 'mydatabaseuser',
'PASSWORD': 'mypassword',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}
Migración de datos
python manage.py migrate
Django apuesta por un sistema de reutilización de código organizado en apps, algo así como aplicaciones internas que implementan funcionalidades específicas.
Las Apps activas en un proyecto de Django, las encontramos definidas en el fichero de configuración settings.py, en la lista INSTALLED_APPS:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
Por lo tanto podríamos concluir en que, mientras una app es una aplicación web que implementa una funcionalidad y por sí misma no sirve para nada, un proyecto es un conjunto de configuraciones a las que se "conectan" esas apps para que todo unido de lugar a un sitio web completo. Un proyecto puede contener múltiples apps, y una app puede ser incluida en múltiples proyectos.
Para crear apps basta con ejecutar el siguiente comando:
python manage.py startapp core
De todos estos ficheros el que nos interesa es ese llamado views.py. Este fichero es uno de los más importantes y en él se definen las vistas de la app. Una vista hace referencia a la lógica que se ejecuta cuando se hace una petición a nuestra web, y lo que vamos a hacer es crear una vista para procesar la petición a la raíz de nuestro sitio, lo que sería la portada.
Este método que nos permite contestar a una petición devolviendo un código, así que vamos a definir una vista para la portada y devolveremos algo de HTML de ejemplo:
from django.shortcuts import render, HttpResponse
def home(request):
return HttpResponse("<h1>Mi Web Personal</h1><h2>Portada</h2>")
Cada vista se corresponde con una función del fichero views.py.
Recibe un argumento llamado request
Para decirle a Django en qué URL tiene que mostrarla yourapp/urls.py
from django.contrib import admin
from django.urls import path
from core import views
urlpatterns = [
path('', views.home, name="home"),
path('admin/', admin.site.urls),
]
¿Qué significa esto? Pues que del package core (init.py) importamos el módulo views, es decir, de la app core importamos las vistas. Y a continuación creamos un patrón url, justo en la raíz del sitio (cadena vacía) desde el que llamaremos a la vista views.home a la que damos el nombre home.
def home(request):
html_response = "<h1>Mi Web Personal</h1>"
for i in range(10):
html_response += "<p>Esto es la portada</p>"
return HttpResponse(html_response)
Django nos ofrece la posibilidad de utilizar plantillas HTML mucho más cómodas y repletas de funcionalidades.
Lo primero es crear un directorio templates en nuestra app, que dentro debe contener otro directorio con el mismo nombre que la app. Los archivos tienen extensión .html
Ahora necesitamos cambiar nuestra vista para que funcione con el template
def home(request):
return render(request, "core/home.html")
Por defecto Django optimiza el uso de la memoria así que no carga las plantillas de una app que no esté instalada en settings.py. Para cargar la app core y sus plantillas en memoria debemos ir al fichero settings.py y añadir la app en la lista INSTALLED_APPS justo abajo del todo:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core', # <====
]
Ahora el home estará cargando desde un template
En programación, si estás repitiendo código, es que lo estás haciendo mal.
Podemos crear un archivo base.html para usar como base
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Mi Web Personal</title>
</head>
<body>
<h1>Mi Web Personal</h1>
<ul>
<li><a href="/">Portada</a></li>
<li><a href="/about/">Acerca de</a></li>
<li><a href="/portfolio/">Portafolio</a></li>
<li><a href="/contact/">Contacto</a></li>
</ul>
{% block content %}{% endblock %}
</body>
</html>
Esto es un template tag, una etiqueta de template, y sirve para añadir lógica de programación dentro del propio HTML.
En este caso el template tag block sirve para definir un bloque de contenido con un nombre.
Ahora podemos heredar en el template home.html
{% extends 'core/base.html' %}
{% block content %}
<h2>Mi Web Personal</h2>
<p>Bienvenidos.</p>
{% endblock %}
Se usa para enlazar templates
{% url 'nombre_en_el_path %'}
En la práctica es lo mismo, pero si vamos a nuestro urls.py y cambiamos una dirección, por ejemplo about por about-me
Sin cambiar absolutamente nada en el template, la url del menú se habrá actualizado y seguirá funcionando perfecto:
Nunca utilices hard-code para tus enlaces, en su lugar usa el template tag url.
Para manejar imagenes Django necesita Pillow
pip install Pillow
Para definir un nuevo modelo Proyecto nos vamos a dirigir al fichero models.py, pero no podemos hacerlo de cualquier forma, debemos seguir unas pautas.
Crear un modelo es fácil, sólo tenemos que crear una clase heredando de una clase padre llamada models.Model.
Es importante que el nombre de la clase siempre sea en singular:
from django.db import models
class Project(models.Model):
title = models.CharField(max_length=200)
description = models.TextField()
image = models.ImageField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
Esta clase representará una tabla dentro de la base de datos. Lo siguiente será crear sus columnas, que no serán otra cosa que los atributos de la clase.
Para migrar la app necesitaremos dos comandos:
El primero es makemigrations, que sirve para indicarle a Django que hay cambios en algún modelo, de manera que creará un fichero de migraciones. Si en el futuro tenemos algún problema, siempre podremos restaurar una migración anterior.
python manage.py makemigrations portfolio
Sólo nos falta aplicarla a la base de datos, que lo haremos con el comando migrate:
python manage.py migrate portfolio
Cada vez que hagamos un cambio en nuestro ficheros models.py ejecutaremos estos dos comandos para crear una migración y posteriormente aplicarla.
El panel de administrador de Django es una funcionalidad que viene creada por defecto. Para acceder tenemos que entrar a la dirección /admin de nuestra página
Sí abres el urls.py del proyecto veréis que ya está configurado por defecto para abrirse en esa dirección como si se tratara de otra app:
No tenemos ningún usuario creado. Vamos a crear uno, pero no uno cualquiera, sino el superusuario.
python manage.py createsuperuser
Para que nuestro modelo aparezca en el administrador tenemos que registrarlo en el fichero {your module app}/admin.py
from django.contrib import admin
from portfolio.models import Project
admin.site.register(Project)
Nuestra aplicación se llama Portfolio en inglés y quizá queremos que en el administrador aparezca Portafolio en español. Para lograrlo hay que cambiar dos cosas, primero añadir un campo verbose_name en el fichero portfolio/apps.py
from django.apps import AppConfig
class PortfolioConfig(AppConfig):
name = 'portfolio'
verbose_name = 'Portafolio'
De esta forma podemos establecer una configuración extendida.
Lo segundo es establecer esta configuración en settings.py, lo cual se hace llamando a esta clase PortfolioConfig en lugar de portfolio a secas:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core',
'portfolio.apps.PortfolioConfig', # <====
]
Vamos de vuelta a nuestro fichero de modelos e introduciremos dos nuevos conceptos para nuestros modelos, la subclase Meta y el método especial str.
Al crear la clase Proyecto hemos decidido ponerle Project para seguir una lógica en todo el proyecto, pero podemos cambiar el nombre a mostrar en el panel de forma muy sencilla creando una subclase con Meta información:
from django.db import models
class Project(models.Model):
title = models.CharField(max_length=200)
description = models.TextField()
image = models.ImageField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "proyecto"
verbose_name_plural = "proyectos"
ordering = ["-created"] # <===
Ordering es una lista porque permite ordenar con prioridades entre distintos campos. Además si añadimos un guión delante del nombre del campo, es posible ordenar de forma revertida. Al hacer -created, le indicamos que nos muestre primero los proyectos de más actuales a más antiguos.
Ahora para que nos aparezca el nombre del proyecto en el desplegable simplemente podemos redefinir el método especial str para que devuelva la cadena que nosotros queramos:
from django.db import models
class Project(models.Model):
title = models.CharField(max_length=200)
description = models.TextField()
image = models.ImageField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "proyecto"
verbose_name_plural = "proyectos"
ordering = ["-created"]
def __str__(self):
return self.title # <=====
En cuanto a los nombres de los campos, también podemos utilizar el atributo verbose_name para cambiarlos:
from django.db import models
class Project(models.Model):
title = models.CharField(max_length=200,
verbose_name="Título")
description = models.TextField(
verbose_name="Descripción")
image = models.ImageField(
verbose_name="Imagen")
created = models.DateTimeField(auto_now_add=True,
verbose_name="Fecha de creación")
updated = models.DateTimeField(auto_now=True,
verbose_name="Fecha de edición")
class Meta:
verbose_name = "proyecto"
verbose_name_plural = "proyectos"
ordering = ["-created"]
def __str__(self):
return self.title
Como podréis observar los campos de fecha y hora automatizados no aparecen en el adminstrador, Django los esconde para que no se puedan modificar, pero podemos mostrarlos como campos de tipo "sólo lectura".
Para hacerlo tenemos que extender un poco la configuración base del administrador de la siguiente forma:
from django.contrib import admin
from .models import Project
class ProjectAdmin(admin.ModelAdmin):
readonly_fields = ('created', 'updated')
admin.site.register(Project, ProjectAdmin)
class PostAdmin(admin.ModelAdmin):
readonly_fields = ('created', 'updated')
list_display = ('title', 'author', 'published')
ordering = ('author', 'published')
search_fields = ('translations__title',)
date_hierarchy = 'published'
list_filter = ('author__username', 'categories')
https://django-parler.readthedocs.io/en/stable/compatibility.html
Si quisiéramos indicar sólo un campo de ordenación igualmente debéis crear una tupla con por lo menos un campo y una coma, sino Django no entenderá que es una tupla.
Por defecto se cargan en la raíz del proyecto.
Para que Django pueda servir ficheros Media durante el desarrollo, necesitaremos crear un directorio donde almacenar todos estos archivos. Normalmente le llamaremos media y lo crearemos en la raíz de nuestro proyecto.
Ahora nos dirigiremos al fichero settings.py y abajo del todo añadiremos estas dos variables, una para indicar indicar la URL externa y otra para el directorio interno donde se encuentran los ficheros media (unido al core_dir del proyecto):
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
Ahora vamos a nuestro modelo Proyecto y vamos a añadir a la imagen un atributo llamado upload_to:
from django.db import models
class Project(models.Model):
title = models.CharField(max_length=200,
verbose_name="Título")
description = models.TextField(
verbose_name="Descripción")
image = models.ImageField(upload_to="projects", # <=====
verbose_name="Imagen")
created = models.DateTimeField(auto_now_add=True,
verbose_name="Fecha de creación")
updated = models.DateTimeField(auto_now=True,
verbose_name="Fecha de edición")
class Meta:
verbose_name = "proyecto"
verbose_name_plural = "proyectos"
ordering = ["-created"]
def __str__(self):
return self.title
Ahora fijaos en una cosa, si abrimos nuestro primer proyecto no podemos acceder a la imagen:
Vamos a nuestro fichero settings/urls.py y vamos a editarlo de la siguiente manera:
from django.contrib import admin
from django.urls import path
from core import views
from django.conf import settings # <=====
urlpatterns = [
path('', views.home, name="home"),
path('about-me/', views.about, name="about"),
path('portfolio/', views.portfolio, name="portfolio"),
path('contact/', views.contact, name="contact"),
path('admin/', admin.site.urls),
]
if settings.DEBUG:
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
Lo que hemos hecho es cargar el módulo de ficheros estáticos genérico y hacer que Django sirva ficheros como algo excepcional, sólo si tenemos el modo DEBUG activo.
Hasta ahora lo que hemos hecho no requería de interactuar con la base de datos Podríamos decir que simplemente se recibe una petición del navegador, se ejecuta la vista correspondiente y se renderiza el Template para que el navegador muestre el HTML resultante:
Sin embargo en el momento en que aparecen las base de datos y los modelos, este proceso se extendiende. Ahora se recibirá la petición, se pasará a la vista, en la vista recuperaremos los datos del modelo correspondiente, y finalmente la renderizaremos el Template pero esta vez integrando los datos dinámicos recuperados del modelo, antes de enviar el HTML final al navegador:
Es buena práctica separar las vistas como aplicaciones
from django.contrib import admin
from django.urls import path
from core import views as core_views
from portfolio import views as portfolio_views
from django.conf import settings
urlpatterns = [
path('', core_views.home, name="home"),
path('about-me/', core_views.about, name="about"),
path('portfolio/', portfolio_views.portfolio, name="portfolio"),
path('contact/', core_views.contact, name="contact"),
path('admin/', admin.site.urls),
]
if settings.DEBUG:
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
Separar las apps siempre viene bien. Nos permite organizar mejor el código y prepararlo para escalarlo en el futuro.
De vuelta a nuestra vista portfolio, necesitamos hacer referencia a nuestro modelo Project para recuperar sus instancias y enviarlas al template:
from django.shortcuts import render
from .models import Project
def portfolio(request):
projects = Project.objects.all()
return render(request, "portfolio/portfolio.html",
{'projects':projects}) # <=====
Con esto, en el template ya se podrá visualizar la información:
{% extends 'core/base.html' %}
{% load static %}
{% block title %}Portafolio{% endblock %}
{% block background %}{% static 'core/img/portfolio-bg.jpg' %}{% endblock %}
{% block headers %}
<h1>Portafolio</h1>
<span class="subheading">Currículo</span>
{% endblock %}
{% block content %}
{% for project in projects %}
Proyecto: {{project.title}} creado {{project.created}} <br>
{% endfor %}
{% endblock %}
Probamos la página y mostrará algo llamado QuerySet, como una lista y dentro aparece nuestro proyecto.
Un QuerySet es la representación del resultado de una consulta a la base de datos, pero devuelta como una lista de instancias. Al ser una especie de lista, lo bueno que tiene es que podemos iterarla con otro templatetag llamado for, y dentro de cada iteración mostrar para cada Proyecto sus atributos.
Hay varios tipos de relaciones que podemos hacer
# Relación por llave foranea
author = models.ForeignKey(User, on_delete=models.CASCADE,
verbose_name=_("Autor"))
# Relación 1 a muchos
categories = models.ManyToManyField
(Category,
verbose_name=_("Categorías"))
https://github.com/iris9112/WWCode-python
Django ofrece un robusto framework de internacionalización y localización para ayudarte a desarrollo aplicaciones para múltiples idiomas y regiones del mundo.
- Internacionalización: proceso de diseñar programas para el uso potencial de cualquier localidad (ciudad, país). Es decir poder traducir a diferentes idiomas elementos de la interfaz, texto, mensajes de error, y además configurar elementos como fechas y horas, de manera que sea posible respetar diferentes estándares locales. Generalmente hecho por desarrolladores.
Dato curioso: Encontrarás a menudo "internacionalización" abreviada como I18N dónde el número 18 se refiere al número de letras omitidos entre la I inicial y la N final.
- Localización: proceso específico de traducir un programa internacionalizado para su uso en una localidad en particular. Generalmente hecho por traductores.
Encontrarás a menudo "localización" abreviada como L10N
python-gettext==4.0
django>=2.2.4
django-parler==2.0.1
https://django-parler.readthedocs.io/en/stable/quickstart.html#installing-django-parler
https://steemit.com/utopian-io/@mateo.vergara/como-usar-el-sistema-de-traducciones-en-django-i18
Editar el Settings
from django.utils.translation import ugettext_lazy as _
...
MIDDLEWARE = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
...
TEMPLATES = [
{
...{
'context_processors': [
...
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.i18n', # Util para internacionalizacion
],
},
},
]
...
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'es-mx'
TIME_ZONE = 'America/Mexico_City'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LANGUAGES = [
('es', ('Español')),
('en', ('English'))
]
LOCALE_PATHS = [
os.path.join(BASE_DIR, 'locale')
]
# configuraciones django-parler
PARLER_LANGUAGES = {
None: (
{'code': 'es', },
{'code': 'en', },
),
'default': {
'fallback': 'es',
'hide_untranslated': True,
}
}
PARLER_DEFAULT_LANGUAGE = 'es'
PARLER_SHOW_EXCLUDED_LANGUAGE_TABS = True
Para todas las cadenas de traducción utiliza lo siguiente, aplica en el settings, modelos y vistas
from django.utils.translation import ugettext_lazy as _
# _("label")
Además en el archivo global de urls agregar:
path('i18n/', include('django.conf.urls.i18n')),
Lo primero es cargar la etiqueta {% load i18n %} en el template de django, para una línea sencilla utilizamos el templatetag {% trans 'texto' %} pero si tenemos contenido un poco más complejo, por ejemplo que incluya multiples elementos HTML o variables, utilizaremos los templatetag blocktrans / endblocktrans.
Ejemplo:
{% load i18n %}
{% comment %}Translators: por favor se creativo{% endcomment %}
<h1>{% trans '¡Bienvenido a nuestro sitio!' %}</h1>
<p>
{% blocktrans trimmed %}
Conoce nuestro servicio del mes {{ service.name }}
{% endblocktrans %}
</p>
https://docs.djangoproject.com/en/3.0/topics/i18n/translation/#gettext-on-windows
-
Debes tener instalado en Windows: https://mlocati.github.io/articles/gettext-iconv-windows.html
-
En linux apt-get install gettext
-
Requiere reiniciar IDE
django-admin makemessages -l en_us
django-admin makemessages -l es_mx
django-admin.py compilemessages
Django-parler ofrece la clase TranslatedModel para los modelos y el wrapper TranslatedFields para envolver los campos de los modelos que se quieren traducir.
# models.py
from django.db import models
from django.utils.translation import ugettext_lazy as _
from parler.models import TranslatableModel, TranslatedFields
class Category(TranslatableModel):
translations = TranslatedFields(
name=models.CharField(max_length=200, verbose_name=_('Nombre Categoría'))
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Fecha de creación"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Fecha de edición"))
class Meta:
verbose_name = _('Categoría')
verbose_name_plural = _('Categorías')
https://fedeg.gitlab.io/django-react-workshop/#/es/
Crear un proyecto Django
django-admin startproject ejemplo
Creamos una aplicación
django-admin startapp links
Agregamos la app en settings.py
Creamos una vista simple
from django.http import HttpResponse
def index(request):
return HttpResponse('Hola mundo')
Crear un componente en React con ES6, Clases, JSX, props Funciona como un template
import React from "react"
export default class HelloWorld extends React.Component{
render(){
return(<div>Hola mundo</div>)
}
}
WebPack Instalamos Django WebPack Loader / React Code Spliting
pip install django-webpack-loader
Una vez instalado el paquete se agrega en INSTALLED_APPS y en el settings.py la dirección de webpack
INSTALLED_APPS = [
...
'webpack_loader',
]
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'links/static'),
]
Consultas en Graphene
Instalar graphene
pip install graphene-django
Se agrega la aplicación
INSTALLED_APPS = [
...
'django.contrib.staticfiles', # Required for GraphiQL
'graphene_django'
]
Pasos de instalación completa
https://docs.graphene-python.org/projects/django/en/latest/installation/
https://django-tenant-schemas.readthedocs.io/en/latest/index.html