mailpipe|handler: centralize student/course extraction from subjects.
authorW. Trevor King <wking@tremily.us>
Sun, 2 Sep 2012 17:17:06 +0000 (13:17 -0400)
committerW. Trevor King <wking@tremily.us>
Sun, 2 Sep 2012 18:27:42 +0000 (14:27 -0400)
pygrader/handler/__init__.py
pygrader/handler/get.py
pygrader/handler/submission.py
pygrader/mailpipe.py

index 158aef5fadaa9cb64d22b1718948b643f12e33f8..d813813b2fb075677208b456cd285ca6b7031208 100644 (file)
@@ -47,6 +47,28 @@ class InvalidSubjectMessage (InvalidMessage):
         self.subject = subject
 
 
+class InvalidStudentSubject (InvalidSubjectMessage):
+    def __init__(self, students=None, **kwargs):
+        if 'error' not in kwargs:
+            if students:
+                kwargs['error'] = 'Subject matches multiple students'
+            else:
+                kwargs['error'] = "Subject doesn't match any student"
+        super(InvalidStudentSubject, self).__init__(**kwargs)
+        self.students = students
+
+
+class InvalidAssignmentSubject (InvalidSubjectMessage):
+    def __init__(self, assignments=None, **kwargs):
+        if 'error' not in kwargs:
+            if assignments:
+                kwargs['error'] = 'Subject matches multiple assignments'
+            else:
+                kwargs['error'] = "Subject doesn't match any assignment"
+        super(InvalidAssignmentSubject, self).__init__(kwargs)
+        self.assignments = assignments
+
+
 class Response (Exception):
     """Exception to bubble out email responses.
 
@@ -58,3 +80,20 @@ class Response (Exception):
         super(Response, self).__init__()
         self.message = message
         self.complete = complete
+
+
+def get_subject_student(course, subject):
+    lsubject = subject.lower()
+    students = [p for p in course.find_people()
+                if p.name.lower() in lsubject]
+    if len(students) == 1:
+        return students[0]
+    raise InvalidStudentSubject(students=students, subject=subject)
+
+def get_subject_assignment(course, subject):
+    lsubject = subject.lower()
+    assignments = [a for a in course.assignments
+                   if a.name.lower() in lsubject]
+    if len(assignments) == 1:
+        return assignments[0]
+    raise InvalidAssignmentSubject(assignments=assignments, subject=subject)
index 2aa7d179200514f391627fc117097e6330d88355..a94310f5249df1ffd8680b81026e7b8630f2fbd4 100644 (file)
@@ -20,20 +20,14 @@ from ..extract_mime import message_time as _message_time
 from ..storage import assignment_path as _assignment_path
 from ..tabulate import tabulate as _tabulate
 from ..template import _student_email as _student_email
-from . import InvalidMessage as _InvalidMessage
-from . import InvalidSubjectMessage as _InvalidSubjectMessage
+from . import get_subject_assignment as _get_subject_assignment
+from . import get_subject_student as _get_subject_student
+from . import InvalidStudentSubject as _InvalidStudentSubject
+from . import InvalidAssignmentSubject as _InvalidAssignmentSubject
 from . import Response as _Response
 from . import UnsignedMessage as _UnsignedMessage
 
 
-class InvalidStudent (_InvalidSubjectMessage):
-    def __init__(self, students=None, **kwargs):
-        if 'error' not in kwargs:
-            kwargs['error'] = 'Subject matches multiple students'
-        super(InvalidStudent, self).__init__(kwargs)
-        self.students = students
-
-
 def run(basedir, course, message, person, subject,
         trust_email_infrastructure=False, dry_run=False, **kwargs):
     """
@@ -446,10 +440,12 @@ def _get_student_submission_email(
         message=message)
 
 def _get_admin_email(basedir, course, person, subject):
-    lsubject = subject.lower()
-    students = [p for p in course.find_people()
-                if p.name.lower() in lsubject]
-    if len(students) == 0:
+    try:
+        student = _get_subject_student(course, subject)
+    except _InvalidStudentSubject as error:
+        if error.students:  # several students
+            raise
+        # no students
         _LOG.debug('construct course grades email for {}'.format(person))
         stream = _io.StringIO()
         _tabulate(
@@ -459,17 +455,20 @@ def _get_admin_email(basedir, course, person, subject):
             author=course.robot, targets=[person],
             subject='All grades for {}'.format(course.name),
             text=text)
