-
Notifications
You must be signed in to change notification settings - Fork 272
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: multi tenancy basic configuration (#480)
* Add multitenancy app with some basic model configuration * Add creation of signup tenant and add it in currentUser query schema. * Add tests * Add short documentation for multi tenancy manager * PR fixes & linter fix * Add migration * Documentation grammar
- Loading branch information
Showing
18 changed files
with
290 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# from django.contrib import admin | ||
|
||
# Register your models here. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class MultitenancyConfig(AppConfig): | ||
default_auto_field = 'django.db.models.BigAutoField' | ||
name = 'apps.multitenancy' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from django.db import models | ||
|
||
|
||
class TenantType(models.TextChoices): | ||
""" | ||
DEFAULT is a tenant type created during user sign-up. | ||
It serves as the default tenant for them, ensuring that they always have at least one. | ||
ORGANIZATION is a tenant type for tenants created manually by the user for the purpose of inviting other members. | ||
""" | ||
|
||
DEFAULT = "default", "Default" | ||
ORGANIZATION = "organization", "Organization" | ||
|
||
|
||
class TenantUserRole(models.TextChoices): | ||
OWNER = 'owner', 'Owner' | ||
ADMIN = 'admin', 'Administrator' | ||
MEMBER = 'member', 'Member' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from django.db import models | ||
|
||
from .constants import TenantType | ||
|
||
|
||
class TenantManager(models.Manager): | ||
def get_or_create_user_default_tenant(self, user): | ||
""" | ||
Description: | ||
Retrieves or creates a default tenant for a given user, ensuring that there is always at least one tenant | ||
instance associated with them. | ||
Parameters: | ||
- user (User): The user for whom the tenant is retrieved or created. | ||
Returns: | ||
Tenant: The associated or newly created tenant instance of SIGN_UP type. | ||
""" | ||
default_tenant = self.filter(creator=user, type=TenantType.DEFAULT).order_by('created_at').first() | ||
if default_tenant: | ||
return default_tenant, False | ||
|
||
new_tenant = self.create(creator=user, type=TenantType.DEFAULT, name=str(user)) | ||
new_tenant.members.add(user) | ||
|
||
return new_tenant, True |
84 changes: 84 additions & 0 deletions
84
packages/backend/apps/multitenancy/migrations/0001_initial.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# Generated by Django 4.2 on 2024-02-14 11:49 | ||
|
||
from django.conf import settings | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
import hashid_field.field | ||
|
||
|
||
class Migration(migrations.Migration): | ||
initial = True | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='Tenant', | ||
fields=[ | ||
('created_at', models.DateTimeField(auto_now_add=True)), | ||
('updated_at', models.DateTimeField(auto_now=True)), | ||
( | ||
'id', | ||
hashid_field.field.HashidAutoField( | ||
alphabet='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', | ||
min_length=7, | ||
prefix='', | ||
primary_key=True, | ||
serialize=False, | ||
), | ||
), | ||
('name', models.CharField(max_length=100, unique=True)), | ||
('slug', models.SlugField(max_length=100, unique=True)), | ||
('type', models.CharField(choices=[('default', 'Default'), ('organization', 'Organization')])), | ||
( | ||
'creator', | ||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), | ||
), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name='TenantMembership', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('created_at', models.DateTimeField(auto_now_add=True)), | ||
('updated_at', models.DateTimeField(auto_now=True)), | ||
( | ||
'role', | ||
models.CharField( | ||
choices=[('owner', 'Owner'), ('admin', 'Administrator'), ('member', 'Member')], default='owner' | ||
), | ||
), | ||
( | ||
'tenant', | ||
models.ForeignKey( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
related_name='user_memberships', | ||
to='multitenancy.tenant', | ||
), | ||
), | ||
( | ||
'user', | ||
models.ForeignKey( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
related_name='tenant_memberships', | ||
to=settings.AUTH_USER_MODEL, | ||
), | ||
), | ||
], | ||
options={ | ||
'unique_together': {('user', 'tenant')}, | ||
}, | ||
), | ||
migrations.AddField( | ||
model_name='tenant', | ||
name='members', | ||
field=models.ManyToManyField( | ||
blank=True, related_name='tenants', through='multitenancy.TenantMembership', to=settings.AUTH_USER_MODEL | ||
), | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import hashid_field | ||
|
||
from django.db import models | ||
from django.conf import settings | ||
from django.utils.text import slugify | ||
|
||
from . import constants | ||
from .managers import TenantManager | ||
from common.models import TimestampedMixin | ||
|
||
|
||
class Tenant(TimestampedMixin, models.Model): | ||
id: str = hashid_field.HashidAutoField(primary_key=True) | ||
creator: settings.AUTH_USER_MODEL = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) | ||
name: str = models.CharField(max_length=100, unique=True) | ||
slug: str = models.SlugField(max_length=100, unique=True) | ||
type: str = models.CharField(choices=constants.TenantType.choices) | ||
members = models.ManyToManyField( | ||
settings.AUTH_USER_MODEL, through='TenantMembership', related_name='tenants', blank=True | ||
) | ||
|
||
objects = TenantManager() | ||
|
||
def __str__(self): | ||
return self.name | ||
|
||
def save(self, *args, **kwargs): | ||
if not self.slug: | ||
self.slug = slugify(self.name) | ||
super().save(*args, **kwargs) | ||
|
||
|
||
class TenantMembership(TimestampedMixin, models.Model): | ||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="tenant_memberships") | ||
role = models.CharField(choices=constants.TenantUserRole.choices, default=constants.TenantUserRole.OWNER) | ||
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name="user_memberships") | ||
|
||
class Meta: | ||
unique_together = ('user', 'tenant') | ||
|
||
def __str__(self): | ||
return f"{self.user.email} {self.tenant.name} {self.role}" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import factory | ||
|
||
from .. import models | ||
from .. import constants | ||
|
||
|
||
class TenantFactory(factory.django.DjangoModelFactory): | ||
creator = factory.SubFactory("apps.users.tests.factories.UserFactory") | ||
type = factory.Iterator(constants.TenantType.values) | ||
name = factory.Faker('pystr') | ||
slug = factory.Faker('pystr') | ||
created_at = factory.Faker('date_time') | ||
updated_at = factory.Faker('date_time') | ||
|
||
class Meta: | ||
model = models.Tenant | ||
|
||
|
||
class TenantMembershipFactory(factory.django.DjangoModelFactory): | ||
user = factory.SubFactory("apps.users.tests.factories.UserFactory") | ||
tenant = factory.SubFactory(TenantFactory) | ||
created_at = factory.Faker('date_time') | ||
updated_at = factory.Faker('date_time') | ||
|
||
class Meta: | ||
model = models.TenantMembership |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import pytest_factoryboy | ||
|
||
from . import factories | ||
|
||
pytest_factoryboy.register(factories.TenantFactory) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.