diff --git a/membershipworks/apps.py b/membershipworks/apps.py index 7f40f1c..a1a1f4d 100644 --- a/membershipworks/apps.py +++ b/membershipworks/apps.py @@ -7,6 +7,7 @@ def post_migrate_callback(sender, **kwargs): from cmsmanage.django_q2_helper import ensure_scheduled + from .tasks.event_survey_emails import send_survey_emails from .tasks.scrape import scrape_events, scrape_membershipworks from .tasks.ucsAccounts import sync_accounts @@ -29,6 +30,12 @@ def post_migrate_callback(sender, **kwargs): minutes=15, ) + ensure_scheduled( + sync_accounts.q_task_group, + send_survey_emails, + schedule_type=Schedule.HOURLY, + ) + class MembershipworksConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" diff --git a/membershipworks/management/commands/send_event_survey_emails.py b/membershipworks/management/commands/send_event_survey_emails.py new file mode 100644 index 0000000..a59b4ba --- /dev/null +++ b/membershipworks/management/commands/send_event_survey_emails.py @@ -0,0 +1,17 @@ +import logging + +from django.core.management.base import BaseCommand + +from membershipworks.tasks.event_survey_emails import logger, send_survey_emails + + +class Command(BaseCommand): + def handle(self, *args, verbosity: int, **options): + verbosity_levels = { + 0: logging.ERROR, + 1: logging.WARNING, + 2: logging.INFO, + 3: logging.DEBUG, + } + logger.setLevel(verbosity_levels.get(verbosity, logging.WARNING)) + send_survey_emails() diff --git a/membershipworks/migrations/0019_eventext_should_survey_eventext_survey_email_sent.py b/membershipworks/migrations/0019_eventext_should_survey_eventext_survey_email_sent.py new file mode 100644 index 0000000..0dd7153 --- /dev/null +++ b/membershipworks/migrations/0019_eventext_should_survey_eventext_survey_email_sent.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.6 on 2024-05-20 22:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("membershipworks", "0018_eventext_details_timestamp"), + ] + + operations = [ + migrations.AddField( + model_name="eventext", + name="should_survey", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="eventext", + name="survey_email_sent", + field=models.BooleanField(default=False), + ), + ] diff --git a/membershipworks/models.py b/membershipworks/models.py index 207a8a1..5b0eafb 100644 --- a/membershipworks/models.py +++ b/membershipworks/models.py @@ -554,6 +554,9 @@ class EventExt(Event): registrations = models.JSONField(null=True, blank=True) + should_survey = models.BooleanField(default=False) + survey_email_sent = models.BooleanField(default=False) + def get_absolute_url(self) -> str: return reverse("membershipworks:event-detail", kwargs={"eid": self.eid}) diff --git a/membershipworks/tasks/event_survey_emails.py b/membershipworks/tasks/event_survey_emails.py new file mode 100644 index 0000000..88d421b --- /dev/null +++ b/membershipworks/tasks/event_survey_emails.py @@ -0,0 +1,62 @@ +import logging +from collections.abc import Iterable +from urllib.parse import quote, urlencode + +from django.conf import settings +from django.core import mail +from django.db.models.functions import Now + +from cmsmanage.email import TemplatedMultipartEmail +from membershipworks.models import EventExt + +logger = logging.getLogger(__name__) + + +class EventSurveyEmail(TemplatedMultipartEmail): + # TODO: better wording + subject = ( + "[Claremont MakerSpace] Please fill out a survey for your recent CMS class!" + ) + from_email = "CMS Classes " + + template = "membershipworks/email/event_survey.dj.html" + + @staticmethod + def survey_url(event: EventExt, attendee_name: str, attendee_email: str) -> str: + return "https://claremontmakerspace.org/class-evaluation-form?" + urlencode( + { + "event_id": event.eid, + "instructor_name": str(event.instructor) if event.instructor else "", + "event_name": event.title, + "event_date": event.start.strftime("%Y-%m-%d %H:%M:%S"), + "participant_name": attendee_name, + "participant_email": attendee_email, + }, + quote_via=quote, + ) + + @classmethod + def render_for_event(cls, event: EventExt) -> Iterable[mail.EmailMessage]: + for name, email in event.attendees.values_list("name", "email"): + sanitized_email = mail.message.sanitize_address( + (name, email), settings.DEFAULT_CHARSET + ) + survey_url = cls.survey_url(event, name, email) + yield cls.render( + {"event": event, "attendee_name": name, "survey_url": survey_url}, + to=[sanitized_email], + ) + + +def send_survey_emails(): + with mail.get_connection() as conn: + for event in EventExt.objects.filter( + occurred=True, should_survey=True, survey_email_sent=False, end__lt=Now() + ): + logger.info("Sending survey messages for event: %s", event) + + # mark as sent even if we don't finish, to prevent sending duplicates + event.survey_email_sent = True + event.save() + + conn.send_messages(list(EventSurveyEmail.render_for_event(event))) diff --git a/membershipworks/templates/membershipworks/email/event_survey.dj.html b/membershipworks/templates/membershipworks/email/event_survey.dj.html new file mode 100644 index 0000000..9c269ac --- /dev/null +++ b/membershipworks/templates/membershipworks/email/event_survey.dj.html @@ -0,0 +1,19 @@ +{% load nh3_tags %} + +

Dear {{ attendee_name }},

+

+ Thank you for recently attending "{{ event.details.ttl|nh3 }}" at CMS on {{ event.start|date }}! We hope you enjoyed the experience and found it both informative and inspiring. + To help us continue to offer high-quality classes and improve our programs, we would greatly appreciate your feedback. + We kindly ask you to take a few minutes to complete a brief survey about your experience. +

+

+ Click here to fill out the survey +

+

+ Your insights are invaluable to us and will directly contribute to enhancing our offerings and ensuring that we meet the needs and expectations of our community. + Thank you in advance for your time and feedback. If you have any additional comments or questions, please feel free to reach out to us at info@claremontmakerspace.org. +

+

+

Best regards,
+
The Claremont MakerSpace Team
+