color: add ColoredFormatter for more transparent coloring.
[pygrader.git] / pygrader / handler / submission.py
index 7d1656812e9f232ab7be2e745e743748172232ac..920a3f884e0c01e3a0c6d115c13dd0127bb23d88 100644 (file)
@@ -11,22 +11,33 @@ import mailbox as _mailbox
 import os as _os
 import os.path as _os_path
 
+import pgp_mime as _pgp_mime
+
 from .. import LOG as _LOG
-from ..color import color_string as _color_string
-from ..color import standard_colors as _standard_colors
+from ..color import GOOD_DEBUG as _GOOD_DEBUG
 from ..extract_mime import extract_mime as _extract_mime
 from ..extract_mime import message_time as _message_time
 from ..storage import assignment_path as _assignment_path
 from ..storage import set_late as _set_late
-from . import respond as _respond
+from . import InvalidMessage as _InvalidMessage
+from . import Response as _Response
+
+
+class InvalidAssignment (_InvalidMessage):
+    def __init__(self, assignment, **kwargs):
+        if 'error' not in kwargs:
+            kwargs['error'] = 'Received invalid {} submission'.format(
+                assignment.name)
+        super(InvalidAssignment, self).__init__(**kwargs)
+        self.assignment = assignment
 
 
-def run(basedir, course, original, message, person, subject,
-        max_late=0, respond=None, use_color=None,
-        dry_run=None, **kwargs):
+def run(basedir, course, message, person, subject, max_late=0, dry_run=None,
+        **kwargs):
     """
     >>> from pgp_mime.email import encodedMIMEText
-    >>> from pygrader.test.course import StubCourse
+    >>> from ..test.course import StubCourse
+    >>> from . import Response
     >>> course = StubCourse()
     >>> person = list(
     ...     course.course.find_people(email='bb@greyhavens.net'))[0]
@@ -37,138 +48,92 @@ def run(basedir, course, original, message, person, subject,
     ...     'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
     ...     'for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)')
     >>> subject = '[submit] assignment 1'
-    >>> def respond(message):
-    ...     print('respond with:\\n{}'.format(message.as_string()))
-    >>> run(basedir=course.basedir, course=course.course, original=message,
-    ...     message=message, person=person, subject=subject,
-    ...     max_late=0, respond=respond)
+    >>> try:
+    ...     run(basedir=course.basedir, course=course.course, message=message,
+    ...         person=person, subject=subject, max_late=0)
+    ... except Response as e:
+    ...     print('respond with:')
+    ...     print(e.message.as_string())
     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
     respond with:
-    Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
-    MIME-Version: 1.0
-    Content-Disposition: inline
-    Date: ...
-    From: Robot101 <phys101@tower.edu>
-    Reply-to: Robot101 <phys101@tower.edu>
-    To: Bilbo Baggins <bb@shire.org>
-    Subject: received Assignment 1 submission
-    <BLANKLINE>
-    --===============...==
-    Content-Type: multipart/mixed; boundary="===============...=="
-    MIME-Version: 1.0
-    <BLANKLINE>
-    --===============...==
     Content-Type: text/plain; charset="us-ascii"
     MIME-Version: 1.0
     Content-Transfer-Encoding: 7bit
     Content-Disposition: inline
-    <BLANKLINE>
-    Billy,
+    Subject: Received Assignment 1 submission
     <BLANKLINE>
     We received your submission for Assignment 1 on ....
-    <BLANKLINE>
-    Yours,
-    phys-101 robot
-    --===============...==
-    Content-Type: message/rfc822
-    MIME-Version: 1.0
-    <BLANKLINE>
-    Content-Type: text/plain; charset="us-ascii"
-    MIME-Version: 1.0
-    Content-Transfer-Encoding: 7bit
-    Content-Disposition: inline
-    Message-ID: <123.456@home.net>
-    Received: from smtp.home.net (smtp.home.net [123.456.123.456]) by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)
-    <BLANKLINE>
-    The answer is 42.
-    --===============...==--
-    --===============...==
-    MIME-Version: 1.0
-    Content-Transfer-Encoding: 7bit
-    Content-Description: OpenPGP digital signature
-    Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
-    <BLANKLINE>
-    -----BEGIN PGP SIGNATURE-----
-    Version: GnuPG v2.0.19 (GNU/Linux)
-    <BLANKLINE>
-    ...
-    -----END PGP SIGNATURE-----
-    <BLANKLINE>
-    --===============...==--
+
     >>> course.cleanup()
     """
