oxutils 0.1.5__py3-none-any.whl → 0.1.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. oxutils/__init__.py +2 -2
  2. oxutils/audit/migrations/0001_initial.py +2 -2
  3. oxutils/audit/models.py +2 -2
  4. oxutils/constants.py +6 -0
  5. oxutils/jwt/auth.py +150 -1
  6. oxutils/jwt/models.py +81 -0
  7. oxutils/jwt/tokens.py +69 -0
  8. oxutils/jwt/utils.py +45 -0
  9. oxutils/logger/__init__.py +10 -0
  10. oxutils/logger/receivers.py +10 -6
  11. oxutils/logger/settings.py +2 -2
  12. oxutils/models/base.py +102 -0
  13. oxutils/models/fields.py +79 -0
  14. oxutils/oxiliere/apps.py +9 -1
  15. oxutils/oxiliere/authorization.py +45 -0
  16. oxutils/oxiliere/caches.py +13 -11
  17. oxutils/oxiliere/checks.py +31 -0
  18. oxutils/oxiliere/constants.py +3 -0
  19. oxutils/oxiliere/context.py +16 -0
  20. oxutils/oxiliere/exceptions.py +16 -0
  21. oxutils/oxiliere/management/commands/grant_tenant_owners.py +19 -0
  22. oxutils/oxiliere/management/commands/init_oxiliere_system.py +30 -11
  23. oxutils/oxiliere/middleware.py +65 -11
  24. oxutils/oxiliere/models.py +146 -9
  25. oxutils/oxiliere/permissions.py +28 -35
  26. oxutils/oxiliere/schemas.py +16 -6
  27. oxutils/oxiliere/signals.py +5 -0
  28. oxutils/oxiliere/utils.py +36 -1
  29. oxutils/pagination/cursor.py +367 -0
  30. oxutils/permissions/__init__.py +0 -0
  31. oxutils/permissions/actions.py +57 -0
  32. oxutils/permissions/admin.py +3 -0
  33. oxutils/permissions/apps.py +10 -0
  34. oxutils/permissions/caches.py +19 -0
  35. oxutils/permissions/checks.py +188 -0
  36. oxutils/permissions/constants.py +0 -0
  37. oxutils/permissions/controllers.py +344 -0
  38. oxutils/permissions/exceptions.py +60 -0
  39. oxutils/permissions/management/__init__.py +0 -0
  40. oxutils/permissions/management/commands/__init__.py +0 -0
  41. oxutils/permissions/management/commands/load_permission_preset.py +112 -0
  42. oxutils/permissions/migrations/0001_initial.py +112 -0
  43. oxutils/permissions/migrations/0002_alter_grant_role.py +19 -0
  44. oxutils/permissions/migrations/0003_alter_grant_options_alter_group_options_and_more.py +33 -0
  45. oxutils/permissions/migrations/__init__.py +0 -0
  46. oxutils/permissions/models.py +171 -0
  47. oxutils/permissions/perms.py +95 -0
  48. oxutils/permissions/queryset.py +92 -0
  49. oxutils/permissions/schemas.py +276 -0
  50. oxutils/permissions/services.py +663 -0
  51. oxutils/permissions/tests.py +3 -0
  52. oxutils/permissions/utils.py +628 -0
  53. oxutils/settings.py +14 -194
  54. oxutils/users/apps.py +1 -1
  55. oxutils/users/migrations/0001_initial.py +47 -0
  56. oxutils/users/migrations/0002_alter_user_first_name_alter_user_last_name.py +23 -0
  57. oxutils/users/models.py +2 -0
  58. oxutils/utils.py +25 -0
  59. {oxutils-0.1.5.dist-info → oxutils-0.1.12.dist-info}/METADATA +21 -11
  60. oxutils-0.1.12.dist-info/RECORD +122 -0
  61. oxutils/jwt/client.py +0 -123
  62. oxutils/jwt/constants.py +0 -1
  63. oxutils/s3/settings.py +0 -34
  64. oxutils/s3/storages.py +0 -130
  65. oxutils-0.1.5.dist-info/RECORD +0 -88
  66. /oxutils/{s3 → pagination}/__init__.py +0 -0
  67. {oxutils-0.1.5.dist-info → oxutils-0.1.12.dist-info}/WHEEL +0 -0
