3 """Handle grade assignment
5 Allow professors and TAs to assign grades via email.
9 import mailbox as _mailbox
10 import os.path as _os_path
12 import pgp_mime as _pgp_mime
14 from .. import LOG as _LOG
15 from ..email import construct_text_email as _construct_text_email
16 from ..extract_mime import message_time as _message_time
17 from ..model.grade import Grade as _Grade
18 from ..storage import load_grade as _load_grade
19 from ..storage import parse_grade as _parse_grade
20 from ..storage import save_grade as _save_grade
21 from . import InvalidMessage as _InvalidMessage
22 from . import get_subject_assignment as _get_subject_assignment
23 from . import get_subject_student as _get_subject_student
24 from . import PermissionViolationMessage as _PermissionViolationMessage
25 from . import Response as _Response
26 from . import UnsignedMessage as _UnsignedMessage
29 class MissingGradeMessage (_InvalidMessage):
30 def __init__(self, **kwargs):
31 if 'error' not in kwargs:
32 kwargs['error'] = 'missing grade'
33 super(MissingGradeMessage, self).__init__(**kwargs)
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 ..test.course import StubCourse
41 >>> from . import InvalidMessage, Response
42 >>> course = StubCourse()
44 ... course.course.find_people(email='eye@tower.edu'))[0]
45 >>> message = encodedMIMEText('10')
46 >>> message['Message-ID'] = '<123.456@home.net>'
47 >>> def process(**kwargs):
50 ... except Response as response:
51 ... print('respond with:')
52 ... print(response.message.as_string().replace('\\t', ' '))
53 ... except InvalidMessage as error:
54 ... print('{} error:'.format(type(error).__name__))
57 Message authentication is handled identically to the ``get`` module.
60 ... basedir=course.basedir, course=course.course, message=message,
61 ... person=person, subject='[grade]')
62 UnsignedMessage error:
65 Students are denied access:
68 ... course.course.find_people(email='bb@greyhavens.net'))[0]
70 ... basedir=course.basedir, course=course.course, message=message,
71 ... person=student, subject='[grade]',
72 ... trust_email_infrastructure=True)
73 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
74 PermissionViolationMessage error:
77 >>> person.pgp_key = None # so we have plain-text to doctest
78 >>> assignment = course.course.assignments[0]
79 >>> message.authenticated = True
81 ... basedir=course.basedir, course=course.course, message=message,
82 ... person=person, subject='[grade] {}, {}'.format(student.name, assignment.name))
83 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
85 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
87 Content-Disposition: inline
89 From: Robot101 <phys101@tower.edu>
90 Reply-to: Robot101 <phys101@tower.edu>
91 To: Sauron <eye@tower.edu>
92 Subject: Set Bilbo Baggins grade on Attendance 1 to 10.0
94 --===============...==
95 Content-Type: text/plain; charset="us-ascii"
97 Content-Transfer-Encoding: 7bit
98 Content-Disposition: inline
104 --===============...==
106 Content-Transfer-Encoding: 7bit
107 Content-Description: OpenPGP digital signature
108 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
110 -----BEGIN PGP SIGNATURE-----
111 Version: GnuPG v2.0.19 (GNU/Linux)
114 -----END PGP SIGNATURE-----
116 --===============...==--
118 >>> message = encodedMIMEText('9\\n\\nUnits!')
119 >>> message['Message-ID'] = '<123.456@home.net>'
120 >>> message.authenticated = True
122 ... basedir=course.basedir, course=course.course, message=message,
123 ... person=person, subject='[grade] {}, {}'.format(student.name, assignment.name))
124 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
126 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
128 Content-Disposition: inline
130 From: Robot101 <phys101@tower.edu>
131 Reply-to: Robot101 <phys101@tower.edu>
132 To: Sauron <eye@tower.edu>
133 Subject: Set Bilbo Baggins grade on Attendance 1 to 9.0
135 --===============...==
136 Content-Type: text/plain; charset="us-ascii"
138 Content-Transfer-Encoding: 7bit
139 Content-Disposition: inline
145 --===============...==
147 Content-Transfer-Encoding: 7bit
148 Content-Description: OpenPGP digital signature
149 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
151 -----BEGIN PGP SIGNATURE-----
152 Version: GnuPG v2.0.19 (GNU/Linux)
155 -----END PGP SIGNATURE-----
157 --===============...==--
161 if trust_email_infrastructure:
165 hasattr(message, 'authenticated') and message.authenticated)
166 if not authenticated:
167 raise _UnsignedMessage()
168 if not ('professors' in person.groups or 'assistants' in person.groups):
169 raise _PermissionViolationMessage(
170 person=person, allowed_groups=['professors', 'assistants'])
171 student = _get_subject_student(course=course, subject=subject)
172 assignment = _get_subject_assignment(course=course, subject=subject)
174 basedir=basedir, message=message, assignment=assignment,
176 _LOG.info('set {} grade on {} to {}'.format(
177 student, assignment, grade.points))
179 _save_grade(basedir=basedir, grade=grade)
180 response = _construct_text_email(
181 author=course.robot, targets=[person],
182 subject='Set {} grade on {} to {}'.format(
183 student.name, assignment.name, grade.points),
184 text='Set comment to:\n\n{}\n'.format(grade.comment))
185 raise _Response(message=response, complete=True)
187 def _get_grade(basedir, message, assignment, student):
189 for part in message.walk():
190 if part.get_content_type() == 'text/plain':
191 charset = part.get_charset()
195 encoding = charset.input_charset
196 text = str(part.get_payload(decode=True), encoding)
198 raise _MissingGradeMessage(message=message)
199 stream = _io.StringIO(text)
200 new_grade = _parse_grade(
201 stream=stream, assignment=assignment, person=student)
203 old_grade = _load_grade(
204 basedir=basedir, assignment=assignment, person=student)
205 except IOError as error:
206 _LOG.warn(str(error))
207 old_grade = _Grade(student=student, assignment=assignment, points=0)
208 old_grade.points = new_grade.points
209 old_grade.comment = new_grade.comment