Skip to content

Commit

Permalink
feat(Email Notifications): Ajout du formulaire d'édition des préféren…
Browse files Browse the repository at this point in the history
…ces (#1595)
  • Loading branch information
SebastienReuiller authored Dec 19, 2024
1 parent 84c637c commit b50530a
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 6 deletions.
11 changes: 9 additions & 2 deletions lemarche/conversations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.urls import reverse
from django.utils.html import format_html

from lemarche.conversations.models import Conversation, TemplateTransactional, TemplateTransactionalSendLog
from lemarche.conversations.models import Conversation, EmailGroup, TemplateTransactional, TemplateTransactionalSendLog
from lemarche.utils.admin.admin_site import admin_site
from lemarche.utils.fields import pretty_print_readonly_jsonfield, pretty_print_readonly_jsonfield_to_table
from lemarche.www.conversations.tasks import send_first_email_from_conversation
Expand Down Expand Up @@ -153,7 +153,7 @@ class TemplateTransactionalAdmin(admin.ModelAdmin):
readonly_fields = ["code", "template_transactional_send_log_count_with_link", "created_at", "updated_at"]

fieldsets = (
(None, {"fields": ("name", "code", "description")}),
(None, {"fields": ("name", "code", "description", "group")}),
("Paramètres d'envoi", {"fields": ("mailjet_id", "brevo_id", "source", "is_active")}),
("Stats", {"fields": ("template_transactional_send_log_count_with_link",)}),
("Dates", {"fields": ("created_at", "updated_at")}),
Expand Down Expand Up @@ -234,3 +234,10 @@ def extra_data_display(self, instance: TemplateTransactionalSendLog = None):
return "-"

extra_data_display.short_description = TemplateTransactionalSendLog._meta.get_field("extra_data").verbose_name


@admin.register(EmailGroup, site=admin_site)
class EmailGroupAdmin(admin.ModelAdmin):
list_display = ["id", "relevant_user_kind", "display_name", "description", "can_be_unsubscribed"]
search_fields = ["id", "display_name"]
readonly_fields = []
11 changes: 10 additions & 1 deletion lemarche/conversations/factories.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import factory
from factory.django import DjangoModelFactory

from lemarche.conversations.models import Conversation, TemplateTransactional
from lemarche.conversations.models import Conversation, EmailGroup, TemplateTransactional
from lemarche.siaes.factories import SiaeFactory


Expand All @@ -17,9 +17,18 @@ class Meta:
initial_body_message = factory.Faker("name", locale="fr_FR")


class EmailGroupFactory(DjangoModelFactory):
class Meta:
model = EmailGroup

display_name = factory.Faker("name", locale="fr_FR")
description = factory.Faker("name", locale="fr_FR")


class TemplateTransactionalFactory(DjangoModelFactory):
class Meta:
model = TemplateTransactional

name = factory.Faker("name", locale="fr_FR")
code = factory.Faker("name", locale="fr_FR")
group = factory.SubFactory(EmailGroupFactory)
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Generated by Django 4.2.15 on 2024-12-11 17:02

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


def create_email_groups(apps, schema_editor):
# Get the model
EmailGroup = apps.get_model("conversations", "EmailGroup")

# Create email groups
email_groups = [
{
"id": 1,
"display_name": "Structure(s) intéressée(s)",
"description": "En désactivant cette option, vous ne serez plus averti par email lorsque des fournisseurs s'intéressent à votre besoin, ce qui pourrait vous faire perdre des opportunités de collaboration rapide et efficace.",
"relevant_user_kind": "BUYER",
"can_be_unsubscribed": True,
},
{
"id": 2,
"display_name": "Communication marketing",
"description": "En désactivant cette option, vous ne recevrez plus par email nos newsletters, enquêtes, invitations à des webinaires et Open Labs, ce qui pourrait vous priver d'informations utiles et de moments d'échange exclusifs.",
"relevant_user_kind": "BUYER",
"can_be_unsubscribed": True,
},
{
"id": 3,
"display_name": "Opportunités commerciales",
"description": "En désactivant cette option, vous ne recevrez plus par email les demandes de devis et les appels d'offres spécialement adaptés à votre activité, ce qui pourrait vous faire manquer des opportunités importantes pour votre entreprise.",
"relevant_user_kind": "SIAE",
"can_be_unsubscribed": True,
},
{
"id": 4,
"display_name": "Demandes de mise en relation",
"description": "En désactivant cette option, vous ne recevrez plus par email les demandes de mise en relation de clients intéressés par votre structure, ce qui pourrait vous faire perdre des opportunités précieuses de collaboration et de développement.",
"relevant_user_kind": "SIAE",
"can_be_unsubscribed": True,
},
{
"id": 5,
"display_name": "Communication marketing",
"description": "En désactivant cette option, vous ne recevrez plus par email nos newsletters, enquêtes, invitations aux webinaires et Open Labs, ce qui pourrait vous faire passer à côté d’informations clés, de ressources utiles et d’événements exclusifs.",
"relevant_user_kind": "SIAE",
"can_be_unsubscribed": True,
},
]

for group in email_groups:
EmailGroup.objects.create(**group)


def delete_email_groups(apps, schema_editor):
# Get the model
EmailGroup = apps.get_model("conversations", "EmailGroup")
# Delete all email groups
EmailGroup.objects.all().delete()


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("conversations", "0016_templatetransactionalsendlog"),
]

operations = [
migrations.CreateModel(
name="EmailGroup",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("display_name", models.CharField(blank=True, max_length=255, verbose_name="Nom")),
("description", models.TextField(blank=True, verbose_name="Description")),
(
"relevant_user_kind",
models.CharField(
choices=[
("SIAE", "Structure"),
("BUYER", "Acheteur"),
("PARTNER", "Partenaire"),
("INDIVIDUAL", "Particulier"),
],
default="BUYER",
max_length=20,
verbose_name="Type d'utilisateur",
),
),
(
"can_be_unsubscribed",
models.BooleanField(default=False, verbose_name="L'utilisateur peut s'y désinscrire"),
),
],
),
migrations.CreateModel(
name="DisabledEmail",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("disabled_at", models.DateTimeField(auto_now_add=True)),
(
"group",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="conversations.emailgroup"),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="disabled_emails",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddField(
model_name="templatetransactional",
name="group",
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to="conversations.emailgroup"
),
),
migrations.AddConstraint(
model_name="disabledemail",
constraint=models.UniqueConstraint(models.F("user"), models.F("group"), name="unique_group_per_user"),
),
migrations.RunPython(create_email_groups, delete_email_groups),
]
31 changes: 31 additions & 0 deletions lemarche/conversations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from shortuuid import uuid

