Ran update-copyright.py.
[pygrader.git] / pygrader / template.py
1 # Copyright (C) 2012 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of pygrader.
4 #
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
8 # version.
9 #
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.
13 #
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/>.
16
17 import io as _io
18
19 from jinja2 import Template
20
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
26
27
28 ASSIGNMENT_TEMPLATE = Template("""
29 {{ grade.student.alias() }},
30
31 You got {{ grade.points }} out of {{ grade.assignment.points }} available points on {{ grade.assignment.name }}.
32 {% if grade.comment %}
33 {{ grade.comment }}
34 {% endif %}
35 Yours,
36 {{ author.alias() }}
37 """.strip())
38 #{{ grade.comment|wordwrap }}
39
40 STUDENT_TEMPLATE = Template("""
41 {{ grades[0].student.alias() }},
42
43 Grades:
44 {%- for grade in grades %}
45   * {{ grade.assignment.name }}:\t{{ grade.points }} out of {{ grade.assignment.points }} available points.
46 {%- endfor %}
47
48 Comments:
49 {%- for grade in grades -%}
50 {% if grade.comment %}
51
52 {{ grade.assignment.name }}
53
54 {{ grade.comment }}
55 {%- endif %}
56 {% endfor %}
57 Yours,
58 {{ author.alias() }}
59 """.strip())
60
61 COURSE_TEMPLATE = Template("""
62 {{ target }},
63
64 Here are the (tab delimited) course grades to date:
65
66 {{ table }}
67 The available points (and weights) for each assignment are:
68 {%- for assignment in course.active_assignments() %}
69   * {{ assignment.name }}:\t{{ assignment.points }}\t{{ assignment.weight }}
70 {%- endfor %}
71
72 Yours,
73 {{ author.alias() }}
74 """.strip())
75
76
77
78
79 class NotifiedCallback (object):
80     """A callback for marking notifications with `_send_emails`
81     """
82     def __init__(self, basedir, grades):
83         self.basedir = basedir
84         self.grades = grades
85
86     def __call__(self, success):
87         if success:
88             for grade in self.grades:
89                 _set_notified(basedir=self.basedir, grade=grade)
90
91
92 def join_with_and(strings):
93     """Join a list of strings.
94
95     >>> join_with_and(['a','b','c'])
96     'a, b, and c'
97     >>> join_with_and(['a','b'])
98     'a and b'
99     >>> join_with_and(['a'])
100     'a'
101     """
102     ret = [strings[0]]
103     for i,s in enumerate(strings[1:]):
104         if len(strings) > 2:
105             ret.append(', ')
106         else:
107             ret.append(' ')
108         if i == len(strings)-2:
109             ret.append('and ')
110         ret.append(s)
111     return ''.join(ret)
112
113 def assignment_email(basedir, author, course, assignment, student=None,
114                      cc=None, smtp=None, use_color=False, debug_target=None,
115                      dry_run=False):
116     """Send each student an email with their grade on `assignment`
117     """
118     _send_emails(
119         emails=_assignment_email(
120             basedir=basedir, author=author, course=course,
121             assignment=assignment, student=student, cc=cc),
122         smtp=smtp, use_color=use_color,
123         debug_target=debug_target, dry_run=dry_run)
124
125 def _assignment_email(basedir, author, course, assignment, student=None,
126                       cc=None):
127     """Iterate through composed assignment `Message`\s
128     """
129     if student:
130         students = [student]
131     else:
132         students = course.people
133     for student in students:
134         try:
135             grade = course.grade(student=student, assignment=assignment)
136         except ValueError:
137             continue
138         if grade.notified:
139             continue
140         yield (construct_assignment_email(author=author, grade=grade, cc=cc),
141                NotifiedCallback(basedir=basedir, grades=[grade]))
142
143 def construct_assignment_email(author, grade, cc=None):
144     """Construct a `Message` notfiying a student of `grade`
145
146     >>> from pygrader.model.person import Person
147     >>> from pygrader.model.assignment import Assignment
148     >>> from pygrader.model.grade import Grade
149     >>> author = Person(name='Jack', emails=['a@b.net'])
150     >>> student = Person(name='Jill', emails=['c@d.net'])
151     >>> assignment = Assignment(name='Exam 1', points=3)
152     >>> grade = Grade(student=student, assignment=assignment, points=2)
153     >>> msg = construct_assignment_email(author=author, grade=grade)
154     >>> print(msg.as_string())  # doctest: +REPORT_UDIFF, +ELLIPSIS
155     Content-Type: text/plain; charset="us-ascii"
156     MIME-Version: 1.0
157     Content-Transfer-Encoding: 7bit
158     Content-Disposition: inline
159     Date: ...
160     From: Jack <a@b.net>
161     Reply-to: Jack <a@b.net>
162     To: Jill <c@d.net>
163     Subject: Your Exam 1 grade
164     <BLANKLINE>
165     Jill,
166     <BLANKLINE>
167     You got 2 out of 3 available points on Exam 1.
168     <BLANKLINE>
169     Yours,
170     Jack
171
172     >>> grade.comment = ('Some comment bla bla bla.').strip()
173     >>> msg = construct_assignment_email(author=author, grade=grade)
174     >>> print(msg.as_string())  # doctest: +REPORT_UDIFF, +ELLIPSIS
175     Content-Type: text/plain; charset="us-ascii"
176     MIME-Version: 1.0
177     Content-Transfer-Encoding: 7bit
178     Content-Disposition: inline
179     Date: ...
180     From: Jack <a@b.net>
181     Reply-to: Jack <a@b.net>
182     To: Jill <c@d.net>
183     Subject: Your Exam 1 grade
184     <BLANKLINE>
185     Jill,
186     <BLANKLINE>
187     You got 2 out of 3 available points on Exam 1.
188     <BLANKLINE>
189     Some comment bla bla bla.
190     <BLANKLINE>
191     Yours,
192     Jack
193     """
194     return _construct_email(
195         author=author, targets=[grade.student], cc=cc,
196         subject='Your {} grade'.format(grade.assignment.name),
197         text=ASSIGNMENT_TEMPLATE.render(author=author, grade=grade))
198
199 def student_email(basedir, author, course, student=None, cc=None, old=False,
200                   smtp=None, use_color=False, debug_target=None,
201                   dry_run=False):
202     """Send each student an email with their grade to date
203     """
204     _send_emails(
205         emails=_student_email(
206             basedir=basedir, author=author, course=course, student=student,
207             cc=cc, old=old),
208         smtp=smtp, use_color=use_color, debug_target=debug_target,
209         dry_run=dry_run)
210
211 def _student_email(basedir, author, course, student=None, cc=None, old=False):
212     """Iterate through composed student `Message`\s
213     """
214     if student:
215         students = [student]
216     else:
217         students = course.people
218     for student in students:
219         grades = [g for g in course.grades if g.student == student]
220         if not old:
221             grades = [g for g in grades if not g.notified]
222         if not grades:
223             continue
224         yield (construct_student_email(author=author, grades=grades, cc=cc),
225                NotifiedCallback(basedir=basedir, grades=grades))
226
227 def construct_student_email(author, grades, cc=None):
228     """Construct a `Message` notfiying a student of `grade`
229
230     >>> from pygrader.model.person import Person
231     >>> from pygrader.model.assignment import Assignment
232     >>> from pygrader.model.grade import Grade
233     >>> author = Person(name='Jack', emails=['a@b.net'])
234     >>> student = Person(name='Jill', emails=['c@d.net'])
235     >>> grades = []
236     >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
237     ...     assignment = Assignment(name=name, points=points)
238     ...     grade = Grade(
239     ...         student=student, assignment=assignment,
240     ...         points=int(points/2.0))
241     ...     grades.append(grade)
242     >>> msg = construct_student_email(author=author, grades=grades)
243     >>> print(msg.as_string())
244     ... # doctest: +REPORT_UDIFF, +ELLIPSIS, +NORMALIZE_WHITESPACE
245     Content-Type: text/plain; charset="us-ascii"
246     MIME-Version: 1.0
247     Content-Transfer-Encoding: 7bit
248     Content-Disposition: inline
249     Date: ...
250     From: Jack <a@b.net>
251     Reply-to: Jack <a@b.net>
252     To: Jill <c@d.net>
253     Subject: Your grade
254     <BLANKLINE>
255     Jill,
256     <BLANKLINE>
257     Grades:
258       * Exam 1:\t5 out of 10 available points.
259       * Homework 1:\t1 out of 3 available points.
260     <BLANKLINE>
261     Comments:
262     <BLANKLINE>
263     <BLANKLINE>
264     Yours,
265     Jack
266
267     >>> grades[0].comment = ('Bla bla bla.  '*20).strip()
268     >>> grades[1].comment = ('Hello world')
269     >>> msg = construct_student_email(author=author, grades=grades)
270     >>> print(msg.as_string())
271     ... # doctest: +REPORT_UDIFF, +ELLIPSIS, +NORMALIZE_WHITESPACE
272     Content-Type: text/plain; charset="us-ascii"
273     MIME-Version: 1.0
274     Content-Transfer-Encoding: 7bit
275     Content-Disposition: inline
276     Date: ...
277     From: Jack <a@b.net>
278     Reply-to: Jack <a@b.net>
279     To: Jill <c@d.net>
280     Subject: Your grade
281     <BLANKLINE>
282     Jill,
283     <BLANKLINE>
284     Grades:
285       * Exam 1:\t5 out of 10 available points.
286       * Homework 1:\t1 out of 3 available points.
287     <BLANKLINE>
288     Comments:
289     <BLANKLINE>
290     Exam 1
291     <BLANKLINE>
292     Hello world
293     <BLANKLINE>
294     <BLANKLINE>
295     <BLANKLINE>
296     Homework 1
297     <BLANKLINE>
298     Bla bla bla.  Bla bla bla.  Bla bla bla.  Bla bla bla.  Bla bla bla.  Bla bla
299     bla.  Bla bla bla.  Bla bla bla.  Bla bla bla.  Bla bla bla.  Bla bla bla.  Bla
300     bla bla.  Bla bla bla.  Bla bla bla.  Bla bla bla.  Bla bla bla.  Bla bla bla.
301     Bla bla bla.  Bla bla bla.  Bla bla bla.
302     <BLANKLINE>
303     <BLANKLINE>
304     Yours,
305     Jack
306     """
307     students = set(g.student for g in grades)
308     assert len(students) == 1, students
309     return _construct_email(
310         author=author, targets=[grades[0].student], cc=cc,
311         subject='Your grade',
312         text=STUDENT_TEMPLATE.render(author=author, grades=sorted(grades)))
313
314 def course_email(basedir, author, course, targets, assignment=None,
315                  student=None, cc=None, smtp=None, use_color=False,
316                  debug_target=None, dry_run=False):
317     """Send the professor an email with all student grades to date
318     """
319     _send_emails(
320         emails=_course_email(
321             basedir=basedir, author=author, course=course, targets=targets,
322             assignment=assignment, student=student, cc=cc),
323         smtp=smtp, use_color=use_color, debug_target=debug_target,
324         dry_run=dry_run)
325
326 def _course_email(basedir, author, course, targets, assignment=None,
327                   student=None, cc=None):
328     """Iterate through composed course `Message`\s
329     """
330     yield (construct_course_email(
331             author=author, course=course, targets=targets, cc=cc),
332            None)
333
334 def construct_course_email(author, course, targets, cc=None):
335     """Construct a `Message` notfiying a professor of all grades to date
336
337     >>> from pygrader.model.person import Person
338     >>> from pygrader.model.assignment import Assignment
339     >>> from pygrader.model.grade import Grade
340     >>> from pygrader.model.course import Course
341     >>> author = Person(name='Jack', emails=['a@b.net'])
342     >>> student = Person(name='Jill', emails=['c@d.net'])
343     >>> prof = Person(name='H.D.', emails=['hd@wall.net'])
344     >>> grades = []
345     >>> for name,points in [('Homework 1', 3), ('Exam 1', 10)]:
346     ...     assignment = Assignment(name=name, points=points, weight=0.5)
347     ...     grade = Grade(
348     ...         student=student, assignment=assignment,
349     ...         points=int(points/2.0))
350     ...     grades.append(grade)
351     >>> assignments = [g.assignment for g in grades]
352     >>> course = Course(
353     ...     assignments=assignments, people=[student], grades=grades)
354     >>> msg = construct_course_email(
355     ...     author=author, course=course, targets=[prof])
356     >>> print(msg.as_string())
357     ... # doctest: +REPORT_UDIFF, +ELLIPSIS, +NORMALIZE_WHITESPACE
358     Content-Type: text/plain; charset="us-ascii"
359     MIME-Version: 1.0
360     Content-Transfer-Encoding: 7bit
361     Content-Disposition: inline
362     Date: ...
363     From: Jack <a@b.net>
364     Reply-to: Jack <a@b.net>
365     To: "H.D." <hd@wall.net>
366     Subject: Course grades
367     <BLANKLINE>
368     H.D.,
369     <BLANKLINE>
370     Here are the (tab delimited) course grades to date:
371     <BLANKLINE>
372     Student\tExam 1\tHomework 1\tTotal
373     Jill\t5\t1\t0.416...
374     --
375     Mean\t5.00\t1.00\t0.416...
376     Std. Dev.\t0.00\t0.00\t0.0
377     <BLANKLINE>
378     The available points (and weights) for each assignment are:
379       * Exam 1:\t10\t0.5
380       * Homework 1:\t3\t0.5
381     <BLANKLINE>
382     Yours,
383     Jack
384     """
385     target = join_with_and([t.alias() for t in targets])
386     table = _io.StringIO()
387     _tabulate(course=course, statistics=True, stream=table)
388     return _construct_email(
389         author=author, targets=targets, cc=cc,
390         subject='Course grades',
391         text=COURSE_TEMPLATE.render(
392             author=author, course=course, target=target,
393             table=table.getvalue()))