@@ -0,0 +1,45 @@
1
+ from django.conf import settings
2
+ from django.db import transaction
3
+ from oxutils.permissions.actions import ACTIONS
4
+ from oxutils.permissions.models import Grant, Group, UserGroup
5
+ from oxutils.oxiliere.utils import get_tenant_user_model
6
+ from oxutils.oxiliere.models import BaseTenant
7
+
8
+
9
+ @transaction.atomic
10
+ def grant_manager_access_to_owners(tenant: BaseTenant):
11
+ tenant_user_model = get_tenant_user_model()
12
+ tenant_users = tenant_user_model.objects.select_related("user").filter(tenant=tenant, is_owner=True)
13
+
14
+ access_scope = getattr(settings, 'ACCESS_MANAGER_SCOPE')
15
+ access_group = getattr(settings, 'ACCESS_MANAGER_GROUP')
16
+
17
+ if access_group:
18
+ try:
19
+ group = Group.objects.get(slug=access_group)
20
+ except Group.DoesNotExist:
21
+ group = None
22
+
23
+ bulk_grant = []
24
+ for tenant_user in tenant_users:
25
+ if group:
26
+ user_group, _ = UserGroup.objects.get_or_create(
27
+ user=tenant_user.user,
28
+ group=group,
29
+ )
30
+ else:
31
+ user_group = None
32
+
33
+ bulk_grant.append(
34
+ Grant(
35
+ user=tenant_user.user,
36
+ scope=access_scope,
37
+ role=None,
38
+ actions=ACTIONS,
39
+ context={},
40
+ user_group=user_group,
41
+ created_by=None,
42
+ )
43
+ )
44
+
45
+ Grant.objects.bulk_create(bulk_grant)
@@ -1,9 +1,16 @@
1
1
  from cacheops import cached_as, cached
2
- from oxutils.oxiliere.models import Tenant, TenantUser
3
- from django_tenants.utils import get_tenant_model
2
+ from oxutils.oxiliere.utils import (
3
+ get_tenant_model,
4
+ get_tenant_user_model,
5
+ get_system_tenant_oxi_id
6
+ )
7
+
8
+
9
+
4
10
 
5
11
 
6
12
  TenantModel = get_tenant_model()
13
+ TenantUserModel = get_tenant_user_model()
7
14
 
8
15
 
9
16
  @cached_as(TenantModel, timeout=60*15)
@@ -12,22 +19,17 @@ def get_tenant_by_oxi_id(oxi_id: str):
12
19
 
13
20
 
14
21
  @cached_as(TenantModel, timeout=60*15)
15
- def get_tenant_by_schema_name(schema_name: str) -> Tenant:
22
+ def get_tenant_by_schema_name(schema_name: str):
16
23
  return TenantModel.objects.get(schema_name=schema_name)
17
24
 
18
25
 
19
- @cached_as(TenantUser, timeout=60*15)
26
+ @cached_as(TenantUserModel, timeout=60*15)
20
27
  def get_tenant_user(oxi_org_id: str, oxi_user_id: str):
