Corvia / django-tenant-users

Adds global user authentication and tenant-specific permissions to django-tenants.
https://django-tenant-users.rtfd.io
MIT License
335 stars 65 forks source link

CircularDependencyError #588

Closed killer24vrccp closed 4 months ago

killer24vrccp commented 4 months ago

Provide a general summary of the issue in the title above.

Expected Behavior

Just migrate data with python manage.py migrate_schemas --shared.

Actual Behavior

I get django.db.migrations.exceptions.CircularDependencyError: company.0001_initial, account.0001_initial

Possible Fix

Not obligatory, but you can suggest a fix or reason for the bug.

Screenshot 2024-05-12 223719

Dresdn commented 4 months ago

What do your models and migrations look like? Guessing you have a dependency between them.

killer24vrccp commented 4 months ago

Look respective screen shot. I have company and account apps.

image

image

image

Dresdn commented 4 months ago

That's a little helpful, but I don't see anything being related to one another. Can you share the full contents of the migrations? Also, maybe your SHARED_APPS and TENANT_APPS settings.

I'm wondering if each migration has dependencies set to one another.

killer24vrccp commented 4 months ago

Yes of course!

My settings is: image

Migration about Account app

# Generated by Django 4.2.13 on 2024-05-11 06:00

from django.db import migrations, models
import tenant_users.permissions.models

class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('company', '__first__'),
    ]

    operations = [
        migrations.CreateModel(
            name='TenantUser',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('password', models.CharField(max_length=128, verbose_name='password')),
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
                ('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email Address')),
                ('is_active', models.BooleanField(default=True, verbose_name='active')),
                ('is_verified', models.BooleanField(default=False, verbose_name='verified')),
                ('name', models.CharField(blank=True, max_length=64)),
                ('tenants', models.ManyToManyField(blank=True, help_text='The tenants this user belongs to.', related_name='user_set', to='company.company', verbose_name='tenants')),
            ],
            options={
                'abstract': False,
            },
            bases=(models.Model, tenant_users.permissions.models.PermissionsMixinFacade),
        ),
    ]

My migration about Company app

# Generated by Django 4.2.13 on 2024-05-11 06:00

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django_tenants.postgresql_backend.base

class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='Company',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('schema_name', models.CharField(db_index=True, max_length=63, unique=True, validators=[django_tenants.postgresql_backend.base._check_schema_name])),
                ('slug', models.SlugField(blank=True, verbose_name='Tenant URL Name')),
                ('created', models.DateTimeField(auto_now_add=True)),
                ('modified', models.DateTimeField(auto_now=True)),
                ('name', models.CharField(max_length=255, verbose_name='Name')),
                ('description', models.CharField(blank=True, null=True, verbose_name='Description')),
                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='Domain',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('domain', models.CharField(db_index=True, max_length=253, unique=True)),
                ('is_primary', models.BooleanField(db_index=True, default=True)),
                ('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', to='company.company')),
            ],
            options={
                'abstract': False,
            },
        ),
    ]
Dresdn commented 4 months ago

Your Company migration depends on the User model, and the User migration depends on the Company. I would delete your migrations and create the Company app first, and then the Account app.

killer24vrccp commented 4 months ago

I try and I have same result.

Migration removed. image

Result with my terminal:

(.venv) PS D:\Projet\TTLtenant> python .\manage.py makemigrations company
Migrations for 'company':
  app\generic\company\migrations\0001_initial.py
    - Create model Company
    - Create model Domain
(.venv) PS D:\Projet\TTLtenant> python .\manage.py makemigrations account
Migrations for 'account':
  app\generic\account\migrations\0001_initial.py
    - Create model TenantUser
