🗃️(models) add MailDomain, MailDomainAccess and Mailbox models
Additional app and models to handle email addresses creation in Desk.
This commit is contained in:
committed by
Marie
parent
a1f9cf0854
commit
cca6c77f00
0
src/backend/mailbox_manager/__init__.py
Normal file
0
src/backend/mailbox_manager/__init__.py
Normal file
1
src/backend/mailbox_manager/apps.py
Normal file
1
src/backend/mailbox_manager/apps.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""People additionnal application, to manage email adresses."""
|
||||||
49
src/backend/mailbox_manager/factories.py
Normal file
49
src/backend/mailbox_manager/factories.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
Mailbox manager application factories
|
||||||
|
"""
|
||||||
|
|
||||||
|
import factory.fuzzy
|
||||||
|
from faker import Faker
|
||||||
|
|
||||||
|
from core import factories as core_factories
|
||||||
|
from core import models as core_models
|
||||||
|
|
||||||
|
from mailbox_manager import models
|
||||||
|
|
||||||
|
fake = Faker()
|
||||||
|
|
||||||
|
|
||||||
|
class MailDomainFactory(factory.django.DjangoModelFactory):
|
||||||
|
"""A factory to create mail domain."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.MailDomain
|
||||||
|
|
||||||
|
name = factory.Faker("domain_name")
|
||||||
|
|
||||||
|
|
||||||
|
class MailDomainAccessFactory(factory.django.DjangoModelFactory):
|
||||||
|
"""A factory to create mail domain accesses."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.MailDomainAccess
|
||||||
|
|
||||||
|
user = factory.SubFactory(core_factories.UserFactory)
|
||||||
|
domain = factory.SubFactory(MailDomainFactory)
|
||||||
|
role = factory.fuzzy.FuzzyChoice([r[0] for r in core_models.RoleChoices.choices])
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxFactory(factory.django.DjangoModelFactory):
|
||||||
|
"""A factory to create mailboxes for mail domain members."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Mailbox
|
||||||
|
|
||||||
|
class Params:
|
||||||
|
"""Parameters for fields."""
|
||||||
|
|
||||||
|
full_name = factory.Faker("name")
|
||||||
|
|
||||||
|
local_part = factory.LazyAttribute(lambda a: a.full_name.lower().replace(" ", "."))
|
||||||
|
domain = factory.SubFactory(MailDomainFactory)
|
||||||
|
secondary_email = factory.Faker("email")
|
||||||
67
src/backend/mailbox_manager/migrations/0001_initial.py
Normal file
67
src/backend/mailbox_manager/migrations/0001_initial.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2024-04-16 12:51
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MailDomain',
|
||||||
|
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')),
|
||||||
|
('name', models.CharField(max_length=150, unique=True, verbose_name='name')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Mail domain',
|
||||||
|
'verbose_name_plural': 'Mail domains',
|
||||||
|
'db_table': 'people_mail_domain',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Mailbox',
|
||||||
|
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')),
|
||||||
|
('local_part', models.CharField(max_length=150, validators=[django.core.validators.RegexValidator(regex='^[a-zA-Z0-9_.+-]+$')], verbose_name='local_part')),
|
||||||
|
('secondary_email', models.EmailField(max_length=254, verbose_name='secondary email address')),
|
||||||
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mail_domain', to='mailbox_manager.maildomain')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Mailbox',
|
||||||
|
'verbose_name_plural': 'Mailboxes',
|
||||||
|
'db_table': 'people_mail_box',
|
||||||
|
'unique_together': {('local_part', 'domain')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MailDomainAccess',
|
||||||
|
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')),
|
||||||
|
('role', models.CharField(choices=[('member', 'Member'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='member', max_length=20)),
|
||||||
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mail_domain_accesses', to='mailbox_manager.maildomain')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mail_domain_accesses', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'User/mail domain relation',
|
||||||
|
'verbose_name_plural': 'User/mail domain relations',
|
||||||
|
'db_table': 'people_mail_domain_accesses',
|
||||||
|
'unique_together': {('user', 'domain')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
src/backend/mailbox_manager/migrations/__init__.py
Normal file
0
src/backend/mailbox_manager/migrations/__init__.py
Normal file
88
src/backend/mailbox_manager/models.py
Normal file
88
src/backend/mailbox_manager/models.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
Declare and configure the models for the People additional application : mailbox_manager
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core import validators
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from core.models import BaseModel, RoleChoices
|
||||||
|
|
||||||
|
|
||||||
|
class MailDomain(BaseModel):
|
||||||
|
"""Domain names from which we will create email addresses (mailboxes)."""
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
_("name"), max_length=150, null=False, blank=False, unique=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "people_mail_domain"
|
||||||
|
verbose_name = _("Mail domain")
|
||||||
|
verbose_name_plural = _("Mail domains")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class MailDomainAccess(BaseModel):
|
||||||
|
"""Allow to manage users' accesses to mail domains."""
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="mail_domain_accesses",
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
)
|
||||||
|
domain = models.ForeignKey(
|
||||||
|
MailDomain,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="mail_domain_accesses",
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
)
|
||||||
|
role = models.CharField(
|
||||||
|
max_length=20, choices=RoleChoices.choices, default=RoleChoices.MEMBER
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "people_mail_domain_accesses"
|
||||||
|
verbose_name = _("User/mail domain relation")
|
||||||
|
verbose_name_plural = _("User/mail domain relations")
|
||||||
|
unique_together = ("user", "domain")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Access of user {self.user!s} on domain {self.domain:s}."
|
||||||
|
|
||||||
|
|
||||||
|
class Mailbox(BaseModel):
|
||||||
|
"""Mailboxes for users from mail domain."""
|
||||||
|
|
||||||
|
local_part = models.CharField(
|
||||||
|
_("local_part"),
|
||||||
|
max_length=150,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
validators=[validators.RegexValidator(regex="^[a-zA-Z0-9_.+-]+$")],
|
||||||
|
)
|
||||||
|
domain = models.ForeignKey(
|
||||||
|
MailDomain,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="mail_domain",
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
)
|
||||||
|
secondary_email = models.EmailField(
|
||||||
|
_("secondary email address"), null=False, blank=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "people_mail_box"
|
||||||
|
verbose_name = _("Mailbox")
|
||||||
|
verbose_name_plural = _("Mailboxes")
|
||||||
|
unique_together = ("local_part", "domain")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.local_part!s}@{self.domain.name:s}."
|
||||||
0
src/backend/mailbox_manager/tests/__init__.py
Normal file
0
src/backend/mailbox_manager/tests/__init__.py
Normal file
91
src/backend/mailbox_manager/tests/test_models_mailboxes.py
Normal file
91
src/backend/mailbox_manager/tests/test_models_mailboxes.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for the mailbox model
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mailbox_manager import factories
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
# LOCAL PART FIELD
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_mailboxes__local_part_cannot_be_empty():
|
||||||
|
"""The "local_part" field should not be empty."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be blank"):
|
||||||
|
factories.MailboxFactory(local_part="")
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_mailboxes__local_part_cannot_be_null():
|
||||||
|
"""The "local_part" field should not be null."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be null"):
|
||||||
|
factories.MailboxFactory(local_part=None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_mailboxes__local_part_matches_expected_format():
|
||||||
|
"""
|
||||||
|
The local part should contain alpha-numeric caracters
|
||||||
|
and a limited set of special caracters ("+", "-", ".", "_").
|
||||||
|
"""
|
||||||
|
factories.MailboxFactory(local_part="Marie-Jose.Perec+JO_2024")
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match="Enter a valid value"):
|
||||||
|
factories.MailboxFactory(local_part="mariejo@unnecessarydomain.com")
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match="Enter a valid value"):
|
||||||
|
factories.MailboxFactory(local_part="!")
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_mailboxes__local_part_unique_per_domain():
|
||||||
|
"""Local parts should be unique per domain."""
|
||||||
|
|
||||||
|
existing_mailbox = factories.MailboxFactory()
|
||||||
|
|
||||||
|
# same local part on another domain should not be a problem
|
||||||
|
factories.MailboxFactory(local_part=existing_mailbox.local_part)
|
||||||
|
|
||||||
|
# same local part on the same domain should not be possible
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationError, match="Mailbox with this Local_part and Domain already exists."
|
||||||
|
):
|
||||||
|
factories.MailboxFactory(
|
||||||
|
local_part=existing_mailbox.local_part, domain=existing_mailbox.domain
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# DOMAIN FIELD
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_mailboxes__domain_must_be_a_maildomain_instance():
|
||||||
|
"""The "domain" field should be an instance of MailDomain."""
|
||||||
|
expected_error = '"Mailbox.domain" must be a "MailDomain" instance.'
|
||||||
|
with pytest.raises(ValueError, match=expected_error):
|
||||||
|
factories.MailboxFactory(domain="")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=expected_error):
|
||||||
|
factories.MailboxFactory(domain="domain-as-string.com")
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_mailboxes__domain_cannot_be_null():
|
||||||
|
"""The "domain" field should not be null."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be null"):
|
||||||
|
factories.MailboxFactory(domain=None)
|
||||||
|
|
||||||
|
|
||||||
|
# SECONDARY_EMAIL FIELD
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_mailboxes__secondary_email_cannot_be_empty():
|
||||||
|
"""The "secondary_email" field should not be empty."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be blank"):
|
||||||
|
factories.MailboxFactory(secondary_email="")
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_mailboxes__secondary_email_cannot_be_null():
|
||||||
|
"""The "secondary_email" field should not be null."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be null"):
|
||||||
|
factories.MailboxFactory(secondary_email=None)
|
||||||
26
src/backend/mailbox_manager/tests/test_models_maildomain.py
Normal file
26
src/backend/mailbox_manager/tests/test_models_maildomain.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for the MailDomain model
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mailbox_manager import factories
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
# NAME FIELD
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_mail_domain__domain_name_should_not_be_empty():
|
||||||
|
"""The domain name field should not be empty."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be blank"):
|
||||||
|
factories.MailDomainFactory(name="")
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_mail_domain__domain_name_should_not_be_null():
|
||||||
|
"""The domain name field should not be null."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be null."):
|
||||||
|
factories.MailDomainFactory(name=None)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for the MailDomainAccess model
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mailbox_manager import factories
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
# USER FIELD
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_maildomainaccess__user_be_a_user_instance():
|
||||||
|
"""The "user" field should be a user instance."""
|
||||||
|
expected_error = '"MailDomainAccess.user" must be a "User" instance.'
|
||||||
|
with pytest.raises(ValueError, match=expected_error):
|
||||||
|
factories.MailDomainAccessFactory(user="")
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_maildomainaccess__user_should_not_be_null():
|
||||||
|
"""The user field should not be null."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be null."):
|
||||||
|
factories.MailDomainAccessFactory(user=None)
|
||||||
|
|
||||||
|
|
||||||
|
# DOMAIN FIELD
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_maildomainaccesses__domain_must_be_a_maildomain_instance():
|
||||||
|
"""The "domain" field should be an instance of MailDomain."""
|
||||||
|
expected_error = '"MailDomainAccess.domain" must be a "MailDomain" instance.'
|
||||||
|
with pytest.raises(ValueError, match=expected_error):
|
||||||
|
factories.MailDomainAccessFactory(domain="")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=expected_error):
|
||||||
|
factories.MailDomainAccessFactory(domain="domain-as-string.com")
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_maildomainaccesses__domain_cannot_be_null():
|
||||||
|
"""The "domain" field should not be null."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be null"):
|
||||||
|
factories.MailDomainAccessFactory(domain=None)
|
||||||
|
|
||||||
|
|
||||||
|
# ROLE FIELD
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_maildomainaccesses__role_cannot_be_empty():
|
||||||
|
"""The "role" field cannot be empty."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be blank"):
|
||||||
|
factories.MailDomainAccessFactory(role="")
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_maildomainaccesses__role_cannot_be_null():
|
||||||
|
"""The "role" field cannot be null."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be null"):
|
||||||
|
factories.MailDomainAccessFactory(role=None)
|
||||||
@@ -190,6 +190,7 @@ class Base(Configuration):
|
|||||||
# People
|
# People
|
||||||
"core",
|
"core",
|
||||||
"demo",
|
"demo",
|
||||||
|
"mailbox_manager",
|
||||||
"drf_spectacular",
|
"drf_spectacular",
|
||||||
# Third party apps
|
# Third party apps
|
||||||
"corsheaders",
|
"corsheaders",
|
||||||
|
|||||||
Reference in New Issue
Block a user