5 from jinja2 import Template
7 from . import LOG as _LOG
8 from .email import construct_email as _construct_email
9 from .email import send_emails as _send_emails
10 from .storage import set_notified as _set_notified
11 from .tabulate import tabulate as _tabulate
14 ASSIGNMENT_TEMPLATE = Template("""
15 {{ grade.student.alias() }},
17 You got {{ grade.points }} out of {{ grade.assignment.points }} available points on {{ grade.assignment.name }}.
18 {% if grade.comment %}
24 #{{ grade.comment|wordwrap }}
26 STUDENT_TEMPLATE = Template("""
27 {{ grades[0].student.alias() }},
30 {%- for grade in grades %}
31 * {{ grade.assignment.name }}:\t{{ grade.points }} out of {{ grade.assignment.points }} available points.
35 {%- for grade in grades -%}
36 {% if grade.comment %}
38 {{ grade.assignment.name }}
47 COURSE_TEMPLATE = Template("""
50 Here are the (tab delimited) course grades to date:
53 The available points (and weights) for each assignment are:
54 {%- for assignment in course.active_assignments() %}
55 * {{ assignment.name }}:\t{{ assignment.points }}\t{{ assignment.weight }}
65 class NotifiedCallback (object):
66 """A callback for marking notifications with `_send_emails`
68 def __init__(self, basedir, grades):
69 self.basedir = basedir
72 def __call__(self, success):
74 for grade in self.grades:
75 _set_notified(basedir=self.basedir, grade=grade)
78 def join_with_and(strings):
79 """Join a list of strings.
81 >>> join_with_and(['a','b','c'])
83 >>> join_with_and(['a','b'])
85 >>> join_with_and(['a'])
89 for i,s in enumerate(strings[1:]):
94 if i == len(strings)-2:
99 def assignment_email(basedir, author, course, assignment, student=None,
100 cc=None, smtp=None, use_color=False, debug_target=None,
102 """Send each student an email with their grade on `assignment`
105 emails=_assignment_email(
106 basedir=basedir, author=author, course=course,
107 assignment=assignment, student=student, cc=cc),
108 smtp=smtp, use_color=use_color,
109 debug_target=debug_target, dry_run=dry_run)
111 def _assignment_email(basedir, author, course, assignment, student=None,
113 """Iterate through composed assignment `Message`\s
118 students = course.people
119 for student in students:
121 grade = course.grade(student=student, assignment=assignment)
126 yield (construct_assignment_email(author=author, grade=grade, cc=cc),
127 NotifiedCallback(basedir=basedir, grades=[grade]))
129 def construct_assignment_email(author, grade, cc=None):
130 """Construct a `Message` notfiying a student of `grade`
132 >>> from pygrader.model.person import Person
133 >>> from pygrader.model.assignment import Assignment
134 >>> from pygrader.model.grade import Grade
135 >>> author = Person(name='Jack', emails=['a@b.net'])
136 >>> student = Person(name='Jill', emails=['c@d.net'])
137 >>> assignment = Assignment(name='Exam 1', points=3)
138 >>> grade = Grade(student=student, assignment=assignment, points=2)
139 >>> msg = construct_assignment_email(author=author, grade=grade)
140 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
141 Content-Type: text/plain; charset="us-ascii"
143 Content-Transfer-Encoding: 7bit
144 Content-Disposition: inline
147 Reply-to: Jack <a@b.net>
149 Subject: Your Exam 1 grade
153 You got 2 out of 3 available points on Exam 1.
158 >>> grade.comment = ('Some comment bla bla bla.').strip()
159 >>> msg = construct_assignment_email(author=author, grade=grade)
160 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
161 Content-Type: text/plain; charset="us-ascii"
163 Content-Transfer-Encoding: 7bit
164 Content-Disposition: inline
167 Reply-to: Jack <a@b.net>
169 Subject: Your Exam 1 grade
173 You got 2 out of 3 available points on Exam 1.
175 Some comment bla bla bla.
180 return _construct_email(
181 author=author, targets=[grade.student], cc=cc,
182 subject='Your {} grade'.format(grade.assignment.name),
183 text=ASSIGNMENT_TEMPLATE.render(author=author, grade=grade))
185 def student_email(basedir, author, course, student=None, cc=None, old=False,
186 smtp=None, use_color=False, debug_target=None,
188 """Send each student an email with their grade to date
191 emails=_student_email(
192 basedir=basedir, author=author, course=course, student=student,
194 smtp=smtp, use_color=use_color, debug_target=debug_target,
197 def _student_email(basedir, author, course, student=None, cc=None, old=False):
198 """Iterate through composed student `Message`\s
203 students = course.people
204 for student in students:
205 grades = [g for g in course.grades if g.student == student]
207 grades = [g for g in grades if not g.notified]
210 yield (construct_student_email(author=author, grades=grades, cc=cc),
211 NotifiedCallback(basedir=basedir, grades=grades))
213 def construct_student_email(author, grades, cc=None):
214 """Construct a `Message` notfiying a student of `grade`
216 >>> from pygrader.model.person import Person
217 >>> from pygrader.model.assignment import Assignment
218 >>> from pygrader.model.grade import Grade
219 >>> author = Person(name='Jack', emails=['a@b.net'])
220 >>> student = Person(name='Jill', emails=['c@d.net'])
222 >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
223 ... assignment = Assignment(name=name, points=points)
225 ... student=student, assignment=assignment,
226 ... points=int(points/2.0))
227 ... grades.append(grade)
228 >>> msg = construct_student_email(author=author, grades=grades)
229 >>> print(msg.as_string())
230 ... # doctest: +REPORT_UDIFF, +ELLIPSIS, +NORMALIZE_WHITESPACE
231 Content-Type: text/plain; charset="us-ascii"
233 Content-Transfer-Encoding: 7bit
234 Content-Disposition: inline
237 Reply-to: Jack <a@b.net>
244 * Exam 1:\t5 out of 10 available points.
245 * Homework 1:\t1 out of 3 available points.
253 >>> grades[0].comment = ('Bla bla bla. '*20).strip()
254 >>> grades[1].comment = ('Hello world')
255 >>> msg = construct_student_email(author=author, grades=grades)
256 >>> print(msg.as_string())
257 ... # doctest: +REPORT_UDIFF, +ELLIPSIS, +NORMALIZE_WHITESPACE
258 Content-Type: text/plain; charset="us-ascii"
260 Content-Transfer-Encoding: 7bit
261 Content-Disposition: inline
264 Reply-to: Jack <a@b.net>
271 * Exam 1:\t5 out of 10 available points.
272 * Homework 1:\t1 out of 3 available points.
284 Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla
285 bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla
286 bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla. Bla bla bla.
287 Bla bla bla. Bla bla bla. Bla bla bla.
293 students = set(g.student for g in grades)
294 assert len(students) == 1, students
295 return _construct_email(
296 author=author, targets=[grades[0].student], cc=cc,
297 subject='Your grade',
298 text=STUDENT_TEMPLATE.render(author=author, grades=sorted(grades)))
300 def course_email(basedir, author, course, targets, assignment=None,
301 student=None, cc=None, smtp=None, use_color=False,
302 debug_target=None, dry_run=False):
303 """Send the professor an email with all student grades to date
306 emails=_course_email(
307 basedir=basedir, author=author, course=course, targets=targets,
308 assignment=assignment, student=student, cc=cc),
309 smtp=smtp, use_color=use_color, debug_target=debug_target,
312 def _course_email(basedir, author, course, targets, assignment=None,
313 student=None, cc=None):
314 """Iterate through composed course `Message`\s
316 yield (construct_course_email(
317 author=author, course=course, targets=targets, cc=cc),
320 def construct_course_email(author, course, targets, cc=None):
321 """Construct a `Message` notfiying a professor of all grades to date
323 >>> from pygrader.model.person import Person
324 >>> from pygrader.model.assignment import Assignment
325 >>> from pygrader.model.grade import Grade
326 >>> from pygrader.model.course import Course
327 >>> author = Person(name='Jack', emails=['a@b.net'])
328 >>> student = Person(name='Jill', emails=['c@d.net'])
329 >>> prof = Person(name='H.D.', emails=['hd@wall.net'])
331 >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
332 ... assignment = Assignment(name=name, points=points, weight=0.5)
334 ... student=student, assignment=assignment,
335 ... points=int(points/2.0))
336 ... grades.append(grade)
337 >>> assignments = [g.assignment for g in grades]
339 ... assignments=assignments, people=[student], grades=grades)
340 >>> msg = construct_course_email(
341 ... author=author, course=course, targets=[prof])
342 >>> print(msg.as_string())
343 ... # doctest: +REPORT_UDIFF, +ELLIPSIS, +NORMALIZE_WHITESPACE
344 Content-Type: text/plain; charset="us-ascii"
346 Content-Transfer-Encoding: 7bit
347 Content-Disposition: inline
350 Reply-to: Jack <a@b.net>
351 To: "H.D." <hd@wall.net>
352 Subject: Course grades
356 Here are the (tab delimited) course grades to date:
358 Student\tExam 1\tHomework 1\tTotal
361 Mean\t5.00\t1.00\t0.416...
362 Std. Dev.\t0.00\t0.00\t0.0
364 The available points (and weights) for each assignment are:
366 * Homework 1:\t3\t0.5
371 target = join_with_and([t.alias() for t in targets])
372 table = _io.StringIO()
373 _tabulate(course=course, statistics=True, stream=table)
374 return _construct_email(
375 author=author, targets=targets, cc=cc,
376 subject='Course grades',
377 text=COURSE_TEMPLATE.render(
378 author=author, course=course, target=target,
379 table=table.getvalue()))