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 ..email import construct_text_email as _construct_text_email
18 from ..email import construct_email as _construct_email
19 from ..extract_mime import message_time as _message_time
20 from ..storage import assignment_path as _assignment_path
21 from ..tabulate import tabulate as _tabulate
22 from ..template import _student_email as _student_email
23 from . import get_subject_assignment as _get_subject_assignment
24 from . import get_subject_student as _get_subject_student
25 from . import InvalidStudentSubject as _InvalidStudentSubject
26 from . import InvalidAssignmentSubject as _InvalidAssignmentSubject
27 from . import Response as _Response
28 from . import UnsignedMessage as _UnsignedMessage
31 def run(basedir, course, message, person, subject,
32 trust_email_infrastructure=False, dry_run=False, **kwargs):
34 >>> from pgp_mime.email import encodedMIMEText
35 >>> from ..model.grade import Grade
36 >>> from ..test.course import StubCourse
37 >>> from . import InvalidMessage, Response
38 >>> course = StubCourse()
40 ... course.course.find_people(email='bb@greyhavens.net'))[0]
41 >>> message = encodedMIMEText('This text is not important.')
42 >>> message['Message-ID'] = '<123.456@home.net>'
43 >>> def process(**kwargs):
46 ... except Response as response:
47 ... print('respond with:')
48 ... print(response.message.as_string().replace('\\t', ' '))
49 ... except InvalidMessage as error:
50 ... print('{} error:'.format(type(error).__name__))
53 Unauthenticated messages are refused by default.
56 ... basedir=course.basedir, course=course.course, message=message,
57 ... person=person, subject='[get]', max_late=0)
58 UnsignedMessage error:
61 Although you can process them by setting the
62 ``trust_email_infrastructure`` option. This might not be too
63 dangerous, since you're sending the email to the user's configured
64 email address, not just replying blindly to the incoming email
65 address. With ``trust_email_infrastructure`` and missing user PGP
66 keys, sysadmins on the intervening systems will be able to read
67 our responses, possibly leaking grade information. If leaking to
68 sysadmins is considered unacceptable, you've can only email users
69 who have registered PGP keys.
71 Students without grades get a reasonable response.
74 ... basedir=course.basedir, course=course.course, message=message,
75 ... person=person, subject='[get]', max_late=0,
76 ... trust_email_infrastructure=True)
77 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
79 Content-Type: text/plain; charset="us-ascii"
81 Content-Transfer-Encoding: 7bit
82 Content-Disposition: inline
83 Subject: No grades for Billy
85 We don't have any of your grades on file for this course.
87 >>> message.authenticated = True
89 ... basedir=course.basedir, course=course.course, message=message,
90 ... person=person, subject='[get]', max_late=0)
91 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
93 Content-Type: text/plain; charset="us-ascii"
95 Content-Transfer-Encoding: 7bit
96 Content-Disposition: inline
97 Subject: No grades for Billy
99 We don't have any of your grades on file for this course.
101 Once we add a grade, they get details on all their grades for the
106 ... assignment=course.course.assignment('Attendance 1'),
108 >>> course.course.grades.append(grade)
111 ... assignment=course.course.assignment('Attendance 2'),
113 >>> course.course.grades.append(grade)
116 ... assignment=course.course.assignment('Assignment 1'),
117 ... points=10, comment='Looks good.')
118 >>> course.course.grades.append(grade)
120 ... basedir=course.basedir, course=course.course, message=message,
121 ... person=person, subject='[get]', max_late=0)
122 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
124 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
126 Content-Disposition: inline
128 From: Robot101 <phys101@tower.edu>
129 Reply-to: Robot101 <phys101@tower.edu>
130 To: Bilbo Baggins <bb@shire.org>
131 Subject: Physics 101 grades
133 --===============...==
134 Content-Type: text/plain; charset="us-ascii"
136 Content-Transfer-Encoding: 7bit
137 Content-Disposition: inline
142 * Attendance 1: 1 out of 1 available points.
143 * Attendance 2: 1 out of 1 available points.
144 * Assignment 1: 10 out of 10 available points.
154 --===============...==
156 Content-Transfer-Encoding: 7bit
157 Content-Description: OpenPGP digital signature
158 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
160 -----BEGIN PGP SIGNATURE-----
161 Version: GnuPG v2.0.19 (GNU/Linux)
164 -----END PGP SIGNATURE-----
166 --===============...==--
168 Professors and TAs can request the grades for the whole course.
172 ... course.course.find_people(email='eye@tower.edu'))[0]
173 >>> person.pgp_key = None
175 ... basedir=course.basedir, course=course.course, message=message,
176 ... person=person, subject='[get]', max_late=0)
177 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
179 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
181 Content-Disposition: inline
183 From: Robot101 <phys101@tower.edu>
184 Reply-to: Robot101 <phys101@tower.edu>
185 To: Sauron <eye@tower.edu>
186 Subject: All grades for Physics 101
188 --===============...==
189 Content-Type: text/plain; charset="us-ascii"
191 Content-Transfer-Encoding: 7bit
192 Content-Disposition: inline
194 Student Attendance 1 Attendance 2 Assignment 1
198 Std. Dev. 0.00 0.00 0.00
200 --===============...==
202 Content-Transfer-Encoding: 7bit
203 Content-Description: OpenPGP digital signature
204 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
206 -----BEGIN PGP SIGNATURE-----
207 Version: GnuPG v2.0.19 (GNU/Linux)
210 -----END PGP SIGNATURE-----
212 --===============...==--
214 They can also request grades for a particular student.
217 ... basedir=course.basedir, course=course.course, message=message,
218 ... person=person, subject='[get] {}'.format(student.name),
220 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
222 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
224 Content-Disposition: inline
226 From: Robot101 <phys101@tower.edu>
227 Reply-to: Robot101 <phys101@tower.edu>
228 To: Sauron <eye@tower.edu>
229 Subject: Physics 101 grades for Bilbo Baggins
231 --===============...==
232 Content-Type: text/plain; charset="us-ascii"
234 Content-Transfer-Encoding: 7bit
235 Content-Disposition: inline
240 * Attendance 1: 1 out of 1 available points.
241 * Attendance 2: 1 out of 1 available points.
242 * Assignment 1: 10 out of 10 available points.
252 --===============...==
254 Content-Transfer-Encoding: 7bit
255 Content-Description: OpenPGP digital signature
256 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
258 -----BEGIN PGP SIGNATURE-----
259 Version: GnuPG v2.0.19 (GNU/Linux)
262 -----END PGP SIGNATURE-----
264 --===============...==--
266 They can also request every submission for a particular student on
267 a particular assignment. Lets give the student a submission email
268 to see how that works.
270 >>> from .submission import run as _handle_submission
271 >>> submission = encodedMIMEText('The answer is 42.')
272 >>> submission['Message-ID'] = '<789.abc@home.net>'
273 >>> submission['Received'] = (
274 ... 'from smtp.home.net (smtp.home.net [123.456.123.456]) '
275 ... 'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
276 ... 'for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)')
278 ... _handle_submission(
279 ... basedir=course.basedir, course=course.course,
280 ... message=submission, person=student,
281 ... subject='[submit] Assignment 1')
282 ... except _Response:
285 Now lets request the submissions.
288 ... basedir=course.basedir, course=course.course, message=message,
290 ... subject='[get] {}, {}'.format(student.name, 'Assignment 1'),
292 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
294 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
296 Content-Disposition: inline
298 From: Robot101 <phys101@tower.edu>
299 Reply-to: Robot101 <phys101@tower.edu>
300 To: Sauron <eye@tower.edu>
301 Subject: Physics 101 assignment submissions for Bilbo Baggins
303 --===============...==
304 Content-Type: multipart/mixed; boundary="===============...=="
307 --===============...==
308 Content-Type: text/plain; charset="us-ascii"
310 Content-Transfer-Encoding: 7bit
311 Content-Disposition: inline
313 Physics 101 assignment submissions for Bilbo Baggins:
316 --===============...==
317 Content-Type: text/plain; charset="us-ascii"
319 Content-Transfer-Encoding: 7bit
320 Content-Disposition: inline
322 Assignment 1 grade: 10
326 --===============...==
327 Content-Type: message/rfc822
330 Content-Type: text/plain; charset="us-ascii"
332 Content-Transfer-Encoding: 7bit
333 Content-Disposition: inline
334 Message-ID: <789.abc@home.net>
335 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)
338 --===============...==--
339 --===============...==
341 Content-Transfer-Encoding: 7bit
342 Content-Description: OpenPGP digital signature
343 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
345 -----BEGIN PGP SIGNATURE-----
346 Version: GnuPG v2.0.19 (GNU/Linux)
349 -----END PGP SIGNATURE-----
351 --===============...==--
355 if trust_email_infrastructure:
359 hasattr(message, 'authenticated') and message.authenticated)
360 if not authenticated:
361 raise _UnsignedMessage()
362 if 'assistants' in person.groups or 'professors' in person.groups:
363 email = _get_admin_email(
364 basedir=basedir, course=course, person=person, subject=subject)
365 elif 'students' in person.groups:
366 email = _get_student_email(
367 basedir=basedir, course=course, person=person)
369 raise NotImplementedError(
370 'strange groups {} for {}'.format(person.groups, person))
371 raise _Response(message=email, complete=True)
373 def _get_student_email(basedir, course, person, student=None):
377 _LOG.debug('construct student grade email about {} for {}'.format(
381 _LOG.debug('construct student grade email about {} for {}'.format(
383 emails = list(_student_email(
384 basedir=basedir, author=course.robot, course=course,
385 student=student, targets=targets, old=True))
389 "We don't have any of your grades on file for this course."
393 "We don't have any grades for {} on file for this course."
394 ).format(student.name)
395 message = _pgp_mime.encodedMIMEText(text)
396 message['Subject'] = 'No grades for {}'.format(student.alias())
397 raise _Response(message=message, complete=True)
398 elif len(emails) > 1:
399 raise NotImplementedError(emails)
400 email,callback = emails[0]
401 # callback records notification, but don't bother here
404 def _get_student_submission_email(
405 basedir, course, person, assignments, student):
406 _LOG.debug('construct student submission email about {} {} for {}'.format(
407 student, assignments, person))
408 subject = '{} assignment submissions for {}'.format(
409 course.name, student.name)
410 text = '{}:\n * {}\n'.format(
411 subject, '\n * '.join(a.name for a in assignments))
412 message = _MIMEMultipart('mixed')
413 message.attach(_pgp_mime.encodedMIMEText(text))
414 for assignment in assignments:
415 grade = course.grade(student=student, assignment=assignment)
416 if grade is not None:
417 text = '{} grade: {}\n'.format(assignment.name, grade.points)
419 text += '\n{}\n'.format(grade.comment)
420 message.attach(_pgp_mime.encodedMIMEText(text))
421 assignment_path = _assignment_path(basedir, assignment, student)
422 mpath = _os_path.join(assignment_path, 'mail')
424 mbox = _mailbox.Maildir(mpath, factory=None, create=False)
425 except _mailbox.NoSuchMailboxError as e:
429 for key,msg in mbox.items():
430 subpath = mbox._lookup(key)
431 if subpath.endswith('.gitignore'):
432 _LOG.debug('skipping non-message {}'.format(subpath))
435 messages.sort(key=_message_time)
437 message.attach(_MIMEMessage(msg))
438 return _construct_email(
439 author=course.robot, targets=[person], subject=subject,
442 def _get_admin_email(basedir, course, person, subject):
444 student = _get_subject_student(course, subject)
445 except _InvalidStudentSubject as error:
446 if error.students: # several students
449 _LOG.debug('construct course grades email for {}'.format(person))
450 stream = _io.StringIO()
452 course=course, statistics=True, stream=stream, use_color=False)
453 text = stream.getvalue()
454 email = _construct_text_email(
455 author=course.robot, targets=[person],
456 subject='All grades for {}'.format(course.name),
458 else: # a single student
460 assignment = _get_subject_assignment(course, subject)
461 except _InvalidAssignmentSubject as error:
462 if error.assignments: # several assignments
463 email = _get_student_submission_email(
464 basedir=basedir, course=course, person=person,
465 student=student, assignments=error.assignments)
466 else: # no assignments
467 email = _get_student_email(
468 basedir=basedir, course=course, person=person,
470 else: # a single assignment
471 email = _get_student_submission_email(
472 basedir=basedir, course=course, person=person, student=student,
473 assignments=[assignment])