0

I'm writing because I have a big problem. Well, I have a project in Django where I am using django-tenants. Unfortunately, I can't run any tests as these end up with the following error when calling migrations: ‘django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table (no schema has been selected to create in LINE 1: CREATE TABLE ‘django_migrations’ (‘id’ bigint NOT NULL PRIMA...’

The problem is quite acute. I am fed up with regression errors and would like to write tests for the code. I will appreciate any suggestion. If you have any suggestions for improvements to the project, I'd love to read about that too.

Project details below. Best regards

Dependencies

    [tool.poetry.dependencies]
    python = "^3.13"
    django = "5.1.8"  # The newest version is not compatible with django-tenants yet
    django-tenants = "^3.7.0"
    dj-database-url = "^2.3.0"
    django-bootstrap5 = "^25.1"
    django-bootstrap-icons = "^0.9.0"
    uvicorn = "^0.34.0"
    uvicorn-worker = "^0.3.0"
    gunicorn = "^23.0.0"
    whitenoise = "^6.8.2"
    encrypt-decrypt-fields = "^1.3.6"
    django-bootstrap-modal-forms = "^3.0.5"
    django-model-utils = "^5.0.0"
    werkzeug = "^3.1.3"
    tzdata = "^2025.2"
    pytz = "^2025.2"
    psycopg = {extras = ["binary", "pool"], version = "^3.2.4"}
    django-colorfield = "^0.13.0"
    sentry-sdk = {extras = ["django"], version = "^2.25.1"}

Settings.py

    import os
    from pathlib import Path
    from uuid import uuid4
    
    # External Dependencies
    import dj_database_url
    from django.contrib.messages import constants as messages
    from django.utils.translation import gettext_lazy as _
    
    BASE_DIR = Path(__file__).resolve().parent.parent
    PROJECT_DIR = os.path.join(BASE_DIR, os.pardir)
    TENANT_APPS_DIR = BASE_DIR / "tenant"
    
    DEBUG = os.environ.get("DEBUG", "False").lower() in ["true", "1", "yes"]
    TEMPLATE_DEBUG = DEBUG
    SECRET_KEY = os.environ.get("SECRET_KEY", str(uuid4())) if DEBUG else os.environ["SECRET_KEY"]
    VERSION = os.environ.get("VERSION", "develop")
    SECURE_SSL_REDIRECT = os.environ.get("SECURE_SSL_REDIRECT", "False").lower() in ["true", "1", "yes"]
    
    try:
        ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(";")
    except KeyError:
        if not DEBUG:
            raise
    
    ALLOWED_HOSTS = ["localhost", ".localhost"]
    
    DEFAULT_DOMAIN = os.environ.get("DEFAULT_DOMAIN", ALLOWED_HOSTS[0])
    
    DEFAULT_FILE_STORAGE = "django_tenants.files.storage.TenantFileSystemStorage"
    
    SHARED_APPS = [
        "django_tenants",
        "sfe.common.apps.CommonConfig",
        "django.contrib.auth",
        "django.contrib.contenttypes",
        "django.contrib.sessions",
        "django.contrib.messages",
        "django.contrib.staticfiles",
    ]
    
    TENANT_APPS = [
        "django.contrib.auth",
        "django.contrib.contenttypes",
        "django.contrib.sessions",
        "django.contrib.messages",
        "django.contrib.staticfiles",
        "django_bootstrap5",
        "django_bootstrap_icons",
        "bootstrap_modal_forms",
        "sfe.tenant.apps.TenantConfig",
        "sfe.tenant.email_controller.apps.EmailControllerConfig",
    ]
    
    INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS]
    
    MIDDLEWARE = [
        "sfe.common.middleware.HealthCheckMiddleware",
        "django.middleware.security.SecurityMiddleware",
        "whitenoise.middleware.WhiteNoiseMiddleware",
        "django.contrib.sessions.middleware.SessionMiddleware",
        "django.middleware.locale.LocaleMiddleware",
        "django.middleware.common.CommonMiddleware",
        "django.middleware.csrf.CsrfViewMiddleware",
        "django.contrib.auth.middleware.AuthenticationMiddleware",
        "django.contrib.messages.middleware.MessageMiddleware",
        "django.middleware.clickjacking.XFrameOptionsMiddleware",
    ]
    
    if DEBUG:
        INTERNAL_IPS = ["localhost", ".localhost"]
    
    ROOT_URLCONF = "sfe.urls_tenant"
    PUBLIC_SCHEMA_URLCONF = "sfe.urls_public"
    
    TEMPLATES = [
        {
            "BACKEND": "django.template.backends.django.DjangoTemplates",
            "DIRS": [],
            "APP_DIRS": True,
            "OPTIONS": {
                "context_processors": [
                    "django.template.context_processors.debug",
                    "django.template.context_processors.request",
                    "django.contrib.auth.context_processors.auth",
                    "django.contrib.messages.context_processors.messages",
                    "sfe.common.context_processor.version",
                    "sfe.common.context_processor.default_domain",
                ],
            },
        },
    ]
    
    WSGI_APPLICATION = "sfe.wsgi.application"
    
    default_db = dj_database_url.config(engine="django_tenants.postgresql_backend")
    DATABASES = {
        "default": {
            "OPTIONS": {"pool": True},
            **default_db,
        }
    }
    DATABASE_ROUTERS = ("django_tenants.routers.TenantSyncRouter",)
    TEST_RUNNER = "django.test.runner.DiscoverRunner"
    
    AUTH_PASSWORD_VALIDATORS = [
        {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
        {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
        {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
        {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
    ]
    
    TENANT_MODEL = "common.SystemTenant"
    TENANT_DOMAIN_MODEL = "common.Domain"
    PUBLIC_SCHEMA_NAME = "public"
    
    LANGUAGE_CODE = "pl"
    LANGUAGES = [("pl", "Polski"), ("en", "English")]
    LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)
    
    TIME_ZONE = "UTC"
    USE_TZ = True
    USE_I18N = True
    
    PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
    STATIC_URL = "/static/"
    STATIC_ROOT = os.path.join(PROJECT_ROOT, "static")
    
    LOGIN_URL = _("/login/")
    LOGOUT_REDIRECT_URL = "/"
    LOGIN_REDIRECT_URL = "/"
    SESSION_COOKIE_AGE = 86400
    
    DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
    STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
    
    IMAP_TIMEOUT = 60
    ADMINS = [("Damian Giebas", "[email protected]")]
    MANAGERS = ADMINS
    
    FIRST_DAY_OF_WEEK = 1

Project structure: Structure

Simple test setup:

# External Dependencies
from django.utils.timezone import now
from django_tenants.test.cases import TenantTestCase
from django_tenants.utils import schema_context

# Current App
from sfe.tenant.models.due_date import DueDate


class DueDateModelTests(TenantTestCase):
    def setUp(self):
        super().setUp()

    def test_create_due_date(self):
        with schema_context(self.tenant.schema_name):
            DueDate.objects.create(date=now().date(), name="TestDueDate")

        assert DueDate.objects.all().count() == 1

EDIT: It turns out that the transition from django-tenant-schemas to django-tenants has not gone well. Unfortunately, running the migration on an empty database also ends with an error. Migrations in the tenant application do not execute. The problem is therefore not located in the configuration.

2 Answers 2

0

use
TEST_RUNNER = "django_tenants.test.runner.TenantTestRunner"
instead of
TEST_RUNNER = "django.test.runner.DiscoverRunner"

Also, i see your test set up, your syyntax is sort of off

modify your

def test_create_due_date(self):
to

def test_create_due_date(self):
    with schema_context(self.tenant.schema_name):
        DueDate.objects.create(date=now().date(), name="TestDueDate")
        self.assertEqual(DueDate.objects.all().count(), 1)
Sign up to request clarification or add additional context in comments.

Comments

0

Problem solved. I had to change one of my migration from:

    # External Dependencies
    from django.conf import settings
    from django.db import migrations
    
    default_tenant_data = {
        "domain_url": settings.DEFAULT_DOMAIN,
        "schema_name": "public",
        "name": "default",
        "paid_until": "2100-12-31",
        "on_trial": True,
    }
    demo_tenant_data = {
        "domain_url": f"demo.{settings.DEFAULT_DOMAIN}",
        "schema_name": "demo",
        "name": "Demo Sp. z o. o.",
        "paid_until": "2100-12-31",
        "on_trial": True,
    }
    
    
    def add_entry(apps, schema_editor):
        del schema_editor
        SystemTenant = apps.get_model("common", "SystemTenant")
        SystemTenant(**default_tenant_data).save()
        SystemTenant(**demo_tenant_data).save()
    
    
    def remove_entry(apps, schema_editor):
        del schema_editor
        SystemTenant = apps.get_model("common", "SystemTenant")
        SystemTenant.objects.filter(**default_tenant_data).delete()
        SystemTenant.objects.filter(**demo_tenant_data).delete()
    
    
    class Migration(migrations.Migration):
        dependencies = [("common", "0001_initial")]
        operations = [
            migrations.RunPython(add_entry, remove_entry),
        ]

to

    # External Dependencies
    from django.conf import settings
    from django.db import migrations
    
    default_tenant_data = {
        "domain_url": settings.DEFAULT_DOMAIN,
        "schema_name": "public",
        "name": "default",
        "paid_until": "2100-12-31",
        "on_trial": True,
    }
    demo_tenant_data = {
        "domain_url": f"demo.{settings.DEFAULT_DOMAIN}",
        "schema_name": "demo",
        "name": "Demo Sp. z o. o.",
        "paid_until": "2100-12-31",
        "on_trial": True,
    }
    
    
    def add_entry(apps, schema_editor):
        del schema_editor
        SystemTenant = apps.get_model("common", "SystemTenant")
        SystemTenant(**default_tenant_data).save()
        SystemTenant(**demo_tenant_data).save()
    
    
    def remove_entry(apps, schema_editor):
        del schema_editor
        SystemTenant = apps.get_model("common", "SystemTenant")
        SystemTenant.objects.filter(**default_tenant_data).delete()
        SystemTenant.objects.filter(**demo_tenant_data).delete()
    
    
    def create_demo_schema(apps, schema_editor):
        schema_editor.execute("CREATE SCHEMA IF NOT EXISTS demo;")
    
    
    def delete_demo_schema(apps, schema_editor):
        schema_editor.execute("DROP SCHEMA IF EXISTS demo CASCADE;")
    
    
    class Migration(migrations.Migration):
        dependencies = [("common", "0001_initial")]
        operations = [
            migrations.RunPython(add_entry, remove_entry),
            migrations.RunPython(create_demo_schema, delete_demo_schema),
        ]

Package django-tenant-schemas can create missing schema, django-tenants cannot. Pretty weird behaviour but I understand it is like it is.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.