Skip to content

Commit

Permalink
✨ collect feedback after newsletter unsubscribe
Browse files Browse the repository at this point in the history
  • Loading branch information
krmax44 committed May 13, 2024
1 parent 7462ad7 commit 3497571
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 34 deletions.
12 changes: 9 additions & 3 deletions fragdenstaat_de/fds_newsletter/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
from froide.helper.csv_utils import export_csv, export_csv_response

from .forms import SubscriberImportForm
from .models import Newsletter, Subscriber
from .models import Newsletter, Subscriber, UnsubscribeFeedback
from .utils import unsubscribe_queryset


@admin.register(Newsletter)
class NewsletterAdmin(admin.ModelAdmin):
list_display = ("title", "visible", "subscriber_count", "admin_subscribers")
prepopulated_fields = {"slug": ("title",)}
Expand Down Expand Up @@ -82,6 +83,7 @@ def import_csv(self, request, pk):
return render(request, "fds_newsletter/admin/import_csv.html", ctx)


@admin.register(Subscriber)
class SubscriberAdmin(SetupMailingMixin, admin.ModelAdmin):
raw_id_fields = ("user",)
list_display = (
Expand Down Expand Up @@ -185,5 +187,9 @@ def setup_mailing_messages(self, mailing, queryset):
)


admin.site.register(Subscriber, SubscriberAdmin)
admin.site.register(Newsletter, NewsletterAdmin)
@admin.register(UnsubscribeFeedback)
class UnsubscribeFeedbackAdmin(admin.ModelAdmin):
list_display = ("reason", "comment", "created")
list_filter = ("reason", "created")
search_fields = ("comment",)
date_hierarchy = "created"
34 changes: 23 additions & 11 deletions fragdenstaat_de/fds_newsletter/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

from django import forms
from django.conf import settings
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _

from froide.helper.spam import SpamProtectionMixin
from froide.helper.widgets import BootstrapCheckboxSelectMultiple
from froide.helper.widgets import BootstrapCheckboxSelectMultiple, BootstrapRadioSelect

from .models import Newsletter, Subscriber
from .utils import import_csv, subscribe
from .models import Newsletter, Subscriber, UnsubscribeFeedback
from .utils import import_csv, subscribe, subscribed_newsletters


class NewsletterForm(SpamProtectionMixin, forms.Form):
Expand Down Expand Up @@ -60,21 +61,16 @@ def save(self, newsletter, user):
class NewslettersUserForm(forms.Form):
newsletters = forms.ModelMultipleChoiceField(
label=_("Newsletters"),
queryset=None,
queryset=Newsletter.objects.get_visible().filter(visible=True),
required=False,
widget=BootstrapCheckboxSelectMultiple,
)

def __init__(self, user, *args, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
newsletters = Newsletter.objects.get_visible()
subscribed_nls = list(
Subscriber.objects.filter(
newsletter__in=newsletters, user=self.user, subscribed__isnull=False
).values_list("newsletter_id", flat=True)
)
self.fields["newsletters"].queryset = newsletters

subscribed_nls = list(subscribed_newsletters(self.user))
self.fields["newsletters"].initial = subscribed_nls

def save(self):
Expand Down Expand Up @@ -162,3 +158,19 @@ def save(self, newsletter):
reference=self.cleaned_data["reference"],
email_confirmed=self.cleaned_data["email_confirmed"],
)


class UnsubscribeFeedbackForm(ModelForm):
class Meta:
model = UnsubscribeFeedback
fields = ["reason", "comment"]
widgets = {
"reason": BootstrapRadioSelect,
"comment": forms.Textarea(
attrs={
"class": "form-control",
"rows": "3",
"placeholder": _("Anything else you want to tell us?"),
}
),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Generated by Django 4.2.4 on 2024-05-13 12:45

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("fds_newsletter", "0004_alter_newslettercmsplugin_cmsplugin_ptr_and_more"),
]

operations = [
migrations.CreateModel(
name="UnsubscribeFeedback",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"reason",
models.CharField(
choices=[
("too_much", "I received too many emails"),
(
"expected_updates",
"I expected more updates regarding certain campaings",
),
(
"specific_request",
"I only signed up regarding a specific FOI request",
),
("not_interested", "The topics are not interesting to me"),
("dislike", "I don't like your work anymore"),
("other", "Other"),
],
max_length=50,
),
),
("comment", models.TextField(blank=True)),
("created", models.DateTimeField(auto_now_add=True)),
(
"newsletter",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="fds_newsletter.newsletter",
),
),
(
"subscriber",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="fds_newsletter.subscriber",
),
),
],
options={
"verbose_name": "Newsletter Unsubscribe Feedback",
},
),
migrations.AddConstraint(
model_name="unsubscribefeedback",
constraint=models.UniqueConstraint(
fields=("subscriber", "newsletter"), name="unique_feedback_subscriber"
),
),
]
31 changes: 31 additions & 0 deletions fragdenstaat_de/fds_newsletter/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,37 @@ def unsubscribe(self, method=""):
unsubscribed.send(sender=self)


UNSUBSCRIBE_REASONS = [
("too_much", _("I received too many emails")),
("expected_updates", _("I expected more updates regarding certain campaings")),
("specific_request", _("I only signed up regarding a specific FOI request")),
("not_interested", _("The topics are not interesting to me")),
("dislike", _("I don't like your work anymore")),
("other", _("Other")),
]


class UnsubscribeFeedback(models.Model):
reason = models.CharField(max_length=50, choices=UNSUBSCRIBE_REASONS)
comment = models.TextField(blank=True)
created = models.DateTimeField(auto_now_add=True)

newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE)

# only stored for one hour, to prevent spam
subscriber = models.ForeignKey(
Subscriber, blank=True, null=True, on_delete=models.CASCADE
)

class Meta:
verbose_name = _("Newsletter Unsubscribe Feedback")
constraints = [
models.UniqueConstraint(
fields=["subscriber", "newsletter"], name="unique_feedback_subscriber"
)
]


class NewsletterCMSPlugin(CMSPlugin):
newsletter = models.ForeignKey(
Newsletter, related_name="+", on_delete=models.CASCADE, null=True, blank=True
Expand Down
7 changes: 6 additions & 1 deletion fragdenstaat_de/fds_newsletter/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@
from froide.celery import app as celery_app

from .analytics import get_analytics
from .utils import cleanup_subscribers, send_onboarding_schedule
from .utils import cleanup_feedback, cleanup_subscribers, send_onboarding_schedule


@celery_app.task(name="fragdenstaat_de.fds_newsletter.cleanup_subscribers")
def cleanup_subscribers_task():
cleanup_subscribers()


@celery_app.task(name="fragdenstaat_de.fds_newsletter.cleanup_feedback")
def cleanup_feedback_task():
cleanup_feedback()


@celery_app.task(name="fragdenstaat_de.fds_newsletter.trigger_onboarding_schedule")
def trigger_onboarding_schedule():
now = timezone.now()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% load i18n %}
{% load form_helper %}
{% block app_body %}
<h1>
{% blocktrans %}
You successfully unsubscribed from {{ newsletter }}.
{% endblocktrans %}
</h1>
<p>{% trans "We'd love to know why you chose to unsubscribe." %}</p>
<form method="post" action="">
{% csrf_token %}
{% render_form form %}
<input type="submit" class="btn btn-primary" value="{% trans "Submit" %}">
</form>
{% endblock app_body %}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ <h5 class="card-header">Ihre Newsletter-Einstellungen</h5>
<p>Bitte wählen Sie, welche Newsletter Sie erhalten wollen.</p>
<form method="POST" action="{% url 'newsletter_user_settings' %}">
{% csrf_token %}
{% render_form form %}
{% render_form newsletter_form %}
<p>
<button class="btn btn-secondary" type="submit">{% trans "Update subscriptions" %}</button>
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
def newsletter_settings(context):
request = context["request"]
user = request.user
return {"form": NewslettersUserForm(user)}
return {"newsletter_form": NewslettersUserForm(user)}


def _get_newsletter(newsletter_slug=None):
Expand Down
7 changes: 7 additions & 0 deletions fragdenstaat_de/fds_newsletter/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from django.urls import path
from django.utils.translation import gettext_lazy as _

from .views import (
confirm_subscribe,
confirm_unsubscribe,
newsletter_ajax_subscribe_request,
newsletter_subscribe_request,
newsletter_user_settings,
unsubscribe_feedback,
)

urlpatterns = [
Expand Down Expand Up @@ -38,6 +40,11 @@
confirm_unsubscribe,
name="newsletter_confirm_unsubscribe",
),
path(
_("<slug:newsletter_slug>/feedback/"),
unsubscribe_feedback,
name="newsletter_unsubscribe_feedback",
),
# re_path(
# r'^(?P<newsletter_slug>[\w-]+)/subscription/'
# r'(?P<email>[-_a-zA-Z0-9@\.\+~]+)/'
Expand Down
16 changes: 15 additions & 1 deletion fragdenstaat_de/fds_newsletter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from froide.helper.email_sending import mail_registry

from .models import Newsletter, Subscriber
from .models import Newsletter, Subscriber, UnsubscribeFeedback


class SubscriptionResult(Enum):
Expand Down Expand Up @@ -142,6 +142,14 @@ def has_newsletter(user, newsletter=None, newsletter_slug=None) -> bool:
).exists()


def subscribed_newsletters(user) -> list[Newsletter]:
# .values_list would only result in a pk-list
return [
s.newsletter
for s in Subscriber.objects.filter(user=user, subscribed__isnull=False)
]


def cleanup_subscribers():
now = timezone.now()
month_ago = now - timedelta(days=30)
Expand All @@ -167,6 +175,12 @@ def cleanup_subscribers():
sub.save()


def cleanup_feedback():
now = timezone.now()
hour_ago = now - timedelta(hours=1)
UnsubscribeFeedback.objects.filter(created__lt=hour_ago).update(subscriber=None)


def send_onboarding_schedule(date: datetime.date):
schedule = getattr(settings, "NEWSLETTER_ONBOARDING_SCHEDULE", [])
for item in schedule:
Expand Down

0 comments on commit 3497571

Please sign in to comment.