-    elif len(students) == 1:
-        student = students[0]
-        assignments = [a for a in course.assignments
-                       if a.name.lower() in lsubject]
-        if len(assignments) == 0:
-            email = _get_student_email(
-                basedir=basedir, course=course, person=person, student=student)
-        else:
+    else:  # a single student
+        try:
+            assignment = _get_subject_assignment(course, subject)
+        except _InvalidAssignmentSubject as error:
+            if error.assignments:  # several assignments
+                email = _get_student_submission_email(
+                    basedir=basedir, course=course, person=person,
+                    student=student, assignments=error.assignments)
+            else: # no assignments
+                email = _get_student_email(
+                    basedir=basedir, course=course, person=person,
+                    student=student)
+        else:  # a single assignment
             email = _get_student_submission_email(
                 basedir=basedir, course=course, person=person, student=student,
-                assignments=assignments)
-    else:
-        raise InvalidStudent(students=students)
+                assignments=[assignment])
     return email
index 920a3f884e0c01e3a0c6d115c13dd0127bb23d88..f7bf78d4e854c5875b75e0b0c381f101c8554bff 100644 (file)
@@ -19,16 +19,16 @@ 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 get_subject_assignment as _get_subject_assignment
 from . import InvalidMessage as _InvalidMessage
 from . import Response as _Response
 
 
-class InvalidAssignment (_InvalidMessage):
-    def __init__(self, assignment, **kwargs):
+class InvalidSubmission (_InvalidMessage):
+    def __init__(self, assignment=None, **kwargs):
         if 'error' not in kwargs:
-            kwargs['error'] = 'Received invalid {} submission'.format(
-                assignment.name)
-        super(InvalidAssignment, self).__init__(**kwargs)
+            kwargs['error'] = 'invalid submission'
+        super(InvalidSubmission, self).__init__(**kwargs)
         self.assignment = assignment
 
 
@@ -87,51 +87,11 @@ def run(basedir, course, message, person, subject, max_late=0, dry_run=None,
     message['Subject'] = 'Received {} submission'.format(assignment.name)
     raise _Response(message=message)
 
-def _match_assignment(assignment, subject):
-    return assignment.name.lower() in subject.lower()
-
 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]
-
+    assignment = _get_subject_assignment(course, subject)
     if not assignment.submittable:
-        raise InvalidAssignment(assignment)
-    return assignments[0]
+        raise InvalidSubmission(assignment=assignment)
+    return assignment
 
 def _save_local_message_copy(msg, person, assignment_path, dry_run=False):
     try:
index 7f3902b40a17060ccb67f1db4f0a5dc946100aa9..ea5a495376a5bcff35c75b6d01cf8d2afc129dd7 100644 (file)
@@ -34,15 +34,16 @@ from .email import construct_response as _construct_response
 from .extract_mime import message_time as _message_time
 from .model.person import Person as _Person
 
+from .handler import InsecureMessage as _InsecureMessage
+from .handler import InvalidAssignmentSubject as _InvalidAssignmentSubject
 from .handler import InvalidMessage as _InvalidMessage
+from .handler import InvalidStudentSubject as _InvalidStudentSubject
 from .handler import InvalidSubjectMessage as _InvalidSubjectMessage
 from .handler import Response as _Response
 from .handler import UnsignedMessage as _UnsignedMessage
-from .handler import InsecureMessage as _InsecureMessage
-from .handler.get import InvalidStudent as _InvalidStudent
 from .handler.get import run as _handle_get
-from .handler.submission import InvalidAssignment as _InvalidAssignment
 from .handler.submission import run as _handle_submission
+from .handler.submission import InvalidSubmission as _InvalidSubmission
 
 
 _TAG_REGEXP = _re.compile('^.*\[([^]]*)\].*$')
