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 -%}{% if grade.comment %}
51 {{ grade.assignment.name }}
54 {%- endif %}{% endfor %}
60 COURSE_TEMPLATE = Template("""
63 Here are the (tab delimited) course grades to date:
66 The available points (and weights) for each assignment are:
67 {%- for assignment in course.active_assignments() %}
68 * {{ assignment.name }}:\t{{ assignment.points }}\t{{ assignment.weight }}
78 class NotifiedCallback (object):
79 """A callback for marking notifications with `_send_emails`
81 def __init__(self, basedir, grades):
82 self.basedir = basedir
85 def __call__(self, success):
87 for grade in self.grades:
88 _set_notified(basedir=self.basedir, grade=grade)
91 def join_with_and(strings):
92 """Join a list of strings.
94 >>> join_with_and(['a','b','c'])
96 >>> join_with_and(['a','b'])
98 >>> join_with_and(['a'])
102 for i,s in enumerate(strings[1:]):
107 if i == len(strings)-2:
112 def assignment_email(basedir, author, course, assignment, student=None,
113 cc=None, smtp=None, use_color=False, debug_target=None,
115 """Send each student an email with their grade on `assignment`
118 emails=_assignment_email(
119 basedir=basedir, author=author, course=course,
120 assignment=assignment, student=student, cc=cc),
121 smtp=smtp, use_color=use_color,
122 debug_target=debug_target, dry_run=dry_run)
124 def _assignment_email(basedir, author, course, assignment, student=None,
126 """Iterate through composed assignment `Message`\s
131 students = course.people
132 for student in students:
134 grade = course.grade(student=student, assignment=assignment)
139 yield (construct_assignment_email(author=author, grade=grade, cc=cc),
140 NotifiedCallback(basedir=basedir, grades=[grade]))
142 def construct_assignment_email(author, grade, cc=None):
143 """Construct a `Message` notfiying a student of `grade`
145 >>> from pygrader.model.person import Person
146 >>> from pygrader.model.assignment import Assignment
147 >>> from pygrader.model.grade import Grade
148 >>> author = Person(name='Jack', emails=['a@b.net'])
149 >>> student = Person(name='Jill', emails=['c@d.net'])
150 >>> assignment = Assignment(name='Exam 1', points=3)
151 >>> grade = Grade(student=student, assignment=assignment, points=2)
152 >>> msg = construct_assignment_email(author=author, grade=grade)
153 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
154 Content-Type: text/plain; charset="us-ascii"
156 Content-Transfer-Encoding: 7bit
157 Content-Disposition: inline
160 Reply-to: Jack <a@b.net>
162 Subject: Your Exam 1 grade
166 You got 2 out of 3 available points on Exam 1.
171 >>> grade.comment = ('Some comment bla bla bla.').strip()
172 >>> msg = construct_assignment_email(author=author, grade=grade)
173 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
174 Content-Type: text/plain; charset="us-ascii"
176 Content-Transfer-Encoding: 7bit
177 Content-Disposition: inline
180 Reply-to: Jack <a@b.net>
182 Subject: Your Exam 1 grade
186 You got 2 out of 3 available points on Exam 1.
188 Some comment bla bla bla.
193 return _construct_email(
194 author=author, targets=[grade.student], cc=cc,
195 subject='Your {} grade'.format(grade.assignment.name),
196 text=ASSIGNMENT_TEMPLATE.render(author=author, grade=grade))
198 def student_email(basedir, author, course, student=None, cc=None, old=False,
199 smtp=None, use_color=False, debug_target=None,
201 """Send each student an email with their grade to date
204 emails=_student_email(
205 basedir=basedir, author=author, course=course, student=student,
207 smtp=smtp, use_color=use_color, debug_target=debug_target,
210 def _student_email(basedir, author, course, student=None, cc=None, old=False):
211 """Iterate through composed student `Message`\s
216 students = course.people
217 for student in students:
218 grades = [g for g in course.grades if g.student == student]
220 grades = [g for g in grades if not g.notified]
223 yield (construct_student_email(author=author, grades=grades, cc=cc),
224 NotifiedCallback(basedir=basedir, grades=grades))
226 def construct_student_email(author, grades, cc=None):
227 """Construct a `Message` notfiying a student of `grade`
229 >>> from pygrader.model.person import Person
230 >>> from pygrader.model.assignment import Assignment
231 >>> from pygrader.model.grade import Grade
232 >>> author = Person(name='Jack', emails=['a@b.net'])
233 >>> student = Person(name='Jill', emails=['c@d.net'])
235 >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
236 ... assignment = Assignment(name=name, points=points)
238 ... student=student, assignment=assignment,
239 ... points=int(points/2.0))
240 ... grades.append(grade)
241 >>> msg = construct_student_email(author=author, grades=grades)
242 >>> print(msg.as_string().replace('\\t', ' '))
243 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
244 Content-Type: text/plain; charset="us-ascii"
246 Content-Transfer-Encoding: 7bit
247 Content-Disposition: inline
250 Reply-to: Jack <a@b.net>
257 * Exam 1: 5 out of 10 available points.
258 * Homework 1: 1 out of 3 available points.
265 >>> grades[0].comment = ('Bla bla bla. '*20).strip()
266 >>> grades[1].comment = ('Hello world')
267 >>> msg = construct_student_email(author=author, grades=grades)
268 >>> print(msg.as_string().replace('\\t', ' '))
269 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
270 Content-Type: text/plain; charset="us-ascii"
272 Content-Transfer-Encoding: 7bit
273 Content-Disposition: inline
276 Reply-to: Jack <a@b.net>
283 * Exam 1: 5 out of 10 available points.
284 * Homework 1: 1 out of 3 available points.
294 Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla.
299 >>> grades[0].comment = 'Work harder!'
300 >>> grades[1].comment = None
301 >>> msg = construct_student_email(author=author, grades=grades)
302 >>> print(msg.as_string().replace('\\t', ' '))
303 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
304 Content-Type: text/plain; charset="us-ascii"
306 Content-Transfer-Encoding: 7bit
307 Content-Disposition: inline
310 Reply-to: Jack <a@b.net>
317 * Exam 1: 5 out of 10 available points.
318 * Homework 1: 1 out of 3 available points.
329 students = set(g.student for g in grades)
330 assert len(students) == 1, students
331 return _construct_email(
332 author=author, targets=[grades[0].student], cc=cc,
333 subject='Your grade',
334 text=STUDENT_TEMPLATE.render(author=author, grades=sorted(grades)))
336 def course_email(basedir, author, course, targets, assignment=None,
337 student=None, cc=None, smtp=None, use_color=False,
338 debug_target=None, dry_run=False):
339 """Send the professor an email with all student grades to date
342 emails=_course_email(
343 basedir=basedir, author=author, course=course, targets=targets,
344 assignment=assignment, student=student, cc=cc),
345 smtp=smtp, use_color=use_color, debug_target=debug_target,
348 def _course_email(basedir, author, course, targets, assignment=None,
349 student=None, cc=None):
350 """Iterate through composed course `Message`\s
352 yield (construct_course_email(
353 author=author, course=course, targets=targets, cc=cc),
356 def construct_course_email(author, course, targets, cc=None):
357 """Construct a `Message` notfiying a professor of all grades to date
359 >>> from pygrader.model.person import Person
360 >>> from pygrader.model.assignment import Assignment
361 >>> from pygrader.model.grade import Grade
362 >>> from pygrader.model.course import Course
363 >>> author = Person(name='Jack', emails=['a@b.net'])
364 >>> student = Person(name='Jill', emails=['c@d.net'])
365 >>> prof = Person(name='H.D.', emails=['hd@wall.net'])
367 >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
368 ... assignment = Assignment(name=name, points=points, weight=0.5)
370 ... student=student, assignment=assignment,
371 ... points=int(points/2.0))
372 ... grades.append(grade)
373 >>> assignments = [g.assignment for g in grades]
375 ... assignments=assignments, people=[student], grades=grades)
376 >>> msg = construct_course_email(
377 ... author=author, course=course, targets=[prof])
378 >>> print(msg.as_string().replace('\\t', ' '))
379 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
380 Content-Type: text/plain; charset="us-ascii"
382 Content-Transfer-Encoding: 7bit
383 Content-Disposition: inline
386 Reply-to: Jack <a@b.net>
387 To: "H.D." <hd@wall.net>
388 Subject: Course grades
392 Here are the (tab delimited) course grades to date:
394 Student Exam 1 Homework 1 Total
397 Mean 5.00 1.00 0.416...
398 Std. Dev. 0.00 0.00 0.0
400 The available points (and weights) for each assignment are:
407 target = join_with_and([t.alias() for t in targets])
408 table = _io.StringIO()
409 _tabulate(course=course, statistics=True, stream=table)
410 return _construct_email(
411 author=author, targets=targets, cc=cc,
412 subject='Course grades',
413 text=COURSE_TEMPLATE.render(
414 author=author, course=course, target=target,
415 table=table.getvalue()))