3 """Handle information requests
5 Allow professors, TAs, and students to request grade information via email.
8 from email.mime.message import MIMEMessage as _MIMEMessage
9 from email.mime.multipart import MIMEMultipart as _MIMEMultipart
11 import mailbox as _mailbox
12 import os.path as _os_path
14 import pgp_mime as _pgp_mime
16 from .. import LOG as _LOG
17 from ..color import color_string as _color_string
18 from ..color import standard_colors as _standard_colors
19 from ..email import construct_text_email as _construct_text_email
20 from ..email import construct_email as _construct_email
21 from ..storage import assignment_path as _assignment_path
22 from ..tabulate import tabulate as _tabulate
23 from ..template import _student_email as _student_email
24 from . import InvalidMessage as _InvalidMessage
25 from . import InvalidSubjectMessage as _InvalidSubjectMessage
26 from . import Response as _Response
27 from . import UnsignedMessage as _UnsignedMessage
30 class InvalidStudent (_InvalidSubjectMessage):
31 def __init__(self, students=None, **kwargs):
32 if 'error' not in kwargs:
33 kwargs['error'] = 'Subject matches multiple students'
34 super(InvalidStudent, self).__init__(kwargs)
35 self.students = students
38 def run(basedir, course, message, person, subject,
39 trust_email_infrastructure=False,
40 use_color=None, dry_run=False, **kwargs):
42 >>> from pgp_mime.email import encodedMIMEText
43 >>> from ..model.grade import Grade
44 >>> from ..test.course import StubCourse
45 >>> from . import InvalidMessage, Response
46 >>> course = StubCourse()
48 ... course.course.find_people(email='bb@greyhavens.net'))[0]
49 >>> message = encodedMIMEText('This text is not important.')
50 >>> message['Message-ID'] = '<123.456@home.net>'
51 >>> def process(**kwargs):
54 ... except Response as response:
55 ... print('respond with:')
56 ... print(response.message.as_string().replace('\\t', ' '))
57 ... except InvalidMessage as error:
58 ... print('{} error:'.format(type(error).__name__))
61 Unauthenticated messages are refused by default.
64 ... basedir=course.basedir, course=course.course, message=message,
65 ... person=person, subject='[get]', max_late=0)
66 UnsignedMessage error:
69 Although you can process them by setting the
70 ``trust_email_infrastructure`` option. This might not be too
71 dangerous, since you're sending the email to the user's configured
72 email address, not just replying blindly to the incoming email
73 address. With ``trust_email_infrastructure`` and missing user PGP
74 keys, sysadmins on the intervening systems will be able to read
75 our responses, possibly leaking grade information. If leaking to
76 sysadmins is considered unacceptable, you've can only email users
77 who have registered PGP keys.
79 Students without grades get a reasonable response.
82 ... basedir=course.basedir, course=course.course, message=message,
83 ... person=person, subject='[get]', max_late=0,
84 ... trust_email_infrastructure=True)
85 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
87 Content-Type: text/plain; charset="us-ascii"
89 Content-Transfer-Encoding: 7bit
90 Content-Disposition: inline
91 Subject: No grades for Billy
93 We don't have any of your grades on file for this course.
95 >>> message.authenticated = True
97 ... basedir=course.basedir, course=course.course, message=message,
98 ... person=person, subject='[get]', max_late=0)
99 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
101 Content-Type: text/plain; charset="us-ascii"
103 Content-Transfer-Encoding: 7bit
104 Content-Disposition: inline
105 Subject: No grades for Billy
107 We don't have any of your grades on file for this course.
109 Once we add a grade, they get details on all their grades for the
114 ... assignment=course.course.assignment('Attendance 1'),
116 >>> course.course.grades.append(grade)
119 ... assignment=course.course.assignment('Attendance 2'),
121 >>> course.course.grades.append(grade)
124 ... assignment=course.course.assignment('Assignment 1'),
125 ... points=10, comment='Looks good.')
126 >>> course.course.grades.append(grade)
128 ... basedir=course.basedir, course=course.course, message=message,
129 ... person=person, subject='[get]', max_late=0)
130 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
132 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
134 Content-Disposition: inline
136 From: Robot101 <phys101@tower.edu>
137 Reply-to: Robot101 <phys101@tower.edu>
138 To: Bilbo Baggins <bb@shire.org>
139 Subject: Physics 101 grades
141 --===============...==
142 Content-Type: text/plain; charset="us-ascii"
144 Content-Transfer-Encoding: 7bit
145 Content-Disposition: inline
150 * Attendance 1: 1 out of 1 available points.
151 * Attendance 2: 1 out of 1 available points.
152 * Assignment 1: 10 out of 10 available points.
162 --===============...==
164 Content-Transfer-Encoding: 7bit
165 Content-Description: OpenPGP digital signature
166 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
168 -----BEGIN PGP SIGNATURE-----
169 Version: GnuPG v2.0.19 (GNU/Linux)
172 -----END PGP SIGNATURE-----
174 --===============...==--
176 Professors and TAs can request the grades for the whole course.
180 ... course.course.find_people(email='eye@tower.edu'))[0]
181 >>> person.pgp_key = None
183 ... basedir=course.basedir, course=course.course, message=message,
184 ... person=person, subject='[get]', max_late=0)
185 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
187 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
189 Content-Disposition: inline
191 From: Robot101 <phys101@tower.edu>
192 Reply-to: Robot101 <phys101@tower.edu>
193 To: Sauron <eye@tower.edu>
194 Subject: All grades for Physics 101
196 --===============...==
197 Content-Type: text/plain; charset="us-ascii"
199 Content-Transfer-Encoding: 7bit
200 Content-Disposition: inline
202 Student Attendance 1 Attendance 2 Assignment 1
206 Std. Dev. 0.00 0.00 0.00
208 --===============...==
210 Content-Transfer-Encoding: 7bit
211 Content-Description: OpenPGP digital signature
212 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
214 -----BEGIN PGP SIGNATURE-----
215 Version: GnuPG v2.0.19 (GNU/Linux)
218 -----END PGP SIGNATURE-----
220 --===============...==--
222 They can also request grades for a particular student.
225 ... basedir=course.basedir, course=course.course, message=message,
226 ... person=person, subject='[get] {}'.format(student.name),
228 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
230 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
232 Content-Disposition: inline
234 From: Robot101 <phys101@tower.edu>
235 Reply-to: Robot101 <phys101@tower.edu>
236 To: Sauron <eye@tower.edu>
237 Subject: Physics 101 grades for Bilbo Baggins
239 --===============...==
240 Content-Type: text/plain; charset="us-ascii"
242 Content-Transfer-Encoding: 7bit
243 Content-Disposition: inline
248 * Attendance 1: 1 out of 1 available points.
249 * Attendance 2: 1 out of 1 available points.
250 * Assignment 1: 10 out of 10 available points.
260 --===============...==
262 Content-Transfer-Encoding: 7bit
263 Content-Description: OpenPGP digital signature
264 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
266 -----BEGIN PGP SIGNATURE-----
267 Version: GnuPG v2.0.19 (GNU/Linux)
270 -----END PGP SIGNATURE-----
272 --===============...==--
274 They can also request every submission for a particular student on
275 a particular assignment. Lets give the student a submission email
276 to see how that works.
278 >>> from .submission import run as _handle_submission
279 >>> submission = encodedMIMEText('The answer is 42.')
280 >>> submission['Message-ID'] = '<789.abc@home.net>'
281 >>> submission['Received'] = (
282 ... 'from smtp.home.net (smtp.home.net [123.456.123.456]) '
283 ... 'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
284 ... 'for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)')
286 ... _handle_submission(
287 ... basedir=course.basedir, course=course.course,
288 ... message=submission, person=student,
289 ... subject='[submit] Assignment 1')
290 ... except _Response:
293 Now lets request the submissions.
296 ... basedir=course.basedir, course=course.course, message=message,
298 ... subject='[get] {}, {}'.format(student.name, 'Assignment 1'),
300 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
302 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
304 Content-Disposition: inline
306 From: Robot101 <phys101@tower.edu>
307 Reply-to: Robot101 <phys101@tower.edu>
308 To: Sauron <eye@tower.edu>
309 Subject: Physics 101 assignment submissions for Bilbo Baggins
311 --===============...==
312 Content-Type: multipart/mixed; boundary="===============...=="
315 --===============...==
316 Content-Type: text/plain; charset="us-ascii"
318 Content-Transfer-Encoding: 7bit
319 Content-Disposition: inline
321 Physics 101 assignment submissions for Bilbo Baggins:
324 --===============...==
325 Content-Type: text/plain; charset="us-ascii"
327 Content-Transfer-Encoding: 7bit
328 Content-Disposition: inline
330 Assignment 1 grade: 10
334 --===============...==
335 Content-Type: message/rfc822
338 Content-Type: text/plain; charset="us-ascii"
340 Content-Transfer-Encoding: 7bit
341 Content-Disposition: inline
342 Message-ID: <789.abc@home.net>
343 Received: from smtp.home.net (smtp.home.net [123.456.123.456]) by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)
346 --===============...==--
347 --===============...==
349 Content-Transfer-Encoding: 7bit
350 Content-Description: OpenPGP digital signature
351 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
353 -----BEGIN PGP SIGNATURE-----
354 Version: GnuPG v2.0.19 (GNU/Linux)
357 -----END PGP SIGNATURE-----
359 --===============...==--
363 if trust_email_infrastructure:
367 hasattr(message, 'authenticated') and message.authenticated)
368 if not authenticated:
369 raise _UnsignedMessage()
370 if 'assistants' in person.groups or 'professors' in person.groups:
371 email = _get_admin_email(
372 basedir=basedir, course=course, person=person, subject=subject,
374 elif 'students' in person.groups:
375 email = _get_student_email(
376 basedir=basedir, course=course, person=person,
379 raise NotImplementedError(
380 'strange groups {} for {}'.format(person.groups, person))
381 raise _Response(message=email)
383 def _get_student_email(basedir, course, person, student=None, use_color=None):
389 emails = list(_student_email(
390 basedir=basedir, author=course.robot, course=course,
391 student=student, targets=targets, old=True))
395 "We don't have any of your grades on file for this course."
399 "We don't have any grades for {} on file for this course."
400 ).format(student.name)
401 message = _pgp_mime.encodedMIMEText(text)
402 message['Subject'] = 'No grades for {}'.format(student.alias())
403 raise _Response(message=message)
404 elif len(emails) > 1:
405 raise NotImplementedError(emails)
406 email,callback = emails[0]
407 # callback records notification, but don't bother here
410 def _get_student_submission_email(
411 basedir, course, person, assignments, student, use_color=None):
412 subject = '{} assignment submissions for {}'.format(
413 course.name, student.name)
414 text = '{}:\n * {}\n'.format(
415 subject, '\n * '.join(a.name for a in assignments))
416 message = _MIMEMultipart('mixed')
417 message.attach(_pgp_mime.encodedMIMEText(text))
418 for assignment in assignments:
419 grade = course.grade(student=student, assignment=assignment)
420 if grade is not None:
421 message.attach(_pgp_mime.encodedMIMEText(
422 '{} grade: {}\n\n{}\n'.format(
423 assignment.name, grade.points, grade.comment)))
424 assignment_path = _assignment_path(basedir, assignment, student)
425 mpath = _os_path.join(assignment_path, 'mail')
427 mbox = _mailbox.Maildir(mpath, factory=None, create=False)
428 except _mailbox.NoSuchMailboxError as e:
432 message.attach(_MIMEMessage(msg))
433 return _construct_email(
434 author=course.robot, targets=[person], subject=subject,
437 def _get_admin_email(basedir, course, person, subject, use_color=True):
438 lsubject = subject.lower()
439 students = [p for p in course.find_people()
440 if p.name.lower() in lsubject]
441 if len(students) == 0:
442 stream = _io.StringIO()
443 _tabulate(course=course, statistics=True, stream=stream)
444 text = stream.getvalue()
445 email = _construct_text_email(
446 author=course.robot, targets=[person],
447 subject='All grades for {}'.format(course.name),
449 elif len(students) == 1:
450 student = students[0]
451 assignments = [a for a in course.assignments
452 if a.name.lower() in lsubject]
453 if len(assignments) == 0:
454 email = _get_student_email(
455 basedir=basedir, course=course, person=person, student=student)
457 email = _get_student_submission_email(
458 basedir=basedir, course=course, person=person, student=student,
459 assignments=assignments, use_color=use_color)
461 raise InvalidStudent(students=students)