From 3ceecad4f5099ada56f81055ff2d814998a47ea5 Mon Sep 17 00:00:00 2001 From: suecarmol Date: Fri, 27 Jun 2025 20:43:54 -0600 Subject: [PATCH 1/3] Add management command that sends monthly reports of logged-in users --- TWLight/emails/tasks.py | 20 ++++++- ...ser_retrieve_monthly_logins-body-html.html | 9 +++ ...ser_retrieve_monthly_logins-body-text.html | 3 + .../user_retrieve_monthly_logins-subject.html | 1 + .../commands/retrieve_monthly_users.py | 55 +++++++++++++++++++ TWLight/users/signals.py | 4 ++ 6 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-html.html create mode 100644 TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-text.html create mode 100644 TWLight/emails/templates/emails/user_retrieve_monthly_logins-subject.html create mode 100644 TWLight/users/management/commands/retrieve_monthly_users.py 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..3c02e46b6f --- /dev/null +++ b/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-html.html @@ -0,0 +1,9 @@ + + +

Hi Vipin,

+

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..c929741631 --- /dev/null +++ b/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-text.html @@ -0,0 +1,3 @@ +Hi Vipin, +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/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.""" From ddfd5be28fb4ed95c894aa45436381fa3ecbdcb9 Mon Sep 17 00:00:00 2001 From: suecarmol Date: Fri, 27 Jun 2025 21:03:41 -0600 Subject: [PATCH 2/3] Add cron task --- TWLight/crons.py | 11 +++++++++++ TWLight/settings/base.py | 1 + 2 files changed, 12 insertions(+) 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/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 From 624bd0ec7805bddb864f094a93e55fc6a200e1bb Mon Sep 17 00:00:00 2001 From: suecarmol Date: Mon, 30 Jun 2025 12:18:24 -0600 Subject: [PATCH 3/3] Change body text of email --- .../emails/user_retrieve_monthly_logins-body-html.html | 2 +- .../emails/user_retrieve_monthly_logins-body-text.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 3c02e46b6f..5393353787 100644 --- a/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-html.html +++ b/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-html.html @@ -1,6 +1,6 @@ -

Hi Vipin,

+

Hi TWL team,

Here is a list of the users that logged-in last month that have approved applications and current authorizations.

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 index c929741631..5cd409ff4f 100644 --- a/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-text.html +++ b/TWLight/emails/templates/emails/user_retrieve_monthly_logins-body-text.html @@ -1,3 +1,3 @@ -Hi Vipin, +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" }}