@@ -240,6 +241,16 @@ def mailpipe(basedir, course, stream=None, mailbox=None, input_=None,
     >>> message['Return-Path'] = '<bb@greyhavens.net>'
     >>> process(message)  # doctest: +REPORT_UDIFF, +ELLIPSIS
     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: text/plain; charset="us-ascii"
     MIME-Version: 1.0
     Content-Disposition: inline
@@ -252,6 +263,19 @@ def mailpipe(basedir, course, stream=None, mailbox=None, input_=None,
     Yours,
     phys-101 robot
     <BLANKLINE>
+    --===============...==
+    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.print_tree()  # doctest: +REPORT_UDIFF, +ELLIPSIS
     Bilbo_Baggins
@@ -283,6 +307,16 @@ def mailpipe(basedir, course, stream=None, mailbox=None, input_=None,
     ...     'for <wking@tremily.us>; Mon, 09 Oct 2011 11:50:46 -0400 (EDT)')
     >>> process(message)  # doctest: +REPORT_UDIFF, +ELLIPSIS
     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: text/plain; charset="us-ascii"
     MIME-Version: 1.0
     Content-Disposition: inline
@@ -295,6 +329,20 @@ def mailpipe(basedir, course, stream=None, mailbox=None, input_=None,
     Yours,
     phys-101 robot
     <BLANKLINE>
+    --===============...==
+    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.print_tree()  # doctest: +REPORT_UDIFF, +ELLIPSIS
     Bilbo_Baggins
     Bilbo_Baggins/Assignment_1
@@ -326,6 +374,16 @@ def mailpipe(basedir, course, stream=None, mailbox=None, input_=None,
     >>> message['Message-ID'] = '<hgi.jlk@home.net>'
     >>> process(message)  # doctest: +REPORT_UDIFF, +ELLIPSIS
     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: text/plain; charset="us-ascii"
     MIME-Version: 1.0
     Content-Disposition: inline
@@ -338,6 +396,19 @@ def mailpipe(basedir, course, stream=None, mailbox=None, input_=None,
     Yours,
     phys-101 robot
     <BLANKLINE>
+    --===============...==
+    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>
+    --===============...==--
 
     Response to a submission on an unsubmittable assignment:
 
@@ -896,7 +967,14 @@ def _get_error_response(error):
     author = error.course.robot
     target = getattr(error, 'person', None)
     subject = str(error)
-    if isinstance(error, InvalidHandlerMessage):
+    if isinstance(error, _InvalidSubmission):
+        subject = 'Received invalid {} submission'.format(
+            error.assignment.name)
+        text = (
+            'We received your submission for {}, but you are not\n'
+            'allowed to submit that assignment via email.'
+            ).format(error.assignment.name)
+    elif isinstance(error, InvalidHandlerMessage):
         targets = sorted(error.handlers.keys())
         if not targets:
             hint = (
@@ -927,6 +1005,31 @@ def _get_error_response(error):
             'or TA.').format(error.course.name)
     elif isinstance(error, NoReturnPath):
         return
+    elif isinstance(error, _InvalidAssignmentSubject):
+        if error.assignments:
+            hint = (
+                'but it matches several assignments:\n'
+                '  * {}').format('\n  * '.join(
+                    a.name for a in error.assignments))
+        else:
+            # prefer a submittable example assignment
+            assignments = [
+                a for a in error.course.assignments if a.submittable]
+            assignments += course.assignments  # but fall back to any one
+            hint = (
+                'Remember to use the full name for the assignment in the\n'
+                'subject.  For example:\n'
+                '  {} submission').format(assignments[0].name)
+        text = (
+            'We got an email from you with the following subject:\n'
+            '  {!r}\n{}').format(error.subject, hint)
+    elif isinstance(error, _InvalidStudentSubject):
+        text = (
+            'We got an email from you with the following subject:\n'
+            '  {!r}\n'
+            'but it matches several students:\n'
+            '  * {}').format(
+            error.subject, '\n  * '.join(s.name for s in error.students))
     elif isinstance(error, _InvalidSubjectMessage):
         text = (
             'We received an email message from you with an invalid\n'
@@ -936,18 +1039,6 @@ def _get_error_response(error):
         text = (
             'We received an email message from you without a valid\n'
             'PGP signature.')
-    elif isinstance(error, _InvalidAssignment):
-        text = (
-            'We received your submission for {}, but you are not\n'
-            'allowed to submit that assignment via email.'
-            ).format(error.assignment.name)
-    elif isinstance(error, _InvalidStudent):
-        text = (
-            'We got an email from you with the following subject:\n'
-            '  {!r}\n'
-            'but it matches several students:\n'
-            '  * {}').format(
-            error.subject, '\n  * '.join(s.name for s in error.students))
     elif isinstance(error, _InvalidMessage):
         text = subject
     else: