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, original, 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, original=message,
65 ... message=message, 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, original=message,
83 ... message=message, 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, original=message,
98 ... message=message, 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, original=message,
129 ... message=message, person=person, subject='[get]',
131 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
133 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
135 Content-Disposition: inline
137 From: Robot101 <phys101@tower.edu>
138 Reply-to: Robot101 <phys101@tower.edu>
139 To: Bilbo Baggins <bb@shire.org>
140 Subject: Physics 101 grades
142 --===============...==
143 Content-Type: text/plain; charset="us-ascii"
145 Content-Transfer-Encoding: 7bit
146 Content-Disposition: inline
151 * Attendance 1: 1 out of 1 available points.
152 * Attendance 2: 1 out of 1 available points.
153 * Assignment 1: 10 out of 10 available points.
163 --===============...==
165 Content-Transfer-Encoding: 7bit
166 Content-Description: OpenPGP digital signature
167 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
169 -----BEGIN PGP SIGNATURE-----
170 Version: GnuPG v2.0.19 (GNU/Linux)
173 -----END PGP SIGNATURE-----
175 --===============...==--
177 Professors and TAs can request the grades for the whole course.
181 ... course.course.find_people(email='eye@tower.edu'))[0]
182 >>> person.pgp_key = None
184 ... basedir=course.basedir, course=course.course, original=message,
185 ... message=message, person=person, subject='[get]',
187 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
189 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
191 Content-Disposition: inline
193 From: Robot101 <phys101@tower.edu>
194 Reply-to: Robot101 <phys101@tower.edu>
195 To: Sauron <eye@tower.edu>
196 Subject: All grades for Physics 101
198 --===============...==
199 Content-Type: text/plain; charset="us-ascii"
201 Content-Transfer-Encoding: 7bit
202 Content-Disposition: inline
204 Student Attendance 1 Attendance 2 Assignment 1
208 Std. Dev. 0.00 0.00 0.00
210 --===============...==
212 Content-Transfer-Encoding: 7bit
213 Content-Description: OpenPGP digital signature
214 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
216 -----BEGIN PGP SIGNATURE-----
217 Version: GnuPG v2.0.19 (GNU/Linux)
220 -----END PGP SIGNATURE-----
222 --===============...==--
224 They can also request grades for a particular student.
227 ... basedir=course.basedir, course=course.course, original=message,
228 ... message=message, person=person,
229 ... subject='[get] {}'.format(student.name),
231 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
233 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
235 Content-Disposition: inline
237 From: Robot101 <phys101@tower.edu>
238 Reply-to: Robot101 <phys101@tower.edu>
239 To: Sauron <eye@tower.edu>
240 Subject: Physics 101 grades for Bilbo Baggins
242 --===============...==
243 Content-Type: text/plain; charset="us-ascii"
245 Content-Transfer-Encoding: 7bit
246 Content-Disposition: inline
251 * Attendance 1: 1 out of 1 available points.
252 * Attendance 2: 1 out of 1 available points.
253 * Assignment 1: 10 out of 10 available points.
263 --===============...==
265 Content-Transfer-Encoding: 7bit
266 Content-Description: OpenPGP digital signature
267 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
269 -----BEGIN PGP SIGNATURE-----
270 Version: GnuPG v2.0.19 (GNU/Linux)
273 -----END PGP SIGNATURE-----
275 --===============...==--
277 They can also request every submission for a particular student on
278 a particular assignment. Lets give the student a submission email
279 to see how that works.
281 >>> from .submission import run as _handle_submission
282 >>> submission = encodedMIMEText('The answer is 42.')
283 >>> submission['Message-ID'] = '<789.abc@home.net>'
284 >>> submission['Received'] = (
285 ... 'from smtp.home.net (smtp.home.net [123.456.123.456]) '
286 ... 'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
287 ... 'for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)')
289 ... _handle_submission(
290 ... basedir=course.basedir, course=course.course,
291 ... original=submission, message=submission, person=student,
292 ... subject='[submit] Assignment 1')
293 ... except _Response:
296 Now lets request the submissions.
299 ... basedir=course.basedir, course=course.course, original=message,
300 ... message=message, person=person,
301 ... subject='[get] {}, {}'.format(student.name, 'Assignment 1'),
303 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
305 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
307 Content-Disposition: inline
309 From: Robot101 <phys101@tower.edu>
310 Reply-to: Robot101 <phys101@tower.edu>
311 To: Sauron <eye@tower.edu>
312 Subject: Physics 101 assignment submissions for Bilbo Baggins
314 --===============...==
315 Content-Type: multipart/mixed; boundary="===============...=="
318 --===============...==
319 Content-Type: text/plain; charset="us-ascii"
321 Content-Transfer-Encoding: 7bit
322 Content-Disposition: inline
324 Physics 101 assignment submissions for Bilbo Baggins:
327 --===============...==
328 Content-Type: text/plain; charset="us-ascii"
330 Content-Transfer-Encoding: 7bit
331 Content-Disposition: inline
333 Assignment 1 grade: 10
337 --===============...==
338 Content-Type: message/rfc822
341 Content-Type: text/plain; charset="us-ascii"
343 Content-Transfer-Encoding: 7bit
344 Content-Disposition: inline
345 Message-ID: <789.abc@home.net>
346 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)
349 --===============...==--
350 --===============...==
352 Content-Transfer-Encoding: 7bit
353 Content-Description: OpenPGP digital signature
354 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
356 -----BEGIN PGP SIGNATURE-----
357 Version: GnuPG v2.0.19 (GNU/Linux)
360 -----END PGP SIGNATURE-----
362 --===============...==--
366 if trust_email_infrastructure:
369 authenticated = hasattr(message, 'authenticated') and message.authenticated
370 if not authenticated:
371 raise _UnsignedMessage()
372 if 'assistants' in person.groups or 'professors' in person.groups:
373 email = _get_admin_email(
374 basedir=basedir, course=course, original=original,
375 person=person, subject=subject, use_color=use_color)
376 elif 'students' in person.groups:
377 email = _get_student_email(
378 basedir=basedir, course=course, original=original,
379 person=person, use_color=use_color)
381 raise NotImplementedError(
382 'strange groups {} for {}'.format(person.groups, person))
383 raise _Response(message=email)
385 def _get_student_email(basedir, course, original, person, student=None,
392 emails = list(_student_email(
393 basedir=basedir, author=course.robot, course=course,
394 student=student, targets=targets, old=True))
398 "We don't have any of your grades on file for this course."
402 "We don't have any grades for {} on file for this course."
403 ).format(student.name)
404 message = _pgp_mime.encodedMIMEText(text)
405 message['Subject'] = 'No grades for {}'.format(student.alias())
406 raise _Response(message=message)
407 elif len(emails) > 1:
408 raise NotImplementedError(emails)
409 email,callback = emails[0]
410 # callback records notification, but don't bother here
413 def _get_student_submission_email(
414 basedir, course, original, person, assignments, student, use_color=None):
415 subject = '{} assignment submissions for {}'.format(
416 course.name, student.name)
417 text = '{}:\n * {}\n'.format(
418 subject, '\n * '.join(a.name for a in assignments))
419 message = _MIMEMultipart('mixed')
420 message.attach(_pgp_mime.encodedMIMEText(text))
421 for assignment in assignments:
422 grade = course.grade(student=student, assignment=assignment)
423 if grade is not None:
424 message.attach(_pgp_mime.encodedMIMEText(
425 '{} grade: {}\n\n{}\n'.format(
426 assignment.name, grade.points, grade.comment)))
427 assignment_path = _assignment_path(basedir, assignment, student)
428 mpath = _os_path.join(assignment_path, 'mail')
430 mbox = _mailbox.Maildir(mpath, factory=None, create=False)
431 except _mailbox.NoSuchMailboxError as e:
435 message.attach(_MIMEMessage(msg))
436 return _construct_email(
437 author=course.robot, targets=[person], subject=subject,
440 def _get_admin_email(basedir, course, original, person, subject,
442 lsubject = subject.lower()
443 students = [p for p in course.find_people()
444 if p.name.lower() in lsubject]
445 if len(students) == 0:
446 stream = _io.StringIO()
447 _tabulate(course=course, statistics=True, stream=stream)
448 text = stream.getvalue()
449 email = _construct_text_email(
450 author=course.robot, targets=[person],
451 subject='All grades for {}'.format(course.name),
453 elif len(students) == 1:
454 student = students[0]
455 assignments = [a for a in course.assignments
456 if a.name.lower() in lsubject]
457 if len(assignments) == 0:
458 email = _get_student_email(
459 basedir=basedir, course=course, original=original,
460 person=person, student=student, use_color=use_color)
462 email = _get_student_submission_email(
463 basedir=basedir, course=course, original=original,
464 person=person, student=student, assignments=assignments,
467 raise InvalidStudent(students=students)