from lemarche.conversations import constants as conversation_constants
from lemarche.users import constants as user_constants
from lemarche.utils.apis import api_brevo, api_mailjet
from lemarche.utils.data import add_validation_error

Expand Down Expand Up @@ -200,6 +201,24 @@ def set_validated(self):
self.save()


class EmailGroup(models.Model):
display_name = models.CharField(verbose_name="Nom", max_length=255, blank=True)
description = models.TextField(verbose_name="Description", blank=True)
relevant_user_kind = models.CharField(
verbose_name="Type d'utilisateur",
max_length=20,
choices=user_constants.KIND_CHOICES,
default=user_constants.KIND_BUYER,
)
can_be_unsubscribed = models.BooleanField(verbose_name="L'utilisateur peut s'y désinscrire", default=False)

def __str__(self):
return f"{self.display_name} ({self.relevant_user_kind if self.relevant_user_kind else 'Tous'})"

def disabled_for_user(self, user):
return DisabledEmail.objects.filter(user=user, group=self).exists()


class TemplateTransactionalQuerySet(models.QuerySet):
def with_stats(self):
return self.annotate(
Expand All @@ -213,6 +232,7 @@ class TemplateTransactional(models.Model):
verbose_name="Nom technique", max_length=255, unique=True, db_index=True, blank=True, null=True
)
description = models.TextField(verbose_name="Description", blank=True)
group = models.ForeignKey("EmailGroup", on_delete=models.CASCADE, null=True)

# email_subject = models.CharField(
# verbose_name="E-mail : objet",
Expand Down Expand Up @@ -363,3 +383,14 @@ class TemplateTransactionalSendLog(models.Model):
class Meta:
verbose_name = "Template transactionnel: logs d'envois"
verbose_name_plural = "Templates transactionnels: logs d'envois"


class DisabledEmail(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="disabled_emails")
group = models.ForeignKey("EmailGroup", on_delete=models.CASCADE)
disabled_at = models.DateTimeField(auto_now_add=True)

class Meta:
constraints = [
models.UniqueConstraint("user", "group", name="unique_group_per_user"),
]
2 changes: 2 additions & 0 deletions lemarche/conversations/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ def test_get_template_id(self):


class TemplateTransactionalModelSaveTest(TransactionTestCase):
reset_sequences = True