-    time = _message_time(message=message, use_color=use_color)
-
-    for assignment in course.assignments:
-        if _match_assignment(assignment, subject):
-            break
-    if not _match_assignment(assignment, subject):
-        response_subject = 'no assignment found in {!r}'.format(subject)
-        if respond:
-            submittable_assignments = [
-                a for a in course.assignments if a.submittable]
-            if not submittable_assignments:
-                hint = (
-                    'In fact, there are no submittable assignments for\n'
-                    'this course!')
-            else:
-                hint = (
-                    'Remember to use the full name for the assignment in the\n'
-                    'subject.  For example:\n'
-                    '  {} submission').format(
-                    submittable_assignments[0].name)
-            _respond(
-                course=course, person=person, original=original,
-                subject=response_subject, text=(
-                    'We got an email from you with the following subject:\n'
-                    '  {!r}\n'
-                    'which does not match any submittable assignment name\n'
-                    'for {}.\n'
-                    '{}').format(subject, course.name, hint),
-                respond=respond)
-        raise ValueError(response_subject)
-
-    if not assignment.submittable:
-        response_subject = 'received invalid {} submission'.format(
-            assignment.name)
-        if respond:
-            _respond(
-                course=course, person=person, original=original,
-                subject=response_subject, text=(
-                    'We received your submission for {}, but you are not\n'
-                    'allowed to submit that assignment via email.'
-                    ).format(assignment.name),
-                respond=respond)
-        raise ValueError(response_subject)
-
-    if respond:
-        response_subject = 'received {} submission'.format(assignment.name)
-        if time:
-            time_str = 'on {}'.format(_formatdate(time))
-        else:
-            time_str = 'at an unknown time'
-        _respond(
-            course=course, person=person, original=original,
-            subject=response_subject, text=(
-                'We received your submission for {} {}.'
-                ).format(assignment.name, time_str),
-            respond=respond)
-
+    time = _message_time(message=message)
+    assignment = _get_assignment(course=course, subject=subject)
     assignment_path = _assignment_path(basedir, assignment, person)
     _save_local_message_copy(
         msg=message, person=person, assignment_path=assignment_path,
-        use_color=use_color, dry_run=dry_run)
+        dry_run=dry_run)
     _extract_mime(message=message, output=assignment_path, dry_run=dry_run)
     _check_late(
         basedir=basedir, assignment=assignment, person=person, time=time,
-        max_late=max_late, use_color=use_color, dry_run=dry_run)
+        max_late=max_late, dry_run=dry_run)
+    if time:
+        time_str = 'on {}'.format(_formatdate(time))
+    else:
+        time_str = 'at an unknown time'
+    message = _pgp_mime.encodedMIMEText((
+            'We received your submission for {} {}.'
+            ).format(
+            assignment.name, time_str))
+    message['Subject'] = 'Received {} submission'.format(assignment.name)
+    raise _Response(message=message)
 
 def _match_assignment(assignment, subject):
     return assignment.name.lower() in subject.lower()
 
-def _save_local_message_copy(msg, person, assignment_path, use_color=None,
-                             dry_run=False):
-    highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
+def _get_assignment(course, subject):
+    assignments = [a for a in course.assignments
+                   if _match_assignment(a, subject)]
+    if len(assignments) != 1:
+        if len(assignments) == 0:
+            response_subject = 'no assignment found in {!r}'.format(subject)
+            error = (
+                'does not match any submittable assignment name\n'
+                'for {}.\n').format(course.name)
+        else:
+            response_subject = 'several assignments found in {!r}'.format(
+                subject)
+            error = (
+                'matches several submittable assignment names\n'
+                'for {}:  * {}\n').format(
+                course.name,
+                '\n  * '.join(a.name for a in assignments))
+        submittable_assignments = [
+            a for a in course.assignments if a.submittable]
+        if not submittable_assignments:
+            hint = (
+                'In fact, there are no submittable assignments for\n'
+                'this course!')
+        else:
+            hint = (
+                'Remember to use the full name for the assignment in the\n'
+                'subject.  For example:\n'
+                '  {} submission').format(
+                submittable_assignments[0].name)
+        message = _pgp_mime.encodedMIMEText((
+                'We got an email from you with the following subject:\n'
+                '  {!r}\n'
+                'which {}.\n\n'
+                '{}\n').format(subject, course.name, hint))
+        message['Subject'] = response_subject
+        raise _Response(
+            message=message, exception=ValueError(response_subject))
+    assignment = assignments[0]
+
+    if not assignment.submittable:
+        raise InvalidAssignment(assignment)
+    return assignments[0]
+
+def _save_local_message_copy(msg, person, assignment_path, dry_run=False):
     try:
         _os.makedirs(assignment_path)
     except OSError:
@@ -177,9 +142,7 @@ def _save_local_message_copy(msg, person, assignment_path, use_color=None,
     try:
         mbox = _mailbox.Maildir(mpath, factory=None, create=not dry_run)
     except _mailbox.NoSuchMailboxError as e:
-        _LOG.debug(_color_string(
-                string='could not open mailbox at {}'.format(mpath),
-                color=bad))
+        _LOG.warn('could not open mailbox at {}'.format(mpath))
         mbox = None
         new_msg = True
     else:
@@ -189,27 +152,21 @@ def _save_local_message_copy(msg, person, assignment_path, use_color=None,
                 new_msg = False
                 break
     if new_msg:
-        _LOG.debug(_color_string(
-                string='saving email from {} to {}'.format(
-                    person, assignment_path), color=good))
+        _LOG.log(_GOOD_DEBUG, 'saving email from {} to {}'.format(
+                person, assignment_path))
         if mbox is not None and not dry_run:
             mdmsg = _mailbox.MaildirMessage(msg)
             mdmsg.add_flag('S')
             mbox.add(mdmsg)
             mbox.close()
     else:
-        _LOG.debug(_color_string(
-                string='already found {} in {}'.format(
-                    msg['Message-ID'], mpath), color=good))
+        _LOG.log(_GOOD_DEBUG, 'already found {} in {}'.format(
+                    msg['Message-ID'], mpath))
 
-def _check_late(basedir, assignment, person, time, max_late=0, use_color=None,
-                dry_run=False):
-    highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
+def _check_late(basedir, assignment, person, time, max_late=0, dry_run=False):
     if time > assignment.due + max_late:
         dt = time - assignment.due
-        _LOG.warn(_color_string(
-                string='{} {} late by {} seconds ({} hours)'.format(
-                    person.name, assignment.name, dt, dt/3600.),
-                color=bad))
+        _LOG.warning('{} {} late by {} seconds ({} hours)'.format(
+            person.name, assignment.name, dt, dt/3600.))
         if not dry_run:
             _set_late(basedir=basedir, assignment=assignment, person=person)