(.venv) PS D:\Projet\TTLtenant> python .\manage.py migrate_schemas --shared
[standard:public] === Starting migration
Traceback (most recent call last):
  File ".\manage.py", line 22, in <module>
    main()
  File ".\manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django\core\management\__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django\core\management\__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django\core\management\base.py", line 412, in run_from_argv
    self.execute(*args, **cmd_options)
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django\core\management\base.py", line 458, in execute
    output = self.handle(*args, **options)
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django_tenants\management\commands\migrate_schemas.py", line 63, in handle
    executor.run_migrations(tenants=[self.PUBLIC_SCHEMA_NAME])
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django_tenants\migration_executors\standard.py", line 11, in run_migrations
    run_migrations(self.args, self.options, self.codename, self.PUBLIC_SCHEMA_NAME)
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django_tenants\migration_executors\base.py", line 59, in run_migrations
    migrate_command_class(stdout=stdout, stderr=stderr).execute(*args, **options)
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django\core\management\base.py", line 458, in execute
    output = self.handle(*args, **options)
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django\core\management\base.py", line 106, in wrapper
    res = handle_func(*args, **kwargs)
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django\core\management\commands\migrate.py", line 117, in handle
    executor = MigrationExecutor(connection, self.migration_progress_callback)
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django\db\migrations\executor.py", line 18, in __init__
    self.loader = MigrationLoader(self.connection)
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django\db\migrations\loader.py", line 58, in __init__
    self.build_graph()
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django\db\migrations\loader.py", line 305, in build_graph
    self.graph.ensure_not_cyclic()
  File "D:\Projet\TTLtenant\.venv\lib\site-packages\django\db\migrations\graph.py", line 284, in ensure_not_cyclic
    raise CircularDependencyError(
django.db.migrations.exceptions.CircularDependencyError: account.0001_initial, company.0001_initial

Thanks you for your helpfull!

Dresdn commented 4 months ago

Interesting ... what do the migrations contain? Try to include details to help troubleshoot.

killer24vrccp commented 4 months ago

This is migrations for account:

# Generated by Django 4.2.13 on 2024-05-16 04:17

from django.db import migrations, models
import tenant_users.permissions.models

class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('company', '0001_initial'),
    ]

    operations = [
        migrations.CreateModel(
            name='TenantUser',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('password', models.CharField(max_length=128, verbose_name='password')),
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
                ('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email Address')),
                ('is_active', models.BooleanField(default=True, verbose_name='active')),
                ('is_verified', models.BooleanField(default=False, verbose_name='verified')),
                ('name', models.CharField(blank=True, max_length=64)),
                ('tenants', models.ManyToManyField(blank=True, help_text='The tenants this user belongs to.', related_name='user_set', to='company.company', verbose_name='tenants')),
            ],
            options={
                'abstract': False,
            },
            bases=(models.Model, tenant_users.permissions.models.PermissionsMixinFacade),
        ),
    ]

Migration for company:

# Generated by Django 4.2.13 on 2024-05-16 04:17

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django_tenants.postgresql_backend.base

class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='Company',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('schema_name', models.CharField(db_index=True, max_length=63, unique=True, validators=[django_tenants.postgresql_backend.base._check_schema_name])),
                ('slug', models.SlugField(blank=True, verbose_name='Tenant URL Name')),
                ('created', models.DateTimeField(auto_now_add=True)),
                ('modified', models.DateTimeField(auto_now=True)),
                ('name', models.CharField(max_length=255, verbose_name='Name')),
                ('description', models.CharField(blank=True, null=True, verbose_name='Description')),
                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='Domain',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('domain', models.CharField(db_index=True, max_length=253, unique=True)),
                ('is_primary', models.BooleanField(db_index=True, default=True)),
                ('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', to='company.company')),
            ],
            options={
                'abstract': False,
            },
        ),
    ]

I don't know if can help but I add some additional settings

image

And my MiddlerWares: image

My project structure: image

If I start with the example that's work but when I copy exactly the samethings that's don't work. It's very weird. I don't understand why and I give that at ChatGPT or Gemini and nothing that's work for resolve it.

Dresdn commented 4 months ago

Looks like it created the same migration dependencies. Are you sure there's no dependency between any of the Company models and the UserModel or anything from Account app?

killer24vrccp commented 4 months ago

It's a new project and I have setup only django-tenants and django-tenant-users. All models include there models is on screenshot 😪

Dresdn commented 4 months ago

I would go step by step. Delete the migrations, create the migrations for Account. See what it contains. Then do Company, see what that contains. Is there a circular dependency or not? Does that work?

look at the test app here and you’ll see the migration dependencies on the initial migrations.

