Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/validate-tutorial.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Validate Tutorial

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
validate-tutorial:
runs-on: ubuntu-latest

defaults:
run:
working-directory: examples/tenant_tutorial

services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: root
POSTGRES_DB: tenant_tutorial
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Change to tutorial directory
run: pwd # Just to verify we're in the right directory

- name: Check dependencies
run: |
./manage.py check

- name: Verify database connection
run: |
echo "SELECT 1 AS test" | ./manage.py dbshell

- name: Run initial migrations
run: |
./manage.py migrate_schemas --shared

- name: Create public tenant
run: |
./manage.py create_client public localhost "Tutorial Public Tenant" --description "Public tenant for tutorial validation"

- name: Create sample tenants
run: |
./manage.py create_client tenant1 tenant1.example.com "Tenant 1 - Test Company" --description "First test tenant for validation"
./manage.py create_client tenant2 tenant2.example.com "Tenant 2 - Another Company" --description "Second test tenant for validation"

- name: Verify tenants were created
run: |
! ./manage.py list_tenants | grep -v -P '^(public|tenant[12])\s'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.DS_Store
VERSION
_build/
build/
dist/
*.egg-info
.eggs/
Expand Down
44 changes: 41 additions & 3 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,48 @@ Examples
===========================
Tenant Tutorial
-----------------
This app comes with an interactive tutorial to teach you how to use ``django-tenant-schemas`` and to demonstrate its capabilities. This example project is available under `examples/tenant_tutorial <https://github.com/bernardopires/django-tenant-schemas/blob/master/examples/tenant_tutorial>`_. You will only need to edit the ``settings.py`` file to configure the ``DATABASES`` variable and then you're ready to run
This app comes with an interactive tutorial to teach you how to use ``django-tenant-schemas`` and to demonstrate its capabilities. This example project is available under `examples/tenant_tutorial <https://github.com/bernardopires/django-tenant-schemas/blob/master/examples/tenant_tutorial>`_.

Setup Instructions
~~~~~~~~~~~~~~~~~~

**Prerequisites**: This tutorial requires `uv <https://docs.astral.sh/uv/>`_, a fast Python package manager. Install it by following the `uv installation guide <https://docs.astral.sh/uv/getting-started/installation/>`_.

1. **Check dependencies**: Navigate to the tutorial directory and run the ``check`` management command:

.. code-block:: bash

cd examples/tenant_tutorial
./manage.py check

2. **Configure the database**: Edit the ``settings.py`` file to configure the ``DATABASES`` variable for your PostgreSQL setup. The default configuration expects a PostgreSQL database named ``tenant_tutorial`` with user ``postgres`` and password ``root`` on localhost.

.. code-block:: bash

echo "SELECT 1 AS test" | ./manage.py dbshell

3. **Run initial migrations**: Use ``migrate_schemas`` instead of the regular ``migrate`` command:

.. code-block:: bash

./manage.py runserver
./manage.py migrate_schemas --shared

4. **Create the public tenant**: Use ``create_client`` command (defined in ``customers`` app:

.. code-block:: bash

./manage.py create_client public localhost "Tutorial Public Tenant" --description "Public tenant for tutorial validation"

4. **Start the development server**:

.. code-block:: bash

./manage.py runserver localhost:9000

All other steps will be explained by following the tutorial, just open ``http://localhost:9000`` in your browser.

**Important Notes:**

All other steps will be explained by following the tutorial, just open ``http://127.0.0.1:8000`` on your browser.
- Always use ``migrate_schemas`` instead of ``migrate`` when working with tenant schemas
- The tutorial uses ``ALLOWED_HOSTS = ['*']`` for development convenience - be more restrictive in production
- Make sure PostgreSQL is installed and running before starting the tutorial
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from django.core.management.base import BaseCommand, CommandError
from customers.models import Client


class Command(BaseCommand):
help = 'Create a new tenant client'

def add_arguments(self, parser):
parser.add_argument('schema', type=str, help='Schema name for the tenant')
parser.add_argument('domain', type=str, help='Domain URL for the tenant')
parser.add_argument('name', type=str, help='Name of the tenant')
parser.add_argument('--description', type=str, help='Description of the tenant', default='')

def handle(self, *args, **options):
schema_name = options['schema']
domain_url = options['domain']
name = options['name']
description = options['description']

# Check if tenant with this schema already exists
if Client.objects.filter(schema_name=schema_name).exists():
raise CommandError(f'Tenant with schema "{schema_name}" already exists')

# Check if tenant with this domain already exists
if Client.objects.filter(domain_url=domain_url).exists():
raise CommandError(f'Tenant with domain "{domain_url}" already exists')

# Create the tenant
client = Client(
schema_name=schema_name,
domain_url=domain_url,
name=name,
description=description
)
client.save()

self.stdout.write(
self.style.SUCCESS(
f'Successfully created tenant "{name}" with schema "{schema_name}" and domain "{domain_url}"'
)
)
24 changes: 10 additions & 14 deletions examples/tenant_tutorial/customers/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-28 15:44
from __future__ import unicode_literals
# Generated by Django 4.2 on 2025-09-26 14:26

from django.db import migrations, models
import tenant_schemas.postgresql_backend.base


class Migration(migrations.Migration):

initial = True

dependencies = [
]
dependencies = []

operations = [
migrations.CreateModel(
name='Client',
name="Client",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('domain_url', models.CharField(max_length=128, unique=True)),
('schema_name', models.CharField(max_length=63, unique=True, validators=[tenant_schemas.postgresql_backend.base._check_schema_name])),
('name', models.CharField(max_length=100)),
('description', models.TextField(max_length=200)),
('created_on', models.DateField(auto_now_add=True)),
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("domain_url", models.CharField(max_length=128, unique=True)),
("schema_name", models.CharField(max_length=63, unique=True, validators=[tenant_schemas.postgresql_backend.base._check_schema_name])),
("name", models.CharField(max_length=100)),
("description", models.TextField(max_length=200)),
("created_on", models.DateField(auto_now_add=True)),
],
options={
'abstract': False,
"abstract": False,
},
),
]
11 changes: 10 additions & 1 deletion examples/tenant_tutorial/manage.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
#!/usr/bin/env python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "django-tenant-schemas[psycopg]",
# ]
#
# [tool.uv.sources]
# django-tenant-schemas = { path = "../../", editable = true }
# ///
import os
import sys

Expand Down
15 changes: 12 additions & 3 deletions examples/tenant_tutorial/tenant_tutorial/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

DEBUG = True

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

ADMINS = (
# ('Your Name', '[email protected]'),
)
Expand All @@ -23,7 +25,7 @@

# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ["localhost", ".trendy-sass.com"]
ALLOWED_HOSTS = ['*'] # For development purposes - be more restrictive in production

# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
Expand Down Expand Up @@ -90,7 +92,7 @@
TEST_RUNNER = "django.test.runner.DiscoverRunner"

MIDDLEWARE = (
"tenant_tutorial.middleware.TenantTutorialMiddleware",
"tenant_schemas.middleware.TenantMiddleware",
"django.middleware.common.CommonMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
Expand Down Expand Up @@ -151,7 +153,14 @@

TENANT_MODEL = "customers.Client" # app.Model

DEFAULT_FILE_STORAGE = "tenant_schemas.storage.TenantFileSystemStorage"
STORAGES = {
"default": {
"BACKEND": "tenant_schemas.storage.TenantFileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}

INSTALLED_APPS = (
"tenant_schemas",
Expand Down
4 changes: 2 additions & 2 deletions examples/tenant_tutorial/tenant_tutorial/urls_public.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.conf.urls import url
from django.urls import path
from tenant_tutorial.views import HomeView


urlpatterns = [
url(r'^$', HomeView.as_view()),
path('', HomeView.as_view()),
]
4 changes: 2 additions & 2 deletions examples/tenant_tutorial/tenant_tutorial/urls_tenants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from customers.views import TenantView
from django.conf.urls import url
from django.urls import path

urlpatterns = [
url(r'^$', TenantView.as_view()),
path('', TenantView.as_view()),
]
10 changes: 10 additions & 0 deletions src/tenant_schemas/management/commands/migrate_schemas.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import django
from django.core.management.commands.migrate import Command as MigrateCommand
from django.db.migrations.autodetector import MigrationAutodetector
from django.db.migrations.exceptions import MigrationSchemaMissing
Expand All @@ -19,6 +20,15 @@ class Command(SyncCommon):

def add_arguments(self, parser):
super().add_arguments(parser)

# Only add --skip-checks for Django 5.2+
if django.VERSION >= (5, 2):
parser.add_argument(
'--skip-checks',
action='store_true',
help='Skip system checks.',
)

command = MigrateCommand()
command.add_arguments(parser)

Expand Down