1 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
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_text_email as _construct_text_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("""
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 }}
76 class NotifiedCallback (object):
77 """A callback for marking notifications with `_send_emails`
79 def __init__(self, basedir, grades):
80 self.basedir = basedir
83 def __call__(self, success):
85 for grade in self.grades:
86 _set_notified(basedir=self.basedir, grade=grade)
89 def join_with_and(strings):
90 """Join a list of strings.
92 >>> join_with_and(['a','b','c'])
94 >>> join_with_and(['a','b'])
96 >>> join_with_and(['a'])
100 for i,s in enumerate(strings[1:]):
105 if i == len(strings)-2:
110 def assignment_email(basedir, author, course, assignment, student=None,
111 cc=None, smtp=None, debug_target=None, dry_run=False):
112 """Send each student an email with their grade on `assignment`
115 emails=_assignment_email(
116 basedir=basedir, author=author, course=course,
117 assignment=assignment, student=student, cc=cc),
118 smtp=smtp, debug_target=debug_target, dry_run=dry_run)
120 def _assignment_email(basedir, author, course, assignment, student=None,
122 """Iterate through composed assignment `Message`\s
127 students = course.people
128 for student in students:
130 grade = course.grade(student=student, assignment=assignment)
135 yield (construct_assignment_email(author=author, grade=grade, cc=cc),
136 NotifiedCallback(basedir=basedir, grades=[grade]))
138 def construct_assignment_email(author, grade, cc=None):
139 """Construct a `Message` notfiying a student of `grade`
141 >>> from pygrader.model.person import Person
142 >>> from pygrader.model.assignment import Assignment
143 >>> from pygrader.model.grade import Grade
144 >>> author = Person(name='Jack', emails=['a@b.net'])
145 >>> student = Person(name='Jill', emails=['c@d.net'])
146 >>> assignment = Assignment(name='Exam 1', points=3)
147 >>> grade = Grade(student=student, assignment=assignment, points=2)
148 >>> msg = construct_assignment_email(author=author, grade=grade)
149 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
150 Content-Type: text/plain; charset="us-ascii"
152 Content-Transfer-Encoding: 7bit
153 Content-Disposition: inline
156 Reply-to: Jack <a@b.net>
158 Subject: Your Exam 1 grade
162 You got 2 out of 3 available points on Exam 1.
167 >>> grade.comment = ('Some comment bla bla bla.').strip()
168 >>> msg = construct_assignment_email(author=author, grade=grade)
169 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
170 Content-Type: text/plain; charset="us-ascii"
172 Content-Transfer-Encoding: 7bit
173 Content-Disposition: inline
176 Reply-to: Jack <a@b.net>
178 Subject: Your Exam 1 grade
182 You got 2 out of 3 available points on Exam 1.
184 Some comment bla bla bla.
189 return _construct_text_email(
190 author=author, targets=[grade.student], cc=cc,
191 subject='Your {} grade'.format(grade.assignment.name),
192 text=ASSIGNMENT_TEMPLATE.render(author=author, grade=grade))
194 def student_email(basedir, author, course, student=None, cc=None, old=False,
195 smtp=None, debug_target=None, dry_run=False):
196 """Send each student an email with their grade to date
199 emails=_student_email(
200 basedir=basedir, author=author, course=course, student=student,
202 smtp=smtp, debug_target=debug_target, dry_run=dry_run)
204 def _student_email(basedir, author, course, student=None, targets=None, cc=None, old=False):
205 """Iterate through composed student `Message`\s
210 students = course.people
211 for student in students:
212 grades = [g for g in course.grades if g.student == student]
214 grades = [g for g in grades if not g.notified]
217 yield (construct_student_email(
218 author=author, course=course, grades=grades, targets=targets,
220 NotifiedCallback(basedir=basedir, grades=grades))
222 def construct_student_email(author, course, grades, targets=None, cc=None):
223 """Construct a `Message` notfiying a student of `grade`
225 >>> from pygrader.model.person import Person
226 >>> from pygrader.model.assignment import Assignment
227 >>> from pygrader.model.course import Course
228 >>> from pygrader.model.grade import Grade
229 >>> course = Course(name='Physics 101')
230 >>> author = Person(name='Jack', emails=['a@b.net'])
231 >>> student = Person(name='Jill', emails=['c@d.net'])
233 >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
234 ... assignment = Assignment(name=name, points=points)
236 ... student=student, assignment=assignment,
237 ... points=int(points/2.0))
238 ... grades.append(grade)
239 >>> msg = construct_student_email(
240 ... author=author, course=course, grades=grades)
241 >>> print(msg.as_string().replace('\\t', ' '))
242 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
243 Content-Type: text/plain; charset="us-ascii"
245 Content-Transfer-Encoding: 7bit
246 Content-Disposition: inline
249 Reply-to: Jack <a@b.net>
251 Subject: Physics 101 grades
256 * Exam 1: 5 out of 10 available points.
257 * Homework 1: 1 out of 3 available points.
264 >>> grades[0].comment = ('Bla bla bla. '*20).strip()
265 >>> grades[1].comment = ('Hello world')
266 >>> msg = construct_student_email(
267 ... author=author, course=course, 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>
278 Subject: Physics 101 grades
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(
302 ... author=author, course=course, grades=grades)
303 >>> print(msg.as_string().replace('\\t', ' '))
304 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
305 Content-Type: text/plain; charset="us-ascii"
307 Content-Transfer-Encoding: 7bit
308 Content-Disposition: inline
311 Reply-to: Jack <a@b.net>
313 Subject: Physics 101 grades
318 * Exam 1: 5 out of 10 available points.
319 * Homework 1: 1 out of 3 available points.
330 You can also send the student grades to alternative targets:
332 >>> prof = Person(name='H.D.', emails=['hd@wall.net'])
333 >>> msg = construct_student_email(
334 ... author=author, course=course, grades=grades, targets=[prof])
335 >>> print(msg.as_string().replace('\\t', ' '))
336 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
337 Content-Type: text/plain; charset="us-ascii"
339 Content-Transfer-Encoding: 7bit
340 Content-Disposition: inline
343 Reply-to: Jack <a@b.net>
344 To: "H.D." <hd@wall.net>
345 Subject: Physics 101 grades for Jill
350 * Exam 1: 5 out of 10 available points.
351 * Homework 1: 1 out of 3 available points.
362 students = set(g.student for g in grades)
363 assert len(students) == 1, students
364 student = students.pop()
365 subject = '{} grades'.format(course.name)
369 subject += ' for {}'.format(student.name)
370 target = join_with_and([t.alias() for t in targets])
371 return _construct_text_email(
372 author=author, targets=targets, cc=cc, subject=subject,
373 text=STUDENT_TEMPLATE.render(
374 author=author, target=target, grades=sorted(grades)))
376 def course_email(basedir, author, course, targets, assignment=None,
377 student=None, cc=None, smtp=None, debug_target=None,
379 """Send the professor an email with all student grades to date
382 emails=_course_email(
383 basedir=basedir, author=author, course=course, targets=targets,
384 assignment=assignment, student=student, cc=cc),
385 smtp=smtp, debug_target=debug_target, dry_run=dry_run)
387 def _course_email(basedir, author, course, targets, assignment=None,
388 student=None, cc=None):
389 """Iterate through composed course `Message`\s
391 yield (construct_course_email(
392 author=author, course=course, targets=targets, cc=cc),
395 def construct_course_email(author, course, targets, cc=None):
396 """Construct a `Message` notfiying a professor of all grades to date
398 >>> from pygrader.model.person import Person
399 >>> from pygrader.model.assignment import Assignment
400 >>> from pygrader.model.grade import Grade
401 >>> from pygrader.model.course import Course
402 >>> author = Person(name='Jack', emails=['a@b.net'])
403 >>> student = Person(name='Jill', emails=['c@d.net'])
404 >>> prof = Person(name='H.D.', emails=['hd@wall.net'])
406 >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
407 ... assignment = Assignment(name=name, points=points, weight=0.5)
409 ... student=student, assignment=assignment,
410 ... points=int(points/2.0))
411 ... grades.append(grade)
412 >>> assignments = [g.assignment for g in grades]
414 ... assignments=assignments, people=[student], grades=grades)
415 >>> msg = construct_course_email(
416 ... author=author, course=course, targets=[prof])
417 >>> print(msg.as_string().replace('\\t', ' '))
418 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
419 Content-Type: text/plain; charset="us-ascii"
421 Content-Transfer-Encoding: 7bit
422 Content-Disposition: inline
425 Reply-to: Jack <a@b.net>
426 To: "H.D." <hd@wall.net>
427 Subject: Course grades
431 Here are the (tab delimited) course grades to date:
433 Student Exam 1 Homework 1 Total
436 Mean 5.00 1.00 0.416...
437 Std. Dev. 0.00 0.00 0.0
439 The available points (and weights) for each assignment are:
446 target = join_with_and([t.alias() for t in targets])
447 table = _io.StringIO()
448 _tabulate(course=course, statistics=True, stream=table, use_color=False)
449 return _construct_text_email(
450 author=author, targets=targets, cc=cc,
451 subject='Course grades',
452 text=COURSE_TEMPLATE.render(
453 author=author, course=course, target=target,
454 table=table.getvalue()))