Skip to content

Commit

Permalink
Implement email unsubscribe page (mozilla#3237)
Browse files Browse the repository at this point in the history
This patch adds a unique user identifier to existing and new users, and uses it to create personalized unsubscribe links.
  • Loading branch information
mathjazz authored May 30, 2024
1 parent fadf697 commit a3e5252
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 3 deletions.
19 changes: 19 additions & 0 deletions pontoon/base/migrations/0063_userprofile_unique_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.11 on 2024-05-22 12:25

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):

dependencies = [
("base", "0062_userprofile_email_consent_dismissed_at"),
]

operations = [
migrations.AddField(
model_name="userprofile",
name="unique_id",
field=models.UUIDField(default=uuid.uuid4, editable=False),
),
]
26 changes: 26 additions & 0 deletions pontoon/base/migrations/0064_populate_unique_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.11 on 2024-05-22 12:25

import uuid

from django.db import migrations


def populate_unique_id(apps, schema_editor):
UserProfile = apps.get_model("base", "UserProfile")
for profile in UserProfile.objects.all():
profile.unique_id = uuid.uuid4()
profile.save()


class Migration(migrations.Migration):

dependencies = [
("base", "0063_userprofile_unique_id"),
]

operations = [
migrations.RunPython(
code=populate_unique_id,
reverse_code=migrations.RunPython.noop,
),
]
4 changes: 4 additions & 0 deletions pontoon/base/models/user_profile.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import uuid

from django.contrib.auth.models import User
from django.contrib.postgres.fields import ArrayField
from django.db import models
Expand All @@ -11,6 +13,8 @@ class UserProfile(models.Model):
User, models.CASCADE, related_name="profile", primary_key=True
)

unique_id = models.UUIDField(default=uuid.uuid4, editable=False)

# Personal information
username = models.SlugField(unique=True, blank=True, null=True)
bio = models.TextField(max_length=160, blank=True, null=True)
Expand Down
9 changes: 8 additions & 1 deletion pontoon/messaging/static/css/email_consent.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ body > header.menu-opened {
border-color: var(--main-border-1);
}

#main {
padding: 0;
}

#main section {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -65,10 +69,13 @@ body > header.menu-opened {
font-size: 22px;
font-weight: 300;
line-height: 36px;
margin-bottom: 60px;
width: 900px;
}

#main p.main-text {
margin-bottom: 60px;
}

#main p.privacy-notice {
font-size: 14px;
}
Expand Down
50 changes: 50 additions & 0 deletions pontoon/messaging/static/css/unsubscribe.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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 {
padding: 0;
}

#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 h1 {
font-size: 64px;
margin-bottom: 10px;
}

#main p {
font-size: 22px;
font-weight: 300;
line-height: 36px;
width: 900px;
}

#main p a {
color: var(--status-translated);
}
20 changes: 20 additions & 0 deletions pontoon/messaging/templates/messaging/subscribe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends 'base.html' %}

{% block title %}Subscribe{% endblock %}

{% block class %}unsubscribe{% endblock %}

{% block middle %}
<section id="main">
<section>
<div class="container">
<h1>You've been subscribed</h1>
<p class="main-text">Subscribed by accident? <a href="{{ url('pontoon.messaging.unsubscribe', uuid) }}">Unsubscribe again</a> or update your email <a href="{{ url('pontoon.contributors.settings') }}">settings</a>.</p>
</div>
</section>
</section>
{% endblock %}

{% block extend_css %}
{% stylesheet 'unsubscribe' %}
{% endblock %}
20 changes: 20 additions & 0 deletions pontoon/messaging/templates/messaging/unsubscribe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends 'base.html' %}

{% block title %}Unsubscribe{% endblock %}

{% block class %}unsubscribe{% endblock %}

{% block middle %}
<section id="main">
<section>
<div class="container">
<h1>You've been unsubscribed</h1>
<p class="main-text">Unsubscribed by accident? <a href="{{ url('pontoon.messaging.subscribe', uuid) }}">Subscribe again</a> or update your email <a href="{{ url('pontoon.contributors.settings') }}">settings</a>.</p>
</div>
</section>
</section>
{% endblock %}

{% block extend_css %}
{% stylesheet 'unsubscribe' %}
{% endblock %}
12 changes: 12 additions & 0 deletions pontoon/messaging/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,16 @@
views.dismiss_email_consent,
name="pontoon.messaging.dismiss_email_consent",
),
# Unsubscribe
path(
"unsubscribe/<uuid:uuid>/",
views.unsubscribe,
name="pontoon.messaging.unsubscribe",
),
# Subscribe again
path(
"subscribe/<uuid:uuid>/",
views.subscribe,
name="pontoon.messaging.subscribe",
),
]
32 changes: 31 additions & 1 deletion pontoon/messaging/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
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.shortcuts import get_object_or_404, render
from django.utils import timezone
from django.views.decorators.http import require_POST

from pontoon.base.models import UserProfile


@login_required(redirect_field_name="", login_url="/403")
def email_consent(request):
Expand Down Expand Up @@ -42,3 +44,31 @@ def dismiss_email_consent(request):
"next": request.session.get("next_path", "/"),
}
)


def unsubscribe(request, uuid):
profile = get_object_or_404(UserProfile, unique_id=uuid)
profile.email_communications_enabled = False
profile.save(update_fields=["email_communications_enabled"])

return render(
request,
"messaging/unsubscribe.html",
{
"uuid": uuid,
},
)


def subscribe(request, uuid):
profile = get_object_or_404(UserProfile, unique_id=uuid)
profile.email_communications_enabled = True
profile.save(update_fields=["email_communications_enabled"])

return render(
request,
"messaging/subscribe.html",
{
"uuid": uuid,
},
)
4 changes: 4 additions & 0 deletions pontoon/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,10 @@ def _default_from_email():
"source_filenames": ("css/email_consent.css",),
"output_filename": "css/email_consent.min.css",
},
"unsubscribe": {
"source_filenames": ("css/unsubscribe.css",),
"output_filename": "css/unsubscribe.min.css",
},
}

PIPELINE_JS = {
Expand Down
2 changes: 1 addition & 1 deletion pontoon/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class LocaleConverter(StringConverter):
path("", include("pontoon.sync.urls")),
path("", include("pontoon.projects.urls")),
path("", include("pontoon.machinery.urls")),
path("", include("pontoon.messaging.urls")),
path("", include("pontoon.insights.urls")),
path("", include("pontoon.contributors.urls")),
path("", include("pontoon.localizations.urls")),
Expand All @@ -70,7 +71,6 @@ 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 a3e5252

Please sign in to comment.