diff --git a/packages/backend/apps/demo/models.py b/packages/backend/apps/demo/models.py index 439389a79..36013260c 100644 --- a/packages/backend/apps/demo/models.py +++ b/packages/backend/apps/demo/models.py @@ -7,6 +7,7 @@ from apps.content import models as content_models from common.storages import UniqueFilePathGenerator +from common.models import TimestampedMixin User = get_user_model() @@ -24,14 +25,11 @@ def __init__(self, *args, **kwargs): self.edited_by: Optional[User] = None -class ContentfulDemoItemFavorite(models.Model): +class ContentfulDemoItemFavorite(TimestampedMixin, models.Model): id = hashid_field.HashidAutoField(primary_key=True) item = models.ForeignKey(content_models.DemoItem, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - class Meta: unique_together = [['item', 'user']] diff --git a/packages/backend/apps/multitenancy/constants.py b/packages/backend/apps/multitenancy/constants.py index bd98dc8df..4093e2a7f 100644 --- a/packages/backend/apps/multitenancy/constants.py +++ b/packages/backend/apps/multitenancy/constants.py @@ -2,7 +2,13 @@ class TenantType(models.TextChoices): - SIGN_UP = "sign_up", "Sign Up" + """ + DEFAULT is a tenant type created during user sign up. + It's default tenant for user to ensure that user always have at least one. + ORGANIZATION is a tenant type for tenants created by user manually for purposes of inviting other members. + """ + + DEFAULT = "default", "Default" ORGANIZATION = "organization", "Organization" diff --git a/packages/backend/apps/multitenancy/managers.py b/packages/backend/apps/multitenancy/managers.py index 05becc50d..60bd02a23 100644 --- a/packages/backend/apps/multitenancy/managers.py +++ b/packages/backend/apps/multitenancy/managers.py @@ -4,21 +4,22 @@ class TenantManager(models.Manager): - def get_or_create_user_sign_up_tenant(self, user): + def get_or_create_user_default_tenant(self, user): """ Description: - Retrieves or creates a tenant for a given user to ensure there is always at least one tenant associated with the user. + Retrieves or creates a default tenant for a given user to ensure that there is always at least one tenant + instance associated with the user. 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. """ - sign_up_tenant = self.filter(creator=user, type=TenantType.SIGN_UP).order_by('created').first() - if sign_up_tenant: - return sign_up_tenant, False + 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.SIGN_UP, name=str(user)) + new_tenant = self.create(creator=user, type=TenantType.DEFAULT, name=str(user)) new_tenant.members.add(user) return new_tenant, True diff --git a/packages/backend/apps/multitenancy/migrations/0001_initial.py b/packages/backend/apps/multitenancy/migrations/0001_initial.py deleted file mode 100644 index b3ef96fe9..000000000 --- a/packages/backend/apps/multitenancy/migrations/0001_initial.py +++ /dev/null @@ -1,78 +0,0 @@ -# Generated by Django 4.2 on 2024-02-13 10:05 - -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=[ - ( - '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=[('sign_up', 'Sign Up'), ('organization', 'Organization')])), - ('created', models.DateTimeField(auto_now_add=True)), - ( - 'creator', - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - ], - ), - migrations.CreateModel( - name='TenantMembership', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ( - 'role', - models.CharField( - choices=[('owner', 'Owner'), ('admin', 'Administrator'), ('member', 'Member')], default='owner' - ), - ), - ( - 'tenant', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='memberships', - to='multitenancy.tenant', - ), - ), - ( - 'user', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='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 - ), - ), - ] diff --git a/packages/backend/apps/multitenancy/models.py b/packages/backend/apps/multitenancy/models.py index 368a65d25..7f32abd00 100644 --- a/packages/backend/apps/multitenancy/models.py +++ b/packages/backend/apps/multitenancy/models.py @@ -6,15 +6,15 @@ from . import constants from .managers import TenantManager +from common.models import TimestampedMixin -class Tenant(models.Model): +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) - created = models.DateTimeField(auto_now_add=True) members = models.ManyToManyField( settings.AUTH_USER_MODEL, through='TenantMembership', related_name='tenants', blank=True ) @@ -30,10 +30,10 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) -class TenantMembership(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="memberships") +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="memberships") + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name="user_memberships") class Meta: unique_together = ('user', 'tenant') diff --git a/packages/backend/apps/multitenancy/tests/factories.py b/packages/backend/apps/multitenancy/tests/factories.py index 3381f593e..67c9b2665 100644 --- a/packages/backend/apps/multitenancy/tests/factories.py +++ b/packages/backend/apps/multitenancy/tests/factories.py @@ -9,7 +9,8 @@ class TenantFactory(factory.django.DjangoModelFactory): type = factory.Iterator(constants.TenantType.values) name = factory.Faker('pystr') slug = factory.Faker('pystr') - created = factory.Faker('date_time') + created_at = factory.Faker('date_time') + updated_at = factory.Faker('date_time') class Meta: model = models.Tenant @@ -18,6 +19,8 @@ class Meta: 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 diff --git a/packages/backend/apps/users/schema.py b/packages/backend/apps/users/schema.py index 4eb486fe8..8905fb244 100644 --- a/packages/backend/apps/users/schema.py +++ b/packages/backend/apps/users/schema.py @@ -128,7 +128,7 @@ class Meta: @staticmethod def resolve_role(parent, info): user = get_user_from_resolver(info) - return parent.memberships.filter(user=user).first().role + return parent.user_memberships.filter(user=user).first().role @permission_classes(policies.AnyoneFullAccess) @@ -180,7 +180,7 @@ def resolve_tenants(parent, info): user = get_user_from_resolver(info) tenants = user.tenants.all() if not len(tenants): - Tenant.objects.get_or_create_user_sign_up_tenant(user) + Tenant.objects.get_or_create_user_default_tenant(user) return tenants diff --git a/packages/backend/apps/users/serializers.py b/packages/backend/apps/users/serializers.py index ad21c26f0..461d632d1 100644 --- a/packages/backend/apps/users/serializers.py +++ b/packages/backend/apps/users/serializers.py @@ -89,7 +89,7 @@ def create(self, validated_data): ).send() # Create user signup tenant - Tenant.objects.get_or_create_user_sign_up_tenant(user) + Tenant.objects.get_or_create_user_default_tenant(user) return {'id': user.id, 'email': user.email, 'access': str(refresh.access_token), 'refresh': str(refresh)} diff --git a/packages/backend/apps/users/tests/factories.py b/packages/backend/apps/users/tests/factories.py index f753b3a54..12ee08b0f 100644 --- a/packages/backend/apps/users/tests/factories.py +++ b/packages/backend/apps/users/tests/factories.py @@ -53,7 +53,7 @@ def sign_up_tenant(self, create: bool, extracted: Optional[List[str]], **kwargs) if not create: return - Tenant.objects.get_or_create_user_sign_up_tenant(self) + Tenant.objects.get_or_create_user_default_tenant(self) @factory.post_generation def admin(self, create, extracted, **kwargs): diff --git a/packages/backend/apps/users/tests/test_schema.py b/packages/backend/apps/users/tests/test_schema.py index 84ced3873..bdc13711a 100644 --- a/packages/backend/apps/users/tests/test_schema.py +++ b/packages/backend/apps/users/tests/test_schema.py @@ -191,7 +191,7 @@ def test_response_data(self, graphene_client, user_factory, user_avatar_factory) assert len(data["tenants"]) > 0 assert data["tenants"][0]["name"] == "test@example.com" assert data["tenants"][0]["role"] == "owner" - assert data["tenants"][0]["type"] == "sign_up" + assert data["tenants"][0]["type"] == "default" def test_not_authenticated(self, graphene_client): executed = graphene_client.query( diff --git a/packages/backend/common/models.py b/packages/backend/common/models.py index 40f073f8f..6e19e0f20 100644 --- a/packages/backend/common/models.py +++ b/packages/backend/common/models.py @@ -1,3 +1,4 @@ +from django.db import models from django.core.files.base import ContentFile from io import BytesIO from common.graphql import exceptions as graphql_exceptions @@ -31,3 +32,11 @@ def save(self, *args, **kwargs): if self.original: self.make_thumbnail() super().save(*args, **kwargs) + + +class TimestampedMixin(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True