Begin versioning! (better late than never)
[pygrader.git] / pygrader / template.py
1 # Copyright
2
3 import io as _io
4
5 from jinja2 import Template
6
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
12
13
14 ASSIGNMENT_TEMPLATE = Template("""
15 {{ grade.student.alias() }},
16
17 You got {{ grade.points }} out of {{ grade.assignment.points }} available points on {{ grade.assignment.name }}.
18 {% if grade.comment %}
19 {{ grade.comment }}
20 {% endif %}
21 Yours,
22 {{ author.alias() }}
23 """.strip())
24 #{{ grade.comment|wordwrap }}
25
26 STUDENT_TEMPLATE = Template("""
27 {{ grades[0].student.alias() }},
28
29 Grades:
30 {%- for grade in grades %}
31   * {{ grade.assignment.name }}:\t{{ grade.points }} out of {{ grade.assignment.points }} available points.
32 {%- endfor %}
33
34 Comments:
35 {%- for grade in grades -%}
36 {% if grade.comment %}
37
38 {{ grade.assignment.name }}
39
40 {{ grade.comment }}
41 {%- endif %}
42 {% endfor %}
43 Yours,
44 {{ author.alias() }}
45 """.strip())
46
47 COURSE_TEMPLATE = Template("""
48 {{ target }},
49
50 Here are the (tab delimited) course grades to date:
51
52 {{ table }}
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 }}
56 {%- endfor %}
57
58 Yours,
59 {{ author.alias() }}
60 """.strip())
61
62
63
64
65 class NotifiedCallback (object):
66     """A callback for marking notifications with `_send_emails`
67     """
68     def __init__(self, basedir, grades):
69         self.basedir = basedir
70         self.grades = grades
71
72     def __call__(self, success):
73         if success:
74             for grade in self.grades:
75                 _set_notified(basedir=self.basedir, grade=grade)
76
77
78 def join_with_and(strings):
79     """Join a list of strings.
80
81     >>> join_with_and(['a','b','c'])
82     'a, b, and c'
83     >>> join_with_and(['a','b'])
84     'a and b'
85     >>> join_with_and(['a'])
86     'a'
87     """
88     ret = [strings[0]]
89     for i,s in enumerate(strings[1:]):
90         if len(strings) > 2:
91             ret.append(', ')
92         else:
93             ret.append(' ')
94         if i == len(strings)-2:
95             ret.append('and ')
96         ret.append(s)
97     return ''.join(ret)
98
99 def assignment_email(basedir, author, course, assignment, student=None,
100                      cc=None, smtp=None, use_color=False, debug_target=None,
101                      dry_run=False):
102     """Send each student an email with their grade on `assignment`
103     """
104     _send_emails(
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)
110
111 def _assignment_email(basedir, author, course, assignment, student=None,
112                       cc=None):
113     """Iterate through composed assignment `Message`\s
114     """
115     if student:
116         students = [student]
117     else:
118         students = course.people
119     for student in students:
120         try:
121             grade = course.grade(student=student, assignment=assignment)
122         except ValueError:
123             continue
124         if grade.notified:
125             continue
126         yield (construct_assignment_email(author=author, grade=grade, cc=cc),
127                NotifiedCallback(basedir=basedir, grades=[grade]))
128
129 def construct_assignment_email(author, grade, cc=None):
130     """Construct a `Message` notfiying a student of `grade`
131
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"
142     MIME-Version: 1.0
143     Content-Transfer-Encoding: 7bit
144     Content-Disposition: inline
145     Date: ...
146     From: Jack <a@b.net>
147     Reply-to: Jack <a@b.net>
148     To: Jill <c@d.net>
149     Subject: Your Exam 1 grade
150     <BLANKLINE>
151     Jill,
152     <BLANKLINE>
153     You got 2 out of 3 available points on Exam 1.
154     <BLANKLINE>
155     Yours,
156     Jack
157
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"
162     MIME-Version: 1.0
163     Content-Transfer-Encoding: 7bit
164     Content-Disposition: inline
165     Date: ...
166     From: Jack <a@b.net>
167     Reply-to: Jack <a@b.net>
168     To: Jill <c@d.net>
169     Subject: Your Exam 1 grade
170     <BLANKLINE>
171     Jill,
172     <BLANKLINE>
173     You got 2 out of 3 available points on Exam 1.
174     <BLANKLINE>
175     Some comment bla bla bla.
176     <BLANKLINE>
177     Yours,
178     Jack
179     """
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))
184
185 def student_email(basedir, author, course, student=None, cc=None, old=False,
186                   smtp=None, use_color=False, debug_target=None,
187                   dry_run=False):
188     """Send each student an email with their grade to date
189     """
190     _send_emails(
191         emails=_student_email(
192             basedir=basedir, author=author, course=course, student=student,
193             cc=cc, old=old),
194         smtp=smtp, use_color=use_color, debug_target=debug_target,
195         dry_run=dry_run)
196
197 def _student_email(basedir, author, course, student=None, cc=None, old=False):
198     """Iterate through composed student `Message`\s
199     """
200     if student:
201         students = [student]
202     else:
203         students = course.people
204     for student in students:
205         grades = [g for g in course.grades if g.student == student]
206         if not old:
207             grades = [g for g in grades if not g.notified]
208         if not grades:
209             continue
210         yield (construct_student_email(author=author, grades=grades, cc=cc),
211                NotifiedCallback(basedir=basedir, grades=grades))
212
213 def construct_student_email(author, grades, cc=None):
214     """Construct a `Message` notfiying a student of `grade`
215
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'])
221     >>> grades = []
222     >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
223     ...     assignment = Assignment(name=name, points=points)
224     ...     grade = Grade(
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"
232     MIME-Version: 1.0
233     Content-Transfer-Encoding: 7bit
234     Content-Disposition: inline
235     Date: ...
236     From: Jack <a@b.net>
237     Reply-to: Jack <a@b.net>
238     To: Jill <c@d.net>
239     Subject: Your grade
240     <BLANKLINE>
241     Jill,
242     <BLANKLINE>
243     Grades:
244       * Exam 1:\t5 out of 10 available points.
245       * Homework 1:\t1 out of 3 available points.
246     <BLANKLINE>
247     Comments:
248     <BLANKLINE>
249     <BLANKLINE>
250     Yours,
251     Jack
252
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"
259     MIME-Version: 1.0
260     Content-Transfer-Encoding: 7bit
261     Content-Disposition: inline
262     Date: ...
263     From: Jack <a@b.net>
264     Reply-to: Jack <a@b.net>
265     To: Jill <c@d.net>
266     Subject: Your grade
267     <BLANKLINE>
268     Jill,
269     <BLANKLINE>
270     Grades:
271       * Exam 1:\t5 out of 10 available points.
272       * Homework 1:\t1 out of 3 available points.
273     <BLANKLINE>
274     Comments:
275     <BLANKLINE>
276     Exam 1
277     <BLANKLINE>
278     Hello world
279     <BLANKLINE>
280     <BLANKLINE>
281     <BLANKLINE>
282     Homework 1
283     <BLANKLINE>
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.
288     <BLANKLINE>
289     <BLANKLINE>
290     Yours,
291     Jack
292     """
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)))
299
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
304     """
305     _send_emails(
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,
310         dry_run=dry_run)
311
312 def _course_email(basedir, author, course, targets, assignment=None,
313                   student=None, cc=None):
314     """Iterate through composed course `Message`\s
315     """
316     yield (construct_course_email(
317             author=author, course=course, targets=targets, cc=cc),
318            None)
319
320 def construct_course_email(author, course, targets, cc=None):
321     """Construct a `Message` notfiying a professor of all grades to date
322
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'])
330     >>> grades = []
331     >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
332     ...     assignment = Assignment(name=name, points=points, weight=0.5)
333     ...     grade = Grade(
334     ...         student=student, assignment=assignment,
335     ...         points=int(points/2.0))
336     ...     grades.append(grade)
337     >>> assignments = [g.assignment for g in grades]
338     >>> course = Course(
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"
345     MIME-Version: 1.0
346     Content-Transfer-Encoding: 7bit
347     Content-Disposition: inline
348     Date: ...
349     From: Jack <a@b.net>
350     Reply-to: Jack <a@b.net>
351     To: "H.D." <hd@wall.net>
352     Subject: Course grades
353     <BLANKLINE>
354     H.D.,
355     <BLANKLINE>
356     Here are the (tab delimited) course grades to date:
357     <BLANKLINE>
358     Student\tExam 1\tHomework 1\tTotal
359     Jill\t5\t1\t0.416...
360     --
361     Mean\t5.00\t1.00\t0.416...
362     Std. Dev.\t0.00\t0.00\t0.0
363     <BLANKLINE>
364     The available points (and weights) for each assignment are:
365       * Exam 1:\t10\t0.5
366       * Homework 1:\t3\t0.5
367     <BLANKLINE>
368     Yours,
369     Jack
370     """
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()))