1 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
3 # This file is part of pygrader.
5 # pygrader is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
10 # pygrader is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along with
15 # pygrader. If not, see <http://www.gnu.org/licenses/>.
17 """Handle grade assignment
19 Allow professors and TAs to assign grades via email.
23 import mailbox as _mailbox
24 import os.path as _os_path
26 import pgp_mime as _pgp_mime
28 from .. import LOG as _LOG
29 from ..email import construct_text_email as _construct_text_email
30 from ..extract_mime import message_time as _message_time
31 from ..model.grade import Grade as _Grade
32 from ..storage import load_grade as _load_grade
33 from ..storage import parse_grade as _parse_grade
34 from ..storage import save_grade as _save_grade
35 from . import InvalidMessage as _InvalidMessage
36 from . import get_subject_assignment as _get_subject_assignment
37 from . import get_subject_student as _get_subject_student
38 from . import PermissionViolationMessage as _PermissionViolationMessage
39 from . import Response as _Response
40 from . import UnsignedMessage as _UnsignedMessage
43 class MissingGradeMessage (_InvalidMessage):
44 def __init__(self, **kwargs):
45 if 'error' not in kwargs:
46 kwargs['error'] = 'missing grade'
47 super(MissingGradeMessage, self).__init__(**kwargs)
50 def run(basedir, course, message, person, subject,
51 trust_email_infrastructure=False, dry_run=False, **kwargs):
53 >>> from pgp_mime.email import encodedMIMEText
54 >>> from ..test.course import StubCourse
55 >>> from . import InvalidMessage, Response
56 >>> course = StubCourse()
58 ... course.course.find_people(email='eye@tower.edu'))[0]
59 >>> message = encodedMIMEText('10')
60 >>> message['Message-ID'] = '<123.456@home.net>'
61 >>> def process(**kwargs):
64 ... except Response as response:
65 ... print('respond with:')
66 ... print(response.message.as_string().replace('\\t', ' '))
67 ... except InvalidMessage as error:
68 ... print('{} error:'.format(type(error).__name__))
71 Message authentication is handled identically to the ``get`` module.
74 ... basedir=course.basedir, course=course.course, message=message,
75 ... person=person, subject='[grade]')
76 UnsignedMessage error:
79 Students are denied access:
82 ... course.course.find_people(email='bb@greyhavens.net'))[0]
84 ... basedir=course.basedir, course=course.course, message=message,
85 ... person=student, subject='[grade]',
86 ... trust_email_infrastructure=True)
87 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
88 PermissionViolationMessage error:
91 >>> person.pgp_key = None # so we have plain-text to doctest
92 >>> assignment = course.course.assignments[0]
93 >>> message.authenticated = True
95 ... basedir=course.basedir, course=course.course, message=message,
96 ... person=person, subject='[grade] {}, {}'.format(student.name, assignment.name))
97 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
99 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
101 Content-Disposition: inline
103 From: Robot101 <phys101@tower.edu>
104 Reply-to: Robot101 <phys101@tower.edu>
105 To: Sauron <eye@tower.edu>
106 Subject: Set Bilbo Baggins grade on Attendance 1 to 10.0
108 --===============...==
109 Content-Type: text/plain; charset="us-ascii"
111 Content-Transfer-Encoding: 7bit
112 Content-Disposition: inline
118 --===============...==
120 Content-Transfer-Encoding: 7bit
121 Content-Description: OpenPGP digital signature
122 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
124 -----BEGIN PGP SIGNATURE-----
125 Version: GnuPG v2.0.19 (GNU/Linux)
128 -----END PGP SIGNATURE-----
130 --===============...==--
132 >>> message = encodedMIMEText('9\\n\\nUnits!')
133 >>> message['Message-ID'] = '<123.456@home.net>'
134 >>> message.authenticated = True
136 ... basedir=course.basedir, course=course.course, message=message,
137 ... person=person, subject='[grade] {}, {}'.format(student.name, assignment.name))
138 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
140 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
142 Content-Disposition: inline
144 From: Robot101 <phys101@tower.edu>
145 Reply-to: Robot101 <phys101@tower.edu>
146 To: Sauron <eye@tower.edu>
147 Subject: Set Bilbo Baggins grade on Attendance 1 to 9.0
149 --===============...==
150 Content-Type: text/plain; charset="us-ascii"
152 Content-Transfer-Encoding: 7bit
153 Content-Disposition: inline
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 --===============...==--
175 if trust_email_infrastructure:
179 hasattr(message, 'authenticated') and message.authenticated)
180 if not authenticated:
181 raise _UnsignedMessage()
182 if not person.is_admin():
183 raise _PermissionViolationMessage(
184 person=person, allowed_groups=person.admin_groups)
185 student = _get_subject_student(course=course, subject=subject)
186 assignment = _get_subject_assignment(course=course, subject=subject)
188 basedir=basedir, message=message, assignment=assignment,
190 _LOG.info('set {} grade on {} to {}'.format(
191 student, assignment, grade.points))
193 _save_grade(basedir=basedir, grade=grade)
194 response = _construct_text_email(
195 author=course.robot, targets=[person],
196 subject='Set {} grade on {} to {}'.format(
197 student.name, assignment.name, grade.points),
198 text='Set comment to:\n\n{}\n'.format(grade.comment))
199 raise _Response(message=response, complete=True)
201 def _get_grade(basedir, message, assignment, student):
203 for part in message.walk():
204 if part.get_content_type() == 'text/plain':
205 charset = part.get_charset()
209 encoding = charset.input_charset
210 text = str(part.get_payload(decode=True), encoding)
212 raise _MissingGradeMessage(message=message)
213 stream = _io.StringIO(text)
214 new_grade = _parse_grade(
215 stream=stream, assignment=assignment, person=student)
217 old_grade = _load_grade(
218 basedir=basedir, assignment=assignment, person=student)
219 except IOError as error:
220 _LOG.warn(str(error))
221 old_grade = _Grade(student=student, assignment=assignment, points=0)
222 old_grade.points = new_grade.points
223 old_grade.comment = new_grade.comment