diff --git a/TWLight/crons.py b/TWLight/crons.py index 526f29972e..654791c7cf 100644 --- a/TWLight/crons.py +++ b/TWLight/crons.py @@ -89,3 +89,14 @@ def do(self): management.call_command("djmail_delete_old_messages", days=100) except Exception as e: capture_exception(e) + + +class RetrieveMonthlyUsers(CronJobBase): + schedule = Schedule(run_monthly_on_days=1) + code = "users.retrieve_monthly_users" + + def do(self): + try: + management.call_command("retrieve_monthly_users") + except Exception as e: + capture_exception(e) diff --git a/TWLight/emails/tasks.py b/TWLight/emails/tasks.py index c98826acb0..0fbf3e0f83 100644 --- a/TWLight/emails/tasks.py +++ b/TWLight/emails/tasks.py @@ -21,9 +21,11 @@ whether to send synchronously or asynchronously based on the value of settings.DJMAIL_REAL_BACKEND. """ + from djmail import template_mail from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail import logging +import os from reversion.models import Version from django_comments.models import Comment @@ -38,7 +40,7 @@ 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 +from TWLight.users.signals import Notice, UserLoginRetrieval logger = logging.getLogger(__name__) @@ -80,6 +82,10 @@ class UserRenewalNotice(template_mail.TemplateMail): name = "user_renewal_notice" +class UserRetrieveMonthlyLogins(template_mail.TemplateMail): + name = "user_retrieve_monthly_logins" + + @receiver(Reminder.coordinator_reminder) def send_coordinator_reminder_emails(sender, **kwargs): """ @@ -492,3 +498,15 @@ def notify_applicants_when_waitlisted(sender, instance, **kwargs): status__in=[Application.PENDING, Application.QUESTION] ): send_waitlist_notification_email(app) + + +@receiver(UserLoginRetrieval.user_retrieve_monthly_logins) +def send_user_login_retrieval_email(sender, **kwargs): + monthly_users = kwargs["monthly_users"] + email = UserRetrieveMonthlyLogins() + logger.info("Email constructed.") + email.send( + os.environ.get("TWLIGHT_ERROR_MAILTO", "wikipedialibrary@wikimedia.org"), + {"monthly_users": monthly_users}, + ) + logger.info("Email queued.") diff --git a/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-html.html b/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-html.html new file mode 100644 index 0000000000..5393353787 --- /dev/null +++ b/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-html.html @@ -0,0 +1,9 @@ + +
+Hi TWL team,
+Here is a list of the users that logged-in last month that have approved + applications and current authorizations.
+ +{{ monthly_users|json_script:"monthly-users" }}
+ + diff --git a/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-text.html b/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-text.html new file mode 100644 index 0000000000..5cd409ff4f --- /dev/null +++ b/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-text.html @@ -0,0 +1,3 @@ +Hi TWL team, +Here is a list of the users that logged-in last month that have approved applications and current authorizations. +{{ monthly_users|json_script:"monthly-users" }} diff --git a/TWLight/emails/templates/emails/user_retrieve_monthly_logins-subject.html b/TWLight/emails/templates/emails/user_retrieve_monthly_logins-subject.html new file mode 100644 index 0000000000..31c992cb72 --- /dev/null +++ b/TWLight/emails/templates/emails/user_retrieve_monthly_logins-subject.html @@ -0,0 +1 @@ +Monthly user report diff --git a/TWLight/settings/base.py b/TWLight/settings/base.py index a16807975e..44c31ecd52 100644 --- a/TWLight/settings/base.py +++ b/TWLight/settings/base.py @@ -155,6 +155,7 @@ def get_django_faker_languages_intersection(languages): "TWLight.crons.UserUpdateEligibilityCronJob", "TWLight.crons.ClearSessions", "TWLight.crons.DeleteOldEmails", + "TWLight.crons.RetrieveMonthlyUsers", ] # We will only be keeping 100 days' worth of cron logs DJANGO_CRON_DELETE_LOGS_OLDER_THAN = 100 diff --git a/TWLight/users/management/commands/retrieve_monthly_users.py b/TWLight/users/management/commands/retrieve_monthly_users.py new file mode 100644 index 0000000000..a2b5c72e0c --- /dev/null +++ b/TWLight/users/management/commands/retrieve_monthly_users.py @@ -0,0 +1,55 @@ +import calendar +import datetime +from dateutil.relativedelta import relativedelta + +from django.core.management.base import BaseCommand +from django.db import connection + +from TWLight.users.signals import UserLoginRetrieval + + +class Command(BaseCommand): + help = "Retrieves user names that have logged-in in the past month and have approved applications and current authorizations." + + def handle(self, *args, **options): + current_date = datetime.datetime.now(datetime.timezone.utc).date() + last_month = current_date - relativedelta(months=1) + first_day_last_month = datetime.date(last_month.year, last_month.month, 1) + _, last_day = calendar.monthrange(last_month.year, last_month.month) + last_day_last_month = datetime.date(last_month.year, last_month.month, last_day) + + raw_query = """SELECT users_editor.wp_username, IF( + -- has application status APPROVED = 2 SENT = 4 + (applications_application.status = 2 OR applications_application.status = 4), 'true', 'false') AS has_approved_apps, + -- has authorizations that were: + IF(( + -- created no more than a year ago or + users_authorization.date_authorized >= date_sub(now(),interval 1 year) OR + -- expired no more than a year ago or + users_authorization.date_expires >= date_sub(now(),interval 1 year) OR + -- are currently active (eg. have currently associated partners) + COUNT(users_authorization_partners.id) > 0 + ), 'true', 'false') AS has_current_auths + FROM auth_user JOIN users_editor ON auth_user.id = users_editor.user_id + -- left outer join used to grab apps for approved_apps virtual column + LEFT OUTER JOIN applications_application ON users_editor.id = applications_application.editor_id + -- left outer join used to grab auths for current_auths virtual column + LEFT OUTER JOIN users_authorization ON auth_user.id = users_authorization.user_id + -- left outer join used to grab auth partners for current_auths virtual column + LEFT OUTER JOIN users_authorization_partners ON users_authorization.id = users_authorization_partners.authorization_id + -- limit to people who logged in within the last month + WHERE auth_user.last_login >= '{first_day_last_month}' AND auth_user.last_login <= '{last_day_last_month}' + GROUP BY users_editor.wp_username;""".format( + first_day_last_month=first_day_last_month, + last_day_last_month=last_day_last_month, + ) + + with connection.cursor() as cursor: + cursor.execute(raw_query) + columns = [col[0] for col in cursor.description] + monthly_users = [dict(zip(columns, row)) for row in cursor.fetchall()] + + if monthly_users: + UserLoginRetrieval.user_retrieve_monthly_logins.send( + sender=self.__class__, monthly_users=monthly_users + ) diff --git a/TWLight/users/signals.py b/TWLight/users/signals.py index 8d5dcc51d7..3020dc9b21 100644 --- a/TWLight/users/signals.py +++ b/TWLight/users/signals.py @@ -25,6 +25,10 @@ class Notice(object): user_renewal_notice = Signal() +class UserLoginRetrieval(object): + user_retrieve_monthly_logins = Signal() + + @receiver(post_save, sender=User) def clear_inactive_user_sessions(sender, instance, **kwargs): """Clear sessions after user is marked as inactive."""