f7bf78d4e854c5875b75e0b0c381f101c8554bff
[pygrader.git] / pygrader / handler / submission.py
1 # Copyright
2
3 """Assignment submission handler
4
5 Allow students to submit assignments via email (if
6 ``Assignment.submittable`` is set).
7 """
8
9 from email.utils import formatdate as _formatdate
10 import mailbox as _mailbox
11 import os as _os
12 import os.path as _os_path
13
14 import pgp_mime as _pgp_mime
15
16 from .. import LOG as _LOG
17 from ..color import GOOD_DEBUG as _GOOD_DEBUG
18 from ..extract_mime import extract_mime as _extract_mime
19 from ..extract_mime import message_time as _message_time
20 from ..storage import assignment_path as _assignment_path
21 from ..storage import set_late as _set_late
22 from . import get_subject_assignment as _get_subject_assignment
23 from . import InvalidMessage as _InvalidMessage
24 from . import Response as _Response
25
26
27 class InvalidSubmission (_InvalidMessage):
28     def __init__(self, assignment=None, **kwargs):
29         if 'error' not in kwargs:
30             kwargs['error'] = 'invalid submission'
31         super(InvalidSubmission, self).__init__(**kwargs)
32         self.assignment = assignment
33
34
35 def run(basedir, course, message, person, subject, max_late=0, dry_run=None,
36         **kwargs):
37     """
38     >>> from pgp_mime.email import encodedMIMEText
39     >>> from ..test.course import StubCourse
40     >>> from . import Response
41     >>> course = StubCourse()
42     >>> person = list(
43     ...     course.course.find_people(email='bb@greyhavens.net'))[0]
44     >>> message = encodedMIMEText('The answer is 42.')
45     >>> message['Message-ID'] = '<123.456@home.net>'
46     >>> message['Received'] = (
47     ...     'from smtp.home.net (smtp.home.net [123.456.123.456]) '
48     ...     'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
49     ...     'for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)')
50     >>> subject = '[submit] assignment 1'
51     >>> try:
52     ...     run(basedir=course.basedir, course=course.course, message=message,
53     ...         person=person, subject=subject, max_late=0)
54     ... except Response as e:
55     ...     print('respond with:')
56     ...     print(e.message.as_string())
57     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
58     respond with:
59     Content-Type: text/plain; charset="us-ascii"
60     MIME-Version: 1.0
61     Content-Transfer-Encoding: 7bit
62     Content-Disposition: inline
63     Subject: Received Assignment 1 submission
64     <BLANKLINE>
65     We received your submission for Assignment 1 on ....
66
67     >>> course.cleanup()
68     """
69     time = _message_time(message=message)
70     assignment = _get_assignment(course=course, subject=subject)
71     assignment_path = _assignment_path(basedir, assignment, person)
72     _save_local_message_copy(
73         msg=message, person=person, assignment_path=assignment_path,
74         dry_run=dry_run)
75     _extract_mime(message=message, output=assignment_path, dry_run=dry_run)
76     _check_late(
77         basedir=basedir, assignment=assignment, person=person, time=time,
78         max_late=max_late, dry_run=dry_run)
79     if time:
80         time_str = 'on {}'.format(_formatdate(time))
81     else:
82         time_str = 'at an unknown time'
83     message = _pgp_mime.encodedMIMEText((
84             'We received your submission for {} {}.'
85             ).format(
86             assignment.name, time_str))
87     message['Subject'] = 'Received {} submission'.format(assignment.name)
88     raise _Response(message=message)
89
90 def _get_assignment(course, subject):
91     assignment = _get_subject_assignment(course, subject)
92     if not assignment.submittable:
93         raise InvalidSubmission(assignment=assignment)
94     return assignment
95
96 def _save_local_message_copy(msg, person, assignment_path, dry_run=False):
97     try:
98         _os.makedirs(assignment_path)
99     except OSError:
100         pass
101     mpath = _os_path.join(assignment_path, 'mail')
102     try:
103         mbox = _mailbox.Maildir(mpath, factory=None, create=not dry_run)
104     except _mailbox.NoSuchMailboxError as e:
105         _LOG.warn('could not open mailbox at {}'.format(mpath))
106         mbox = None
107         new_msg = True
108     else:
109         new_msg = True
110         for other_msg in mbox:
111             if other_msg['Message-ID'] == msg['Message-ID']:
112                 new_msg = False
113                 break
114     if new_msg:
115         _LOG.log(_GOOD_DEBUG, 'saving email from {} to {}'.format(
116                 person, assignment_path))
117         if mbox is not None and not dry_run:
118             mdmsg = _mailbox.MaildirMessage(msg)
119             mdmsg.add_flag('S')
120             mbox.add(mdmsg)
121             mbox.close()
122     else:
123         _LOG.log(_GOOD_DEBUG, 'already found {} in {}'.format(
124                     msg['Message-ID'], mpath))
125
126 def _check_late(basedir, assignment, person, time, max_late=0, dry_run=False):
127     if time > assignment.due + max_late:
128         dt = time - assignment.due
129         _LOG.warning('{} {} late by {} seconds ({} hours)'.format(
130             person.name, assignment.name, dt, dt/3600.))
131         if not dry_run:
132             _set_late(basedir=basedir, assignment=assignment, person=person)