3 """Assignment submission handler
5 Allow students to submit assignments via email (if
6 ``Assignment.submittable`` is set).
9 from email.utils import formatdate as _formatdate
10 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 ..extract_mime import extract_mime as _extract_mime
20 from ..extract_mime import message_time as _message_time
21 from ..storage import assignment_path as _assignment_path
22 from ..storage import set_late as _set_late
23 from . import InvalidMessage as _InvalidMessage
24 from . import Response as _Response
27 class InvalidAssignment (_InvalidMessage):
28 def __init__(self, assignment, **kwargs):
29 if 'error' not in kwargs:
30 kwargs['error'] = 'Received invalid {} submission'.format(
32 super(InvalidAssignment, self).__init__(**kwargs)
33 self.assignment = assignment
36 def run(basedir, course, message, person, subject,
37 max_late=0, use_color=None, dry_run=None, **kwargs):
39 >>> from pgp_mime.email import encodedMIMEText
40 >>> from ..test.course import StubCourse
41 >>> from . import Response
42 >>> course = StubCourse()
44 ... course.course.find_people(email='bb@greyhavens.net'))[0]
45 >>> message = encodedMIMEText('The answer is 42.')
46 >>> message['Message-ID'] = '<123.456@home.net>'
47 >>> message['Received'] = (
48 ... 'from smtp.home.net (smtp.home.net [123.456.123.456]) '
49 ... 'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
50 ... 'for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)')
51 >>> subject = '[submit] assignment 1'
53 ... run(basedir=course.basedir, course=course.course, message=message,
54 ... person=person, subject=subject, max_late=0)
55 ... except Response as e:
56 ... print('respond with:')
57 ... print(e.message.as_string())
58 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
60 Content-Type: text/plain; charset="us-ascii"
62 Content-Transfer-Encoding: 7bit
63 Content-Disposition: inline
64 Subject: Received Assignment 1 submission
66 We received your submission for Assignment 1 on ....
70 time = _message_time(message=message, use_color=use_color)
71 assignment = _get_assignment(
72 course=course, subject=subject, use_color=use_color)
73 assignment_path = _assignment_path(basedir, assignment, person)
74 _save_local_message_copy(
75 msg=message, person=person, assignment_path=assignment_path,
76 use_color=use_color, dry_run=dry_run)
77 _extract_mime(message=message, output=assignment_path, dry_run=dry_run)
79 basedir=basedir, assignment=assignment, person=person, time=time,
80 max_late=max_late, use_color=use_color, dry_run=dry_run)
82 time_str = 'on {}'.format(_formatdate(time))
84 time_str = 'at an unknown time'
85 message = _pgp_mime.encodedMIMEText((
86 'We received your submission for {} {}.'
88 assignment.name, time_str))
89 message['Subject'] = 'Received {} submission'.format(assignment.name)
90 raise _Response(message=message)
92 def _match_assignment(assignment, subject):
93 return assignment.name.lower() in subject.lower()
95 def _get_assignment(course, subject, use_color):
96 assignments = [a for a in course.assignments
97 if _match_assignment(a, subject)]
98 if len(assignments) != 1:
99 if len(assignments) == 0:
100 response_subject = 'no assignment found in {!r}'.format(subject)
102 'does not match any submittable assignment name\n'
103 'for {}.\n').format(course.name)
105 response_subject = 'several assignments found in {!r}'.format(
108 'matches several submittable assignment names\n'
109 'for {}: * {}\n').format(
111 '\n * '.join(a.name for a in assignments))
112 submittable_assignments = [
113 a for a in course.assignments if a.submittable]
114 if not submittable_assignments:
116 'In fact, there are no submittable assignments for\n'
120 'Remember to use the full name for the assignment in the\n'
121 'subject. For example:\n'
122 ' {} submission').format(
123 submittable_assignments[0].name)
124 message = _pgp_mime.encodedMIMEText((
125 'We got an email from you with the following subject:\n'
128 '{}\n').format(subject, course.name, hint))
129 message['Subject'] = response_subject
131 message=message, exception=ValueError(response_subject))
132 assignment = assignments[0]
134 if not assignment.submittable:
135 raise InvalidAssignment(assignment)
136 return assignments[0]
138 def _save_local_message_copy(msg, person, assignment_path, use_color=None,
140 highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
142 _os.makedirs(assignment_path)
145 mpath = _os_path.join(assignment_path, 'mail')
147 mbox = _mailbox.Maildir(mpath, factory=None, create=not dry_run)
148 except _mailbox.NoSuchMailboxError as e:
149 _LOG.debug(_color_string(
150 string='could not open mailbox at {}'.format(mpath),
156 for other_msg in mbox:
157 if other_msg['Message-ID'] == msg['Message-ID']:
161 _LOG.debug(_color_string(
162 string='saving email from {} to {}'.format(
163 person, assignment_path), color=good))
164 if mbox is not None and not dry_run:
165 mdmsg = _mailbox.MaildirMessage(msg)
170 _LOG.debug(_color_string(
171 string='already found {} in {}'.format(
172 msg['Message-ID'], mpath), color=good))
174 def _check_late(basedir, assignment, person, time, max_late=0, use_color=None,
176 highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
177 if time > assignment.due + max_late:
178 dt = time - assignment.due
179 _LOG.warn(_color_string(
180 string='{} {} late by {} seconds ({} hours)'.format(
181 person.name, assignment.name, dt, dt/3600.),
184 _set_late(basedir=basedir, assignment=assignment, person=person)