Skip to content
Open
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
339 changes: 171 additions & 168 deletions dailyClassReminder.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,30 @@
datefmt="%Y-%m-%d %H:%H:%S",
)


# Get events for the next DELTA_DAYS days
TODAY = datetime.date.today()
logging.info("\n\n----- Beginning class reminders for %s -----\n\n", TODAY.isoformat())
DELTA_DAYS = (TODAY + datetime.timedelta(days=2)).isoformat()
SEARCH_FIELDS = [
{
"field": "Event Start Date",
"operator": "GREATER_AND_EQUAL",
"value": TODAY.isoformat(),
},
{"field": "Event Start Date", "operator": "LESS_AND_EQUAL", "value": DELTA_DAYS},
{"field": "Event Archived", "operator": "EQUAL", "value": "No"},
]

OUTPUT_FIELDS = [
EMAIL_TEMPLATE ="""
Hi {teacher_first_name},

This is an automated email to remind you of the upcoming classes you are scheduled to teach at Asmbly.
Thank you for sharing your knowledge with the community!

{pretty_events}

Please note these are the registrations as of the time of this email and may not reflect final registrations for your class.
You can see more details about these events and registrants in your Neon backend account.
The login URL is https://asmbly.z2systems.com/np/admin/content/contentList.do
Email classes@asmbly.org if you have any questions about the above schedule.

\t* Note: Some registrants are purchased under a single account and thus end up with the same email and phone number.


Thanks again!
Asmbly AdminBot
"""


NEON_EVENT_OUTPUT_FIELDS = [
"Event Name",
"Event ID",
"Event Topic",
Expand All @@ -59,177 +67,172 @@
"Waiting List Status",
]

RESPONSE_EVENTS = neon.postEventSearch(SEARCH_FIELDS, OUTPUT_FIELDS)

def get_neon_events_in_next_2_days(n_days):
delta_days = (TODAY + datetime.timedelta(days=2)).isoformat()
search_fields = [
{
"field": "Event Start Date",
"operator": "GREATER_AND_EQUAL",
"value": TODAY.isoformat(),
},
{"field": "Event Start Date", "operator": "LESS_AND_EQUAL", "value": delta_days},
{"field": "Event Archived", "operator": "EQUAL", "value": "No"},
]

# Remove duplicates in the list of teachers
TEACHERS = {item.get("Event Topic") for item in RESPONSE_EVENTS["searchResults"]}
return neon.postEventSearch(search_fields, NEON_EVENT_OUTPUT_FIELDS)['searchResults']

# Import teacher contact info
CONTACT_INFO = "teachers.json"
with open(CONTACT_INFO, "r", encoding="utf-8") as f:
TEACHER_EMAILS = json.load(f)

# For use if script ran and failed to complete
ALREADY_SENT = []
def load_teacher_contact_info():
# Import teacher contact info
contact_info = "teachers.json"
with open(contact_info, "r", encoding="utf-8") as f:
TEACHER_EMAILS = json.load(f)

# Begin gathering data for emailing each teacher
# Send each teacher an email reminder about classes they are scheduled to teach
for teacher in TEACHERS:
if teacher is None:
logging.info("WARNING: No teacher assigned!")
TEACHER_EMAILS[None] = "classes@asmbly.org"
if teacher in ALREADY_SENT:
logging.info("Already emailed %s", teacher)
continue

# Find all events for each teacher
events = list(
filter(
# Find all events for each teacher
def get_events_by_teacher(all_events):

# Remove duplicates in the list of teachers
teachers = {item.get("Event Topic") for item in all_events}

events_by_teacher = {}
for teacher in teachers:
events = list(filter(
lambda x, teach=teacher: x["Event Topic"] == teach,
RESPONSE_EVENTS["searchResults"],
all_events,
))
sorted_events = sorted(
events, key=lambda x: datetime.datetime.fromisoformat(x["Event Start Date"])
)
events_by_teacher[teacher] = sorted_events
return events_by_teacher


# Retrieve email and phone associated with this account ID
# Registrations with multiple attendees may have different emails listed in the UI
# but these aren't accessible from the API, so we will just use the info from the main account
def prettify_registrant(registrant):
acct_info = neon.getAccountIndividual(registrant['registrantAccountId'])
primary_contact = acct_info['individualAccount']['primaryContact']
email = primary_contact['email1']

phone = [addr['phone1'] for addr in primary_contact['addresses'] if addr['phone1']][0]
if not phone:
phone = "N/A"

results = ''
for attendee in registrant['tickets'][0]['attendees']:
results += f'\t{attendee["firstName"]} {attendee["lastName"]}: {email}, {phone}\n\t'
return results


def send_reminder_email(to_addr, cc_addr, subject, content):
mime_message = MIMEMultipart()
mime_message["To"] = to_addr
if cc_addr:
mime_message['CC'] = cc_addr
mime_message["Subject"] = subject

mime_message.attach(MIMEText(content, "plain"))
sendMIMEmessage(mime_message)


# Build up formatted event info for email body
def format_event_info(event, pretty_registrants):
raw_time = event["Event Start Time"]
raw_date = event["Event Start Date"]

datetime_date = datetime.datetime.strptime(raw_date, "%Y-%m-%d").date()
formatted_date = datetime.date.strftime(datetime_date, "%B %d")
start_time = datetime.datetime.strptime(raw_time, "%H:%M:%S").strftime(
"%I:%M %p"
)
logging.info("\n\n_____\n\nEmailing %s about %s event(s)...", teacher, len(events))
sorted_events = sorted(
events, key=lambda x: datetime.datetime.fromisoformat(x["Event Start Date"])
if datetime_date == TODAY:
date_string = f"TODAY - {formatted_date}"
elif datetime_date == TODAY + datetime.timedelta(days=1):
date_string = f"Tomorrow - {formatted_date}"
else:
date_string = formatted_date
info = f"""
{event["Event Name"]}
Date: {date_string}
Time: {start_time}
Number of registrants: {event["Registrants"]}
{pretty_registrants}
"""
return info


def compose_and_send_email(teacher, pretty_events):
if not teacher:
logging.info("WARNING: No teacher assigned!")
teacher_first_name = "N/A"
to_addr = 'classes@asmbly.org'
cc_addr = None
subject = f'Failed Class Reminder - {teacher}'
else:
teacher_first_name = teacher[: teacher.index(" ")]
to_addr = TEACHER_EMAILS[teacher]
cc_addr = 'classes@asmbly.org'
subject = 'Your upcoming classes at Asmbly'

email_msg = EMAIL_TEMPLATE.format(
teacher_first_name=teacher_first_name, pretty_events=pretty_events
)
send_reminder_email(to_addr, cc_addr, subject, email_msg)

# Reformat event data so it looks nice in email
pretty_events = ""
for event in sorted_events:
event_id = event["Event ID"]
logging.info(event_id)
logging.info(event["Event Name"])

individual_event_reg = neon.getEventRegistrants(event_id)
# logging.info(individualEventReg)

# Declare empty variable that may or may not get filled depending on whether there are registrations
# registrantDict will be a dictionary of dictionaries
### outer key is the registration status
### inner key is registrant account id
registrant_dict = {
"SUCCEEDED": [],
"DEFERRED": [],
"CANCELED": [],
"FAILED": [],
"REFUNDED": [],
}
# Registrant info formatted for email
pretty_registrants = ""

# Get total number of attendees - This does not always coordinate with number of account IDs
attendee_count = neon.getEventRegistrantCount(
individual_event_reg["eventRegistrations"]
)
logging.info(attendee_count)

# Only add info if there are registrations
if attendee_count > 0:

# Iterate over response to add registrant account IDs to dictionary organized by registration status
for registrant in individual_event_reg["eventRegistrations"]:
status = registrant["tickets"][0]["attendees"][0]["registrationStatus"]
acct_id = registrant["registrantAccountId"]

# Retrieve email and phone associated with this account ID
# Registrations with multiple attendees may have different emails listed in the UI
# but these aren't accessible from the API, so we will just use the info from the main account
acct_info = neon.getAccountIndividual(acct_id)
email = acct_info["individualAccount"]["primaryContact"]["email1"]
phone = ""
try:
phone = acct_info["individualAccount"]["primaryContact"][
"addresses"
][0]["phone1"]
except KeyError:
phone = acct_info["individualAccount"]["primaryContact"][
"addresses"
][1]["phone1"]

# Build a dictionary list of attendee names under this registration
attendee_list = {"name": [], "email": email, "phone": phone}
for attendee in registrant["tickets"][0]["attendees"]:
attendee = f'{attendee["firstName"]} {attendee["lastName"]}'
attendee_list["name"].append(attendee)

# Build entry to add to registrantDict with all attendees associated with this acct Id
entry = {acct_id: attendee_list}

# Add to registrantDict under the appropriate status
# First check that this registration status is in the dictionary
if status not in registrant_dict:
registrant_dict[status] = []
registrant_dict[status].append(entry)

for account in registrant_dict["SUCCEEDED"]:
for k, v in account.items():
for it in v["name"]:
student = f"{it}: {v['email']}, {v['phone']}"
pretty_registrants += f"\t{student}\n\t"
else:
pretty_registrants += "\tNo attendees registered currently. Check Neon for updates as event approaches.\n\t"

# Build up formatted event info for email body
raw_time = event["Event Start Time"]
raw_date = event["Event Start Date"]
datetime_date = datetime.datetime.strptime(raw_date, "%Y-%m-%d").date()
formatted_date = datetime.date.strftime(datetime_date, "%B %d")
start_time = datetime.datetime.strptime(raw_time, "%H:%M:%S").strftime(
"%I:%M %p"
)
if datetime_date == TODAY:
date_string = f"TODAY - {formatted_date}"
elif datetime_date == TODAY + datetime.timedelta(days=1):
date_string = f"Tomorrow - {formatted_date}"
else:
date_string = formatted_date
info = f"""
{event["Event Name"]}
Date: {date_string}
Time: {start_time}
Number of registrants: {event["Registrants"]}
{pretty_registrants}
"""
pretty_events += info

##### GMAIL #####
# Reformat date for email subject
formatted_today = TODAY.strftime("%B %d")

teacher_first_name = teacher[: teacher.index(" ")]

# Compose email
email_msg = f"""
Hi {teacher_first_name},

This is an automated email to remind you of the upcoming classes you are scheduled to teach at Asmbly.
Thank you for sharing your knowledge with the community!
def get_successful_registrants(event_registration):
return [r for r in event_registration
if r['tickets'][0]['attendees'][0]['registrationStatus'] == 'SUCCEEDED']

{pretty_events}

Please note these are the registrations as of the time of this email and may not reflect final registrations for your class.
You can see more details about these events and registrants in your Neon backend account.
The login URL is https://asmbly.z2systems.com/np/admin/content/contentList.do
Email classes@asmbly.org if you have any questions about the above schedule.
def prettify_registrants(registrants, attendee_count):
result = ""
if registrants and attendee_count > 0:
# Iterate over response to add registrant account IDs to
# dictionary organized by registration status
for registrant in registants:
result += prettify_registrant(registrant)
else:
result += "\tNo attendees registered currently. Check Neon for updates as event approaches.\n\t"
return result

\t* Note: Some registrants are purchased under a single account and thus end up with the same email and phone number.

def main():
# Get events for the next 2 days
logging.info("\n\n----- Beginning class reminders for %s -----\n\n", TODAY.isoformat())
load_teacher_contact_info()

Thanks again!
Asmbly AdminBot
"""
all_events = get_neon_events_in_next_2_days()

mime_message = MIMEMultipart()
try:
mime_message["To"] = TEACHER_EMAILS[teacher]
mime_message["CC"] = "classes@asmbly.org"
mime_message["Subject"] = "Your upcoming classes at Asmbly"
except KeyError:
mime_message["To"] = "classes@asmbly.org"
mime_message["Subject"] = f"Failed Class Reminder - {teacher}"
# Begin gathering data for emailing each teacher
# Send each teacher an email reminder about classes they are scheduled to teach
events_by_teacher = get_events_by_teacher(all_events)
for teacher, events in events_by_teacher.items():
logging.info(f"\n\n_____\n\nEmailing {teacher} about {len(event)} event(s)...")

mime_message.attach(MIMEText(email_msg, "plain"))
# Reformat event data so it looks nice in email
pretty_events = ""
for event in events_by_teacher[teacher]:
event_id = event["Event ID"]
logging.info(event_id)
logging.info(event["Event Name"])

sendMIMEmessage(mime_message)
individual_event_reg = neon.getEventRegistrants(event_id)

# Get total number of attendees (doesn't always coincide with number of account IDs)
registration = individual_event_reg['eventRegistrations']
attendee_count = neon.getEventRegistrantCount(registration)
logging.info(attendee_count)

# Registrant info formatted for email
successful_registrants = get_successful_registrants(registration)
pretty_registrants = prettify_registrants(successful_registrants, attendee_count)
pretty_events += format_event_info(event, pretty_registrants)

compose_and_send_email(teacher, pretty_events)

if __name__ == '__main__':
main()
11 changes: 11 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
certifi==2025.6.15
charset-normalizer==3.4.2
idna==3.10
iniconfig==2.1.0
packaging==25.0
pluggy==1.6.0
Pygments==2.19.2
pytest==8.4.1
pytz==2025.2
requests==2.32.4
urllib3==2.5.0
Loading