From 6634366e7c68dd7512ba807f7815fa264c4acc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matja=C5=BE=20Horvat?= Date: Wed, 15 May 2024 20:12:39 +0200 Subject: [PATCH] Create email consent page --- docker/config/server.env.template | 6 ++ docs/admin/deployment.rst | 18 ++++- pontoon/base/middleware.py | 25 +++++++ ..._userprofile_email_consent_dismissed_at.py | 18 +++++ pontoon/base/models/user_profile.py | 1 + pontoon/messaging/__init__.py | 0 .../messaging/static/css/email_consent.css | 75 +++++++++++++++++++ pontoon/messaging/static/js/email_consent.js | 26 +++++++ .../templates/messaging/email_consent.html | 29 +++++++ pontoon/messaging/urls.py | 17 +++++ pontoon/messaging/views.py | 43 +++++++++++ pontoon/settings/base.py | 15 ++++ pontoon/urls.py | 1 + 13 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 pontoon/base/migrations/0062_userprofile_email_consent_dismissed_at.py create mode 100644 pontoon/messaging/__init__.py create mode 100644 pontoon/messaging/static/css/email_consent.css create mode 100644 pontoon/messaging/static/js/email_consent.js create mode 100644 pontoon/messaging/templates/messaging/email_consent.html create mode 100644 pontoon/messaging/urls.py create mode 100644 pontoon/messaging/views.py diff --git a/docker/config/server.env.template b/docker/config/server.env.template index 450bac549b..33837a4019 100644 --- a/docker/config/server.env.template +++ b/docker/config/server.env.template @@ -9,4 +9,10 @@ FXA_CLIENT_ID=727f0251c388a993 FXA_SECRET_KEY=e43fd751ca5687d28288098e3e9b1294792ed9954008388e39b1cdaac0a1ebd6 FXA_OAUTH_ENDPOINT=https://oauth.stage.mozaws.net/v1 FXA_PROFILE_ENDPOINT=https://profile.stage.mozaws.net/v1 + +EMAIL_CONSENT_ENABLED = False +EMAIL_COMMUNICATIONS_MAIN_TEXT = Want to stay up to date and informed about all localization matters at Mozilla? Just hit the button below to get the latest updates, announcements about new Pontoon features, invitations to contributor events and more. We won’t spam you — promise! +EMAIL_COMMUNICATIONS_PRIVACY_NOTICE = By enabling email communications, I agree to Mozilla handling my personal information as explained in this Privacy Notice. +EMAIL_COMMUNICATIONS_HELP_TEXT = Get the latest updates about localization at Mozilla, announcements about new Pontoon features, invitations to contributor events and more.

