зеркало из https://github.com/mozilla/pontoon.git
Create email consent page (#3231)
- Adds Email consent screen as per the spec for existing and new users. - Prevent random test failures
This commit is contained in:
Родитель
071829c820
Коммит
7f78be7d7f
|
@ -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!
|
||||
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
|
||||
|
|
|
@ -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,29 @@ 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``
|
||||
Optional. Enables Email consent page (default: ``False``).
|
||||
|
||||
``EMAIL_CONSENT_TITLE``
|
||||
Optional, unless ``EMAIL_CONSENT_ENABLED`` is ``True``.
|
||||
Title of the Email consent page.
|
||||
|
||||
``EMAIL_CONSENT_MAIN_TEXT``
|
||||
Optional, unless ``EMAIL_CONSENT_ENABLED`` is ``True``.
|
||||
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.
|
||||
|
||||
``EMAIL_COMMUNICATIONS_HELP_TEXT``
|
||||
Optional. Help text to use under the Email communications checkbox in user settings.
|
||||
|
|
|
@ -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,30 @@ class BlockedIpMiddleware(MiddlewareMixin):
|
|||
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)
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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():
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
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 .buttons button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
box-sizing: content-box;
|
||||
color: var(--white-1);
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
width: 120px;
|
||||
height: 40px;
|
||||
padding: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#main .buttons 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);
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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">
|
||||
<button class="enable">Enable email communications</button>
|
||||
<button class="disable">No, thanks</button>
|
||||
</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 %}
|
|
@ -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"] == "/"
|
|
@ -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",
|
||||
),
|
||||
]
|
|
@ -0,0 +1,44 @@
|
|||
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
|
||||
|
||||
|
||||
@login_required(redirect_field_name="", login_url="/403")
|
||||
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", "/"),
|
||||
}
|
||||
)
|
|
@ -212,6 +212,10 @@ EMAIL_USE_SSL = os.environ.get("EMAIL_USE_SSL", "False") != "False"
|
|||
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_CONSENT_TITLE = os.environ.get("EMAIL_CONSENT_TITLE", "")
|
||||
EMAIL_CONSENT_MAIN_TEXT = os.environ.get("EMAIL_CONSENT_MAIN_TEXT", "")
|
||||
EMAIL_CONSENT_PRIVACY_NOTICE = os.environ.get("EMAIL_CONSENT_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 +237,7 @@ INSTALLED_APPS = (
|
|||
"pontoon.insights",
|
||||
"pontoon.localizations",
|
||||
"pontoon.machinery",
|
||||
"pontoon.messaging",
|
||||
"pontoon.projects",
|
||||
"pontoon.sync",
|
||||
"pontoon.tags",
|
||||
|
@ -287,6 +292,7 @@ MIDDLEWARE = (
|
|||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"csp.middleware.CSPMiddleware",
|
||||
"pontoon.base.middleware.EmailConsentMiddleware",
|
||||
)
|
||||
|
||||
CONTEXT_PROCESSORS = (
|
||||
|
@ -511,6 +517,10 @@ PIPELINE_CSS = {
|
|||
"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 +655,10 @@ PIPELINE_JS = {
|
|||
"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 = {
|
||||
|
|
|
@ -70,6 +70,7 @@ urlpatterns = [
|
|||
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"),
|
||||
]
|
||||
|
|
Загрузка…
Ссылка в новой задаче