From b8b2cb2bcaef9519ca84107f74630059e17e8248 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 2 Sep 2012 13:17:06 -0400 Subject: [PATCH] mailpipe|handler: centralize student/course extraction from subjects. --- pygrader/handler/__init__.py | 39 +++++++++++ pygrader/handler/get.py | 49 +++++++------ pygrader/handler/submission.py | 56 +++------------ pygrader/mailpipe.py | 123 ++++++++++++++++++++++++++++----- 4 files changed, 178 insertions(+), 89 deletions(-) diff --git a/pygrader/handler/__init__.py b/pygrader/handler/__init__.py index 158aef5..d813813 100644 --- a/pygrader/handler/__init__.py +++ b/pygrader/handler/__init__.py @@ -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) diff --git a/pygrader/handler/get.py b/pygrader/handler/get.py index 2aa7d17..a94310f 100644 --- a/pygrader/handler/get.py +++ b/pygrader/handler/get.py @@ -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 diff --git a/pygrader/handler/submission.py b/pygrader/handler/submission.py index 920a3f8..f7bf78d 100644 --- a/pygrader/handler/submission.py +++ b/pygrader/handler/submission.py @@ -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: diff --git a/pygrader/mailpipe.py b/pygrader/mailpipe.py index 7f3902b..ea5a495 100644 --- a/pygrader/mailpipe.py +++ b/pygrader/mailpipe.py @@ -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'] = '' >>> 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 + Reply-to: Robot101 + To: Bilbo Baggins + Subject: Received Assignment 1 submission + + --===============...== 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 + --===============...== + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Description: OpenPGP digital signature + Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii" + + -----BEGIN PGP SIGNATURE----- + Version: GnuPG v2.0.19 (GNU/Linux) + + ... + -----END PGP SIGNATURE----- + + --===============...==-- >>> course.print_tree() # doctest: +REPORT_UDIFF, +ELLIPSIS Bilbo_Baggins @@ -283,6 +307,16 @@ def mailpipe(basedir, course, stream=None, mailbox=None, input_=None, ... 'for ; 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 + Reply-to: Robot101 + To: Bilbo Baggins + Subject: Received Assignment 1 submission + + --===============...== 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 + --===============...== + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Description: OpenPGP digital signature + Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii" + + -----BEGIN PGP SIGNATURE----- + Version: GnuPG v2.0.19 (GNU/Linux) + + ... + -----END PGP SIGNATURE----- + + --===============...==-- + >>> 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'] = '' >>> 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 + Reply-to: Robot101 + To: Bilbo Baggins + Subject: Received Assignment 1 submission + + --===============...== 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 + --===============...== + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Description: OpenPGP digital signature + Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii" + + -----BEGIN PGP SIGNATURE----- + Version: GnuPG v2.0.19 (GNU/Linux) + + ... + -----END PGP SIGNATURE----- + + --===============...==-- 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: -- 2.26.2