diff --git a/TWLight/emails/backends/__init__.py b/TWLight/emails/backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/TWLight/emails/backends/mediawiki.py b/TWLight/emails/backends/mediawiki.py new file mode 100644 index 0000000000..dd1a358506 --- /dev/null +++ b/TWLight/emails/backends/mediawiki.py @@ -0,0 +1,289 @@ +""" +Email backend that POSTs messages to the MediaWiki Emailuser endpoint. +see: https://www.mediawiki.org/wiki/API:Emailuser +""" +import logging +from json import dumps +from requests import Session +from requests.exceptions import ConnectionError +from requests.structures import CaseInsensitiveDict +from time import sleep + +from django.conf import settings +from django.core.mail.backends.base import BaseEmailBackend + +from TWLight.users.models import Editor + +logger = logging.getLogger(__name__) + + +def retry_conn(): + """A decorator that handles connection retries.""" + retry_delay = 0 + retry_on_connection_error = 10 + retry_after_conn = 5 + + def wrapper(func): + def conn(*args, **kwargs): + try_count = 0 + try_count_conn = 0 + while True: + try_count += 1 + try_count_conn += 1 + + if retry_delay: + sleep(retry_delay) + try: + return func(*args, **kwargs) + except ConnectionError as e: + no_retry_conn = 0 <= retry_on_connection_error < try_count_conn + if no_retry_conn: + logger.warning("ConnectionError exhausted retries") + raise e + logger.warning( + "ConnectionError, retrying in {}s".format(retry_after_conn) + ) + sleep(retry_after_conn) + continue + + return conn + + return wrapper + + +class EmailBackend(BaseEmailBackend): + def __init__( + self, + url=None, + timeout=None, + delay=None, + retry_delay=None, + maxlag=None, + username=None, + password=None, + fail_silently=False, + **kwargs, + ): + super().__init__(fail_silently=fail_silently) + self.url = settings.MW_API_URL if url is None else url + self.headers = CaseInsensitiveDict() + self.headers["User-Agent"] = "{}/0.0.1".format(__name__) + self.url = settings.MW_API_URL if url is None else url + self.timeout = settings.MW_API_REQUEST_TIMEOUT if timeout is None else timeout + self.delay = settings.MW_API_REQUEST_DELAY if delay is None else delay + self.retry_delay = ( + settings.MW_API_REQUEST_RETRY_DELAY if retry_delay is None else retry_delay + ) + self.maxlag = settings.MW_API_MAXLAG if maxlag is None else maxlag + self.username = settings.MW_API_EMAIL_USER if username is None else username + self.password = settings.MW_API_EMAIL_PASSWORD if password is None else password + self.email_token = None + self.session = None + logger.info("Email connection constructed.") + + def _handle_request(self, response, try_count=0): + """ + A helper method that handles MW API responses + including maxlag retries. + """ + # Raise for any HTTP response errors + if response.status_code != 200: + raise Exception("HTTP {} error".format(response.status_code)) + data = response.json() + error = data.get("error", {}) + if "warnings" in data: + logger.warning(dumps(data["warnings"], indent=True)) + # raise for any api error codes besides max lag + try: + if error.get("code") != "maxlag": + raise Exception(dumps(error)) + except: + # return data if there are no errors + return data + + # handle retries with max lag + lag = error.get("lag") + request = response.request + retry_after = float(response.headers.get("Retry-After", 5)) + retry_on_lag_error = 50 + no_retry = 0 <= retry_on_lag_error < try_count + message = "Server exceeded maxlag" + if not no_retry: + message += ", retrying in {}s".format(retry_after) + if lag: + message += ", lag={}".format(lag) + message += ", url={}".format(self.url) + log = logger.warning if no_retry else logger.info + log( + message, + { + "code": "maxlag-retry", + "retry-after": retry_after, + "lag": lag, + "x-database-lag": response.headers.get("X-Database-Lag", 5), + }, + ) + if no_retry: + raise Exception(message) + + sleep(retry_after) + try_count += 1 + return self._handle_request(self.session.send(request), try_count) + + @retry_conn() + def open(self): + """ + Ensure an open session to the API server. Return whether or not a + new session was required (True or False) or None if an exception + passed silently. + """ + if self.session: + # Nothing to do if the session exists + return False + + try: + self.session = Session() + self.session.headers = self.headers + logger.info("Session created, getting login token...") + + # GET request to fetch login token + login_token_params = { + "action": "query", + "meta": "tokens", + "type": "login", + "maxlag": self.maxlag, + "format": "json", + } + login_token_response = self._handle_request( + self.session.get(url=self.url, params=login_token_params) + ) + login_token = login_token_response["query"]["tokens"]["logintoken"] + if not login_token: + self.session = None + raise Exception(dumps(login_token_response)) + + # POST request to log in. Use of main account for login is not + # supported. Obtain credentials via Special:BotPasswords + # (https://www.mediawiki.org/wiki/Special:BotPasswords) for lgname & lgpassword + login_params = { + "action": "login", + "lgname": self.username, + "lgpassword": self.password, + "lgtoken": login_token, + "maxlag": self.maxlag, + "format": "json", + } + logger.info("Signing in...") + login_response = self._handle_request( + self.session.post(url=self.url, data=login_params) + ) + + # GET request to fetch Email token + # see: https://www.mediawiki.org/wiki/API:Emailuser#Token + email_token_params = {"action": "query", "meta": "tokens", "format": "json"} + + logger.info("Getting email token...") + email_token_response = self._handle_request( + self.session.get(url=self.url, params=email_token_params) + ) + email_token = email_token_response["query"]["tokens"]["csrftoken"] + if not email_token: + self.session = None + raise Exception(dumps(email_token_response)) + + # Assign the email token + self.email_token = email_token + logger.info("Email API session ready.") + return True + except: + if not self.fail_silently: + raise + + def close(self): + """Unset the session.""" + self.email_token = None + self.session = None + logger.info("Session destroyed.") + + def send_messages(self, email_messages): + """ + Send one or more EmailMessage objects and return the number of email + messages sent. + """ + if not email_messages: + return 0 + new_session_created = self.open() + if not self.session or new_session_created is None: + # We failed silently on open(). + # Trying to send would be pointless. + return 0 + num_sent = 0 + for message in email_messages: + sent = self._send(message) + if sent: + num_sent += 1 + if new_session_created: + self.close() + return num_sent + + @retry_conn() + def _send(self, email_message): + """A helper method that does the actual sending.""" + if not email_message.recipients(): + return False + + try: + for recipient in email_message.recipients(): + # lookup the target editor from the email address + target_qs = Editor.objects.filter(user__email=recipient).values_list( + "wp_username", flat=True + ) + target_qs_count = target_qs.count() + if target_qs_count > 1: + raise Exception( + "skip shared email address: {}".format(list(target_qs)) + ) + + target = target_qs.first() + + # GET request to check if user is emailable + emailable_params = { + "action": "query", + "list": "users", + "ususers": target, + "usprop": "emailable", + "maxlag": self.maxlag, + "format": "json", + } + + emailable_response = self._handle_request( + self.session.post(url=self.url, data=emailable_params) + ) + emailable = "emailable" in emailable_response["query"]["users"][0] + if not emailable: + raise Exception("skip not emailable: {}".format(target)) + + # POST request to send an email + email_params = { + "action": "emailuser", + "target": target, + "subject": email_message.subject, + "text": email_message.body, + "token": self.email_token, + "maxlag": self.maxlag, + "format": "json", + } + + logger.info("Sending email...") + emailuser_response = self._handle_request( + self.session.post(url=self.url, data=email_params) + ) + if emailuser_response["emailuser"]["result"] != "Success": + raise Exception(dumps(emailuser_response)) + + logger.info("Email sent.") + except: + if not self.fail_silently: + raise + return False + return True diff --git a/TWLight/emails/management/commands/__init__.py b/TWLight/emails/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/TWLight/emails/management/commands/cleanup_email.py b/TWLight/emails/management/commands/cleanup_email.py new file mode 100644 index 0000000000..bed748b4c7 --- /dev/null +++ b/TWLight/emails/management/commands/cleanup_email.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from django.core.management.base import BaseCommand + +from TWLight.emails.models import Message + + +class Command(BaseCommand): + help = "Delete unsent emails." + + def add_arguments(self, parser): + parser.add_argument( + "--subject", + type=str, + required=False, + help="Email subject", + ) + + def handle(self, *args, **options): + subject = options["subject"] + + if subject is None: + Message.twl.unsent().delete() + else: + Message.twl.filter(subject=subject).unsent().delete() diff --git a/TWLight/emails/models.py b/TWLight/emails/models.py new file mode 100644 index 0000000000..7a2fab82fa --- /dev/null +++ b/TWLight/emails/models.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from django.conf import settings +from django.contrib.auth.models import User +from django.db import models +from django.utils.translation import gettext as _, override + +from djmail.models import Message + +from TWLight.users.models import UserProfile + + +class MessageQuerySet(models.QuerySet): + def unsent(self): + return self.exclude(status=Message.STATUS_SENT) + + def users_with_unsent(self): + email_addresses = self.unsent().values_list("to_email", flat=True) + return User.objects.filter(email__in=email_addresses) + + def userprofiles_with_unsent(self): + email_addresses = self.unsent().values_list("to_email", flat=True) + return UserProfile.objects.select_related("user").filter( + user__email__in=email_addresses + ) + + def user_pks_with_subject_list(self, subject, users): + if users is None: + return [] + + subjects = [] + # Get the localized subject for each available language + for lang_code, _lang_name in settings.LANGUAGES: + try: + with override(lang_code): + # Translators: do not translate + subjects.append(_(subject)) + except ValueError: + pass + + # Search for repients of sent messages with the one of the localized email subjects. + previous_recipients = self.filter( + status=Message.STATUS_SENT, + subject__in=subjects, + ).values_list("to_email", flat=True) + + # return a list of pks for users with matching email addresses + return users.filter(email__in=previous_recipients).values_list("pk", flat=True) + + +class MessageManager(models.Manager): + def get_queryset(self): + return MessageQuerySet(self.model, using=self._db) + + def unsent(self): + return self.get_queryset().unsent() + + def userprofiles_with_unsent(self): + return self.get_queryset().userprofiles_with_unsent() + + def user_pks_with_subject_list(self, subject, users): + return self.get_queryset().user_pks_with_subject_list( + subject=subject, users=users + ) + + +# add "twl" manager to Message +Message.add_to_class("twl", MessageManager()) diff --git a/TWLight/emails/tasks.py b/TWLight/emails/tasks.py index 7e283e95c4..babbbd0af8 100644 --- a/TWLight/emails/tasks.py +++ b/TWLight/emails/tasks.py @@ -26,12 +26,15 @@ from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail import logging import os +from uuid import uuid4 from reversion.models import Version from django_comments.models import Comment from django_comments.signals import comment_was_posted from django.contrib.sites.shortcuts import get_current_site +from django.core.mail import get_connection from django.urls import reverse_lazy +from django.utils import timezone from django.db.models.signals import pre_save from django.dispatch import receiver from django.shortcuts import get_object_or_404 @@ -40,8 +43,8 @@ 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, Survey, UserLoginRetrieval - +from TWLight.users.signals import Notice, TestEmail, UserLoginRetrieval +from djmail.models import Message logger = logging.getLogger(__name__) @@ -74,6 +77,10 @@ class SurveyActiveUser(template_mail.TemplateMail): name = "survey_active_user" +class Test(template_mail.TemplateMail): + name = "test" + + class CoordinatorReminderNotification(template_mail.TemplateMail): name = "coordinator_reminder_notification" @@ -169,12 +176,22 @@ def send_user_renewal_notice_emails(sender, **kwargs): ) -@receiver(Survey.survey_active_user) -def send_survey_active_user_emails(sender, **kwargs): +def send_survey_active_user_email(**kwargs): """ Any time the related managment command is run, this sends a survey invitation to qualifying editors. """ + backend = ( + kwargs["backend"] + if "backend" in kwargs + else "TWLight.emails.backends.mediawiki.EmailBackend" + ) + connection = ( + kwargs["connection"] + if "connection" in kwargs + else get_connection(backend=backend) + ) + user_email = kwargs["user_email"] user_lang = kwargs["user_lang"] survey_id = kwargs["survey_id"] @@ -192,15 +209,28 @@ def send_survey_active_user_emails(sender, **kwargs): base=base_url, id=survey_id, lang=survey_lang ) - email = SurveyActiveUser() + template_email = SurveyActiveUser() - email.send( + email = template_email.make_email_object( user_email, { "lang": user_lang, "link": link, }, + connection=connection, + ) + email.send() + + +@receiver(TestEmail.test) +def send_test(sender, **kwargs): + user_email = kwargs["email"] + connection = get_connection( + backend="TWLight.emails.backends.mediawiki.EmailBackend" ) + template_email = Test() + email = template_email.make_email_object(user_email, {}, connection=connection) + email.send() @receiver(comment_was_posted) diff --git a/TWLight/emails/templates/emails/test-body-html.html b/TWLight/emails/templates/emails/test-body-html.html new file mode 100644 index 0000000000..27da8f6c1a --- /dev/null +++ b/TWLight/emails/templates/emails/test-body-html.html @@ -0,0 +1 @@ +

