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 ..storage import assignment_path as _assignment_path
20 from ..tabulate import tabulate as _tabulate
21 from ..template import _student_email as _student_email
22 from . import InvalidMessage as _InvalidMessage
23 from . import InvalidSubjectMessage as _InvalidSubjectMessage
24 from . import Response as _Response
25 from . import UnsignedMessage as _UnsignedMessage
28 class InvalidStudent (_InvalidSubjectMessage):
29 def __init__(self, students=None, **kwargs):
30 if 'error' not in kwargs:
31 kwargs['error'] = 'Subject matches multiple students'
32 super(InvalidStudent, self).__init__(kwargs)
33 self.students = students
36 def run(basedir, course, message, person, subject,
37 trust_email_infrastructure=False, dry_run=False, **kwargs):
39 >>> from pgp_mime.email import encodedMIMEText
40 >>> from ..model.grade import Grade
41 >>> from ..test.course import StubCourse
42 >>> from . import InvalidMessage, Response
43 >>> course = StubCourse()
45 ... course.course.find_people(email='bb@greyhavens.net'))[0]
46 >>> message = encodedMIMEText('This text is not important.')
47 >>> message['Message-ID'] = '<123.456@home.net>'
48 >>> def process(**kwargs):
51 ... except Response as response:
52 ... print('respond with:')
53 ... print(response.message.as_string().replace('\\t', ' '))
54 ... except InvalidMessage as error:
55 ... print('{} error:'.format(type(error).__name__))
58 Unauthenticated messages are refused by default.
61 ... basedir=course.basedir, course=course.course, message=message,
62 ... person=person, subject='[get]', max_late=0)
63 UnsignedMessage error:
66 Although you can process them by setting the
67 ``trust_email_infrastructure`` option. This might not be too
68 dangerous, since you're sending the email to the user's configured
69 email address, not just replying blindly to the incoming email
70 address. With ``trust_email_infrastructure`` and missing user PGP
71 keys, sysadmins on the intervening systems will be able to read
72 our responses, possibly leaking grade information. If leaking to
73 sysadmins is considered unacceptable, you've can only email users
74 who have registered PGP keys.
76 Students without grades get a reasonable response.
79 ... basedir=course.basedir, course=course.course, message=message,
80 ... person=person, subject='[get]', max_late=0,
81 ... trust_email_infrastructure=True)
82 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
84 Content-Type: text/plain; charset="us-ascii"
86 Content-Transfer-Encoding: 7bit
87 Content-Disposition: inline
88 Subject: No grades for Billy
90 We don't have any of your grades on file for this course.
92 >>> message.authenticated = True
94 ... basedir=course.basedir, course=course.course, message=message,
95 ... person=person, subject='[get]', max_late=0)
96 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
98 Content-Type: text/plain; charset="us-ascii"
100 Content-Transfer-Encoding: 7bit
101 Content-Disposition: inline
102 Subject: No grades for Billy
104 We don't have any of your grades on file for this course.
106 Once we add a grade, they get details on all their grades for the
111 ... assignment=course.course.assignment('Attendance 1'),
113 >>> course.course.grades.append(grade)
116 ... assignment=course.course.assignment('Attendance 2'),
118 >>> course.course.grades.append(grade)
121 ... assignment=course.course.assignment('Assignment 1'),
122 ... points=10, comment='Looks good.')
123 >>> course.course.grades.append(grade)
125 ... basedir=course.basedir, course=course.course, message=message,
126 ... person=person, subject='[get]', max_late=0)
127 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
129 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
131 Content-Disposition: inline
133 From: Robot101 <phys101@tower.edu>
134 Reply-to: Robot101 <phys101@tower.edu>
135 To: Bilbo Baggins <bb@shire.org>
136 Subject: Physics 101 grades
138 --===============...==
139 Content-Type: text/plain; charset="us-ascii"
141 Content-Transfer-Encoding: 7bit
142 Content-Disposition: inline
147 * Attendance 1: 1 out of 1 available points.
148 * Attendance 2: 1 out of 1 available points.
149 * Assignment 1: 10 out of 10 available points.
159 --===============...==
161 Content-Transfer-Encoding: 7bit
162 Content-Description: OpenPGP digital signature
163 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
165 -----BEGIN PGP SIGNATURE-----
166 Version: GnuPG v2.0.19 (GNU/Linux)
169 -----END PGP SIGNATURE-----
171 --===============...==--
173 Professors and TAs can request the grades for the whole course.
177 ... course.course.find_people(email='eye@tower.edu'))[0]
178 >>> person.pgp_key = None
180 ... basedir=course.basedir, course=course.course, message=message,
181 ... person=person, subject='[get]', max_late=0)
182 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
184 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
186 Content-Disposition: inline
188 From: Robot101 <phys101@tower.edu>
189 Reply-to: Robot101 <phys101@tower.edu>
190 To: Sauron <eye@tower.edu>
191 Subject: All grades for Physics 101
193 --===============...==
194 Content-Type: text/plain; charset="us-ascii"
196 Content-Transfer-Encoding: 7bit
197 Content-Disposition: inline
199 Student Attendance 1 Attendance 2 Assignment 1
203 Std. Dev. 0.00 0.00 0.00
205 --===============...==
207 Content-Transfer-Encoding: 7bit
208 Content-Description: OpenPGP digital signature
209 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
211 -----BEGIN PGP SIGNATURE-----
212 Version: GnuPG v2.0.19 (GNU/Linux)
215 -----END PGP SIGNATURE-----
217 --===============...==--
219 They can also request grades for a particular student.
222 ... basedir=course.basedir, course=course.course, message=message,
223 ... person=person, subject='[get] {}'.format(student.name),
225 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
227 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
229 Content-Disposition: inline
231 From: Robot101 <phys101@tower.edu>
232 Reply-to: Robot101 <phys101@tower.edu>
233 To: Sauron <eye@tower.edu>
234 Subject: Physics 101 grades for Bilbo Baggins
236 --===============...==
237 Content-Type: text/plain; charset="us-ascii"
239 Content-Transfer-Encoding: 7bit
240 Content-Disposition: inline
245 * Attendance 1: 1 out of 1 available points.
246 * Attendance 2: 1 out of 1 available points.
247 * Assignment 1: 10 out of 10 available points.
257 --===============...==
259 Content-Transfer-Encoding: 7bit
260 Content-Description: OpenPGP digital signature
261 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
263 -----BEGIN PGP SIGNATURE-----
264 Version: GnuPG v2.0.19 (GNU/Linux)
267 -----END PGP SIGNATURE-----
269 --===============...==--
271 They can also request every submission for a particular student on
272 a particular assignment. Lets give the student a submission email
273 to see how that works.
275 >>> from .submission import run as _handle_submission
276 >>> submission = encodedMIMEText('The answer is 42.')
277 >>> submission['Message-ID'] = '<789.abc@home.net>'
278 >>> submission['Received'] = (
279 ... 'from smtp.home.net (smtp.home.net [123.456.123.456]) '
280 ... 'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
281 ... 'for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)')
283 ... _handle_submission(
284 ... basedir=course.basedir, course=course.course,
285 ... message=submission, person=student,
286 ... subject='[submit] Assignment 1')
287 ... except _Response:
290 Now lets request the submissions.
293 ... basedir=course.basedir, course=course.course, message=message,
295 ... subject='[get] {}, {}'.format(student.name, 'Assignment 1'),
297 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
299 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
301 Content-Disposition: inline
303 From: Robot101 <phys101@tower.edu>
304 Reply-to: Robot101 <phys101@tower.edu>
305 To: Sauron <eye@tower.edu>
306 Subject: Physics 101 assignment submissions for Bilbo Baggins
308 --===============...==
309 Content-Type: multipart/mixed; boundary="===============...=="
312 --===============...==
313 Content-Type: text/plain; charset="us-ascii"
315 Content-Transfer-Encoding: 7bit
316 Content-Disposition: inline
318 Physics 101 assignment submissions for Bilbo Baggins:
321 --===============...==
322 Content-Type: text/plain; charset="us-ascii"
324 Content-Transfer-Encoding: 7bit
325 Content-Disposition: inline
327 Assignment 1 grade: 10
331 --===============...==
332 Content-Type: message/rfc822
335 Content-Type: text/plain; charset="us-ascii"
337 Content-Transfer-Encoding: 7bit
338 Content-Disposition: inline
339 Message-ID: <789.abc@home.net>
340 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)
343 --===============...==--
344 --===============...==
346 Content-Transfer-Encoding: 7bit
347 Content-Description: OpenPGP digital signature
348 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
350 -----BEGIN PGP SIGNATURE-----
351 Version: GnuPG v2.0.19 (GNU/Linux)
354 -----END PGP SIGNATURE-----
356 --===============...==--
360 if trust_email_infrastructure:
364 hasattr(message, 'authenticated') and message.authenticated)
365 if not authenticated:
366 raise _UnsignedMessage()
367 if 'assistants' in person.groups or 'professors' in person.groups:
368 email = _get_admin_email(
369 basedir=basedir, course=course, person=person, subject=subject)
370 elif 'students' in person.groups:
371 email = _get_student_email(
372 basedir=basedir, course=course, person=person)
374 raise NotImplementedError(
375 'strange groups {} for {}'.format(person.groups, person))
376 raise _Response(message=email)
378 def _get_student_email(basedir, course, person, student=None):
384 emails = list(_student_email(
385 basedir=basedir, author=course.robot, course=course,
386 student=student, targets=targets, old=True))
390 "We don't have any of your grades on file for this course."
394 "We don't have any grades for {} on file for this course."
395 ).format(student.name)
396 message = _pgp_mime.encodedMIMEText(text)
397 message['Subject'] = 'No grades for {}'.format(student.alias())
398 raise _Response(message=message)
399 elif len(emails) > 1:
400 raise NotImplementedError(emails)
401 email,callback = emails[0]
402 # callback records notification, but don't bother here
405 def _get_student_submission_email(
406 basedir, course, person, assignments, student):
407 subject = '{} assignment submissions for {}'.format(
408 course.name, student.name)
409 text = '{}:\n * {}\n'.format(
410 subject, '\n * '.join(a.name for a in assignments))
411 message = _MIMEMultipart('mixed')
412 message.attach(_pgp_mime.encodedMIMEText(text))
413 for assignment in assignments:
414 grade = course.grade(student=student, assignment=assignment)
415 if grade is not None:
416 message.attach(_pgp_mime.encodedMIMEText(
417 '{} grade: {}\n\n{}\n'.format(
418 assignment.name, grade.points, grade.comment)))
419 assignment_path = _assignment_path(basedir, assignment, student)
420 mpath = _os_path.join(assignment_path, 'mail')
422 mbox = _mailbox.Maildir(mpath, factory=None, create=False)
423 except _mailbox.NoSuchMailboxError as e:
427 message.attach(_MIMEMessage(msg))
428 return _construct_email(
429 author=course.robot, targets=[person], subject=subject,
432 def _get_admin_email(basedir, course, person, subject):
433 lsubject = subject.lower()
434 students = [p for p in course.find_people()
435 if p.name.lower() in lsubject]
436 if len(students) == 0:
437 stream = _io.StringIO()
439 course=course, statistics=True, stream=stream, use_color=False)
440 text = stream.getvalue()
441 email = _construct_text_email(
442 author=course.robot, targets=[person],
443 subject='All grades for {}'.format(course.name),
445 elif len(students) == 1:
446 student = students[0]
447 assignments = [a for a in course.assignments
448 if a.name.lower() in lsubject]
449 if len(assignments) == 0:
450 email = _get_student_email(
451 basedir=basedir, course=course, person=person, student=student)
453 email = _get_student_submission_email(
454 basedir=basedir, course=course, person=person, student=student,
455 assignments=assignments)
457 raise InvalidStudent(students=students)