From 9ee1ef5ba0dfe95f0cbcf5c4e37de11d168ac894 Mon Sep 17 00:00:00 2001 From: Marie PUPO JEAMMET Date: Fri, 7 Feb 2025 18:54:10 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F(models)=20create=20abstra?= =?UTF-8?q?ct=20BaseInvitation=20and=20DomainInvitation=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create abstract BaseInvitation models to factorize common elements between existing Invitation model team-side and new DomainInvitation model --- src/backend/core/models.py | 100 +++++++++--------- .../migrations/0024_domaininvitation.py | 35 ++++++ src/backend/mailbox_manager/models.py | 63 ++++++++++- 3 files changed, 149 insertions(+), 49 deletions(-) create mode 100644 src/backend/mailbox_manager/migrations/0024_domaininvitation.py diff --git a/src/backend/core/models.py b/src/backend/core/models.py index b82b72f..c913b36 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -895,18 +895,10 @@ class TeamWebhook(BaseModel): return headers -class Invitation(BaseModel): - """User invitation to teams.""" +class BaseInvitation(BaseModel): + """Abstract base invitation model, surcharged for teams or domains.""" email = models.EmailField(_("email address"), null=False, blank=False) - team = models.ForeignKey( - Team, - on_delete=models.CASCADE, - related_name="invitations", - ) - role = models.CharField( - max_length=20, choices=RoleChoices.choices, default=RoleChoices.MEMBER - ) issuer = models.ForeignKey( User, on_delete=models.CASCADE, @@ -914,17 +906,7 @@ class Invitation(BaseModel): ) class Meta: - db_table = "people_invitation" - verbose_name = _("Team invitation") - verbose_name_plural = _("Team invitations") - constraints = [ - models.UniqueConstraint( - fields=["email", "team"], name="email_and_team_unique_together" - ) - ] - - def __str__(self): - return f"{self.email} invited to {self.team}" + abstract = True def save(self, *args, **kwargs): """Make invitations read-only.""" @@ -953,31 +935,6 @@ class Invitation(BaseModel): validity_duration = timedelta(seconds=settings.INVITATION_VALIDITY_DURATION) return timezone.now() > (self.created_at + validity_duration) - def get_abilities(self, user): - """Compute and return abilities for a given user.""" - can_delete = False - role = None - - if user.is_authenticated: - try: - role = self.user_role - except AttributeError: - try: - role = self.team.accesses.filter(user=user).values("role")[0][ - "role" - ] - except (self._meta.model.DoesNotExist, IndexError): - role = None - - can_delete = role in [RoleChoices.OWNER, RoleChoices.ADMIN] - - return { - "delete": can_delete, - "get": bool(role), - "patch": False, - "put": False, - } - def email_invitation(self): """Email invitation to the user.""" try: @@ -1002,5 +959,52 @@ class Invitation(BaseModel): logger.error("invitation to %s was not sent: %s", self.email, exception) -# It's not clear yet how best to split this file. -# pylint: disable=C0302 +class Invitation(BaseInvitation): + """User invitation to teams.""" + + team = models.ForeignKey( + Team, + on_delete=models.CASCADE, + related_name="invitations", + ) + role = models.CharField( + max_length=20, choices=RoleChoices.choices, default=RoleChoices.MEMBER + ) + + class Meta: + db_table = "people_invitation" + verbose_name = _("Team invitation") + verbose_name_plural = _("Team invitations") + constraints = [ + models.UniqueConstraint( + fields=["email", "team"], name="email_and_team_unique_together" + ) + ] + + def __str__(self): + return f"{self.email} invited to {self.team}" + + def get_abilities(self, user): + """Compute and return abilities for a given user.""" + can_delete = False + role = None + + if user.is_authenticated: + try: + role = self.user_role + except AttributeError: + try: + role = self.team.accesses.filter(user=user).values("role")[0][ + "role" + ] + except (self._meta.model.DoesNotExist, IndexError): + role = None + + can_delete = role in [RoleChoices.OWNER, RoleChoices.ADMIN] + + return { + "delete": can_delete, + "get": bool(role), + "patch": False, + "put": False, + } diff --git a/src/backend/mailbox_manager/migrations/0024_domaininvitation.py b/src/backend/mailbox_manager/migrations/0024_domaininvitation.py new file mode 100644 index 0000000..5933133 --- /dev/null +++ b/src/backend/mailbox_manager/migrations/0024_domaininvitation.py @@ -0,0 +1,35 @@ +# Generated by Django 5.1.5 on 2025-02-07 17:27 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailbox_manager', '0023_mailbox_email_mailbox_last_login_mailbox_password'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='DomainInvitation', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created at')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated at')), + ('email', models.EmailField(max_length=254, verbose_name='email address')), + ('role', models.CharField(choices=[('viewer', 'Viewer'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='viewer', max_length=20)), + ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domain_invitations', to='mailbox_manager.maildomain')), + ('issuer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domain_invitations', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Domain invitation', + 'verbose_name_plural': 'Domain invitations', + 'db_table': 'people_domain_invitation', + 'constraints': [models.UniqueConstraint(fields=('email', 'domain'), name='email_and_domain_unique_together')], + }, + ), + ] diff --git a/src/backend/mailbox_manager/models.py b/src/backend/mailbox_manager/models.py index 2f082ba..d2494e0 100644 --- a/src/backend/mailbox_manager/models.py +++ b/src/backend/mailbox_manager/models.py @@ -9,7 +9,7 @@ from django.db import models from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ -from core.models import BaseModel, Organization +from core.models import BaseInvitation, BaseModel, Organization, User from mailbox_manager.enums import ( MailboxStatusChoices, @@ -273,3 +273,64 @@ class Mailbox(AbstractBaseUser, BaseModel): def get_email(self): """Return the email address of the mailbox.""" return f"{self.local_part}@{self.domain.name}" + + +class DomainInvitation(BaseInvitation): + """User invitation to teams.""" + + issuer = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name="domain_invitations", + ) + domain = models.ForeignKey( + MailDomain, + on_delete=models.CASCADE, + related_name="domain_invitations", + ) + role = models.CharField( + max_length=20, + choices=MailDomainRoleChoices.choices, + default=MailDomainRoleChoices.VIEWER, + ) + + class Meta: + db_table = "people_domain_invitation" + verbose_name = _("Domain invitation") + verbose_name_plural = _("Domain invitations") + constraints = [ + models.UniqueConstraint( + fields=["email", "domain"], name="email_and_domain_unique_together" + ) + ] + + def __str__(self): + return f"{self.email} invited to {self.domain}" + + def get_abilities(self, user): + """Compute and return abilities for a given user.""" + can_delete = False + role = None + + if user.is_authenticated: + try: + role = self.user_role + except AttributeError: + try: + role = self.domain.accesses.filter(user=user).values("role")[0][ + "role" + ] + except (self._meta.model.DoesNotExist, IndexError): + role = None + + can_delete = role in [ + MailDomainRoleChoices.OWNER, + MailDomainRoleChoices.ADMIN, + ] + + return { + "delete": can_delete, + "get": bool(role), + "patch": False, + "put": False, + }