Skip to content

Instantly share code, notes, and snippets.

@drussellmrichie
Last active October 5, 2016 02:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save drussellmrichie/e170d03ec11b7587a3b7392612c9c1b4 to your computer and use it in GitHub Desktop.
Save drussellmrichie/e170d03ec11b7587a3b7392612c9c1b4 to your computer and use it in GitHub Desktop.
Script to extract HW grades from a table, and send separate emails to students with their grades. Potentially useful for a TA of large classes. (Any elegance or cleverness is due to @hsharrison, not me!)
"""Usage:
gradeEmailer.py [--name=NAME] [--password=PASSWORD] [--dry-run] <sender-email> <grades-path>
Options:
--name=NAME The name of the grader.
Used in the signature and From field of the email.
If not given, <sender-email> will be used.
--password=PASSWORD The password to the sender's email account.
If not given, it will be prompted for.
--dry-run To test, send emails to yourself instead of to the students.
This reads a table of graded homeworks and emails each student their grade and feedback.
It expects a CSV or XLS/XLSX file at <grades-path> structured like the example below.
Usually, you'll have additional columns for individual questions, students' responses to such questions,
and your grades/comments on such responses.
However, these additional columns aren't strictly necessary for the script to work.
First Name Last Name Email Total
Total possible points 10
Russell Richie drussellmrichie@gmail.com 9.5
Student McStudentFace student.mcstudentface@uconn.edu 10
Current Meme current.meme@uconn.edu 9.75
Note that the name of this file will go in the subject line of the email,
so be sure it is appropriate for students to see this file name.
Total possible points must be in the first row.
"""
# Authors:
#
# Russell Richie
# github.com/drussellmrichie
# drussellmrichie@gmail.com
#
# Henry S. Harrison
# github.com/hsharrison
# henry.schafer.harrison@gmail.com
# This is largely cribbed from Automate the Boring Stuff with Python:
# https://automatetheboringstuff.com/chapter16/
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from getpass import getpass
from io import StringIO
import os.path
import re
import smtplib
from textwrap import dedent
from docopt import docopt
import pandas as pd
from toolz import curry
subject = '{course}: HW{hwid} feedback'
body = dedent("""
Dear {First Name} {Last Name},
This is an automatically generated email providing your grade and feedback on HW{hwid} in {course}.
You received {Total} points (out of {total_possible}).
See the attachment for details.
Let me know if you have any questions.
Best,
{grader_name}
""").lstrip()
def send_all_emails(from_address, grades_path, password=None, dry_run=False, from_name=None):
if password is None:
password = getpass()
grades_dir = os.path.dirname(grades_path)
grades_filename = os.path.basename(grades_path)
without_ext, ext = os.path.splitext(grades_filename)
if ext.lower() == '.csv':
hw_df = pd.read_csv(grades_path)
elif ext.lower() in {'.xlsx', '.xls'}:
hw_df = pd.read_excel(grades_path)
else:
raise NotImplementedError('Extension {} not supported'.format(ext))
if dry_run:
hw_df['Email'] = from_address
global_info = dict(
hwid=re.search(r'HW[0-9]+',grades_filename).group(0)[2:],
course=grades_filename.split(' - ')[0],
total_possible=hw_df.loc[0, 'Total'],
grader_name=from_name or from_address,
from_address=from_address,
)
print('Connecting and logging in to SMTP server...', end='')
smtp_obj = smtplib.SMTP('smtp.gmail.com', 587)
smtp_obj.starttls()
smtp_obj.login(from_address, password)
print('done.')
try:
hw_df.loc[1:, :].apply(send_email(smtp_obj, hw_df, **global_info), axis=1)
finally:
smtp_obj.quit()
print('Sent all emails.')
@curry
def send_email(smtp_obj, df, row, **info):
msg = MIMEMultipart()
msg['Subject'] = subject.format(**info)
msg['From'] = '{grader_name} <{from_address}>'.format(**info)
msg.attach(MIMEText(body.format(**info, **row), 'plain'))
attachment_file = StringIO()
df.loc[[row.name, 0], :].to_csv(attachment_file, index=False)
attachment_file.seek(0)
attachment = MIMEText(attachment_file.read(), _subtype='csv')
attachment.add_header('Content-Disposition', 'attachment', filename='{Last Name} HW{hwid}.csv'.format(**info, **row))
msg.attach(attachment)
print('Emailing {First Name} {Last Name} <{Email}>...'.format(**row), end='')
smtp_obj.sendmail(info['from_address'], row['Email'], msg.as_string())
print('done.')
def main(argv=None):
args = docopt(__doc__, argv=argv)
send_all_emails(args['<sender-email>'], args['<grades-path>'], dry_run=args['--dry-run'], password=args['--password'], from_name=args['--name'])
if __name__ == '__main__':
main()
@hsharrison
Copy link

hsharrison commented Oct 3, 2016

Here's my fork: https://gist.github.com/hsharrison/111e2148a22c7ec9942f66f4ddfbe3db

Changes include

  • pass everything in from the command line
  • no need for temporary files
  • dry run option (sends to you)
  • include the first row in each student's CSV (so they can see the total possible points for each question)

You will need two extra third-party packages: docopt and toolz. Also it will now require python >= 3.5.

@drussellmrichie
Copy link
Author

Wow, this is great. I'll look at this all more carefully, but at first glance, it all looks hunky dory to me.

@drussellmrichie
Copy link
Author

Okay, I merged the changes in your fork back into my branch. Your version was way better. Hope that was okay / doesn't give the wrong impression about authorship (see the little bit I added in the description)!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment