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, use_color=False, debug_target=None,
113 """Send each student an email with their grade on `assignment`
116 emails=_assignment_email(
117 basedir=basedir, author=author, course=course,
118 assignment=assignment, student=student, cc=cc),
119 smtp=smtp, use_color=use_color,
120 debug_target=debug_target, dry_run=dry_run)
122 def _assignment_email(basedir, author, course, assignment, student=None,
124 """Iterate through composed assignment `Message`\s
129 students = course.people
130 for student in students:
132 grade = course.grade(student=student, assignment=assignment)
137 yield (construct_assignment_email(author=author, grade=grade, cc=cc),
138 NotifiedCallback(basedir=basedir, grades=[grade]))
140 def construct_assignment_email(author, grade, cc=None):
141 """Construct a `Message` notfiying a student of `grade`
143 >>> from pygrader.model.person import Person
144 >>> from pygrader.model.assignment import Assignment
145 >>> from pygrader.model.grade import Grade
146 >>> author = Person(name='Jack', emails=['a@b.net'])
147 >>> student = Person(name='Jill', emails=['c@d.net'])
148 >>> assignment = Assignment(name='Exam 1', points=3)
149 >>> grade = Grade(student=student, assignment=assignment, points=2)
150 >>> msg = construct_assignment_email(author=author, grade=grade)
151 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
152 Content-Type: text/plain; charset="us-ascii"
154 Content-Transfer-Encoding: 7bit
155 Content-Disposition: inline
158 Reply-to: Jack <a@b.net>
160 Subject: Your Exam 1 grade
164 You got 2 out of 3 available points on Exam 1.
169 >>> grade.comment = ('Some comment bla bla bla.').strip()
170 >>> msg = construct_assignment_email(author=author, grade=grade)
171 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
172 Content-Type: text/plain; charset="us-ascii"
174 Content-Transfer-Encoding: 7bit
175 Content-Disposition: inline
178 Reply-to: Jack <a@b.net>
180 Subject: Your Exam 1 grade
184 You got 2 out of 3 available points on Exam 1.
186 Some comment bla bla bla.
191 return _construct_text_email(
192 author=author, targets=[grade.student], cc=cc,
193 subject='Your {} grade'.format(grade.assignment.name),
194 text=ASSIGNMENT_TEMPLATE.render(author=author, grade=grade))
196 def student_email(basedir, author, course, student=None, cc=None, old=False,
197 smtp=None, use_color=False, debug_target=None,
199 """Send each student an email with their grade to date
202 emails=_student_email(
203 basedir=basedir, author=author, course=course, student=student,
205 smtp=smtp, use_color=use_color, debug_target=debug_target,
208 def _student_email(basedir, author, course, student=None, targets=None, cc=None, old=False):
209 """Iterate through composed student `Message`\s
214 students = course.people
215 for student in students:
216 grades = [g for g in course.grades if g.student == student]
218 grades = [g for g in grades if not g.notified]
221 yield (construct_student_email(
222 author=author, course=course, grades=grades, targets=targets,
224 NotifiedCallback(basedir=basedir, grades=grades))
226 def construct_student_email(author, course, grades, targets=None, 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.course import Course
232 >>> from pygrader.model.grade import Grade
233 >>> course = Course(name='Physics 101')
234 >>> author = Person(name='Jack', emails=['a@b.net'])
235 >>> student = Person(name='Jill', emails=['c@d.net'])
237 >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
238 ... assignment = Assignment(name=name, points=points)
240 ... student=student, assignment=assignment,
241 ... points=int(points/2.0))
242 ... grades.append(grade)
243 >>> msg = construct_student_email(
244 ... author=author, course=course, grades=grades)
245 >>> print(msg.as_string().replace('\\t', ' '))
246 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
247 Content-Type: text/plain; charset="us-ascii"
249 Content-Transfer-Encoding: 7bit
250 Content-Disposition: inline
253 Reply-to: Jack <a@b.net>
255 Subject: Physics 101 grades
260 * Exam 1: 5 out of 10 available points.
261 * Homework 1: 1 out of 3 available points.
268 >>> grades[0].comment = ('Bla bla bla. '*20).strip()
269 >>> grades[1].comment = ('Hello world')
270 >>> msg = construct_student_email(
271 ... author=author, course=course, grades=grades)
272 >>> print(msg.as_string().replace('\\t', ' '))
273 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
274 Content-Type: text/plain; charset="us-ascii"
276 Content-Transfer-Encoding: 7bit
277 Content-Disposition: inline
280 Reply-to: Jack <a@b.net>
282 Subject: Physics 101 grades
287 * Exam 1: 5 out of 10 available points.
288 * Homework 1: 1 out of 3 available points.
298 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.
303 >>> grades[0].comment = 'Work harder!'
304 >>> grades[1].comment = None
305 >>> msg = construct_student_email(
306 ... author=author, course=course, grades=grades)
307 >>> print(msg.as_string().replace('\\t', ' '))
308 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
309 Content-Type: text/plain; charset="us-ascii"
311 Content-Transfer-Encoding: 7bit
312 Content-Disposition: inline
315 Reply-to: Jack <a@b.net>
317 Subject: Physics 101 grades
322 * Exam 1: 5 out of 10 available points.
323 * Homework 1: 1 out of 3 available points.
334 You can also send the student grades to alternative targets:
336 >>> prof = Person(name='H.D.', emails=['hd@wall.net'])
337 >>> msg = construct_student_email(
338 ... author=author, course=course, grades=grades, targets=[prof])
339 >>> print(msg.as_string().replace('\\t', ' '))
340 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
341 Content-Type: text/plain; charset="us-ascii"
343 Content-Transfer-Encoding: 7bit
344 Content-Disposition: inline
347 Reply-to: Jack <a@b.net>
348 To: "H.D." <hd@wall.net>
349 Subject: Physics 101 grades for Jill
354 * Exam 1: 5 out of 10 available points.
355 * Homework 1: 1 out of 3 available points.
366 students = set(g.student for g in grades)
367 assert len(students) == 1, students
368 student = students.pop()
369 subject = '{} grades'.format(course.name)
373 subject += ' for {}'.format(student.name)
374 target = join_with_and([t.alias() for t in targets])
375 return _construct_text_email(
376 author=author, targets=targets, cc=cc, subject=subject,
377 text=STUDENT_TEMPLATE.render(
378 author=author, target=target, grades=sorted(grades)))
380 def course_email(basedir, author, course, targets, assignment=None,
381 student=None, cc=None, smtp=None, use_color=False,
382 debug_target=None, dry_run=False):
383 """Send the professor an email with all student grades to date
386 emails=_course_email(
387 basedir=basedir, author=author, course=course, targets=targets,
388 assignment=assignment, student=student, cc=cc),
389 smtp=smtp, use_color=use_color, debug_target=debug_target,
392 def _course_email(basedir, author, course, targets, assignment=None,
393 student=None, cc=None):
394 """Iterate through composed course `Message`\s
396 yield (construct_course_email(
397 author=author, course=course, targets=targets, cc=cc),
400 def construct_course_email(author, course, targets, cc=None):
401 """Construct a `Message` notfiying a professor of all grades to date
403 >>> from pygrader.model.person import Person
404 >>> from pygrader.model.assignment import Assignment
405 >>> from pygrader.model.grade import Grade
406 >>> from pygrader.model.course import Course
407 >>> author = Person(name='Jack', emails=['a@b.net'])
408 >>> student = Person(name='Jill', emails=['c@d.net'])
409 >>> prof = Person(name='H.D.', emails=['hd@wall.net'])
411 >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
412 ... assignment = Assignment(name=name, points=points, weight=0.5)
414 ... student=student, assignment=assignment,
415 ... points=int(points/2.0))
416 ... grades.append(grade)
417 >>> assignments = [g.assignment for g in grades]
419 ... assignments=assignments, people=[student], grades=grades)
420 >>> msg = construct_course_email(
421 ... author=author, course=course, targets=[prof])
422 >>> print(msg.as_string().replace('\\t', ' '))
423 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
424 Content-Type: text/plain; charset="us-ascii"
426 Content-Transfer-Encoding: 7bit
427 Content-Disposition: inline
430 Reply-to: Jack <a@b.net>
431 To: "H.D." <hd@wall.net>
432 Subject: Course grades
436 Here are the (tab delimited) course grades to date:
438 Student Exam 1 Homework 1 Total
441 Mean 5.00 1.00 0.416...
442 Std. Dev. 0.00 0.00 0.0
444 The available points (and weights) for each assignment are:
451 target = join_with_and([t.alias() for t in targets])
452 table = _io.StringIO()
453 _tabulate(course=course, statistics=True, stream=table, use_color=False)
454 return _construct_text_email(
455 author=author, targets=targets, cc=cc,
456 subject='Course grades',
457 text=COURSE_TEMPLATE.render(
458 author=author, course=course, target=target,
459 table=table.getvalue()))