-
Notifications
You must be signed in to change notification settings - Fork 419
Expand file tree
/
Copy pathmodels.py
More file actions
179 lines (149 loc) · 6.6 KB
/
Copy pathmodels.py
File metadata and controls
179 lines (149 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import hashid_field
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import IntegrityError, models, transaction
from django.db.models import Q, UniqueConstraint
from django.utils import timezone
from django.utils.text import slugify
from common.models import TimestampedMixin
from . import constants
from .managers import TenantManager, TenantMembershipManager
class Tenant(TimestampedMixin, models.Model):
"""
Represents a tenant within the application.
Fields:
- id: A unique identifier for the tenant.
- creator: The user who created the tenant.
- name: The name of the tenant.
- slug: A URL-friendly version of the name.
- type: The type of the tenant.
- billing_email: Address used for billing purposes and it is provided to Stripe
- members: Many-to-many relationship with users through TenantMembership.
Methods:
- save: Overrides the default save method to ensure unique slug generation based on the name field.
Initialization:
- __original_name: Private attribute to track changes to the name field during the instance's lifecycle.
Slug Generation:
- The save method ensures the generation of a unique slug for the tenant. If the name is modified or the slug is
not provided, it generates a slug based on the name. In case of a name collision, a counter is appended to the
base slug to ensure uniqueness.
"""
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=False)
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,
through_fields=('tenant', 'user'),
)
billing_email = models.EmailField(
db_collation="case_insensitive",
verbose_name="billing email address",
max_length=255,
unique=False,
blank=True,
)
objects = TenantManager()
MAX_SAVE_ATTEMPTS = 10
def __str__(self):
return self.name
def save(self, *args, **kwargs):
counter = 0
while counter < self.MAX_SAVE_ATTEMPTS:
try:
with transaction.atomic():
if not counter:
self.slug = slugify(self.name)
else:
self.slug = f"{slugify(self.name)}-{counter}"
super().save(*args, **kwargs)
break
except IntegrityError as e:
if 'duplicate key' in str(e).lower():
counter += 1
else:
raise e
@property
def email(self):
return self.billing_email if self.billing_email else self.creator.email
@property
def owners_count(self):
"""
Calculate the total number of tenant owners for this tenant.
Returns the count of tenant owners.
"""
return self.members.filter(tenant_memberships__role=constants.TenantUserRole.OWNER).count()
@property
def owners(self):
"""
Returns the list of Users with an owner role.
"""
return self.members.filter(tenant_memberships__role=constants.TenantUserRole.OWNER).all()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__original_name = self.name
class TenantMembership(TimestampedMixin, models.Model):
"""
Represents the membership of a user in a tenant. As well accepted as not accepted (invitations).
Fields:
- id: A unique identifier for the membership.
- user: The user associated with the membership.
- role: The role of the user in the tenant. Can be owner, admin or member.
- tenant: The tenant to which the user belongs.
- is_accepted: Indicates whether the membership invitation is accepted.
- invitation_accepted_at: Timestamp when the invitation was accepted.
- invitee_email_address: The email address of the invited user if not connected to an existing user.
Constraints:
- unique_non_null_user_and_tenant: Ensures the uniqueness of non-null user and tenant combinations.
- unique_non_null_user_and_invitee_email_address: Ensures the uniqueness of non-null user and invitee email address
combinations.
"""
id: str = hashid_field.HashidAutoField(primary_key=True)
# User - Tenant connection fields
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="tenant_memberships", null=True
)
creator: settings.AUTH_USER_MODEL = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="created_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")
# Invitation connected fields
is_accepted = models.BooleanField(default=False)
invitation_accepted_at = models.DateTimeField(null=True)
invitee_email_address = models.EmailField(
db_collation="case_insensitive",
verbose_name="invitee email address",
max_length=255,
default="",
)
objects = TenantMembershipManager()
class Meta:
constraints = [
UniqueConstraint(
name="unique_non_null_user_and_tenant", fields=["user", "tenant"], condition=Q(user__isnull=False)
),
UniqueConstraint(
name="unique_non_null_user_and_invitee_email_address",
fields=["invitee_email_address", "tenant"],
condition=~Q(invitee_email_address__exact=""),
),
]
def __str__(self):
if self.user:
return f"{self.user.email} - {self.tenant.name} - {self.role}"
else:
return f"{self.invitee_email_address} (pending) - {self.tenant.name} - {self.role}"
def save(self, *args, **kwargs):
if self.is_accepted and not self.invitation_accepted_at:
self.invitation_accepted_at = timezone.now()
elif not self.is_accepted:
self.invitation_accepted_at = None
super().save(*args, **kwargs)
def clean(self):
if not self.is_accepted and self.invitation_accepted_at:
raise ValidationError("Pending invitations cannot have an accepted date")