killer24vrccp commented 4 months ago

I have rerun migration and I compare difference:

My model: image

Example model:

from django.db import migrations, models

from tenant_users.permissions.models import PermissionsMixinFacade

class Migration(migrations.Migration):
    """Initial migration for test_django_tenants.users app."""

    initial = True

    dependencies = [
        ("companies", "0001_initial"),
    ]

    operations = [
        migrations.CreateModel(
            name="TenantUser",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                (
                    "password",
                    models.CharField(
                        max_length=128,
                        verbose_name="password",
                    ),
                ),
                (
                    "last_login",
                    models.DateTimeField(
                        blank=True,
                        null=True,
                        verbose_name="last login",
                    ),
                ),
                (
                    "email",
                    models.EmailField(
                        db_index=True,
                        max_length=254,
                        unique=True,
                        verbose_name="Email Address",
                    ),
                ),
                (
                    "is_active",
                    models.BooleanField(
                        default=True,
                        verbose_name="active",
                    ),
                ),
                (
                    "is_verified",
                    models.BooleanField(default=False, verbose_name="verified"),
                ),
                ("name", models.CharField(blank=True, max_length=64)),
                (
                    "tenants",
                    models.ManyToManyField(
                        blank=True,
                        help_text="The tenants this user belongs to.",
                        related_name="user_set",
                        to="companies.Company",
                        verbose_name="tenants",
                    ),
                ),
            ],
            options={
                "abstract": False,
            },
            bases=(
                models.Model,
                PermissionsMixinFacade,
            ),
        ),
    ]

In my company:

# Generated by Django 4.2.13 on 2024-05-16 05:05

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django_tenants.postgresql_backend.base

class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='Company',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('schema_name', models.CharField(db_index=True, max_length=63, unique=True, validators=[django_tenants.postgresql_backend.base._check_schema_name])),
                ('slug', models.SlugField(blank=True, verbose_name='Tenant URL Name')),
                ('created', models.DateTimeField(auto_now_add=True)),
                ('modified', models.DateTimeField(auto_now=True)),
                ('name', models.CharField(max_length=255, verbose_name='Name')),
                ('description', models.CharField(blank=True, null=True, verbose_name='Description')),
                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='Domain',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('domain', models.CharField(db_index=True, max_length=253, unique=True)),
                ('is_primary', models.BooleanField(db_index=True, default=True)),
                ('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', to='company.company')),
            ],
            options={
                'abstract': False,
            },
        ),
    ]

In example migration:

from django.db import migrations, models
from django_tenants.postgresql_backend.base import (
    _check_schema_name as check_schema_name,
)

class Migration(migrations.Migration):
    """Initial migration for test_django_tenants.companies app."""

    initial = True

    dependencies = []

    operations = [
        migrations.CreateModel(
            name="Company",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                (
                    "schema_name",
                    models.CharField(
                        db_index=True,
                        max_length=63,
                        unique=True,
                        validators=[check_schema_name],
                    ),
                ),
                (
                    "slug",
                    models.SlugField(
                        blank=True,
                        verbose_name="Tenant URL Name",
                    ),
                ),
                ("created", models.DateTimeField()),
                ("modified", models.DateTimeField(blank=True)),
                ("name", models.CharField(max_length=64)),
                ("description", models.TextField()),
            ],
            options={
                "abstract": False,
            },
        ),
        migrations.CreateModel(
            name="Domain",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                (
                    "domain",
                    models.CharField(
                        db_index=True,
                        max_length=253,
                        unique=True,
                    ),
                ),
                (
                    "is_primary",
                    models.BooleanField(
                        db_index=True,
                        default=True,
                    ),
                ),
                (
                    "tenant",
                    models.ForeignKey(
                        on_delete=models.deletion.CASCADE,
                        related_name="domains",
                        to="companies.company",
                    ),
                ),
            ],
            options={
                "abstract": False,
            },
        ),
    ]
killer24vrccp commented 4 months ago

It's better if I start with example project ?

killer24vrccp commented 4 months ago

I have fixed issue. I have copied django-tenant-users test on this Github and I stole existing migration. I think migration is write manually.

image