21
- return TenantUser.objects.get(
28
+ return TenantUserModel.objects.get(
22
29
  tenant__oxi_id=oxi_org_id,
23
30
  user__oxi_id=oxi_user_id
24
31
  )
25
32
 
26
33
  @cached(timeout=60*15)
27
34
  def get_system_tenant():
28
- from oxutils.oxiliere.utils import oxid_to_schema_name
29
-
30
- system_schema_name = oxid_to_schema_name(
31
- getattr(settings, 'OXI_SYSTEM_TENANT', 'tenant_oxisystem')
32
- )
33
- return get_tenant_model().objects.get(schema_name=system_schema_name)
35
+ return get_tenant_model().objects.get(oxi_id=get_system_tenant_oxi_id())
@@ -0,0 +1,31 @@
1
+ """
2
+ Check TENANT_MODEL & TENANT_USER_MODEL
3
+ """
4
+ from django.conf import settings
5
+ from django.core.checks import Error, register, Tags
6
+
7
+
8
+ @register(Tags.models)
9
+ def check_tenant_settings(app_configs, **kwargs):
10
+ """Check that TENANT_MODEL and TENANT_USER_MODEL are defined in settings."""
11
+ errors = []
12
+
13
+ if not hasattr(settings, 'TENANT_MODEL'):
14
+ errors.append(
15
+ Error(
16
+ 'TENANT_MODEL is not defined in settings',
17
+ hint='Add TENANT_MODEL = "app_label.ModelName" to your settings',
18
+ id='oxiliere.E001',
19
+ )
20
+ )
21
+
22
+ if not hasattr(settings, 'TENANT_USER_MODEL'):
23
+ errors.append(
24
+ Error(
25
+ 'TENANT_USER_MODEL is not defined in settings',
26
+ hint='Add TENANT_USER_MODEL = "app_label.ModelName" to your settings',
27
+ id='oxiliere.E002',
28
+ )
29
+ )
30
+
31
+ return errors
@@ -0,0 +1,3 @@
1
+ OXI_SYSTEM_TENANT = 'tenant_oxisystem'
2
+ OXI_SYSTEM_DOMAIN = 'system.oxiliere.com'
3
+ OXI_SYSTEM_OWNER_EMAIL = 'dev@oxiliere.com'
@@ -0,0 +1,16 @@
1
+ import contextvars
2
+ from oxutils.oxiliere.utils import get_system_tenant_oxi_id
3
+
4
+
5
+ current_tenant_schema_name: contextvars.ContextVar[str] = contextvars.ContextVar(
6
+ "current_tenant_schema_name",
7
+ default=f"[oxi_id] {get_system_tenant_oxi_id()}"
8
+ )
9
+
10
+
11
+ def get_current_tenant_schema_name() -> str:
12
+ return current_tenant_schema_name.get()
13
+
14
+
15
+ def set_current_tenant_schema_name(schema_name: str):
16
+ current_tenant_schema_name.set(schema_name)
@@ -0,0 +1,16 @@
1
+ # exceptions
2
+
3
+ class InactiveError(Exception):
4
+ pass
5
+
6
+
7
+ class ExistsError(Exception):
8
+ pass
9
+
10
+
11
+ class DeleteError(Exception):
12
+ pass
13
+
14
+
15
+ class SchemaError(Exception):
16
+ pass
@@ -0,0 +1,19 @@
1
+ from django.core.management.base import BaseCommand
2
+ from django.db import connection
3
+ from django_tenants.management.commands import InteractiveTenantOption
4
+ from oxutils.oxiliere.authorization import grant_manager_access_to_owners
5
+
6
+
7
+ class Command(InteractiveTenantOption, BaseCommand):
8
+ help = "Wrapper around django commands for use with an individual tenant"
9
+
10
+ def add_arguments(self, parser):
11
+ super().add_arguments(parser)
12
+
13
+ def handle(self, *args, **options):
14
+ tenant = self.get_tenant_from_options_or_interactive(**options)
15
+ connection.set_tenant(tenant)
16
+ options.pop('schema_name', None)
17
+
18
+ grant_manager_access_to_owners(tenant)
19
+ self.stdout.write(self.style.SUCCESS('Successfully granted manager access to owners'))
@@ -1,11 +1,22 @@
1
1
  import uuid
2
2
  from django.core.management.base import BaseCommand
3
3
  from django.conf import settings
4
- from django.db import transaction
4
+ from django.db import transaction, connection
5
5
  from django.contrib.auth import get_user_model
6
- from django_tenants.utils import get_tenant_model
7
- from oxutils.oxiliere.models import Domain, TenantUser
8
- from oxutils.oxiliere.utils import oxid_to_schema_name
6
+ from django_tenants.utils import (
7
+ get_tenant_model,
8
+ get_tenant_domain_model,
9
+ )
10
+ from oxutils.oxiliere.utils import (
11
+ oxid_to_schema_name,
12
+ get_tenant_user_model
13
+ )
14
+ from oxutils.oxiliere.constants import (
15
+ OXI_SYSTEM_TENANT,
16
+ OXI_SYSTEM_DOMAIN,
17
+ OXI_SYSTEM_OWNER_EMAIL
18
+ )
19
+ from oxutils.oxiliere.authorization import grant_manager_access_to_owners
9
20
 
10
21
 
11
22
 
@@ -18,17 +29,17 @@ class Command(BaseCommand):
18
29
  UserModel = get_user_model()
19
30
 
20
31
  # Configuration du tenant système depuis settings
21
- system_slug = getattr(settings, 'OXI_SYSTEM_TENANT', 'tenant_oxisystem')
32
+ system_slug = getattr(settings, 'OXI_SYSTEM_TENANT', OXI_SYSTEM_TENANT)
22
33
  schema_name = oxid_to_schema_name(system_slug)
23
- system_domain = getattr(settings, 'OXI_SYSTEM_DOMAIN', 'system.oxiliere.com')
24
- owner_email = getattr(settings, 'OXI_SYSTEM_OWNER_EMAIL', 'dev@oxiliere.com')
34
+ system_domain = getattr(settings, 'OXI_SYSTEM_DOMAIN', OXI_SYSTEM_DOMAIN)
35
+ owner_email = getattr(settings, 'OXI_SYSTEM_OWNER_EMAIL', OXI_SYSTEM_OWNER_EMAIL)
25
36
  owner_oxi_id = uuid.uuid4()
26
37
 
27
38
  self.stdout.write(self.style.WARNING(f'Initialisation du tenant système...'))
28
39
 
29
40
  # Vérifier si le tenant système existe déjà
30
- if TenantModel.objects.filter(schema_name=schema_name).exists():
31
- self.stdout.write(self.style.ERROR(f'Le tenant système "{schema_name}" existe déjà!'))
41
+ if TenantModel.objects.filter(oxi_id=system_slug).exists():
42
+ self.stdout.write(self.style.ERROR(f'Le tenant système "{system_slug}" existe déjà!'))
32
43
  return
33
44
 
34
45
  # Créer le tenant système
@@ -44,12 +55,15 @@ class Command(BaseCommand):
44
55
 
45
56
  # Créer le domaine pour le tenant système
46
57
  self.stdout.write(f'Création du domaine: {system_domain}')
47
- domain = Domain.objects.create(
58
+
59
+ domain = get_tenant_domain_model().objects.create(
48
60
  domain=system_domain,
49
61
  tenant=tenant,
50
62
  is_primary=True
51
63
  )
52
64
  self.stdout.write(self.style.SUCCESS(f'✓ Domaine créé: {domain.domain}'))
65
+
66
+ connection.set_tenant(tenant)
53
67
 
54
68
  self.stdout.write(f'Création du superuser: {owner_email}')
55
69
  try:
@@ -66,7 +80,7 @@ class Command(BaseCommand):
66
80
 
67
81
  # Lier le superuser au tenant système
68
82
  self.stdout.write('Liaison du superuser au tenant système')
69
- tenant_user, created = TenantUser.objects.get_or_create(
83
+ tenant_user, created = get_tenant_user_model().objects.get_or_create(
70
84
  tenant=tenant,
71
85
  user=superuser,
72
86
  defaults={
@@ -76,6 +90,11 @@ class Command(BaseCommand):
76
90
  )
77
91
  if created:
78
92
  self.stdout.write(self.style.SUCCESS(f'✓ Superuser lié au tenant système'))
93
+ try:
94
+ grant_manager_access_to_owners(tenant)
95
+ self.stdout.write(self.style.SUCCESS(f'✓ Droits mis en place'))
96
+ except Exception as e:
97
+ self.stdout.write(self.style.ERROR(f'Erreur lors de la mise en place des droits: {str(e)}'))
79
98
  else:
80
99
  self.stdout.write(self.style.WARNING(f'⚠ Liaison existe déjà'))
81
100
 
@@ -7,9 +7,22 @@ from django.utils.deprecation import MiddlewareMixin
7
7
 
8
8
  from django_tenants.utils import (
9
9
  get_public_schema_name,
10
- get_public_schema_urlconf
10
+ get_public_schema_urlconf,
11
+ get_tenant_types,
12
+ has_multi_type_tenants,
11
13
  )
12
- from oxutils.constants import ORGANIZATION_HEADER_KEY
14
+ from oxutils.settings import oxi_settings
15
+ from oxutils.constants import (
16
+ ORGANIZATION_HEADER_KEY,
17
+ ORGANIZATION_TOKEN_COOKIE_KEY
18
+ )
19
+ from oxutils.oxiliere.utils import is_system_tenant
20
+ from oxutils.jwt.models import TokenTenant
21
+ from oxutils.jwt.tokens import OrganizationAccessToken
22
+ from oxutils.oxiliere.context import set_current_tenant_schema_name
23
+
24
+
25
+
13
26
 
14
27
  class TenantMainMiddleware(MiddlewareMixin):
15
28
  TENANT_NOT_FOUND_EXCEPTION = Http404
@@ -38,21 +51,62 @@ class TenantMainMiddleware(MiddlewareMixin):
38
51
  connection.set_schema_to_public()
39
52
 
40
53
  oxi_id = self.get_org_id_from_request(request)
41
- if not oxi_id:
42
- from django.http import HttpResponseBadRequest
43
- return HttpResponseBadRequest('Missing X-Organization-ID header')
44
54
 
45
- tenant_model = connection.tenant_model
46
- try:
47
- tenant = self.get_tenant(tenant_model, oxi_id)
48
- except tenant_model.DoesNotExist:
49
- default_tenant = self.no_tenant_found(request, oxi_id)
50
- return default_tenant
55
+ # Try to get tenant from cookie token first
56
+ tenant_token = request.COOKIES.get(ORGANIZATION_TOKEN_COOKIE_KEY)
57
+ tenant = None
58
+ request._should_set_tenant_cookie = False
59
+
60
+ if tenant_token:
61
+ tenant = TokenTenant.for_token(tenant_token)
62
+ # Verify the token's oxi_id matches the request
63
+ if not is_system_tenant(tenant) and tenant.oxi_id != oxi_id:
64
+ tenant = None
65
+
66
+ # If no valid token, fetch from database
67
+ if not tenant:
68
+ if oxi_id: # fetch with oxi_id on tenant
69
+ tenant_model = connection.tenant_model
70
+ try:
71
+ tenant = self.get_tenant(tenant_model, oxi_id)
72
+ # Mark that we need to set the cookie in the response
73
+ request._should_set_tenant_cookie = True
74
+ except tenant_model.DoesNotExist:
75
+ default_tenant = self.no_tenant_found(request, oxi_id)
76
+ return default_tenant
77
+ else: # try to return the system tenant
78
+ try:
79
+ from oxutils.oxiliere.caches import get_system_tenant
80
+ tenant = get_system_tenant()
81
+ request._should_set_tenant_cookie = True
82
+ except Exception as e:
83
+ from django.http import HttpResponseBadRequest
84
+ return HttpResponseBadRequest('Missing X-Organization-ID header')
85
+
86
+ if tenant.is_deleted or not tenant.is_active:
87
+ return self.no_tenant_found(request, oxi_id)
51
88
 
52
89
  request.tenant = tenant
90
+ set_current_tenant_schema_name(tenant.schema_name)
53
91
  connection.set_tenant(request.tenant)
54
92
  self.setup_url_routing(request)
55
93
 
94
+ def process_response(self, request, response):
95
+ """Set the tenant token cookie if needed."""
96
+ if hasattr(request, '_should_set_tenant_cookie') and request._should_set_tenant_cookie:
97
+ if hasattr(request, 'tenant') and not isinstance(request.tenant, TokenTenant):
98
+ # Generate token from DB tenant
99
+ token = OrganizationAccessToken.for_tenant(request.tenant)
100
+ response.set_cookie(
101
+ key=ORGANIZATION_TOKEN_COOKIE_KEY,
102
+ value=str(token),
103
+ max_age=60 * oxi_settings.jwt_org_access_token_lifetime,
104
+ httponly=True,
105
+ secure=getattr(settings, 'SESSION_COOKIE_SECURE', False),
106
+ samesite='Lax',
107
+ )
108
+ return response
109
+
56
110
  def no_tenant_found(self, request, oxi_id):
57
111
  """ What should happen if no tenant is found.
58
112
  This makes it easier if you want to override the default behavior """
@@ -1,18 +1,44 @@
1
+ import time
2
+ import uuid
3
+ import structlog
1
4
  from django.db import models
5
+ from django.utils import timezone
6
+ from django.contrib.auth.models import AbstractBaseUser
2
7
  from django.conf import settings
3
- from django_tenants.models import TenantMixin, DomainMixin
8
+ from django.utils.translation import gettext_lazy as _
9
+ from django_tenants.models import TenantMixin
4
10
  from oxutils.models import (
5
- TimestampMixin,
6
11
  BaseModelMixin,
7
12
  )
8
13
  from oxutils.oxiliere.enums import TenantStatus
14
+ from oxutils.oxiliere.exceptions import DeleteError
15
+ from oxutils.oxiliere.signals import (
16
+ tenant_user_removed,
17
+ tenant_user_added,
18
+ )
19
+ from oxutils.oxiliere.utils import (
20
+ is_system_tenant,
21
+ generate_schema_name,
22
+ )
23
+
24
+ logger = structlog.get_logger(__name__)
25
+
9
26
 
27
+ class TenantQuerySet(models.QuerySet):
28
+ def active(self):
29
+ return self.filter(is_deleted=False)
10
30
 
31
+ def deleted(self):
32
+ return self.filter(is_deleted=True)
11
33
 
34
+ class TenantManager(models.Manager):
35
+ def get_queryset(self):
36
+ return TenantQuerySet(self.model, using=self._db).active()
12
37
 
13
- class Tenant(TenantMixin, TimestampMixin):
38
+
39
+ class BaseTenant(TenantMixin, BaseModelMixin):
14
40
  name = models.CharField(max_length=100)
15
- oxi_id = models.UUIDField(unique=True)
41
+ oxi_id = models.CharField(unique=True, max_length=25)
16
42
  subscription_plan = models.CharField(max_length=255, null=True, blank=True)
17
43
  subscription_status = models.CharField(max_length=255, null=True, blank=True)
18
44
  subscription_end_date = models.DateTimeField(null=True, blank=True)
@@ -22,21 +48,126 @@ class Tenant(TenantMixin, TimestampMixin):
22
48
  default=TenantStatus.ACTIVE
23
49
  )
24
50
 
51
+ # soft delete
52
+ is_deleted = models.BooleanField(default=False)
53
+ deleted_at = models.DateTimeField(null=True, blank=True)
54
+
55
+ suffix = models.CharField(max_length=8, editable=False)
56
+
25
57
  # default true, schema will be automatically created and synced when it is saved
26
58
  auto_create_schema = True
59
+ # Schema will be automatically deleted when related tenant is deleted
60
+ auto_drop_schema = True
61
+
62
+ objects = models.Manager()
63
+ active = TenantManager()
64
+
65
+
66
+ def __str__(self):
67
+ return self.name
68
+
69
+ def save(self, *args, **kwargs):
70
+ if self._state.adding:
71
+ self.suffix = uuid.uuid4().hex[:8]
72
+ self.schema_name = generate_schema_name(self.oxi_id, self.suffix)
73
+ super().save(*args, **kwargs)
74
+
75
+ def delete(self, *args, force_drop: bool = False, **kwargs) -> None:
76
+ """Override deleting of Tenant object.
77
+
78
+ Args:
79
+ force_drop (bool): If True, forces the deletion of the object. Defaults to False.
80
+ *args: Variable length argument list.
81
+ **kwargs: Arbitrary keyword arguments.
82
+ """
83
+ if force_drop:
84
+ super().delete(force_drop, *args, **kwargs)
85
+ else:
86
+ logger.warning("Tenant deletion is not allowed. Use delete_tenant to delete the tenant.")
87
+ raise DeleteError(_("Tenant deletion is not allowed. Use delete_tenant to delete the tenant."))
88
+
89
+ def delete_tenant(self) -> None:
90
+ """Mark tenant for deletion."""
27
91
 
92
+ if self.is_deleted:
93
+ return
28
94
 
29
- class Domain(DomainMixin):
30
- pass
95
+ # Prevent public tenant schema from being deleted
96
+ if is_system_tenant(self):
97
+ logger.warning("Cannot delete public tenant schema.")
98
+ raise ValueError(_("Cannot delete public tenant schema"))
31
99
 
100
+ time_string = str(int(time.time()))
101
+ new_id = f"{time_string}-deleted-{self.oxi_id}"
32
102
 
103
+ self.oxi_id = new_id
104
+ self.deleted_at = timezone.now()
105
+ self.is_deleted = True
106
+ self.is_active = False
107
+ self.status = TenantStatus.DELETED
33
108
 
34
- class TenantUser(BaseModelMixin):
109
+ self.save(update_fields=[
110
+ 'oxi_id', 'deleted_at', 'is_deleted', 'is_active', 'status'
111
+ ])
112
+
113
+ def restore(self):
114
+ if not self.is_deleted:
115
+ return
116
+
117
+ oxi_id = self.oxi_id.split("-deleted-")[1]
118
+ self.oxi_id = oxi_id
119
+ self.is_deleted = False
120
+ self.deleted_at = None
121
+ self.is_active = True
122
+ self.status = TenantStatus.ACTIVE
123
+ self.save(update_fields=["oxi_id", "is_deleted", "deleted_at", "is_active", "status"])
124
+
125
+
126
+ def add_user(self, user: AbstractBaseUser, is_owner: bool = False, is_admin: bool = False):
127
+ """Add user to tenant."""
128
+
129
+ if self.users.filter(user=user).exists():
130
+ logger.warning("User is already a member of this tenant.")
131
+ raise ValueError(_("User is already a member of this tenant."))
132
+
133
+ self.users.create(user=user, is_owner=is_owner, is_admin=is_admin)
134
+ tenant_user_added.send(sender=self.__class__, tenant=self, user=user)
135
+
136
+
137
+ def remove_user(self, user: AbstractBaseUser):
138
+ """Remove user from tenant."""
139
+
140
+ if not self.users.filter(user=user).exists():
141
+ logger.warning("User is not a member of this tenant.")
142
+ raise ValueError("User is not a member of this tenant.")
143
+
144
+ self.users.filter(user=user).delete()
145
+ logger.info("User removed from tenant.")
146
+ tenant_user_removed.send(sender=self.__class__, tenant=self, user=user)
147
+
148
+
149
+ class Meta:
150
+ abstract = True
151
+ verbose_name = _('Tenant')
152
+ verbose_name_plural = _('Tenants')
153
+ indexes = [
154
+ models.Index(fields=['schema_name']),
155
+ models.Index(fields=['oxi_id']),
156
+ models.Index(fields=['is_deleted']),
157
+ models.Index(fields=['oxi_id', 'is_deleted'])
158
+ ]
159
+
160
+
161
+ class BaseTenantUser(BaseModelMixin):
35
162
  tenant = models.ForeignKey(
36
- settings.TENANT_MODEL, on_delete=models.CASCADE
163
+ settings.TENANT_MODEL,
164
+ on_delete=models.CASCADE,
165
+ related_name='users'
37
166
  )
38
167
  user = models.ForeignKey(
39
- settings.AUTH_USER_MODEL, on_delete=models.CASCADE
168
+ settings.AUTH_USER_MODEL,
169
+ on_delete=models.CASCADE,
170
+ related_name='tenants'
40
171
  )
41
172
  is_owner = models.BooleanField(default=False)
42
173
  is_admin = models.BooleanField(default=False)
@@ -47,9 +178,15 @@ class TenantUser(BaseModelMixin):
47
178
  )
48
179
 
49
180
  class Meta:
181
+ abstract = True
182
+ verbose_name = 'Tenant User'
183
+ verbose_name_plural = 'Tenant Users'
50
184
  constraints = [
51
185
  models.UniqueConstraint(
52
186
  fields=['tenant', 'user'],
53
187
  name='unique_tenant_user'
54
188
  )
55
189
  ]
190
+ indexes = [
191
+ models.Index(fields=['tenant', 'user'])
192
+ ]