Skip to content

Commit

Permalink
feat(Email Notifications): add for editing notification settings
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienReuiller committed Dec 12, 2024
1 parent b1275ca commit 3b8f520
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 5 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 = []
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ésincrire"),
),
],
),
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ésincrire", default=False)

def __str__(self):
return self.display_name

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"),
]
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()
39 changes: 39 additions & 0 deletions lemarche/www/dashboard/tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.test import TestCase
from django.urls import reverse

from lemarche.conversations.models import EmailGroup
from lemarche.users.factories import UserFactory
from lemarche.users.models import User

Expand Down Expand Up @@ -64,3 +65,41 @@ def test_viewing_dashboard_should_update_stats(self):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertIsNotNone(User.objects.get(id=self.user.id).dashboard_last_seen_date)


class DisabledEmailEditViewTest(TestCase):
def setUp(self):
self.user = UserFactory(kind=User.KIND_BUYER)
self.url = reverse("dashboard:notifications_edit")

def test_login_required(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 302)
self.assertIn("/accounts/login/", response.url)

def test_get_form_displays_correctly(self):
self.client.force_login(self.user)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "dashboard/disabled_email_edit.html")

# Check that the groups are displayed and that the 2 checkboxes are checked by default
for group in EmailGroup.objects.filter(relevant_user_kind=self.user.kind):
self.assertContains(response, group.display_name)
self.assertContains(response, " checked>", count=2)

def test_form_submission_updates_preferences(self):
self.assertEqual(self.user.disabled_emails.count(), 0)
self.client.force_login(self.user)
response = self.client.post(
self.url,
{
"email_group_1": True,
"email_group_2": False,
},
follow=True,
)
self.assertContains(response, "Vos préférences de notifications ont été mises à jour.")
self.user.refresh_from_db()
self.assertEqual(self.user.disabled_emails.count(), 1)
self.assertEqual(self.user.disabled_emails.first().group.pk, 2)
Loading

0 comments on commit 3b8f520

Please sign in to comment.