Skip to content

Commit

Permalink
Create email consent page
Browse files Browse the repository at this point in the history
  • Loading branch information
mathjazz committed May 15, 2024
1 parent 890be4a commit 441bd52
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 4 deletions.
18 changes: 14 additions & 4 deletions docs/admin/deployment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
21 changes: 21 additions & 0 deletions pontoon/base/middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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 raygun4py.middleware.django import Provider

Expand Down Expand Up @@ -34,3 +36,22 @@ def process_request(self, request):
return HttpResponseForbidden("<h1>Forbidden</h1>")

return None


class EmailConsentMiddleware(MiddlewareMixin):
def process_request(self, request):
qualified_user = (
request.user.is_authenticated
and request.user.profile.email_consent_dismissed_at is None
)

qualified_path = request.path not in [
reverse("pontoon.messaging.email_consent"),
reverse("pontoon.messaging.dismiss_email_consent"),
]

if qualified_user and qualified_path:
request.session["next_path"] = request.get_full_path()
return redirect("pontoon.messaging.email_consent")

return None
Original file line number Diff line number Diff line change
@@ -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),
),
]
1 change: 1 addition & 0 deletions pontoon/base/models/user_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Empty file added pontoon/messaging/__init__.py
Empty file.
75 changes: 75 additions & 0 deletions pontoon/messaging/static/css/email_consent.css
Original file line number Diff line number Diff line change
@@ -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);
}
26 changes: 26 additions & 0 deletions pontoon/messaging/static/js/email_consent.js
Original file line number Diff line number Diff line change
@@ -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');
}
},
});
});
});
29 changes: 29 additions & 0 deletions pontoon/messaging/templates/messaging/email_consent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{% extends 'base.html' %}

{% block title %}Email consent{% endblock %}

{% block class %}email-consent{% endblock %}

{% block middle %}
<section id="main">
<section>
<div class="container">
<h1>Let’s keep in touch</h1>
<p class="main-text">{{ settings.EMAIL_COMMUNICATIONS_MAIN_TEXT|safe }}</p>
<div class="buttons">
<a class="button enable" href="/">Enable email communications</a>
<a class="button disable" href="/">No, thanks</a>
</div>
<p class="privacy-notice">{{ settings.EMAIL_COMMUNICATIONS_PRIVACY_NOTICE|safe }}</p>
</div>
</section>
</section>
{% endblock %}

{% block extend_css %}
{% stylesheet 'email_consent' %}
{% endblock %}

{% block extend_js %}
{% javascript 'email_consent' %}
{% endblock %}
17 changes: 17 additions & 0 deletions pontoon/messaging/urls.py
Original file line number Diff line number Diff line change
@@ -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",
),
]
43 changes: 43 additions & 0 deletions pontoon/messaging/views.py
Original file line number Diff line number Diff line change
@@ -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", "/"),
}
)
17 changes: 17 additions & 0 deletions pontoon/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ 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_COMMUNICATIONS_MAIN_TEXT", "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.
Expand All @@ -233,6 +240,7 @@ def _default_from_email():
"pontoon.insights",
"pontoon.localizations",
"pontoon.machinery",
"pontoon.messaging",
"pontoon.projects",
"pontoon.sync",
"pontoon.tags",
Expand Down Expand Up @@ -287,6 +295,7 @@ def _default_from_email():
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"csp.middleware.CSPMiddleware",
"pontoon.base.middleware.EmailConsentMiddleware",
)

CONTEXT_PROCESSORS = (
Expand Down Expand Up @@ -511,6 +520,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 = {
Expand Down Expand Up @@ -645,6 +658,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 = {
Expand Down
1 change: 1 addition & 0 deletions pontoon/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<locale:locale>/", team, name="pontoon.teams.team"),
]

0 comments on commit 441bd52

Please sign in to comment.