This is my first exercise about DJANGO MULTITENANT based on the tutorial and django-blogx made by Academy Omen on Youtube
My Github repository: https://github.com/gurnitha/2022-multitenant-django-blogx
-> Download starter files for this project
-> Create Virtual environment
# Windows
py -3 -m venv env
# Linux and Mac
python -m venv env
-> Activate environment
# Windows
.\env\Scripts\activate
# Linux and Mac
source env/bin/activate
-> Install Requirements
pip install -r requirements.txt
-> Create Django project in the present directory
# the '.' tells python to create the project in the present folder
django-admin startproject core .
-> Create Blog app
python manage.py startapp blog
-> Register blog app in project settings file
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# register blog here
'blog',
]
-> Apply migrations
python manage.py migrate
-> Create basic views
from django.shortcuts import render
def home(request):
return render(request, 'index.html')
def article(request):
return render(request, 'article.html')
-> Create the blog app urls file
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.home, name='homepage'),
path('post/', views.article, name='article'),
]
-> Register the blog urls file in the project urls file
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# import include and add blog urls.py
path('', include('blog.urls', namespace='blog')),
]
-> Create blog app template directory in blog app directory
<!-- Example Home page -->
<h1>Home Page</h1>
-> Configure static files in settings file
import os
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'static/images')
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
-> Place the css files in static/css, images in static/images and the html in blog/templates
-> Load the static files in the html files
<!-- place this at top -->
{% load static %}
<!-- example -->
<link rel="stylesheet" href="{% static 'css/style.css' %}"">
-> Create Models and register to admin interface
python manage.py makemigrations
python manage.py migrate
# create superuser
python manage.py createsuperuser
python manage.py runserver
# blog admin.py file
from django.contrib import admin
from . import models
admin.site.register(models.Tag)
admin.site.register(models.Profile)
@admin.register(models.Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ('headline', 'status', 'slug', 'author')
prepopulated_fields = {'slug': ('headline',), }
-> Tell django where to get static files in development
# core.urls.py
from django.conf.urls.static import static
from django.conf import settings
# .
# .
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
-> Add ckeditor to installed app and add the settings
# settings.py
INSTALLED_APPS = [
# .
# .
'ckeditor',
'ckeditor_uploader',
]
# CKEditor settigs
CKEDITOR_UPLOAD_PATH = 'uploads/'
CKEDITOR_CONFIGS = {
'default': {
'toolbar': 'full',
'height': 300,
'width': '100%',
},
}
-> Add ckeditor urls
# core.urls.py
# .
# .
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls', namespace='blog')),
path('ckeditor/', include('ckeditor_uploader.urls')),
]
-> Collect statics all static so as to copy ckeditor required media files
python manage.py collectstatic
-> Add content to your database
-> Update home views and add load data on template
def home(request):
# feature articles on the home page
featured = Article.articlemanager.filter(featured=True)[0:3]
context = {
'articles': featured
}
return render(request, 'index.html', context)
-> Update articles views and add load data on template
# Django Q objects use to create complex queries
from django.db.models import Q
def articles(request):
# get query from request
query = request.GET.get('query')
# print(query)
# Set query to '' if None
if query == None:
query = ''
# articles = Article.articlemanager.all()
# search for query in headline, sub headline, body
articles = Article.articlemanager.filter(
Q(headline__icontains=query) |
Q(sub_headline__icontains=query) |
Q(body__icontains=query)
)
tags = Tag.objects.all()
context = {
'articles': articles,
'tags': tags,
}
return render(request, 'articles.html', context)
-> Add get_absolute_url to Article model which will be used to get a single article
# models.py file
def get_absolute_url(self):
return reverse('blog:article', args=[self.slug])
class Meta:
ordering = ('-publish',)
-> Update the blog urls file
# .
# .
urlpatterns = [
path('', views.home, name='home'),
path('articles/', views.articles, name='articles'),
# update the article url
path('<slug:article>/', views.article, name='article'),
]
# .
# .
-> Update articles views and add load data on template
def article(request, article):
article = get_object_or_404(Article, slug=article, status='published')
context = {
'article': article
}
return render(request, 'article.html', context)
-> 1. Clonning Django-Blogx for Multi Tenant Exercise
.gitignore
.vscode/
LICENSE
ReadMe.md
blog/
core/
db.sqlite3
manage.py
requirements.txt
static/
staticfiles/
-> 2. Install django tenants, psycopg2 and update requirements.txt file
pip install django-tenants
pip install psycopg2
pip freeze > requirements.txt
modified: ReadMe.md
modified: requirements.txt
-> 3. Modify Readme.md file
modified: ReadMe.md
-> 4. Create Postgres database and connect it with the blogx
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'django_dukcapildesabojongbaru_new',
'USERNAME': 'postgres',
'PASSWORD': 'xxx',
'HOST': 'localhost',
'PORT': '5432'
}
}
-> 5. Create superuser
# Create tables
(multitenant) λ python manage.py makemigrations
(multitenant) λ python manage.py migrate
# Create superuser
(multitenant) λ python manage.py createsuperuser
Username (leave blank to use 'hp'): admin
Email address: admin@admin.com
Password:
Password (again):
The password is too similar to the username.
This password is too short. It must contain at least 8 characters.
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
-> 6. Create new users and add some posts
new file: static/images/article/gladiator.jpg
new file: static/images/article/limitless.JPG
new file: static/images/article/luther.JPG
new file: static/images/profile/darling.PNG
new file: static/images/profile/ing.PNG
-> 7. Add DATABASE_ROUTERS
# DATABASE ROUTER
DATABASE_ROUTERS = (
'django_tenants.routers.TenantSyncRouter',
)
-> 8. Setup Middleware
MIDDLEWARE = [
# add this at the top
# django tenant middleware
'django_tenants.middleware.main.TenantMainMiddleware',
-> 9. Create tenant app and add tenant app to settings.py
# Create tenant app
(multitenant) λ python manage.py startapp tenant
# Add tentant app to settings.py
INSTALLED_APPS = [
...
'blog',
'tenant',
'ckeditor',
'ckeditor_uploader',
]
# new files
modified: .gitignore
modified: core/__pycache__/settings.cpython-39.pyc
modified: core/settings.py
new file: tenant/__init__.py
new file: tenant/admin.py
new file: tenant/apps.py
new file: tenant/migrations/__init__.py
new file: tenant/models.py
new file: tenant/tests.py
new file: tenant/views.py
-> 10. Add DATABASE_ROUTERS (repeating step no. 7 because of mistaken)
# DATABASE ROUTER
DATABASE_ROUTERS = (
'django_tenants.routers.TenantSyncRouter',
)
-> 11. Modified db engine, create Tenant model and run migration
# Change db engine
DATABASES = {
'default': {
'ENGINE': 'django_tenants.postgresql_backend',
# 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'multitenant_django_blogx_2022',
'USERNAME': 'postgres',
'PASSWORD': 'ing',
'HOST': 'localhost',
'PORT': '5432'
}
}
# Create Tenant model
from django.db import models
from django.contrib.auth.models import User
from django_tenants.models import DomainMixin, TenantMixin
class Tenant(TenantMixin):
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
blog_name = models.CharField(max_length=50)
blog_image = models.ImageField(null=True, blank=True, upload_to="profile")
featured = models.BooleanField(default=False)
updated_at = models.DateTimeField(auto_now=True, null=True)
description = models.TextField(blank=True)
is_active = models.BooleanField(default=False, blank=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
"""
USE THIS WITH CAUTION!
Set this flag to true on a parent class if you want the schema to be
automatically deleted if the tenant row gets deleted.
"""
auto_drop_schema = True
class Meta:
ordering = ('-featured', '-updated_at')
def __str__(self):
return f"{self.blog_name}"
class Domain(DomainMixin):
pass
# Run migration
# create migrations files
python manage.py makemigrations
# You may need to run migrations for specific app
python manage.py makemigrations blog
# Apply migrations
python manage.py migrate_schemas
# New/modified files
modified: ReadMe.md
modified: core/__pycache__/settings.cpython-39.pyc
modified: core/settings.py
modified: tenant/admin.py
new file: tenant/migrations/0001_initial.py
modified: tenant/models.py
-> 12. Setup Initial User, Tenant and Admin
# create first user
python manage.py createsuperuser
# Create the Public Schema
(multitenant) λ python manage.py create_tenant
schema name: public
user: 1
blog name: Main
blog image:
featured:
description:
is active: True
domain: localhost
is primary (leave blank to use 'True'):
# Create the Administrator
(multitenant) λ python manage.py create_tenant_superuser
Enter Tenant Schema ('?' to list schemas): ?
public - localhost
Enter Tenant Schema ('?' to list schemas): public
Username (leave blank to use 'hp'): admin1
Email address: admin1@email.com
Password:
Password (again):
The password is too similar to the username.
This password is too short. It must contain at least 8 characters.
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
# Run server
python manage.py runserver
# Logout and login as admin1
Go to: http://127.0.0.1:8000/admin/ and logout then login as admin1
# New/changed files
modified: ReadMe.md
-> 13. Create a custom middleware
from django_tenants.middleware.main import TenantMainMiddleware
class TenantMiddleware(TenantMainMiddleware):
"""
Field is_active can be used to temporary disable tenant and
block access to their site. Modifying get_tenant method from
TenantMiddleware allows us to check if tenant should be available
"""
def get_tenant(self, domain_model, hostname):
tenant = super().get_tenant(domain_model, hostname)
if not tenant.is_active:
raise self.TENANT_NOT_FOUND_EXCEPTION("Tenant is inactive")
return tenant
-> 14. Add the middleware and re-peat poin 8 (mistaken)
# add this at the top
# django tenant middleware
'django_tenants.middleware.main.TenantMainMiddleware',
# custom tenant middleware
'core.middleware.TenantMiddleware',
-> 15. Created blog1 schema from tenant's admin panel
# Check in the db you will see db schema like bellow:
multitenant_django_blogx_2022
> blog1
> public
# NOTE:
1. I created a new post, but it was display on the tenant's page.
2. To make the owner of blog1 to publish posts, he must first
create superuser.
-> 16. CreatE superuser for blog1, login and create posts
# Create superuser
(multitenant) λ python manage.py create_tenant_superuser
Enter Tenant Schema ('?' to list schemas): ?
blog1 - blog1.localhost
public - localhost
Enter Tenant Schema ('?' to list schemas): blog1
Username (leave blank to use 'hp'): adminblog1
Email address: adminblog1@email.com
Password:(adminblog1)
Password (again):(adminblog1)
The password is too similar to the username.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
# Login as admin of blog1
Go to: http://blog1.localhost:8000/admin
Username and password: adminblog1
# Create post
Create some post
# Testing
Go to: http://blog1.localhost:8000/
Refresh the browser
DONE :)
-> 17. Create a new db schema 'blog2'
STEPS:
1. Parent creates a new tenant, named 'blog2'
2. Activate
3. Create: create_tenant_superuser
4. Login
5. Create posts
6. Run the server, refresh browser
NOTE: Step 3 --> see poin 16 above
# Create superuser
(multitenant) λ python manage.py create_tenant_superuser
Enter Tenant Schema ('?' to list schemas): ?
blog1 - blog1.localhost
blog2 - blog2.localhost
public - localhost
Enter Tenant Schema ('?' to list schemas): blog2
Username (leave blank to use 'hp'): adminblog2
Email address: adminblog2@email.com
Password:
Password (again):
Error: Your passwords didn't match.
Password:
Password (again):
The password is too similar to the username.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
# New/modified files
modified: ReadMe.md
new file: static/images/article/darling.PNG
new file: static/images/profile/postgresql-card.png