Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create email consent page #3231

Merged
merged 9 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docker/config/server.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,11 @@ 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_CONSENT_TITLE = Let’s keep in touch
EMAIL_CONSENT_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!
mathjazz marked this conversation as resolved.
Show resolved Hide resolved
EMAIL_CONSENT_PRIVACY_NOTICE = By enabling email communications, I agree to Mozilla handling my personal information as explained in this <a href="https://www.mozilla.org/privacy/websites/">Privacy Notice</a>.
EMAIL_COMMUNICATIONS_HELP_TEXT = Get the latest updates about localization at Mozilla, announcements about new Pontoon features, invitations to contributor events and more.<br/><br/>By enabling email communications, I agree to Mozilla handling my personal information as explained in this <a href="https://www.mozilla.org/privacy/websites/">Privacy Notice</a>.

PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
22 changes: 18 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,27 @@ 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_CONSENT_TITLE``
Optional. Title of the Email consent page.

``EMAIL_CONSENT_MAIN_TEXT``
Optional. Main text of the Email consent page. You can use that to explain what type
of communication to expect among other things.

``EMAIL_CONSENT_PRIVACY_NOTICE``
Optional. Privacy notice on the Email consent page. It's possible to use HTML and
link to external privacy notice page.
eemeli marked this conversation as resolved.
Show resolved Hide resolved

``EMAIL_COMMUNICATIONS_HELP_TEXT``
Optional. Help text to use under the Email communications checkbox in user settings.
Expand Down
30 changes: 30 additions & 0 deletions pontoon/base/middleware.py
Original file line number Diff line number Diff line change
@@ -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


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

return None


class EmailConsentMiddleware:
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
response = self.get_response(request)

if not settings.EMAIL_CONSENT_ENABLED:
return response

if not request.user.is_authenticated:
return response

if request.user.profile.email_consent_dismissed_at is not None:
return response

if is_ajax(request):
return response

email_consent_url = "pontoon.messaging.email_consent"
if request.path == reverse(email_consent_url):
return response

request.session["next_path"] = request.get_full_path()
return redirect(email_consent_url)
mathjazz marked this conversation as resolved.
Show resolved Hide resolved
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
31 changes: 31 additions & 0 deletions pontoon/base/tests/test_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import pytest

from django.urls import reverse
from django.utils import timezone


@pytest.mark.django_db
def test_EmailConsentMiddleware(client, member, settings):
# By default, Email consent page is disabled
response = member.client.get("/")
assert response.status_code == 200

# If Email consent page is enabled, redirect any view to it
settings.EMAIL_CONSENT_ENABLED = True
response = member.client.get("/")
assert response.status_code == 302

# Unless that view is the Email consent page itself
response = member.client.get(reverse("pontoon.messaging.email_consent"))
assert response.status_code == 200

# Or the request is AJAX
response = member.client.get("/", headers={"x-requested-with": "XMLHttpRequest"})
assert response.status_code == 200

# Or the user has already dismissed the Email consent
profile = member.user.profile
profile.email_consent_dismissed_at = timezone.now()
profile.save()
response = member.client.get("/")
assert response.status_code == 200
20 changes: 14 additions & 6 deletions pontoon/base/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,20 +556,28 @@ def test_get_m2m_removed(user_a, user_b):

@pytest.mark.django_db
def test_get_m2m_mixed(user_a, user_b, user_c):
assert get_m2m_changes(
changes = get_m2m_changes(
get_user_model().objects.filter(pk__in=[user_b.pk, user_c.pk]),
get_user_model().objects.filter(pk__in=[user_a.pk, user_b.pk]),
) == ([user_a], [user_c])
)
assert [user_a] == changes[0]
assert [user_c] == changes[1]

assert get_m2m_changes(
changes = get_m2m_changes(
get_user_model().objects.filter(pk__in=[user_a.pk, user_b.pk]),
get_user_model().objects.filter(pk__in=[user_c.pk]),
) == ([user_c], [user_a, user_b])
)
assert [user_c] == changes[0]
assert user_a in changes[1]
assert user_b in changes[1]

assert get_m2m_changes(
changes = get_m2m_changes(
get_user_model().objects.filter(pk__in=[user_b.pk]),
get_user_model().objects.filter(pk__in=[user_c.pk, user_a.pk]),
) == ([user_a, user_c], [user_b])
)
assert user_a in changes[0]
assert user_c in changes[0]
assert [user_b] == changes[1]


def test_util_base_extension_in():
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);
}
25 changes: 25 additions & 0 deletions pontoon/messaging/static/js/email_consent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
$(function () {
$('.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>{{ settings.EMAIL_CONSENT_TITLE }}</h1>
<p class="main-text">{{ settings.EMAIL_CONSENT_MAIN_TEXT|safe }}</p>
<div class="buttons">
<a class="button enable" href="/">Enable email communications</a>
<a class="button disable" href="/">No, thanks</a>
eemeli marked this conversation as resolved.
Show resolved Hide resolved
</div>
<p class="privacy-notice">{{ settings.EMAIL_CONSENT_PRIVACY_NOTICE|safe }}</p>
</div>
</section>
</section>
{% endblock %}

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

{% block extend_js %}
{% javascript 'email_consent' %}
{% endblock %}
Empty file.
32 changes: 32 additions & 0 deletions pontoon/messaging/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pytest

from pontoon.base.models import User


@pytest.mark.django_db
def test_dismiss_email_consent(member):
"""Test if dismiss_email_consent view works and fails as expected."""
params = {}
response = member.client.post(f"/dismiss-email-consent/", params)
assert response.status_code == 400
assert response.json()["message"] == "Bad Request: Value not set"

params = {
"value": "false",
}
response = member.client.post(f"/dismiss-email-consent/", params)
profile = User.objects.get(pk=member.user.pk).profile
assert profile.email_communications_enabled is False
assert profile.email_consent_dismissed_at is not None
assert response.status_code == 200
assert response.json()["next"] == "/"

params = {
"value": "true",
}
response = member.client.post(f"/dismiss-email-consent/", params)
profile = User.objects.get(pk=member.user.pk).profile
assert profile.email_communications_enabled is True
assert profile.email_consent_dismissed_at is not None
assert response.status_code == 200
assert response.json()["next"] == "/"
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",
),
]
Loading
Loading