Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions TWLight/applications/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
form that takes a dict of required fields, and constructs the form accordingly.
(See the docstring of BaseApplicationForm for the expected dict format.)
"""

from dal import autocomplete
from crispy_forms.bootstrap import InlineField
from crispy_forms.helper import FormHelper
Expand Down
1 change: 1 addition & 0 deletions TWLight/applications/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Examples: users apply for access; coordinators evaluate applications and assign
status.
"""

import logging
import urllib.error
import urllib.parse
Expand Down
44 changes: 39 additions & 5 deletions TWLight/emails/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,12 @@
from TWLight.applications.signals import Reminder
from TWLight.resources.models import AccessCode, Partner
from TWLight.users.groups import get_restricted
from TWLight.users.signals import Notice, UserLoginRetrieval
from TWLight.users.signals import Notice, Survey, UserLoginRetrieval


logger = logging.getLogger(__name__)


# COMMENT NOTIFICATION
# ------------------------------------------------------------------------------


class CommentNotificationEmailEditors(template_mail.TemplateMail):
name = "comment_notification_editors"

Expand All @@ -74,6 +70,10 @@ class RejectionNotification(template_mail.TemplateMail):
name = "rejection_notification"


class SurveyActiveUser(template_mail.TemplateMail):
name = "survey_active_user"


class CoordinatorReminderNotification(template_mail.TemplateMail):
name = "coordinator_reminder_notification"

Expand Down Expand Up @@ -169,6 +169,40 @@ def send_user_renewal_notice_emails(sender, **kwargs):
)


@receiver(Survey.survey_active_user)
def send_survey_active_user_emails(sender, **kwargs):
"""
Any time the related managment command is run, this sends a survey
invitation to qualifying editors.
"""
user_email = kwargs["user_email"]
user_lang = kwargs["user_lang"]
survey_id = kwargs["survey_id"]
survey_langs = kwargs["survey_langs"]

# Default survey language is english
survey_lang = "en"

# Set survey language to user language if available
if user_lang in survey_langs:
survey_lang = user_lang

base_url = "https://wikimediafoundation.limesurvey.net/"
link = "{base}{id}?lang={lang}".format(
base=base_url, id=survey_id, lang=survey_lang
)

email = SurveyActiveUser()

email.send(
user_email,
{
"lang": user_lang,
"link": link,
},
)


@receiver(comment_was_posted)
def send_comment_notification_emails(sender, **kwargs):
"""
Expand Down
23 changes: 23 additions & 0 deletions TWLight/emails/templates/emails/survey_active_user-body-html.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% load i18n %}
<html>
<body>
{% comment %}Translators: This email is sent to users when we invite them to respond to a survey. Don't translate Jinja variables in curly braces like {{ link }}. Translate Wikipedia Library in the same way as the global branch is named (click through from https://meta.wikimedia.org/wiki/The_Wikipedia_Library).{% endcomment %}
{% blocktranslate trimmed %}
<p>Hi,</p>
<p>We are currently running a survey to understand how editors use
The Wikipedia Library. According to our records, you have logged in to
The Wikipedia Library at least once, so we would love to hear from you!</p>
<p><a href="{{ link }}">{{ link }}</a></p>
<p>We really appreciate your feedback - this survey will help us understand how
The Wikipedia Library is currently being used, and where it could use
improvement. Your responses will directly impact how the Wikimedia Foundation
prioritises technical and partnership work for this project.</p>
<p>The survey should take 5-10 minutes to complete.</p>
<p>If you would like to opt-out of being contacted with future Wikipedia Library
surveys, please let us know by emailing wikipedialibrary@wikimedia.org and we
will remove you from any future mailing.</p>
<p>Thanks,</p>
<p>The Wikipedia Library team</p>
{% endblocktranslate %}
</body>
</html>
26 changes: 26 additions & 0 deletions TWLight/emails/templates/emails/survey_active_user-body-text.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% load i18n %}
{% comment %}Translators: This email is sent to users when we invite them to respond to a survey. Don't translate Jinja variables in curly braces like {{ link }}. Translate Wikipedia Library in the same way as the global branch is named (click through from https://meta.wikimedia.org/wiki/The_Wikipedia_Library).{% endcomment %}
{% blocktranslate trimmed %}
Hi,

We are currently running a survey to understand how editors use
The Wikipedia Library. According to our records, you have logged in to
The Wikipedia Library at least once, so we would love to hear from you!

{{ link }}

We really appreciate your feedback - this survey will help us understand how
The Wikipedia Library is currently being used, and where it could use
improvement. Your responses will directly impact how the Wikimedia Foundation
prioritises technical and partnership work for this project.

The survey should take 5-10 minutes to complete.

If you would like to opt-out of being contacted with future Wikipedia Library
surveys, please let us know by emailing wikipedialibrary@wikimedia.org and we
will remove you from any future mailing.

Thanks,

The Wikipedia Library team
{% endblocktranslate %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load i18n %}

{% comment %}Translators: This is the subject of an email sent to users when we invite them to respond to a survey. Translate Wikipedia Library in the same way as the global branch is named (click through from https://meta.wikimedia.org/wiki/The_Wikipedia_Library).{% endcomment %}
{% trans 'The Wikipedia Library needs your help!' %}
141 changes: 141 additions & 0 deletions TWLight/emails/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.core import mail
from django.core.management import call_command
from django.urls import reverse
from django.utils import timezone
from django.test import TestCase, RequestFactory

from TWLight.applications.factories import (
Expand All @@ -33,6 +34,7 @@
send_approval_notification_email,
send_rejection_notification_email,
send_user_renewal_notice_emails,
send_survey_active_user_emails,
)


Expand Down Expand Up @@ -712,3 +714,142 @@ def test_send_coordinator_reminder_email(self):
self.assertIn("1 pending application", mail.outbox[0].body)
self.assertIn("1 under discussion application", mail.outbox[0].body)
self.assertIn("1 approved application", mail.outbox[0].body)


class SurveyActiveUsersEmailTest(TestCase):
@classmethod
def setUpTestData(cls):
"""
Creates a survey-eligible user and several eligible users.
Returns
-------
None
"""

now = timezone.now()

eligible = EditorFactory(user__email="editor@example.com")
eligible.wp_not_blocked = True
eligible.wp_bundle_eligible = True
eligible.wp_account_old_enough = True
eligible.wp_registered = now - timedelta(days=182)
eligible.wp_enough_edits = True
eligible.user.userprofile.terms_of_use = True
eligible.user.userprofile.save()
eligible.user.last_login = now
eligible.user.save()
eligible.save()
cls.eligible = eligible.user

blocked = EditorFactory(user__email="blocked@example.com")
blocked.wp_not_blocked = False
blocked.wp_bundle_eligible = True
blocked.wp_account_old_enough = True
blocked.wp_registered = now - timedelta(days=182)
blocked.wp_enough_edits = True
blocked.user.userprofile.terms_of_use = True
blocked.user.userprofile.save()
blocked.user.last_login = now
blocked.user.save()
blocked.save()

already_sent = EditorFactory(user__email="alreadysent@example.com")
already_sent.wp_not_blocked = True
already_sent.wp_bundle_eligible = True
already_sent.wp_account_old_enough = True
already_sent.wp_registered = now - timedelta(days=182)
already_sent.wp_enough_edits = True
already_sent.user.userprofile.terms_of_use = True
already_sent.user.userprofile.survey_email_sent = True
already_sent.user.userprofile.save()
already_sent.user.last_login = now
already_sent.user.save()
already_sent.save()

wmf_email = EditorFactory(user__email="editor@wikimedia.org")
wmf_email.wp_not_blocked = True
wmf_email.wp_bundle_eligible = True
wmf_email.wp_account_old_enough = True
wmf_email.wp_registered = now - timedelta(days=182)
wmf_email.wp_enough_edits = True
wmf_email.user.userprofile.terms_of_use = True
wmf_email.user.userprofile.save()
wmf_email.user.last_login = now
wmf_email.user.save()
wmf_email.save()

too_new_at_login = EditorFactory(user__email="toonew@example.com")
too_new_at_login.wp_not_blocked = True
too_new_at_login.wp_bundle_eligible = True
too_new_at_login.wp_account_old_enough = True
too_new_at_login.wp_registered = now - timedelta(days=182)
too_new_at_login.wp_enough_edits = True
too_new_at_login.user.userprofile.terms_of_use = True
too_new_at_login.user.userprofile.save()
too_new_at_login.user.last_login = now - timedelta(days=30)
too_new_at_login.user.save()
too_new_at_login.save()

not_enough_edits = EditorFactory(user__email="notenoughedits@example.com")
not_enough_edits.wp_not_blocked = True
not_enough_edits.wp_bundle_eligible = True
not_enough_edits.wp_account_old_enough = True
not_enough_edits.wp_registered = now - timedelta(days=182)
not_enough_edits.wp_enough_edits = False
not_enough_edits.user.userprofile.terms_of_use = True
not_enough_edits.user.userprofile.save()
not_enough_edits.user.last_login = now
not_enough_edits.user.save()
not_enough_edits.save()

inactive = EditorFactory(user__email="inactive@example.com")
inactive.wp_not_blocked = True
inactive.wp_bundle_eligible = True
inactive.wp_account_old_enough = True
inactive.wp_registered = now - timedelta(days=182)
inactive.wp_enough_edits = True
inactive.user.userprofile.terms_of_use = True
inactive.user.userprofile.save()
inactive.user.last_login = now
inactive.user.is_active = False
inactive.user.save()
inactive.save()

staff = EditorFactory(user__email="staff@example.com")
staff.wp_not_blocked = True
staff.wp_bundle_eligible = True
staff.wp_account_old_enough = True
staff.wp_registered = now - timedelta(days=182)
staff.wp_enough_edits = True
staff.user.userprofile.terms_of_use = True
staff.user.userprofile.save()
staff.user.last_login = now
staff.user.is_staff = True
staff.user.save()
staff.save()

superuser = EditorFactory(user__email="superuser@example.com")
superuser.wp_not_blocked = True
superuser.wp_bundle_eligible = True
superuser.wp_account_old_enough = True
superuser.wp_registered = now - timedelta(days=182)
superuser.wp_enough_edits = True
superuser.user.userprofile.terms_of_use = True
superuser.user.userprofile.save()
superuser.user.last_login = now
superuser.user.is_superuser = True
superuser.user.save()
superuser.save()

def test_survey_active_users_command(self):
self.assertFalse(self.eligible.userprofile.survey_email_sent)
call_command(
"survey_active_users",
"000001",
"en",
)

self.eligible.refresh_from_db()
self.assertTrue(self.eligible.userprofile.survey_email_sent)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, [self.eligible.email])
77 changes: 77 additions & 0 deletions TWLight/users/management/commands/survey_active_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from django.db.models import DurationField, ExpressionWrapper, F, Q
from django.db.models.functions import TruncDate
from django.utils.timezone import timedelta

from TWLight.users.groups import get_restricted
from TWLight.users.signals import Survey


class Command(BaseCommand):
help = "Sends survey invitation email to active users."

def add_arguments(self, parser):
parser.add_argument(
"--staff_test",
action="store_true",
help="A flag to email only to staff users who qualify other than staff status",
)
parser.add_argument(
"survey_id", type=int, help="ID number for corresponding survey"
)
parser.add_argument(
"lang",
nargs="+",
type=str,
help="List of localized language codes for this survey",
)

def handle(self, *args, **options):
staff_check = options["staff_test"]

# All Wikipedia Library users who:
for user in (
User.objects.select_related("editor", "userprofile")
.annotate(
# calculate account age at last login
last_login_age=ExpressionWrapper(
TruncDate(F("last_login")) - F("editor__wp_registered"),
output_field=DurationField(),
)
)
.filter(
# have not restricted data processing
~Q(groups__name__in=[get_restricted()]),
# meet the block criterion or have the 'ignore wp blocks' exemption
Q(editor__wp_not_blocked=True) | Q(editor__ignore_wp_blocks=True),
# have an non-wikimedia.org email address
Q(email__isnull=False) & ~Q(email__endswith="@wikimedia.org"),
# have not already received the email
userprofile__survey_email_sent=False,
# meet the 6 month criterion as of last login
last_login_age__gte=timedelta(days=182),
# meet the 500 edit criterion
editor__wp_enough_edits=True,
# are 'active'
is_active=True,
# are not staff
is_staff=staff_check,
# are not superusers
is_superuser=False,
)
.order_by("last_login")
):
# Send the email
Survey.survey_active_user.send(
sender=self.__class__,
user_email=user.email,
user_lang=user.userprofile.lang,
survey_id=options["survey_id"],
survey_langs=options["lang"],
)

# Record that we sent the email so that we only send one.
user.userprofile.survey_email_sent = True
user.userprofile.save()
4 changes: 3 additions & 1 deletion TWLight/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,14 @@ class Meta:
default=True,
help_text="Does this coordinator want approved app reminder notices?",
)

favorites = models.ManyToManyField(
Partner,
blank=True,
help_text="The partner(s) that the user has marked as favorite.",
)
survey_email_sent = models.BooleanField(
default=False, help_text="Has this user recieved the most recent survey email?"
)

def delete_my_library_cache(self):
"""
Expand Down
Loading
Loading