By enabling email communications, I agree to Mozilla handling my personal information as explained in this Privacy Notice. + PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python diff --git a/docs/admin/deployment.rst b/docs/admin/deployment.rst index d164949750..04f645b2ae 100644 --- a/docs/admin/deployment.rst +++ b/docs/admin/deployment.rst @@ -108,7 +108,7 @@ you create: Adds some additional django apps that can be helpful during day to day development. ``EMAIL_HOST`` - SMTP host (default: ``'smtp.sendgrid.net'``) + SMTP host (default: ``'smtp.sendgrid.net'``). ``EMAIL_HOST_PASSWORD`` Password for the SMTP connection. @@ -117,13 +117,23 @@ you create: Username for the SMTP connection (default: ``'apikey'``). ``EMAIL_PORT`` - SMTP port (default: ``587``) + SMTP port (default: ``587``). ``EMAIL_USE_TLS`` - Use explicit TLS for the SMTP connection (default: ``True``) + Use explicit TLS for the SMTP connection (default: ``True``). ``EMAIL_USE_SSL`` - Use implicit TLS for the SMTP connection (default: ``False``) + Use implicit TLS for the SMTP connection (default: ``False``). + +``EMAIL_CONSENT_ENABLED`` + Enables Email consent page (default: ``False``). + +``EMAIL_COMMUNICATIONS_MAIN_TEXT`` + Optional. Main text to use on the Email consent page, possibly explaining what type + of communication to expect. + +``EMAIL_COMMUNICATIONS_PRIVACY_NOTICE`` + Optional. Text and link to the privacy notice, used on the Email consent page. ``EMAIL_COMMUNICATIONS_HELP_TEXT`` Optional. Help text to use under the Email communications checkbox in user settings. diff --git a/pontoon/base/middleware.py b/pontoon/base/middleware.py index aa73ca92cc..36e86d08fc 100644 --- a/pontoon/base/middleware.py +++ b/pontoon/base/middleware.py @@ -1,7 +1,10 @@ from django.conf import settings from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponseForbidden +from django.shortcuts import redirect +from django.urls import reverse from django.utils.deprecation import MiddlewareMixin +from pontoon.base.utils import is_ajax from raygun4py.middleware.django import Provider @@ -34,3 +37,25 @@ def process_request(self, request): return HttpResponseForbidden("

Forbidden

") return None + + +class EmailConsentMiddleware(MiddlewareMixin): + def process_request(self, request): + if not settings.EMAIL_CONSENT_ENABLED: + return None + + if not request.user.is_authenticated: + return None + + if request.user.profile.email_consent_dismissed_at is not None: + return None + + if is_ajax(request): + return None + + email_consent_url = "pontoon.messaging.email_consent" + if request.path == reverse(email_consent_url): + return None + + request.session["next_path"] = request.get_full_path() + return redirect(email_consent_url) diff --git a/pontoon/base/migrations/0062_userprofile_email_consent_dismissed_at.py b/pontoon/base/migrations/0062_userprofile_email_consent_dismissed_at.py new file mode 100644 index 0000000000..3a3fed8645 --- /dev/null +++ b/pontoon/base/migrations/0062_userprofile_email_consent_dismissed_at.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2024-05-15 16:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("base", "0061_userprofile_email_communications_enabled"), + ] + + operations = [ + migrations.AddField( + model_name="userprofile", + name="email_consent_dismissed_at", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/pontoon/base/models/user_profile.py b/pontoon/base/models/user_profile.py index fe12d20db7..2062bfcaee 100644 --- a/pontoon/base/models/user_profile.py +++ b/pontoon/base/models/user_profile.py @@ -19,6 +19,7 @@ class UserProfile(models.Model): contact_email = models.EmailField("Contact email address", blank=True, null=True) contact_email_verified = models.BooleanField(default=False) email_communications_enabled = models.BooleanField(default=False) + email_consent_dismissed_at = models.DateTimeField(null=True, blank=True) # Theme class Themes(models.TextChoices): diff --git a/pontoon/messaging/__init__.py b/pontoon/messaging/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pontoon/messaging/static/css/email_consent.css b/pontoon/messaging/static/css/email_consent.css new file mode 100644 index 0000000000..15624924de --- /dev/null +++ b/pontoon/messaging/static/css/email_consent.css @@ -0,0 +1,75 @@ +body > header { + background: transparent; + border-color: transparent; + position: fixed; + width: 100%; + z-index: 10; +} + +body > header.menu-opened { + border-color: var(--main-border-1); +} + +#main section { + display: flex; + flex-direction: column; + height: 100vh; + + background-image: var(--homepage-background-image); + background-attachment: fixed; + background-size: cover; +} + +#main section .container { + display: flex; + flex: 1; + flex-direction: column; + align-items: start; + justify-content: center; + overflow: hidden; +} + +#main .buttons { + display: flex; + margin-bottom: 10px; +} + +#main .button { + background: transparent; + color: var(--white-1); + display: flex; + font-size: 16px; + border-radius: 2px; + width: 120px; + height: 40px; + justify-content: center; + align-items: center; + font-weight: 400; +} + +#main .button.enable { + background-color: var(--status-translated); + color: var(--homepage-tour-button-color); + width: 320px; +} + +#main h1 { + font-size: 64px; + margin-bottom: 10px; +} + +#main p { + font-size: 22px; + font-weight: 300; + line-height: 36px; + margin-bottom: 60px; + width: 900px; +} + +#main p.privacy-notice { + font-size: 14px; +} + +#main p.privacy-notice a { + color: var(--status-translated); +} diff --git a/pontoon/messaging/static/js/email_consent.js b/pontoon/messaging/static/js/email_consent.js new file mode 100644 index 0000000000..e4c0ec28bd --- /dev/null +++ b/pontoon/messaging/static/js/email_consent.js @@ -0,0 +1,26 @@ +$(function () { + // Toggle user profile attribute + $('.buttons .button').click(function (e) { + e.preventDefault(); + const self = $(this); + + $.ajax({ + url: '/dismiss-email-consent/', + type: 'POST', + data: { + csrfmiddlewaretoken: $('body').data('csrf'), + value: self.is('.enable'), + }, + success: function (data) { + window.location.href = data.next; + }, + error: function (request) { + if (request.responseText === 'error') { + Pontoon.endLoader('Oops, something went wrong.', 'error'); + } else { + Pontoon.endLoader(request.responseText, 'error'); + } + }, + }); + }); +}); diff --git a/pontoon/messaging/templates/messaging/email_consent.html b/pontoon/messaging/templates/messaging/email_consent.html new file mode 100644 index 0000000000..525f894b8b --- /dev/null +++ b/pontoon/messaging/templates/messaging/email_consent.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block title %}Email consent{% endblock %} + +{% block class %}email-consent{% endblock %} + +{% block middle %} +
+
+
+

Let’s keep in touch

+

{{ settings.EMAIL_COMMUNICATIONS_MAIN_TEXT|safe }}

+ +

{{ settings.EMAIL_COMMUNICATIONS_PRIVACY_NOTICE|safe }}

+
+
+
+{% endblock %} + +{% block extend_css %} + {% stylesheet 'email_consent' %} +{% endblock %} + +{% block extend_js %} + {% javascript 'email_consent' %} +{% endblock %} diff --git a/pontoon/messaging/urls.py b/pontoon/messaging/urls.py new file mode 100644 index 0000000000..0faf2ffa5c --- /dev/null +++ b/pontoon/messaging/urls.py @@ -0,0 +1,17 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + # Email consent + path( + "email-consent/", + views.email_consent, + name="pontoon.messaging.email_consent", + ), + path( + "dismiss-email-consent/", + views.dismiss_email_consent, + name="pontoon.messaging.dismiss_email_consent", + ), +] diff --git a/pontoon/messaging/views.py b/pontoon/messaging/views.py new file mode 100644 index 0000000000..43e2540e65 --- /dev/null +++ b/pontoon/messaging/views.py @@ -0,0 +1,43 @@ +import json + +from django.contrib.auth.decorators import login_required +from django.db import transaction +from django.http import JsonResponse +from django.shortcuts import render +from django.utils import timezone +from django.views.decorators.http import require_POST + + +def email_consent(request): + return render( + request, + "messaging/email_consent.html", + ) + + +@login_required(redirect_field_name="", login_url="/403") +@require_POST +@transaction.atomic +def dismiss_email_consent(request): + value = request.POST.get("value", None) + + if not value: + return JsonResponse( + { + "status": False, + "message": "Bad Request: Value not set", + }, + status=400, + ) + + profile = request.user.profile + profile.email_communications_enabled = json.loads(value) + profile.email_consent_dismissed_at = timezone.now() + profile.save() + + return JsonResponse( + { + "status": True, + "next": request.session.get("next_path", "/"), + } + ) diff --git a/pontoon/settings/base.py b/pontoon/settings/base.py index 3b0b23a1ee..59fa36f57b 100644 --- a/pontoon/settings/base.py +++ b/pontoon/settings/base.py @@ -212,6 +212,11 @@ def _default_from_email(): EMAIL_HOST_PASSWORD = os.environ.get( "EMAIL_HOST_PASSWORD", os.environ.get("SENDGRID_PASSWORD", "") ) +EMAIL_CONSENT_ENABLED = os.environ.get("EMAIL_CONSENT_ENABLED", "False") != "False" +EMAIL_COMMUNICATIONS_MAIN_TEXT = os.environ.get("EMAIL_COMMUNICATIONS_MAIN_TEXT", "") +EMAIL_COMMUNICATIONS_PRIVACY_NOTICE = os.environ.get( + "EMAIL_COMMUNICATIONS_PRIVACY_NOTICE", "" +) EMAIL_COMMUNICATIONS_HELP_TEXT = os.environ.get("EMAIL_COMMUNICATIONS_HELP_TEXT", "") # Log emails to console if the SendGrid credentials are missing. @@ -233,6 +238,7 @@ def _default_from_email(): "pontoon.insights", "pontoon.localizations", "pontoon.machinery", + "pontoon.messaging", "pontoon.projects", "pontoon.sync", "pontoon.tags", @@ -287,6 +293,7 @@ def _default_from_email(): "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "csp.middleware.CSPMiddleware", + "pontoon.base.middleware.EmailConsentMiddleware", ) CONTEXT_PROCESSORS = ( @@ -511,6 +518,10 @@ def _default_from_email(): "source_filenames": ("css/homepage.css",), "output_filename": "css/homepage.min.css", }, + "email_consent": { + "source_filenames": ("css/email_consent.css",), + "output_filename": "css/email_consent.min.css", + }, } PIPELINE_JS = { @@ -645,6 +656,10 @@ def _default_from_email(): "source_filenames": ("js/homepage.js",), "output_filename": "js/homepage.min.js", }, + "email_consent": { + "source_filenames": ("js/email_consent.js",), + "output_filename": "js/email_consent.min.js", + }, } PIPELINE = { diff --git a/pontoon/urls.py b/pontoon/urls.py index 3811c14141..57ef1a6cee 100644 --- a/pontoon/urls.py +++ b/pontoon/urls.py @@ -70,6 +70,7 @@ class LocaleConverter(StringConverter): path("", include("pontoon.api.urls")), path("", include("pontoon.homepage.urls")), path("", include("pontoon.uxactionlog.urls")), + path("", include("pontoon.messaging.urls")), # Team page: Must be at the end path("/", team, name="pontoon.teams.team"), ]