1 # Copyright (C) 2012 W. Trevor King <wking@drexel.edu>
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/>.
19 from jinja2 import Template
21 from . import LOG as _LOG
22 from .email import construct_email as _construct_email
23 from .email import send_emails as _send_emails
24 from .storage import set_notified as _set_notified
25 from .tabulate import tabulate as _tabulate
28 ASSIGNMENT_TEMPLATE = Template("""
29 {{ grade.student.alias() }},
31 You got {{ grade.points }} out of {{ grade.assignment.points }} available points on {{ grade.assignment.name }}.
32 {% if grade.comment %}
38 #{{ grade.comment|wordwrap }}
40 STUDENT_TEMPLATE = Template("""
41 {{ grades[0].student.alias() }},
44 {%- for grade in grades %}
45 * {{ grade.assignment.name }}:\t{{ grade.points }} out of {{ grade.assignment.points }} available points.
49 {%- for grade in grades -%}
50 {% if grade.comment %}
52 {{ grade.assignment.name }}
61 COURSE_TEMPLATE = Template("""
64 Here are the (tab delimited) course grades to date:
67 The available points (and weights) for each assignment are:
68 {%- for assignment in course.active_assignments() %}
69 * {{ assignment.name }}:\t{{ assignment.points }}\t{{ assignment.weight }}
79 class NotifiedCallback (object):
80 """A callback for marking notifications with `_send_emails`
82 def __init__(self, basedir, grades):
83 self.basedir = basedir
86 def __call__(self, success):
88 for grade in self.grades:
89 _set_notified(basedir=self.basedir, grade=grade)
92 def join_with_and(strings):
93 """Join a list of strings.
95 >>> join_with_and(['a','b','c'])
97 >>> join_with_and(['a','b'])
99 >>> join_with_and(['a'])
103 for i,s in enumerate(strings[1:]):
108 if i == len(strings)-2:
113 def assignment_email(basedir, author, course, assignment, student=None,
114 cc=None, smtp=None, use_color=False, debug_target=None,
116 """Send each student an email with their grade on `assignment`
119 emails=_assignment_email(
120 basedir=basedir, author=author, course=course,
121 assignment=assignment, student=student, cc=cc),
122 smtp=smtp, use_color=use_color,
123 debug_target=debug_target, dry_run=dry_run)
125 def _assignment_email(basedir, author, course, assignment, student=None,
127 """Iterate through composed assignment `Message`\s
132 students = course.people
133 for student in students:
135 grade = course.grade(student=student, assignment=assignment)
140 yield (construct_assignment_email(author=author, grade=grade, cc=cc),
141 NotifiedCallback(basedir=basedir, grades=[grade]))
143 def construct_assignment_email(author, grade, cc=None):
144 """Construct a `Message` notfiying a student of `grade`
146 >>> from pygrader.model.person import Person
147 >>> from pygrader.model.assignment import Assignment
148 >>> from pygrader.model.grade import Grade
149 >>> author = Person(name='Jack', emails=['a@b.net'])
150 >>> student = Person(name='Jill', emails=['c@d.net'])
151 >>> assignment = Assignment(name='Exam 1', points=3)
152 >>> grade = Grade(student=student, assignment=assignment, points=2)
153 >>> msg = construct_assignment_email(author=author, grade=grade)
154 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
155 Content-Type: text/plain; charset="us-ascii"
157 Content-Transfer-Encoding: 7bit
158 Content-Disposition: inline
161 Reply-to: Jack <a@b.net>
163 Subject: Your Exam 1 grade
167 You got 2 out of 3 available points on Exam 1.
172 >>> grade.comment = ('Some comment bla bla bla.').strip()
173 >>> msg = construct_assignment_email(author=author, grade=grade)
174 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
175 Content-Type: text/plain; charset="us-ascii"
177 Content-Transfer-Encoding: 7bit
178 Content-Disposition: inline
181 Reply-to: Jack <a@b.net>
183 Subject: Your Exam 1 grade
187 You got 2 out of 3 available points on Exam 1.
189 Some comment bla bla bla.
194 return _construct_email(
195 author=author, targets=[grade.student], cc=cc,
196 subject='Your {} grade'.format(grade.assignment.name),
197 text=ASSIGNMENT_TEMPLATE.render(author=author, grade=grade))
199 def student_email(basedir, author, course, student=None, cc=None, old=False,
200 smtp=None, use_color=False, debug_target=None,
202 """Send each student an email with their grade to date
205 emails=_student_email(
206 basedir=basedir, author=author, course=course, student=student,
208 smtp=smtp, use_color=use_color, debug_target=debug_target,
211 def _student_email(basedir, author, course, student=None, cc=None, old=False):
212 """Iterate through composed student `Message`\s
217 students = course.people
218 for student in students:
219 grades = [g for g in course.grades if g.student == student]
221 grades = [g for g in grades if not g.notified]
224 yield (construct_student_email(author=author, grades=grades, cc=cc),
225 NotifiedCallback(basedir=basedir, grades=grades))
227 def construct_student_email(author, grades, cc=None):
228 """Construct a `Message` notfiying a student of `grade`
230 >>> from pygrader.model.person import Person
231 >>> from pygrader.model.assignment import Assignment
232 >>> from pygrader.model.grade import Grade
233 >>> author = Person(name='Jack', emails=['a@b.net'])
234 >>> student = Person(name='Jill', emails=['c@d.net'])
236 >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
237 ... assignment = Assignment(name=name, points=points)
239 ... student=student, assignment=assignment,
240 ... points=int(points/2.0))
241 ... grades.append(grade)
242 >>> msg = construct_student_email(author=author, grades=grades)
243 >>> print(msg.as_string())
244 ... # doctest: +REPORT_UDIFF, +ELLIPSIS, +NORMALIZE_WHITESPACE
245 Content-Type: text/plain; charset="us-ascii"
247 Content-Transfer-Encoding: 7bit
248 Content-Disposition: inline
251 Reply-to: Jack <a@b.net>
258 * Exam 1:\t5 out of 10 available points.
259 * Homework 1:\t1 out of 3 available points.
267 >>> grades[0].comment = ('Bla bla bla. '*20).strip()
268 >>> grades[1].comment = ('Hello world')
269 >>> msg = construct_student_email(author=author, grades=grades)
270 >>> print(msg.as_string())
271 ... # doctest: +REPORT_UDIFF, +ELLIPSIS, +NORMALIZE_WHITESPACE
272 Content-Type: text/plain; charset="us-ascii"
274 Content-Transfer-Encoding: 7bit
275 Content-Disposition: inline
278 Reply-to: Jack <a@b.net>
285 * Exam 1:\t5 out of 10 available points.
286 * Homework 1:\t1 out of 3 available points.
298 Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla
299 bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla
300 bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla.
301 Bla bla bla. Bla bla bla. Bla bla bla.
307 students = set(g.student for g in grades)
308 assert len(students) == 1, students
309 return _construct_email(
310 author=author, targets=[grades[0].student], cc=cc,
311 subject='Your grade',
312 text=STUDENT_TEMPLATE.render(author=author, grades=sorted(grades)))
314 def course_email(basedir, author, course, targets, assignment=None,
315 student=None, cc=None, smtp=None, use_color=False,
316 debug_target=None, dry_run=False):
317 """Send the professor an email with all student grades to date
320 emails=_course_email(
321 basedir=basedir, author=author, course=course, targets=targets,
322 assignment=assignment, student=student, cc=cc),
323 smtp=smtp, use_color=use_color, debug_target=debug_target,
326 def _course_email(basedir, author, course, targets, assignment=None,
327 student=None, cc=None):
328 """Iterate through composed course `Message`\s
330 yield (construct_course_email(
331 author=author, course=course, targets=targets, cc=cc),
334 def construct_course_email(author, course, targets, cc=None):
335 """Construct a `Message` notfiying a professor of all grades to date
337 >>> from pygrader.model.person import Person
338 >>> from pygrader.model.assignment import Assignment
339 >>> from pygrader.model.grade import Grade
340 >>> from pygrader.model.course import Course
341 >>> author = Person(name='Jack', emails=['a@b.net'])
342 >>> student = Person(name='Jill', emails=['c@d.net'])
343 >>> prof = Person(name='H.D.', emails=['hd@wall.net'])
345 >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
346 ... assignment = Assignment(name=name, points=points, weight=0.5)
348 ... student=student, assignment=assignment,
349 ... points=int(points/2.0))
350 ... grades.append(grade)
351 >>> assignments = [g.assignment for g in grades]
353 ... assignments=assignments, people=[student], grades=grades)
354 >>> msg = construct_course_email(
355 ... author=author, course=course, targets=[prof])
356 >>> print(msg.as_string())
357 ... # doctest: +REPORT_UDIFF, +ELLIPSIS, +NORMALIZE_WHITESPACE
358 Content-Type: text/plain; charset="us-ascii"
360 Content-Transfer-Encoding: 7bit
361 Content-Disposition: inline
364 Reply-to: Jack <a@b.net>
365 To: "H.D." <hd@wall.net>
366 Subject: Course grades
370 Here are the (tab delimited) course grades to date:
372 Student\tExam 1\tHomework 1\tTotal
375 Mean\t5.00\t1.00\t0.416...
376 Std. Dev.\t0.00\t0.00\t0.0
378 The available points (and weights) for each assignment are:
380 * Homework 1:\t3\t0.5
385 target = join_with_and([t.alias() for t in targets])
386 table = _io.StringIO()
387 _tabulate(course=course, statistics=True, stream=table)
388 return _construct_email(
389 author=author, targets=targets, cc=cc,
390 subject='Course grades',
391 text=COURSE_TEMPLATE.render(
392 author=author, course=course, target=target,
393 table=table.getvalue()))