Please disregard this message; this is only a test

diff --git a/TWLight/emails/templates/emails/test-body-text.html b/TWLight/emails/templates/emails/test-body-text.html new file mode 100644 index 0000000000..c76a832c15 --- /dev/null +++ b/TWLight/emails/templates/emails/test-body-text.html @@ -0,0 +1 @@ +Please disregard this message; this is only a test diff --git a/TWLight/emails/templates/emails/test-subject.html b/TWLight/emails/templates/emails/test-subject.html new file mode 100644 index 0000000000..853a60e1c9 --- /dev/null +++ b/TWLight/emails/templates/emails/test-subject.html @@ -0,0 +1 @@ +test message; please disregard diff --git a/TWLight/emails/tests.py b/TWLight/emails/tests.py index b4ef0cdd21..a049476ffd 100644 --- a/TWLight/emails/tests.py +++ b/TWLight/emails/tests.py @@ -14,6 +14,7 @@ from django.urls import reverse from django.utils import timezone from django.test import TestCase, RequestFactory +from django.test.utils import override_settings from TWLight.applications.factories import ( ApplicationFactory, @@ -34,7 +35,7 @@ send_approval_notification_email, send_rejection_notification_email, send_user_renewal_notice_emails, - send_survey_active_user_emails, + send_survey_active_user_email, ) @@ -720,7 +721,7 @@ class SurveyActiveUsersEmailTest(TestCase): @classmethod def setUpTestData(cls): """ - Creates a survey-eligible user and several eligible users. + Creates a survey-eligible user, several ineligible users, and one already sent message. Returns ------- None @@ -760,7 +761,6 @@ def setUpTestData(cls): 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() @@ -841,15 +841,26 @@ def setUpTestData(cls): superuser.user.save() superuser.save() + # Use the same override as djmail itself since the command dynamically changes the backend + @override_settings( + EMAIL_BACKEND="djmail.backends.default.EmailBackend", + DJMAIL_REAL_BACKEND="django.core.mail.backends.locmem.EmailBackend", + ) def test_survey_active_users_command(self): - self.assertFalse(self.eligible.userprofile.survey_email_sent) + # pre-send an email to the "alreadysent" editor + already_sent_msg = mail.EmailMessage( + "The Wikipedia Library needs your help!", + "Body", + "sender@example.com", + ["alreadysent@example.com"], + ) + already_sent_msg.send() + call_command( "survey_active_users", "000001", "en", + backend="djmail.backends.default.EmailBackend", ) - - 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]) + self.assertEqual(len(mail.outbox), 2) + self.assertEqual(mail.outbox[1].to, [self.eligible.email]) diff --git a/TWLight/settings/base.py b/TWLight/settings/base.py index ad5437b4f6..a353f7b3f6 100644 --- a/TWLight/settings/base.py +++ b/TWLight/settings/base.py @@ -419,6 +419,13 @@ def show_toolbar(request): # ------------------------------------------------------------------------------ TWLIGHT_API_PROVIDER_ENDPOINT = os.environ.get("TWLIGHT_API_PROVIDER_ENDPOINT", None) +MW_API_URL = os.environ.get("MW_API_URL", None) +MW_API_REQUEST_TIMEOUT = os.environ.get("MW_API_REQUEST_TIMEOUT", 60) +MW_API_REQUEST_DELAY = os.environ.get("MW_API_REQUEST_DELAY", 0) +MW_API_REQUEST_RETRY_DELAY = os.environ.get("MW_API_REQUEST_RETRY_DELAY", 5) +MW_API_MAXLAG = os.environ.get("MW_API_MAXLAG", 5) +MW_API_EMAIL_USER = os.environ.get("MW_API_EMAIL_USER", None) +MW_API_EMAIL_PASSWORD = os.environ.get("MW_API_EMAIL_PASSWORD", None) # COMMENTS CONFIGURATION # ------------------------------------------------------------------------------ diff --git a/TWLight/users/admin.py b/TWLight/users/admin.py index 7a41481e50..ed8509f293 100644 --- a/TWLight/users/admin.py +++ b/TWLight/users/admin.py @@ -125,7 +125,11 @@ class AuthorizationInline(admin.StackedInline): class UserAdmin(AuthUserAdmin): inlines = [EditorInline, UserProfileInline, AuthorizationInline] list_display = ["username", "get_wp_username", "email", "is_staff"] - list_filter = ["is_staff", "is_active", "is_superuser"] + list_filter = [ + "is_staff", + "is_active", + "is_superuser", + ] default_filters = ["is_active__exact=1"] search_fields = ["editor__wp_username", "username", "email"] diff --git a/TWLight/users/management/commands/survey_active_users.py b/TWLight/users/management/commands/survey_active_users.py index f4f9fa3258..7da9d4dee4 100644 --- a/TWLight/users/management/commands/survey_active_users.py +++ b/TWLight/users/management/commands/survey_active_users.py @@ -1,23 +1,26 @@ # -*- coding: utf-8 -*- +import logging + +from django.conf import settings from django.contrib.auth.models import User -from django.core.management.base import BaseCommand +from django.core.mail import get_connection +from django.core.management.base import BaseCommand, CommandError from django.db.models import DurationField, ExpressionWrapper, F, Q from django.db.models.functions import TruncDate from django.utils.timezone import timedelta +from django.utils.translation import gettext_lazy as _ +from TWLight.emails.models import Message +from TWLight.emails.tasks import send_survey_active_user_email from TWLight.users.groups import get_restricted -from TWLight.users.signals import Survey + +logger = logging.getLogger(__name__) 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" ) @@ -27,17 +30,58 @@ def add_arguments(self, parser): type=str, help="List of localized language codes for this survey", ) + parser.add_argument( + "--staff_test", + action="store_true", + required=False, + help="Email only staff users who qualify other than staff status", + ) + parser.add_argument( + "--batch_size", + type=int, + required=False, + help="number of emails to send; default is 1000", + ) + parser.add_argument( + "--backend", + type=str, + required=False, + help="djmail backend to use; default is TWLight.emails.backends.mediawiki.EmailBackend", + ) def handle(self, *args, **options): + # Validate the lang args + survey_langs = options["lang"] + valid_langs = [] + invalid_langs = [] + for lang_code, _lang_name in settings.LANGUAGES: + valid_langs.append(lang_code) + + for survey_lang in survey_langs: + if survey_lang not in valid_langs: + invalid_langs.append(survey_lang) + + if invalid_langs: + raise CommandError( + "invalid lang argument in list: {}".format(invalid_langs) + ) + # default mode excludes users who are staff or superusers role_filter = Q(is_staff=False) & Q(is_superuser=False) + batch_size = options["batch_size"] if options["batch_size"] else 1000 + backend = ( + options["backend"] + if options["backend"] + else "TWLight.emails.backends.mediawiki.EmailBackend" + ) + # test mode excludes users who are not staff and ignores superuser status if options["staff_test"]: role_filter = Q(is_staff=True) # All Wikipedia Library users who: - for user in ( + users = ( User.objects.select_related("editor", "userprofile") .annotate( # calculate account age at last login @@ -51,30 +95,59 @@ def handle(self, *args, **options): role_filter, # have not restricted data processing ~Q(groups__name__in=[get_restricted()]), - # meet the block criterion or have the 'ignore wp blocks' exemption + # meet the block criterion or are exempt 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, + Q(email__isnull=False) + & ~Q(email="") + & ~Q(email__endswith="@wikimedia.org"), + # meet the 6 month criterion as of last login or are exempt + Q(last_login_age__gte=timedelta(days=182)) + | Q(editor__ignore_wp_account_age_requirement=True), + # meet the 500 edit criterion or are exempt + Q(editor__wp_enough_edits=True) + | Q(editor__ignore_wp_edit_requirement=True), # are 'active' is_active=True, ) - .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"], + ) + logger.info("{} users qualify".format(users.count())) + previously_sent_user_pks = Message.twl.user_pks_with_subject_list( + # Translators: email subject line + subject=_("The Wikipedia Library needs your help!"), + users=users, + ) + logger.info( + "{} users previously sent message will be skipped".format( + len(previously_sent_user_pks) ) + ) + users = ( + users.exclude(pk__in=previously_sent_user_pks) + .distinct() + .order_by("last_login") + ) + logger.info("{} remaining users qualify".format(users.count())) + users = users[:batch_size] + logger.info("attempting to send to {} users".format(users.count())) + + # Use a single connection to send all emails + connection = get_connection(backend=backend) + connection.open() + + # send the emails + for user in users: + try: + send_survey_active_user_email( + sender=self.__class__, + backend=backend, # allows setting the djmail backend back to default for testing + connection=connection, # passing in the connection lets us handle these in bulk + user_email=user.email, + user_lang=user.userprofile.lang, + survey_id=options["survey_id"], + survey_langs=survey_langs, + ) + except Exception as e: + logger.error(e) - # Record that we sent the email so that we only send one. - user.userprofile.survey_email_sent = True - user.userprofile.save() + connection.close() diff --git a/TWLight/users/management/commands/test_email.py b/TWLight/users/management/commands/test_email.py new file mode 100644 index 0000000000..552a05bd07 --- /dev/null +++ b/TWLight/users/management/commands/test_email.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + +from TWLight.users.signals import TestEmail + + +class Command(BaseCommand): + help = "Sends testmail to a wikipedia editor." + + def add_arguments(self, parser): + parser.add_argument( + "wp_username", + type=str, + help="The wikipedia editor to send the test email to", + ) + + def handle(self, *args, **options): + user = User.objects.select_related("editor").get( + editor__wp_username=options["wp_username"] + ) + TestEmail.test.send( + sender=self.__class__, + wp_username=user.editor.wp_username, + email=user.email, + ) diff --git a/TWLight/users/models.py b/TWLight/users/models.py index 93bfb7e0c0..d252f75505 100644 --- a/TWLight/users/models.py +++ b/TWLight/users/models.py @@ -195,6 +195,7 @@ class Meta: blank=True, help_text="The partner(s) that the user has marked as favorite.", ) + # @TODO: drop this field survey_email_sent = models.BooleanField( default=False, help_text="Has this user recieved the most recent survey email?" ) diff --git a/TWLight/users/signals.py b/TWLight/users/signals.py index ab346d8104..67d274b0a1 100644 --- a/TWLight/users/signals.py +++ b/TWLight/users/signals.py @@ -25,8 +25,8 @@ class Notice(object): user_renewal_notice = Signal() -class Survey(object): - survey_active_user = Signal() +class TestEmail(object): + test = Signal() class UserLoginRetrieval(object): diff --git a/conf/local.twlight.env b/conf/local.twlight.env index d61d8e4be3..3de1c323c3 100644 --- a/conf/local.twlight.env +++ b/conf/local.twlight.env @@ -19,5 +19,6 @@ DEBUG=True TWLIGHT_OAUTH_PROVIDER_URL=https://meta.wikimedia.org/w/index.php TWLIGHT_API_PROVIDER_ENDPOINT=https://meta.wikimedia.org/w/api.php TWLIGHT_EZPROXY_URL=https://ezproxy.dev.localdomain:2443 +MW_API_URL=http://host.docker.internal:8080/w/api.php # seeem to be having troubles with --workers > 1 GUNICORN_CMD_ARGS=--name twlight --workers 1 --backlog 2048 --timeout 300 --bind=0.0.0.0:80 --forwarded-allow-ips * --reload --log-level=info diff --git a/conf/production.twlight.env b/conf/production.twlight.env index 26ed9cf526..6882113805 100644 --- a/conf/production.twlight.env +++ b/conf/production.twlight.env @@ -20,4 +20,5 @@ DEBUG=False TWLIGHT_OAUTH_PROVIDER_URL=https://meta.wikimedia.org/w/index.php TWLIGHT_API_PROVIDER_ENDPOINT=https://meta.wikimedia.org/w/api.php TWLIGHT_EZPROXY_URL=https://wikipedialibrary.idm.oclc.org +MW_API_URL=https://meta.wikimedia.org/w/api.php GUNICORN_CMD_ARGS=--name twlight --worker-class gthread --workers 9 --threads 1 --timeout 30 --backlog 2048 --bind=0.0.0.0:80 --forwarded-allow-ips * --reload --log-level=info diff --git a/conf/staging.twlight.env b/conf/staging.twlight.env index ef8cbf693d..20519ee589 100644 --- a/conf/staging.twlight.env +++ b/conf/staging.twlight.env @@ -20,4 +20,5 @@ DEBUG=False TWLIGHT_OAUTH_PROVIDER_URL=https://meta.wikimedia.org/w/index.php TWLIGHT_API_PROVIDER_ENDPOINT=https://meta.wikimedia.org/w/api.php TWLIGHT_EZPROXY_URL=https://wikipedialibrary.idm.oclc.org:9443 +MW_API_URL=https://meta.wikimedia.org/w/api.php GUNICORN_CMD_ARGS=--name twlight --worker-class gthread --workers 4 --threads 1 --timeout 30 --backlog 2048 --bind=0.0.0.0:80 --forwarded-allow-ips * --reload --log-level=info diff --git a/docker-compose.override.yml b/docker-compose.override.yml index c09de97e7d..a020ba52be 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -25,9 +25,14 @@ secrets: services: twlight: + extra_hosts: + - "host.docker.internal:host-gateway" image: quay.io/wikipedialibrary/twlight:local env_file: - - ./conf/local.twlight.env + - path: ./conf/local.twlight.env + required: true + - path: .env + required: false # Local environment should mount things from the code directory volumes: - type: bind