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 """Assignment submission handler
19 Allow students to submit assignments via email (if
20 ``Assignment.submittable`` is set).
23 from email.utils import formatdate as _formatdate
24 import mailbox as _mailbox
26 import os.path as _os_path
28 import pgp_mime as _pgp_mime
30 from .. import LOG as _LOG
31 from ..color import GOOD_DEBUG as _GOOD_DEBUG
32 from ..extract_mime import extract_mime as _extract_mime
33 from ..extract_mime import message_time as _message_time
34 from ..storage import assignment_path as _assignment_path
35 from ..storage import set_late as _set_late
36 from . import get_subject_assignment as _get_subject_assignment
37 from . import InvalidMessage as _InvalidMessage
38 from . import Response as _Response
41 class InvalidSubmission (_InvalidMessage):
42 def __init__(self, assignment=None, **kwargs):
43 if 'error' not in kwargs:
44 kwargs['error'] = 'invalid submission'
45 super(InvalidSubmission, self).__init__(**kwargs)
46 self.assignment = assignment
49 def run(basedir, course, message, person, subject, max_late=0, dry_run=None,
52 >>> from pgp_mime.email import encodedMIMEText
53 >>> from ..test.course import StubCourse
54 >>> from . import Response
55 >>> course = StubCourse()
57 ... course.course.find_people(email='bb@greyhavens.net'))[0]
58 >>> message = encodedMIMEText('The answer is 42.')
59 >>> message['Message-ID'] = '<123.456@home.net>'
60 >>> message['Received'] = (
61 ... 'from smtp.home.net (smtp.home.net [123.456.123.456]) '
62 ... 'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
63 ... 'for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)')
64 >>> subject = '[submit] assignment 1'
66 ... run(basedir=course.basedir, course=course.course, message=message,
67 ... person=person, subject=subject, max_late=0)
68 ... except Response as e:
69 ... print('respond with:')
70 ... print(e.message.as_string())
71 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
73 Content-Type: text/plain; charset="us-ascii"
75 Content-Transfer-Encoding: 7bit
76 Content-Disposition: inline
77 Subject: Received Assignment 1 submission
79 We received your submission for Assignment 1 on ....
83 time = _message_time(message=message)
84 assignment = _get_assignment(course=course, subject=subject)
85 assignment_path = _assignment_path(basedir, assignment, person)
86 _save_local_message_copy(
87 msg=message, person=person, assignment_path=assignment_path,
89 _extract_mime(message=message, output=assignment_path, dry_run=dry_run)
91 basedir=basedir, assignment=assignment, person=person, time=time,
92 max_late=max_late, dry_run=dry_run)
94 time_str = 'on {}'.format(_formatdate(time))
96 time_str = 'at an unknown time'
97 message = _pgp_mime.encodedMIMEText((
98 'We received your submission for {} {}.'
100 assignment.name, time_str))
101 message['Subject'] = 'Received {} submission'.format(assignment.name)
102 raise _Response(message=message)
104 def _get_assignment(course, subject):
105 assignment = _get_subject_assignment(course, subject)
106 if not assignment.submittable:
107 raise InvalidSubmission(assignment=assignment)
110 def _save_local_message_copy(msg, person, assignment_path, dry_run=False):
112 _os.makedirs(assignment_path)
115 mpath = _os_path.join(assignment_path, 'mail')
117 mbox = _mailbox.Maildir(mpath, factory=None, create=not dry_run)
118 except _mailbox.NoSuchMailboxError as e:
119 _LOG.warn('could not open mailbox at {}'.format(mpath))
124 for other_msg in mbox:
125 if other_msg['Message-ID'] == msg['Message-ID']:
129 _LOG.log(_GOOD_DEBUG, 'saving email from {} to {}'.format(
130 person, assignment_path))
131 if mbox is not None and not dry_run:
132 mdmsg = _mailbox.MaildirMessage(msg)
137 _LOG.log(_GOOD_DEBUG, 'already found {} in {}'.format(
138 msg['Message-ID'], mpath))
140 def _check_late(basedir, assignment, person, time, max_late=0, dry_run=False):
141 if time > assignment.due + max_late:
142 dt = time - assignment.due
143 _LOG.warning('{} {} late by {} seconds ({} hours)'.format(
144 person.name, assignment.name, dt, dt/3600.))
146 _set_late(basedir=basedir, assignment=assignment, person=person)