@classmethod
def setUpTestData(cls):
pass
Expand Down
62 changes: 62 additions & 0 deletions lemarche/templates/dashboard/disabled_email_edit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{% extends "layouts/base.html" %}
{% load static widget_tweaks dsfr_tags process_dict theme_inclusion %}
{% block page_title %}
Notifications{{ block.super }}
{% endblock page_title %}
{% block breadcrumb %}
{% process_dict root_dir=HOME_PAGE_PATH current="Notifications" as breadcrumb_data %}
{% dsfr_breadcrumb breadcrumb_data %}
{% endblock breadcrumb %}
{% block content %}
<div class="fr-grid-row fr-grid-row-gutters fr-grid-row--center">
<div class="fr-col-12 fr-col-lg-10">
<div class="fr-container fr-px-md-0 fr-py-2v fr-py-md-4v">
<div class="fr-grid-row fr-grid-row-gutters fr-grid-row--center">
<div class="fr-col-12 fr-col-lg-8">
<h1>Notifications</h1>
<div>
<form method="post">
{% csrf_token %}
<fieldset class="fr-fieldset">
<div class="fr-fieldset__element">
{% if form.non_field_errors %}
<section class="fr-my-4v fr-input-group fr-input-group--error">
{{ form.non_field_errors }}
</section>
{% endif %}
<ul class="fr-toggle__list">
{% for group_item in form.group_items %}
{% get_form_field form group_item.field_name as field %}
<li>
<div class="fr-toggle fr-toggle--label-left fr-toggle--border-bottom fr-mt-8v">
{% with aria_describedby="aria-describedby:"|add:field.auto_id|add:"-hint-text" %}
{{ field|dsfr_input_class_attr|attr:"type:checkbox"|attr:aria_describedby|attr:"class:fr-toggle__input" }}
{% endwith %}
<label class="fr-toggle__label"
for="{{ field.id_for_label }}"
data-fr-checked-label="Activé"
data-fr-unchecked-label="Désactivé">
{{ group_item.group.display_name }}
</label>
<p class="fr-hint-text" id="{{ field.id_for_label }}-hint-text">{{ group_item.group.description }}</p>
</div>
</li>
{% endfor %}
</ul>
</div>
<div class="fr-fieldset__element">
<ul class="fr-btns-group--right fr-btns-group fr-btns-group--inline">
<li>
<button class="fr-mt-2v fr-btn fr-btn" type="submit">Sauvegarder</button>
</li>
</ul>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
6 changes: 6 additions & 0 deletions lemarche/utils/templatetags/theme_inclusion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Exist also in the base code of C1 :
https://github.com/betagouv/itou/blob/master/itou/utils/templatetags/theme_inclusion.py
"""

from django import template
from django.templatetags.static import static
from django.utils.safestring import mark_safe
Expand Down Expand Up @@ -99,3 +100,8 @@ def import_static_JS_theme_inclusion():
else:
scripts_import += '<script src="{}"></script>'.format(static_theme(js_dep["src"]))
return mark_safe(scripts_import)


@register.simple_tag
def get_form_field(form, field_name):
return form[field_name]
34 changes: 34 additions & 0 deletions lemarche/www/dashboard/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django import forms

from lemarche.conversations.models import DisabledEmail, EmailGroup
from lemarche.sectors.models import Sector
from lemarche.users.models import User
from lemarche.utils.fields import GroupedModelMultipleChoiceField
Expand Down Expand Up @@ -32,3 +33,36 @@ def __init__(self, *args, **kwargs):

# Disabled fields
self.fields["email"].disabled = True


class DisabledEmailEditForm(forms.Form):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None)
self.group_items = []
super().__init__(*args, **kwargs)

disabled_groups = [disable_email.group for disable_email in self.user.disabled_emails.all()]
for email_group in EmailGroup.objects.filter(can_be_unsubscribed=True, relevant_user_kind=self.user.kind):
field_name = f"email_group_{email_group.pk}"
self.fields[field_name] = forms.BooleanField(
required=False,
label=email_group.display_name,
initial=email_group not in disabled_groups,
widget=forms.CheckboxInput(),
)
self.group_items.append({"group": email_group, "field_name": field_name})

def save(self):
disabled_emails = []

# add unchecked fields to disabled_emails
for field_name, value in self.cleaned_data.items():
if field_name.startswith("email_group_"):
if not value:
group = EmailGroup.objects.get(pk=int(field_name.replace("email_group_", "")))
disabled_email, _ = DisabledEmail.objects.get_or_create(user=self.user, group=group)
disabled_emails.append(disabled_email)
self.user.disabled_emails.set(disabled_emails)

# remove old disabled_emails
DisabledEmail.objects.exclude(pk__in=[de.pk for de in disabled_emails]).delete()
Loading

0 comments on commit b50530a

Please sign in to comment.