Merge lp:~ubuntu-branches/ubuntu/precise/schooltool.lyceum.journal/precise-201112231905 into lp:ubuntu/precise/schooltool.lyceum.journal
- Precise (12.04)
- precise-201112231905
- Merge into precise
Status: | Rejected |
---|---|
Rejected by: | James Westby |
Proposed branch: | lp:~ubuntu-branches/ubuntu/precise/schooltool.lyceum.journal/precise-201112231905 |
Merge into: | lp:ubuntu/precise/schooltool.lyceum.journal |
Diff against target: |
1450 lines (+1423/-0) 4 files modified
.pc/.version (+1/-0) .pc/applied-patches (+1/-0) .pc/cells-css.patch/src/schooltool/lyceum/journal/browser/journal.py (+1190/-0) .pc/cells-css.patch/src/schooltool/lyceum/journal/browser/templates/f_journal.pt (+231/-0) |
To merge this branch: | bzr merge lp:~ubuntu-branches/ubuntu/precise/schooltool.lyceum.journal/precise-201112231905 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu branches | Pending | ||
Review via email:
|
Commit message
Description of the change
The package importer has detected a possible inconsistency between the package history in the archive and the history in bzr. As the archive is authoritative the importer has made lp:ubuntu/precise/schooltool.lyceum.journal reflect what is in the archive and the old bzr branch has been pushed to lp:~ubuntu-branches/ubuntu/precise/schooltool.lyceum.journal/precise-201112231905. This merge proposal was created so that an Ubuntu developer can review the situations and perform a merge/upload if necessary. There are three typical cases where this can happen.
1. Where someone pushes a change to bzr and someone else uploads the package without that change. This is the reason that this check is done by the importer. If this appears to be the case then a merge/upload should be done if the changes that were in bzr are still desirable.
2. The importer incorrectly detected the above situation when someone made a change in bzr and then uploaded it.
3. The importer incorrectly detected the above situation when someone just uploaded a package and didn't touch bzr.
If this case doesn't appear to be the first situation then set the status of the merge proposal to "Rejected" and help avoid the problem in future by filing a bug at https:/
(this is an automatically generated message)
Unmerged revisions
- 10. By Gediminas Paulauskas
-
* New upstream release.
* debian/patches/ cells-css. patch: remove, fixed upstream.
* debian/rules: install upstream changelog.
Preview Diff
1 | === added directory '.pc' |
2 | === added file '.pc/.version' |
3 | --- .pc/.version 1970-01-01 00:00:00 +0000 |
4 | +++ .pc/.version 2011-12-23 19:10:30 +0000 |
5 | @@ -0,0 +1,1 @@ |
6 | +2 |
7 | |
8 | === added file '.pc/applied-patches' |
9 | --- .pc/applied-patches 1970-01-01 00:00:00 +0000 |
10 | +++ .pc/applied-patches 2011-12-23 19:10:30 +0000 |
11 | @@ -0,0 +1,1 @@ |
12 | +cells-css.patch |
13 | |
14 | === added directory '.pc/cells-css.patch' |
15 | === added directory '.pc/cells-css.patch/src' |
16 | === added directory '.pc/cells-css.patch/src/schooltool' |
17 | === added directory '.pc/cells-css.patch/src/schooltool/lyceum' |
18 | === added directory '.pc/cells-css.patch/src/schooltool/lyceum/journal' |
19 | === added directory '.pc/cells-css.patch/src/schooltool/lyceum/journal/browser' |
20 | === added file '.pc/cells-css.patch/src/schooltool/lyceum/journal/browser/journal.py' |
21 | --- .pc/cells-css.patch/src/schooltool/lyceum/journal/browser/journal.py 1970-01-01 00:00:00 +0000 |
22 | +++ .pc/cells-css.patch/src/schooltool/lyceum/journal/browser/journal.py 2011-12-23 19:10:30 +0000 |
23 | @@ -0,0 +1,1190 @@ |
24 | +# |
25 | +# SchoolTool - common information systems platform for school administration |
26 | +# Copyright (c) 2007 Shuttleworth Foundation |
27 | +# |
28 | +# This program is free software; you can redistribute it and/or modify |
29 | +# it under the terms of the GNU General Public License as published by |
30 | +# the Free Software Foundation; either version 2 of the License, or |
31 | +# (at your option) any later version. |
32 | +# |
33 | +# This program is distributed in the hope that it will be useful, |
34 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
35 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
36 | +# GNU General Public License for more details. |
37 | +# |
38 | +# You should have received a copy of the GNU General Public License |
39 | +# along with this program; if not, write to the Free Software |
40 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
41 | +# |
42 | +""" |
43 | +Lyceum journal views. |
44 | +""" |
45 | +import pytz |
46 | +import urllib |
47 | +import base64 |
48 | + |
49 | +from zope.security.proxy import removeSecurityProxy |
50 | +from zope.security import checkPermission |
51 | +from zope.proxy import sameProxiedObjects |
52 | +from zope.viewlet.interfaces import IViewlet |
53 | +from zope.exceptions.interfaces import UserError |
54 | +from zope.publisher.browser import BrowserView |
55 | +from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile |
56 | +from zope.formlib.widget import quoteattr |
57 | +from zope.component import queryMultiAdapter |
58 | +from zope.i18n import translate |
59 | +from zope.i18n.interfaces.locales import ICollator |
60 | +from zope.interface import implements |
61 | +from zope.traversing.browser.absoluteurl import absoluteURL |
62 | +from zope.component import getUtility |
63 | + |
64 | +from zc.table.column import GetterColumn |
65 | +from zc.table.interfaces import IColumn |
66 | +from zope.cachedescriptors.property import Lazy |
67 | + |
68 | +from schooltool.skin import flourish |
69 | +from schooltool.course.interfaces import ILearner, IInstructor |
70 | +from schooltool.common.inlinept import InlineViewPageTemplate |
71 | +from schooltool.person.interfaces import IPerson |
72 | +from schooltool.app.browser.cal import month_names |
73 | +from schooltool.app.interfaces import IApplicationPreferences |
74 | +from schooltool.app.interfaces import ISchoolToolApplication |
75 | +from schooltool.term.interfaces import ITerm |
76 | +from schooltool.term.interfaces import ITermContainer |
77 | +from schooltool.term.interfaces import IDateManager |
78 | +from schooltool.table.interfaces import ITableFormatter, IIndexedTableFormatter |
79 | +from schooltool.table.table import simple_form_key |
80 | +from schooltool.timetable.interfaces import IScheduleCalendarEvent |
81 | +from schooltool.timetable.interfaces import IScheduleContainer |
82 | +from schooltool.schoolyear.interfaces import ISchoolYear |
83 | + |
84 | +from schooltool.lyceum.journal.journal import (ABSENT, TARDY, |
85 | + getCurrentSectionTaught, setCurrentSectionTaught) |
86 | +from schooltool.lyceum.journal.interfaces import ISectionJournal |
87 | +from schooltool.lyceum.journal.browser.interfaces import IIndependentColumn |
88 | +from schooltool.lyceum.journal.browser.interfaces import ISelectableColumn |
89 | +from schooltool.lyceum.journal.browser.table import SelectStudentCellFormatter |
90 | +from schooltool.lyceum.journal.browser.table import SelectableRowTableFormatter |
91 | +from schooltool.lyceum.journal import LyceumMessage as _ |
92 | + |
93 | + |
94 | +# set up translation from data base data to locale representation and back |
95 | +ABSENT_LETTER = translate(_(u"Single letter that represents an absent mark for a student", |
96 | + default='a')) |
97 | +TARDY_LETTER = translate(_(u"Single letter that represents an tardy mark for a student", |
98 | + default='t')) |
99 | + |
100 | +ATTENDANCE_DATA_TO_TRANSLATION = {ABSENT: ABSENT_LETTER, |
101 | + TARDY: TARDY_LETTER} |
102 | +ATTENDANCE_TRANSLATION_TO_DATA = {ABSENT_LETTER: ABSENT, |
103 | + TARDY_LETTER: TARDY} |
104 | + |
105 | + |
106 | +class JournalCalendarEventViewlet(object): |
107 | + """Viewlet for section meeting calendar events. |
108 | + |
109 | + Adds an Attendance link to all section meeting events. |
110 | + """ |
111 | + |
112 | + def attendanceLink(self): |
113 | + """Construct the URL for the attendance form for a section meeting. |
114 | + |
115 | + Returns None if the calendar event is not a section meeting event. |
116 | + """ |
117 | + event_for_display = self.manager.event |
118 | + calendar_event = event_for_display.context |
119 | + journal = ISectionJournal(calendar_event, None) |
120 | + if journal: |
121 | + return '%s/index.html?event_id=%s' % ( |
122 | + absoluteURL(journal, self.request), |
123 | + urllib.quote(event_for_display.context.unique_id.encode('utf-8'))) |
124 | + |
125 | + |
126 | +class FlourishJournalCalendarEventViewlet(JournalCalendarEventViewlet): |
127 | + |
128 | + def attendanceLink(self): |
129 | + """Construct the URL for the attendance form for a section meeting. |
130 | + |
131 | + Returns None if the calendar event is not a section meeting event. |
132 | + """ |
133 | + event_for_display = self.manager.event |
134 | + calendar_event = event_for_display.context |
135 | + journal = ISectionJournal(calendar_event, None) |
136 | + if journal and checkPermission('schooltool.view', journal): |
137 | + return '%s/index.html?event_id=%s' % ( |
138 | + absoluteURL(journal, self.request), |
139 | + urllib.quote(event_for_display.context.unique_id.encode('utf-8'))) |
140 | + |
141 | + |
142 | +class StudentNumberColumn(GetterColumn): |
143 | + |
144 | + def getter(self, item, formatter): |
145 | + return formatter.row |
146 | + |
147 | + def renderCell(self, item, formatter): |
148 | + value = self.getter(item, formatter) |
149 | + cell = u'%d<input type="hidden" value=%s class="person_id" />' % ( |
150 | + value, quoteattr(item.__name__)) |
151 | + return cell |
152 | + |
153 | + def renderHeader(self, formatter): |
154 | + return '<span>%s</span>' % translate(_("Nr."), |
155 | + context=formatter.request) |
156 | + |
157 | +class GradesColumn(object): |
158 | + def getGrades(self, person): |
159 | + """Get the grades for the person.""" |
160 | + grades = [] |
161 | + for meeting in self.journal.recordedMeetings(person): |
162 | + if meeting.dtstart.date() in self.term: |
163 | + grade = self.journal.getGrade(person, meeting) |
164 | + if grade and grade.strip(): |
165 | + grades.append(grade) |
166 | + return grades |
167 | + |
168 | + |
169 | +class PersonGradesColumn(GradesColumn): |
170 | + implements(ISelectableColumn, IIndependentColumn) |
171 | + |
172 | + def __init__(self, meeting, journal, selected=False): |
173 | + self.meeting = meeting |
174 | + self.selected = selected |
175 | + self.journal = journal |
176 | + |
177 | + def today(self): |
178 | + return getUtility(IDateManager).today |
179 | + |
180 | + @property |
181 | + def name(self): |
182 | + return self.meeting.unique_id |
183 | + |
184 | + def meetingDate(self): |
185 | + app = ISchoolToolApplication(None) |
186 | + tzinfo = pytz.timezone(IApplicationPreferences(app).timezone) |
187 | + date = self.meeting.dtstart.astimezone(tzinfo).date() |
188 | + return date |
189 | + |
190 | + def extra_parameters(self, request): |
191 | + parameters = [] |
192 | + for info in ['TERM', 'month', 'student']: |
193 | + if info in request: |
194 | + parameters.append((info, request[info].encode('utf-8'))) |
195 | + return parameters |
196 | + |
197 | + def journalUrl(self, request): |
198 | + return absoluteURL(self.journal, request) |
199 | + |
200 | + def renderHeader(self, formatter): |
201 | + meetingDate = self.meetingDate() |
202 | + header = meetingDate.strftime("%d") |
203 | + |
204 | + today_class = "" |
205 | + if meetingDate == self.today(): |
206 | + today_class = ' today' |
207 | + |
208 | + if not self.selected: |
209 | + url = "%s/index.html?%s" % ( |
210 | + self.journalUrl(formatter.request), |
211 | + urllib.urlencode([('event_id', self.meeting.unique_id.encode('utf-8'))] + |
212 | + self.extra_parameters(formatter.request))) |
213 | + try: |
214 | + if self.meeting.period is not None: |
215 | + short_title = self.meeting.period.title[:3] |
216 | + else: |
217 | + short_title = '' |
218 | + period = '<br />' + short_title |
219 | + if period[-1] == ':': |
220 | + period = period[:-1] |
221 | + except: |
222 | + period = '' |
223 | + header = '<a href="%s">%s%s</a>' % (url, header, period) |
224 | + |
225 | + span = '<span class="select-column%s" title="%s">%s</span>' % ( |
226 | + today_class, meetingDate.strftime("%Y-%m-%d"), header) |
227 | + event_id = '<input type="hidden" value="%s" class="event_id" />' % ( |
228 | + urllib.quote(base64.encodestring(self.meeting.unique_id.encode('utf-8')))) |
229 | + return span + event_id |
230 | + |
231 | + def getCellValue(self, item): |
232 | + if self.hasMeeting(item): |
233 | + grade = self.journal.getGrade(item, self.meeting, default="") |
234 | + return ATTENDANCE_DATA_TO_TRANSLATION.get(grade, grade) |
235 | + return "X" |
236 | + |
237 | + def hasMeeting(self, item): |
238 | + return self.journal.hasMeeting(item, self.meeting) |
239 | + |
240 | + def template(self, item, selected): |
241 | + value = self.getCellValue(item) |
242 | + name = "%s.%s" % (item.__name__, self.meeting.__name__) |
243 | + |
244 | + if not selected: |
245 | + return "<td>%s</td>" % value |
246 | + else: |
247 | + klass = ' class="selected-column"' |
248 | + input = """<td%(class)s><input type="text" style="width: 1.4em" |
249 | + name="%(name)s" |
250 | + value="%(value)s" /></td>""" |
251 | + return input % {'class': klass, 'name':name, 'value':value} |
252 | + |
253 | + def renderSelectedCell(self, item, formatter): |
254 | + selected = self.hasMeeting(item) |
255 | + return self.template(item, selected) |
256 | + |
257 | + def renderCell(self, item, formatter): |
258 | + selected = self.selected and self.hasMeeting(item) |
259 | + return self.template(item, selected) |
260 | + |
261 | + |
262 | +class SectionTermGradesColumn(GradesColumn): |
263 | + implements(IColumn) |
264 | + |
265 | + def __init__(self, journal, term): |
266 | + self.term = term |
267 | + self.name = term.__name__ + "grades" |
268 | + self.journal = journal |
269 | + |
270 | + def renderCell(self, person, formatter): |
271 | + grades = [] |
272 | + for grade in self.getGrades(person): |
273 | + try: |
274 | + grade = int(grade) |
275 | + except ValueError: |
276 | + continue |
277 | + grades.append(grade) |
278 | + if not grades: |
279 | + return "" |
280 | + else: |
281 | + return ",".join(["%s" % grade for grade in grades]) |
282 | + |
283 | + def renderHeader(self, formatter): |
284 | + return '<span>%s</span>' % translate(_("Grades"), |
285 | + context=formatter.request) |
286 | + |
287 | +class SectionTermAverageGradesColumn(GradesColumn): |
288 | + implements(IColumn) |
289 | + |
290 | + def __init__(self, journal, term): |
291 | + self.term = term |
292 | + self.name = term.__name__ + "average" |
293 | + self.journal = journal |
294 | + |
295 | + def renderCell(self, person, formatter): |
296 | + grades = [] |
297 | + for grade in self.getGrades(person): |
298 | + try: |
299 | + grade = int(grade) |
300 | + except ValueError: |
301 | + continue |
302 | + grades.append(grade) |
303 | + if not grades: |
304 | + return "" |
305 | + else: |
306 | + return "%.3f" % (sum(grades) / float(len(grades))) |
307 | + |
308 | + def renderHeader(self, formatter): |
309 | + return '<span>%s</span>' % translate(_("Average"), |
310 | + context=formatter.request) |
311 | + |
312 | + |
313 | +class SectionTermAttendanceColumn(GradesColumn): |
314 | + implements(IColumn) |
315 | + |
316 | + def __init__(self, journal, term): |
317 | + self.term = term |
318 | + self.name = term.__name__ + "attendance" |
319 | + self.journal = journal |
320 | + |
321 | + def renderCell(self, person, formatter): |
322 | + absences = 0 |
323 | + for grade in self.getGrades(person): |
324 | + if (grade.strip().lower() == ABSENT): |
325 | + absences += 1 |
326 | + |
327 | + if absences == 0: |
328 | + return "" |
329 | + else: |
330 | + return str(absences) |
331 | + |
332 | + def renderHeader(self, formatter): |
333 | + return '<span>%s</span>' % translate(_("Absences"), |
334 | + context=formatter.request) |
335 | + |
336 | + |
337 | +class SectionTermTardiesColumn(GradesColumn): |
338 | + implements(IColumn) |
339 | + |
340 | + def __init__(self, journal, term): |
341 | + self.term = term |
342 | + self.name = term.__name__ + "tardies" |
343 | + self.journal = journal |
344 | + |
345 | + def renderCell(self, person, formatter): |
346 | + tardies = 0 |
347 | + for grade in self.getGrades(person): |
348 | + if (grade.strip().lower() == TARDY): |
349 | + tardies += 1 |
350 | + |
351 | + if tardies == 0: |
352 | + return "" |
353 | + else: |
354 | + return str(tardies) |
355 | + |
356 | + def renderHeader(self, formatter): |
357 | + return '<span>%s</span>' % translate(_("Tardies"), |
358 | + context=formatter.request) |
359 | + |
360 | +def journal_grades(): |
361 | + grades = [ |
362 | + {'keys': [ABSENT_LETTER.lower(), ABSENT_LETTER.upper()], |
363 | + 'value': ABSENT_LETTER, |
364 | + 'legend': _('Absent')}, |
365 | + {'keys': [TARDY_LETTER.lower(), TARDY_LETTER.upper()], |
366 | + 'value': TARDY_LETTER, |
367 | + 'legend': _('Tardy')}] |
368 | + for i in range(9): |
369 | + grades.append({'keys': [chr(i + ord('1'))], |
370 | + 'value': unicode(i+1), |
371 | + 'legend': u''}) |
372 | + grades.append({'keys': ['0'], |
373 | + 'value': u'10', |
374 | + 'legend': u''}) |
375 | + return grades |
376 | + |
377 | + |
378 | +class SectionJournalJSView(BrowserView): |
379 | + |
380 | + def grading_events(self): |
381 | + for grade in journal_grades(): |
382 | + event_check = ' || '.join([ |
383 | + 'event.which == %d' % ord(key) |
384 | + for key in grade['keys']]) |
385 | + yield {'js_condition': event_check, |
386 | + 'grade_value': "'%s'" % grade['value']} |
387 | + |
388 | + |
389 | +class StudentSelectionMixin(object): |
390 | + |
391 | + selected_students = None |
392 | + |
393 | + def selectStudents(self, table_formatter): |
394 | + self.selected_students = [] |
395 | + if 'student' in self.request: |
396 | + student_id = self.request['student'] |
397 | + app = ISchoolToolApplication(None) |
398 | + student = app['persons'].get(student_id) |
399 | + self.selected_students = [student] |
400 | + |
401 | + if IIndexedTableFormatter.providedBy(table_formatter): |
402 | + self.selected_students = table_formatter.indexItems( |
403 | + self.selected_students) |
404 | + |
405 | + |
406 | +class LyceumSectionJournalView(StudentSelectionMixin): |
407 | + |
408 | + template = ViewPageTemplateFile("templates/journal.pt") |
409 | + no_timetable_template = ViewPageTemplateFile("templates/no_timetable_journal.pt") |
410 | + no_periods_template = ViewPageTemplateFile("templates/no_periods_journal.pt") |
411 | + |
412 | + def __init__(self, context, request): |
413 | + self.context, self.request = context, request |
414 | + |
415 | + def __call__(self): |
416 | + schedules = IScheduleContainer(self.context.section) |
417 | + if not schedules: |
418 | + return self.no_timetable_template() |
419 | + |
420 | + meetings = self.all_meetings |
421 | + if not meetings: |
422 | + return self.no_periods_template() |
423 | + |
424 | + if 'UPDATE_SUBMIT' in self.request: |
425 | + self.updateGradebook() |
426 | + |
427 | + app = ISchoolToolApplication(None) |
428 | + person_container = app['persons'] |
429 | + self.gradebook = queryMultiAdapter((person_container, self.request), |
430 | + ITableFormatter) |
431 | + |
432 | + self.selectStudents(self.gradebook) |
433 | + |
434 | + columns_before = [StudentNumberColumn(title=_('Nr.'), name='nr')] |
435 | + |
436 | + self.gradebook.setUp(items=self.members(), |
437 | + formatters=[SelectStudentCellFormatter(self.context)] * 2, |
438 | + columns_before=columns_before, |
439 | + columns_after=self.gradeColumns(), |
440 | + table_formatter=self.formatterFactory, |
441 | + batch_size=0) |
442 | + |
443 | + return self.template() |
444 | + |
445 | + def getLegendItems(self): |
446 | + for grade in journal_grades(): |
447 | + yield {'keys': u', '.join(grade['keys']), |
448 | + 'value': grade['value'], |
449 | + 'description': grade['legend']} |
450 | + |
451 | + def encodedSelectedEventId(self): |
452 | + event = self.selectedEvent() |
453 | + if event: |
454 | + return urllib.quote(base64.encodestring(event.unique_id.encode('utf-8'))) |
455 | + |
456 | + def formatterFactory(self, *args, **kwargs): |
457 | + kwargs['selected_items'] = self.selected_students |
458 | + return SelectableRowTableFormatter(*args, **kwargs) |
459 | + |
460 | + def allMeetings(self): |
461 | + term = removeSecurityProxy(self.selected_term) |
462 | + |
463 | + # maybe expand would be better in here |
464 | + by_uid = dict([(removeSecurityProxy(e).unique_id, e) |
465 | + for e in self.context.meetings]) |
466 | + |
467 | + insecure_events = [removeSecurityProxy(e) |
468 | + for e in by_uid.values()] |
469 | + insecure_events[:] = filter(lambda e: e.dtstart.date() in term, |
470 | + insecure_events) |
471 | + meetings = [by_uid[e.unique_id] for e in sorted(insecure_events)] |
472 | + return meetings |
473 | + |
474 | + @Lazy |
475 | + def all_meetings(self): |
476 | + return self.allMeetings() |
477 | + |
478 | + def meetings(self): |
479 | + for event in self.all_meetings: |
480 | + insecure_event = removeSecurityProxy(event) |
481 | + if insecure_event.dtstart.date().month == self.active_month: |
482 | + yield event |
483 | + |
484 | + def members(self): |
485 | + members = list(self.context.members) |
486 | + collator = ICollator(self.request.locale) |
487 | + members.sort(key=lambda a: collator.key( |
488 | + removeSecurityProxy(a).first_name)) |
489 | + members.sort(key=lambda a: collator.key( |
490 | + removeSecurityProxy(a).last_name)) |
491 | + return members |
492 | + |
493 | + def updateGradebook(self): |
494 | + members = self.members() |
495 | + for meeting in self.meetings(): |
496 | + for person in members: |
497 | + cell_id = "%s.%s" % (person.__name__, meeting.__name__) |
498 | + cell_value = self.request.get(cell_id, None) |
499 | + if cell_value is not None: |
500 | + cell_value = ATTENDANCE_TRANSLATION_TO_DATA.get(cell_value, cell_value) |
501 | + self.context.setGrade(person, meeting, cell_value) |
502 | + |
503 | + def gradeColumns(self): |
504 | + columns = [] |
505 | + selected_meeting = self.selectedEvent() |
506 | + for meeting in self.meetings(): |
507 | + # Arguably anyone who can look at this journal |
508 | + # should be able to look at meeting grades |
509 | + insecure_meeting = removeSecurityProxy(meeting) |
510 | + selected = selected_meeting and selected_meeting == insecure_meeting |
511 | + columns.append(PersonGradesColumn(insecure_meeting, self.context, |
512 | + selected=selected)) |
513 | + columns.append(SectionTermAverageGradesColumn(self.context, |
514 | + self.selected_term)) |
515 | + columns.append(SectionTermAttendanceColumn(self.context, |
516 | + self.selected_term)) |
517 | + columns.append(SectionTermTardiesColumn(self.context, |
518 | + self.selected_term)) |
519 | + return columns |
520 | + |
521 | + def getSelectedTerm(self): |
522 | + terms = ITermContainer(self.context) |
523 | + term_id = self.request.get('TERM', None) |
524 | + if term_id and term_id in terms: |
525 | + term = terms[term_id] |
526 | + if term in self.scheduled_terms: |
527 | + return term |
528 | + |
529 | + return self.getCurrentTerm() |
530 | + |
531 | + @Lazy |
532 | + def selected_term(self): |
533 | + return self.getSelectedTerm() |
534 | + |
535 | + def selectedEvent(self): |
536 | + event_id = self.request.get('event_id', None) |
537 | + if event_id is not None: |
538 | + try: |
539 | + return self.context.findMeeting(event_id) |
540 | + except KeyError: |
541 | + pass |
542 | + |
543 | + def selectedDate(self): |
544 | + event = self.selectedEvent() |
545 | + if event: |
546 | + app = ISchoolToolApplication(None) |
547 | + tzinfo = pytz.timezone(IApplicationPreferences(app).timezone) |
548 | + date = event.dtstart.astimezone(tzinfo).date() |
549 | + return date |
550 | + else: |
551 | + return getUtility(IDateManager).today |
552 | + |
553 | + def getCurrentTerm(self): |
554 | + event = self.selectedEvent() |
555 | + if event: |
556 | + calendar = event.__parent__ |
557 | + owner = calendar.__parent__ |
558 | + term = ITerm(owner) |
559 | + return term |
560 | + return self.scheduled_terms[-1] |
561 | + |
562 | + @property |
563 | + def scheduled_terms(self): |
564 | + linked_sections = self.context.section.linked_sections |
565 | + linked_sections = [section for section in linked_sections |
566 | + if IScheduleContainer(section)] |
567 | + terms = [ITerm(section) for section in linked_sections] |
568 | + return sorted(terms, key=lambda term: term.last) |
569 | + |
570 | + def monthsInSelectedTerm(self): |
571 | + month = -1 |
572 | + for meeting in self.all_meetings: |
573 | + insecure_meeting = removeSecurityProxy(meeting) |
574 | + # XXX: what about time zones? |
575 | + if insecure_meeting.dtstart.date().month != month: |
576 | + yield insecure_meeting.dtstart.date().month |
577 | + month = insecure_meeting.dtstart.date().month |
578 | + |
579 | + @Lazy |
580 | + def selected_months(self): |
581 | + return list(self.monthsInSelectedTerm()) |
582 | + |
583 | + def monthTitle(self, number): |
584 | + return translate(month_names[number], context=self.request) |
585 | + |
586 | + def monthURL(self, month_id): |
587 | + url = absoluteURL(self.context, self.request) |
588 | + url = "%s/index.html?%s" % ( |
589 | + url, |
590 | + urllib.urlencode([('month', month_id)] + |
591 | + self.extra_parameters(self.request))) |
592 | + return url |
593 | + |
594 | + @Lazy |
595 | + def active_year(self): |
596 | + event = self.selectedEvent() |
597 | + if event: |
598 | + return event.dtstart.year |
599 | + |
600 | + available_months = list(self.selected_months) |
601 | + selected_month = None |
602 | + if 'month' in self.request: |
603 | + month = int(self.request['month']) |
604 | + if month in available_months: |
605 | + selected_month = month |
606 | + |
607 | + if not selected_month: |
608 | + selected_month = available_months[0] |
609 | + |
610 | + for meeting in self.all_meetings: |
611 | + insecure_meeting = removeSecurityProxy(meeting) |
612 | + if insecure_meeting.dtstart.date().month == selected_month: |
613 | + return insecure_meeting.dtstart.year |
614 | + |
615 | + @Lazy |
616 | + def active_month(self): |
617 | + available_months = list(self.selected_months) |
618 | + if 'month' in self.request: |
619 | + month = int(self.request['month']) |
620 | + if month in available_months: |
621 | + return month |
622 | + |
623 | + term = self.selected_term |
624 | + date = self.selectedDate() |
625 | + if term.first <= date <= term.last: |
626 | + month = date.month |
627 | + if month in available_months: |
628 | + return month |
629 | + |
630 | + return available_months[0] |
631 | + |
632 | + def extra_parameters(self, request): |
633 | + parameters = [] |
634 | + for info in ['TERM', 'student']: |
635 | + if info in request: |
636 | + parameters.append((info, request[info].encode('utf-8'))) |
637 | + return parameters |
638 | + |
639 | + |
640 | +class SectionJournalAjaxView(BrowserView): |
641 | + |
642 | + def __call__(self): |
643 | + person_id = self.request['person_id'] |
644 | + app = ISchoolToolApplication(None) |
645 | + person = app['persons'].get(person_id) |
646 | + if not person: |
647 | + raise UserError('Person was invalid!') |
648 | + meeting = self.context.findMeeting(base64.decodestring(urllib.unquote(self.request['event_id'])).decode("utf-8")) |
649 | + grade = self.request['grade'] |
650 | + grade = ATTENDANCE_TRANSLATION_TO_DATA.get(grade, grade) |
651 | + self.context.setGrade(person, meeting, grade); |
652 | + return "" |
653 | + |
654 | + |
655 | +class SectionListView(BrowserView): |
656 | + |
657 | + def getSectionsForPerson(self, person): |
658 | + current_term = getUtility(IDateManager).current_term |
659 | + sections = IInstructor(person).sections() |
660 | + results = [] |
661 | + for section in sections: |
662 | + term = ITerm(section) |
663 | + if sameProxiedObjects(current_term, term): |
664 | + url = "%s/journal/" % absoluteURL(section, self.request) |
665 | + results.append({'title': removeSecurityProxy(section).title, |
666 | + 'url': url}) |
667 | + |
668 | + collator = ICollator(self.request.locale) |
669 | + results.sort(key=lambda s: collator.key(s['title'])) |
670 | + return results |
671 | + |
672 | + |
673 | +class TeacherJournalView(SectionListView): |
674 | + """A view that lists all the sections teacher is teaching to. |
675 | + |
676 | + The links go to the journals of these sections and only sections |
677 | + in the current term are displayed. |
678 | + """ |
679 | + |
680 | + def getSections(self): |
681 | + return self.getSectionsForPerson(self.context) |
682 | + |
683 | + |
684 | +class TeacherJournalTabViewlet(SectionListView): |
685 | + implements(IViewlet) |
686 | + |
687 | + def enabled(self): |
688 | + person = IPerson(self.request.principal, None) |
689 | + if not person: |
690 | + return False |
691 | + return bool(list(self.getSectionsForPerson(person))) |
692 | + |
693 | + |
694 | +class JournalNavViewlet(flourish.page.LinkViewlet, SectionListView): |
695 | + |
696 | + @property |
697 | + def person(self): |
698 | + return IPerson(self.request.principal, None) |
699 | + |
700 | + @property |
701 | + def title(self): |
702 | + person = self.person |
703 | + if person is None: |
704 | + return '' |
705 | + taught_sections = list(self.getSectionsForPerson(person)) |
706 | + learner_sections = list(ILearner(person).sections()) |
707 | + if not (taught_sections or learner_sections): |
708 | + return '' |
709 | + return _('Journal') |
710 | + |
711 | + @property |
712 | + def url(self): |
713 | + if self.person is None: |
714 | + return '' |
715 | + base_url = absoluteURL(self.person, self.request) |
716 | + return '%s/journal.html' % base_url |
717 | + |
718 | + def getSectionsForPerson(self, person): |
719 | + return list(IInstructor(person).sections()) |
720 | + |
721 | + |
722 | +class StudentGradebookTabViewlet(object): |
723 | + implements(IViewlet) |
724 | + |
725 | + def enabled(self): |
726 | + person = IPerson(self.request.principal, None) |
727 | + if not person: |
728 | + return False |
729 | + return bool(list(ILearner(person).sections())) |
730 | + |
731 | + |
732 | +class FlourishLyceumSectionJournalView(flourish.page.WideContainerPage, |
733 | + LyceumSectionJournalView): |
734 | + |
735 | + has_header = False |
736 | + page_class = 'page grid' |
737 | + no_timetable = False |
738 | + no_periods = False |
739 | + render_journal = True |
740 | + |
741 | + def updateGradebook(self): |
742 | + members = self.members() |
743 | + for meeting in self.meetings: |
744 | + for person in members: |
745 | + cell_id = "%s_%s" % (meeting.__name__, person.__name__) |
746 | + cell_value = self.request.get(cell_id, None) |
747 | + if cell_value is not None: |
748 | + cell_value = ATTENDANCE_TRANSLATION_TO_DATA.get(cell_value, cell_value) |
749 | + self.context.setGrade(person, meeting, cell_value) |
750 | + |
751 | + def update(self): |
752 | + schedules = IScheduleContainer(self.context.section) |
753 | + if not schedules: |
754 | + self.no_timetable = True |
755 | + self.render_journal = False |
756 | + return |
757 | + |
758 | + meetings = self.all_meetings |
759 | + if not meetings: |
760 | + self.no_periods = True |
761 | + self.render_journal = False |
762 | + return |
763 | + |
764 | + person = IPerson(self.request.principal, None) |
765 | + if person is not None: |
766 | + setCurrentSectionTaught(person, self.context.section) |
767 | + |
768 | + if 'UPDATE_SUBMIT' in self.request: |
769 | + self.updateGradebook() |
770 | + |
771 | + app = ISchoolToolApplication(None) |
772 | + self.tzinfo = pytz.timezone(IApplicationPreferences(app).timezone) |
773 | + |
774 | + def table(self): |
775 | + result = [] |
776 | + collator = ICollator(self.request.locale) |
777 | + for person in self.members(): |
778 | + grades = [] |
779 | + for meeting in self.meetings: |
780 | + insecure_meeting = removeSecurityProxy(meeting) |
781 | + grade = self.context.getGrade(person, insecure_meeting, default='') |
782 | + value = ATTENDANCE_DATA_TO_TRANSLATION.get(grade, grade) |
783 | + grade_data = { |
784 | + 'id': '%s_%s' % (meeting.__name__, person.__name__), |
785 | + 'sortKey': meeting.__name__, |
786 | + 'value': value, |
787 | + 'editable': True, |
788 | + } |
789 | + grades.append(grade_data) |
790 | + if flourish.canView(person): |
791 | + person = removeSecurityProxy(person) |
792 | + result.append( |
793 | + {'student': {'title': person.title, |
794 | + 'id': person.username, |
795 | + 'sortKey': collator.key(person.title), |
796 | + 'url': absoluteURL(person, self.request)}, |
797 | + 'grades': grades, |
798 | + 'average': self.average(person), |
799 | + 'absences': self.absences(person), |
800 | + 'tardies': self.tardies(person), |
801 | + }) |
802 | + self.sortBy = self.request.get('sort_by') |
803 | + return sorted(result, key=self.sortKey) |
804 | + |
805 | + def sortKey(self, row): |
806 | + if self.sortBy == 'student': |
807 | + return row['student']['sortKey'] |
808 | + elif self.sortBy == 'average': |
809 | + try: |
810 | + return (float(row['average']), row['student']['sortKey']) |
811 | + except (ValueError,): |
812 | + return ('', row['student']['sortKey']) |
813 | + elif self.sortBy == 'absences': |
814 | + return (int(row['absences']), row['student']['sortKey']) |
815 | + elif self.sortBy == 'tardies': |
816 | + return (int(row['tardies']), row['student']['sortKey']) |
817 | + else: |
818 | + grades = dict([(grade['sortKey'], grade['value']) |
819 | + for grade in row['grades']]) |
820 | + if self.sortBy in grades: |
821 | + grade = grades.get(self.sortBy) |
822 | + try: |
823 | + grade = int(grade) |
824 | + except (ValueError,): |
825 | + grade = ATTENDANCE_TRANSLATION_TO_DATA.get(grade) |
826 | + if grade == ABSENT: |
827 | + return (1, row['student']['sortKey']) |
828 | + elif grade == TARDY: |
829 | + return (2, row['student']['sortKey']) |
830 | + else: |
831 | + return (3, row['student']['sortKey']) |
832 | + return (0, grade, row['student']['sortKey']) |
833 | + else: |
834 | + return (1, row['student']['sortKey']) |
835 | + |
836 | + def activities(self): |
837 | + result = [] |
838 | + for meeting in self.meetings: |
839 | + info = {'hash': meeting.__name__} |
840 | + insecure_meeting = removeSecurityProxy(meeting) |
841 | + meetingDate = insecure_meeting.dtstart.astimezone(self.tzinfo).date() |
842 | + info['shortTitle'] = meetingDate.strftime("%d") |
843 | + info['longTitle'] = meetingDate.strftime("%Y-%m-%d") |
844 | + try: |
845 | + if meeting.period is not None: |
846 | + short_title = meeting.period.title[:3] |
847 | + else: |
848 | + short_title = '' |
849 | + period = short_title |
850 | + if period[-1] == ':': |
851 | + period = period[:-1] |
852 | + except: |
853 | + period = '' |
854 | + info['period'] = period |
855 | + result.append(info) |
856 | + return result |
857 | + |
858 | + def getSelectedTerm(self): |
859 | + term = ITerm(self.context.section) |
860 | + if term in self.scheduled_terms: |
861 | + return term |
862 | + |
863 | + def getGrades(self, person): |
864 | + grades = [] |
865 | + term = self.selected_term |
866 | + for meeting in self.context.recordedMeetings(person): |
867 | + insecure_meeting = removeSecurityProxy(meeting) |
868 | + if insecure_meeting.dtstart.date() in term: |
869 | + grade = self.context.getGrade( |
870 | + person, insecure_meeting, default=None) |
871 | + if (grade is not None) and (grade.strip() != ""): |
872 | + grades.append(grade) |
873 | + return grades |
874 | + |
875 | + def average(self, person): |
876 | + grades = [] |
877 | + for grade in self.getGrades(person): |
878 | + try: |
879 | + grade = int(grade) |
880 | + except ValueError: |
881 | + continue |
882 | + grades.append(grade) |
883 | + if not grades: |
884 | + return _('N/A') |
885 | + else: |
886 | + return "%.1f" % (sum(grades) / float(len(grades))) |
887 | + |
888 | + def absences(self, person): |
889 | + absences = 0 |
890 | + for grade in self.getGrades(person): |
891 | + if (grade.strip().lower() == "n"): |
892 | + absences += 1 |
893 | + if absences == 0: |
894 | + return "0" |
895 | + else: |
896 | + return str(absences) |
897 | + |
898 | + def tardies(self, person): |
899 | + tardies = 0 |
900 | + for grade in self.getGrades(person): |
901 | + if (grade.strip().lower() == "p"): |
902 | + tardies += 1 |
903 | + |
904 | + if tardies == 0: |
905 | + return "0" |
906 | + else: |
907 | + return str(tardies) |
908 | + |
909 | + def breakJSString(self, origstr): |
910 | + newstr = unicode(origstr) |
911 | + newstr = newstr.replace('\n', '') |
912 | + newstr = newstr.replace('\r', '') |
913 | + newstr = "\\'".join(newstr.split("'")) |
914 | + newstr = '\\"'.join(newstr.split('"')) |
915 | + return newstr |
916 | + |
917 | + def scorableActivities(self): |
918 | + return self.activities() |
919 | + |
920 | + @property |
921 | + def warningText(self): |
922 | + return _('You have some changes that have not been saved. Click OK to save now or CANCEL to continue without saving.') |
923 | + |
924 | + @Lazy |
925 | + def meetings(self): |
926 | + result = [] |
927 | + for event in self.all_meetings: |
928 | + insecure_event = removeSecurityProxy(event) |
929 | + if insecure_event.dtstart.date().month == self.active_month: |
930 | + result.append(event) |
931 | + return result |
932 | + |
933 | + |
934 | +class JournalTertiaryNavigationManager(flourish.page.TertiaryNavigationManager): |
935 | + |
936 | + template = InlineViewPageTemplate(""" |
937 | + <ul tal:attributes="class view/list_class"> |
938 | + <li tal:repeat="item view/items" |
939 | + tal:attributes="class item/class" |
940 | + tal:content="structure item/viewlet"> |
941 | + </li> |
942 | + </ul> |
943 | + """) |
944 | + |
945 | + @Lazy |
946 | + def items(self): |
947 | + result = [] |
948 | + for month_id in self.view.selected_months: |
949 | + url = self.view.monthURL(month_id) |
950 | + title = self.view.monthTitle(month_id) |
951 | + result.append({ |
952 | + 'class': month_id == self.view.active_month and 'active' or None, |
953 | + 'viewlet': u'<a href="%s" title="%s">%s</a>' % (url, title, title), |
954 | + }) |
955 | + return result |
956 | + |
957 | + |
958 | +class FlourishJournalNavigationViewletBase(flourish.viewlet.Viewlet): |
959 | + |
960 | + @property |
961 | + def person(self): |
962 | + return IPerson(self.request.principal) |
963 | + |
964 | + def render(self, *args, **kw): |
965 | + return self.template(*args, **kw) |
966 | + |
967 | + def getUserSections(self): |
968 | + return list(IInstructor(self.person).sections()) |
969 | + |
970 | + |
971 | +class FlourishJournalYearNavigation(flourish.page.RefineLinksViewlet): |
972 | + """Journal year navigation viewlet.""" |
973 | + |
974 | + |
975 | +class FlourishJournalYearNavigationViewlet(FlourishJournalNavigationViewletBase): |
976 | + template = InlineViewPageTemplate(''' |
977 | + <form method="post" |
978 | + tal:attributes="action string:${context/@@absolute_url}"> |
979 | + <select name="currentYear" class="navigator" |
980 | + onchange="this.form.submit()"> |
981 | + <tal:block repeat="year view/getYears"> |
982 | + <option |
983 | + tal:attributes="value year/form_id; |
984 | + selected year/selected" |
985 | + tal:content="year/title" /> |
986 | + </tal:block> |
987 | + </select> |
988 | + </form> |
989 | + ''') |
990 | + |
991 | + def getYears(self): |
992 | + currentSection = self.context.section |
993 | + currentYear = ISchoolYear(ITerm(currentSection)) |
994 | + years = [] |
995 | + for section in self.getUserSections(): |
996 | + year = ISchoolYear(ITerm(section)) |
997 | + if year not in years: |
998 | + years.append(year) |
999 | + return [{'title': year.title, |
1000 | + 'form_id': year.__name__, |
1001 | + 'selected': year is currentYear and 'selected' or None} |
1002 | + for year in years] |
1003 | + |
1004 | + def update(self): |
1005 | + super(FlourishJournalYearNavigationViewlet, self).update() |
1006 | + if 'currentYear' in self.request: |
1007 | + currentSection = self.context.section |
1008 | + currentYear = ISchoolYear(ITerm(currentSection)) |
1009 | + requestYearId = self.request['currentYear'] |
1010 | + if requestYearId != currentYear.__name__: |
1011 | + for section in self.getUserSections(): |
1012 | + year = ISchoolYear(ITerm(section)) |
1013 | + if year.__name__ == requestYearId: |
1014 | + newSection = section |
1015 | + break |
1016 | + else: |
1017 | + return |
1018 | + url = absoluteURL(newSection, self.request) + '/journal' |
1019 | + self.request.response.redirect(url) |
1020 | + |
1021 | + |
1022 | +class FlourishJournalTermNavigation(flourish.page.RefineLinksViewlet): |
1023 | + """Journal term navigation viewlet.""" |
1024 | + |
1025 | + |
1026 | +class FlourishJournalTermNavigationViewlet(FlourishJournalNavigationViewletBase): |
1027 | + template = InlineViewPageTemplate(''' |
1028 | + <form method="post" |
1029 | + tal:attributes="action string:${context/@@absolute_url}"> |
1030 | + <select name="currentTerm" class="navigator" |
1031 | + onchange="this.form.submit()"> |
1032 | + <tal:block repeat="term view/getTerms"> |
1033 | + <option |
1034 | + tal:attributes="value term/form_id; |
1035 | + selected term/selected" |
1036 | + tal:content="term/title" /> |
1037 | + </tal:block> |
1038 | + </select> |
1039 | + </form> |
1040 | + ''') |
1041 | + |
1042 | + def getTerms(self): |
1043 | + currentSection = self.context.section |
1044 | + currentTerm = ITerm(currentSection) |
1045 | + currentYear = ISchoolYear(currentTerm) |
1046 | + terms = [] |
1047 | + for section in self.getUserSections(): |
1048 | + term = ITerm(section) |
1049 | + if term not in terms and ISchoolYear(term) == currentYear: |
1050 | + terms.append(term) |
1051 | + return [{'title': term.title, |
1052 | + 'form_id': self.getTermId(term), |
1053 | + 'selected': term is currentTerm and 'selected' or None} |
1054 | + for term in terms] |
1055 | + |
1056 | + def update(self): |
1057 | + super(FlourishJournalTermNavigationViewlet, self).update() |
1058 | + if 'currentTerm' in self.request: |
1059 | + currentSection = self.context.section |
1060 | + try: |
1061 | + currentCourse = list(currentSection.courses)[0] |
1062 | + except (IndexError,): |
1063 | + currentCourse = None |
1064 | + currentTerm = ITerm(currentSection) |
1065 | + requestTermId = self.request['currentTerm'] |
1066 | + if requestTermId != self.getTermId(currentTerm): |
1067 | + newSection = None |
1068 | + for section in self.getUserSections(): |
1069 | + term = ITerm(section) |
1070 | + if self.getTermId(term) == requestTermId: |
1071 | + try: |
1072 | + temp = list(section.courses)[0] |
1073 | + except (IndexError,): |
1074 | + temp = None |
1075 | + if currentCourse == temp: |
1076 | + newSection = section |
1077 | + break |
1078 | + if newSection is None: |
1079 | + newSection = section |
1080 | + url = absoluteURL(newSection, self.request) + '/journal' |
1081 | + self.request.response.redirect(url) |
1082 | + |
1083 | + def getTermId(self, term): |
1084 | + year = ISchoolYear(term) |
1085 | + return '%s.%s' % (simple_form_key(year), simple_form_key(term)) |
1086 | + |
1087 | + |
1088 | +class FlourishJournalSectionNavigation(flourish.page.RefineLinksViewlet): |
1089 | + """Journal section navigation viewlet.""" |
1090 | + |
1091 | + |
1092 | +class FlourishJournalSectionNavigationViewlet(FlourishJournalNavigationViewletBase): |
1093 | + template = InlineViewPageTemplate(''' |
1094 | + <form method="post" |
1095 | + tal:attributes="action string:${context/@@absolute_url}"> |
1096 | + <select name="currentSection" class="navigator" |
1097 | + onchange="this.form.submit()"> |
1098 | + <tal:block repeat="section view/getSections"> |
1099 | + <option |
1100 | + tal:attributes="value section/form_id; |
1101 | + selected section/selected;" |
1102 | + tal:content="section/title" /> |
1103 | + </tal:block> |
1104 | + </select> |
1105 | + </form> |
1106 | + ''') |
1107 | + |
1108 | + def getSections(self): |
1109 | + currentSection = self.context.section |
1110 | + currentTerm = ITerm(currentSection) |
1111 | + for section in self.getUserSections(): |
1112 | + term = ITerm(section) |
1113 | + if term != currentTerm: |
1114 | + continue |
1115 | + yield { |
1116 | + 'title': section.title, |
1117 | + 'form_id': self.getSectionId(section), |
1118 | + 'selected': section == currentSection and 'selected' or None, |
1119 | + } |
1120 | + |
1121 | + def getSectionId(self, section): |
1122 | + term = ITerm(section) |
1123 | + year = ISchoolYear(term) |
1124 | + return '%s.%s.%s' % (simple_form_key(year), simple_form_key(term), |
1125 | + simple_form_key(section)) |
1126 | + |
1127 | + def update(self): |
1128 | + super(FlourishJournalSectionNavigationViewlet, self).update() |
1129 | + if 'currentSection' in self.request: |
1130 | + for section in self.getUserSections(): |
1131 | + if self.getSectionId(section) == self.request['currentSection']: |
1132 | + if section == self.context.section: |
1133 | + break |
1134 | + url = absoluteURL(section, self.request) + '/journal' |
1135 | + self.request.response.redirect(url) |
1136 | + return |
1137 | + |
1138 | + |
1139 | +class FlourishJournalRedirectView(flourish.page.Page): |
1140 | + |
1141 | + def render(self): |
1142 | + url = absoluteURL(self.context, self.request) |
1143 | + person = IPerson(self.request.principal, None) |
1144 | + if person is not None: |
1145 | + section = getCurrentSectionTaught(person) |
1146 | + if section is None: |
1147 | + sections = list(IInstructor(person).sections()) |
1148 | + if sections: |
1149 | + section = sections[0] |
1150 | + if section is not None: |
1151 | + url = absoluteURL(section, self.request) + '/journal' |
1152 | + self.request.response.redirect(url) |
1153 | + |
1154 | + |
1155 | +class FlourishJournalActionsLinks(flourish.page.RefineLinksViewlet): |
1156 | + """Journal action links viewlet.""" |
1157 | + |
1158 | + |
1159 | +class FlourishJournalHelpViewlet(flourish.page.ModalFormLinkViewlet): |
1160 | + |
1161 | + @property |
1162 | + def dialog_title(self): |
1163 | + title = _(u'Journal Help') |
1164 | + return translate(title, context=self.request) |
1165 | + |
1166 | + |
1167 | +class FlourishJournalHelpView(flourish.form.Dialog): |
1168 | + |
1169 | + def updateDialog(self): |
1170 | + # XXX: fix the width of dialog content in css |
1171 | + if self.ajax_settings['dialog'] != 'close': |
1172 | + self.ajax_settings['dialog']['width'] = 144 + 16 |
1173 | + |
1174 | + def initDialog(self): |
1175 | + self.ajax_settings['dialog'] = { |
1176 | + 'autoOpen': True, |
1177 | + 'modal': False, |
1178 | + 'resizable': True, |
1179 | + 'draggable': True, |
1180 | + 'position': ['center','middle'], |
1181 | + 'width': 'auto', |
1182 | + } |
1183 | + |
1184 | + def getLegendItems(self): |
1185 | + for grade in journal_grades(): |
1186 | + yield {'keys': u', '.join(grade['keys']), |
1187 | + 'value': grade['value'], |
1188 | + 'description': grade['legend']} |
1189 | + |
1190 | + |
1191 | +class SectionJournalLinkViewlet(flourish.page.LinkViewlet): |
1192 | + |
1193 | + @Lazy |
1194 | + def journal(self): |
1195 | + journal = ISectionJournal(self.context, None) |
1196 | + return journal |
1197 | + |
1198 | + @property |
1199 | + def url(self): |
1200 | + journal = self.journal |
1201 | + if journal is None: |
1202 | + return None |
1203 | + return absoluteURL(journal, self.request) |
1204 | + |
1205 | + @property |
1206 | + def enabled(self): |
1207 | + if not super(SectionJournalLinkViewlet, self).enabled: |
1208 | + return False |
1209 | + journal = self.journal |
1210 | + if journal is None: |
1211 | + return None |
1212 | + can_view = flourish.canView(journal) |
1213 | + return can_view |
1214 | |
1215 | === added directory '.pc/cells-css.patch/src/schooltool/lyceum/journal/browser/templates' |
1216 | === added file '.pc/cells-css.patch/src/schooltool/lyceum/journal/browser/templates/f_journal.pt' |
1217 | --- .pc/cells-css.patch/src/schooltool/lyceum/journal/browser/templates/f_journal.pt 1970-01-01 00:00:00 +0000 |
1218 | +++ .pc/cells-css.patch/src/schooltool/lyceum/journal/browser/templates/f_journal.pt 2011-12-23 19:10:30 +0000 |
1219 | @@ -0,0 +1,231 @@ |
1220 | +<div i18n:domain="schooltool.lyceum.journal" |
1221 | + tal:define="table view/table; |
1222 | + activities view/activities"> |
1223 | + <tal:block replace="resource_library:schooltool.lyceum.journal.flourish" /> |
1224 | + <div tal:condition="not:view/render_journal"> |
1225 | + <h3 tal:condition="view/no_timetable" i18n:translate=""> |
1226 | + This section is not scheduled for any term, to use the journal |
1227 | + you should add a timetable first. |
1228 | + </h3> |
1229 | + <h3 tal:condition="view/no_periods" i18n:translate=""> |
1230 | + No periods have been assigned in timetables of this section. |
1231 | + </h3> |
1232 | + <h3 i18n:translate=""> |
1233 | + You can manage timetables for this section here: |
1234 | + <a i18n:name="timetable_link" |
1235 | + tal:attributes="href string:${view/context/section/@@absolute_url}/schedule" |
1236 | + i18n:translate="">Schedule</a> |
1237 | + </h3> |
1238 | + </div> |
1239 | + <metal:block tal:replace="structure string:<script type="text/javascript">" /> |
1240 | + var numstudents = <tal:block replace="python: len(view.members())"/>; |
1241 | + var students = new Array(numstudents); |
1242 | + <tal:loop repeat="row table"> |
1243 | + students[<tal:block replace="repeat/row/index"/>] = '<tal:block replace="python: view.breakJSString(row['student']['id'])"/>'; |
1244 | + </tal:loop> |
1245 | + var numactivities = <tal:block replace="python: len(view.scorableActivities())"/>; |
1246 | + var activities = new Array(numactivities); |
1247 | + <tal:loop repeat="activity activities"> |
1248 | + activities[<tal:block replace="repeat/activity/index"/>] = '<tal:block replace="activity/hash"/>'; |
1249 | + </tal:loop> |
1250 | + var edited = false; |
1251 | + var currentCell; |
1252 | + var currentDesc = ''; |
1253 | + window.onload = onLoadHandler; |
1254 | + window.onunload = checkChanges; |
1255 | + warningText = '<tal:block replace="view/warningText" />'; |
1256 | + <metal:block tal:replace="structure string:</script>" /> |
1257 | + <form method="post" class="grid-form" |
1258 | + tal:condition="view/render_journal" |
1259 | + tal:attributes="action request/URL"> |
1260 | + |
1261 | + <input tal:condition="request/month|nothing" |
1262 | + type="hidden" |
1263 | + name="month" |
1264 | + tal:attributes="value request/month" /> |
1265 | + <input tal:condition="request/event_id|nothing" |
1266 | + type="hidden" |
1267 | + name="event_id" |
1268 | + tal:attributes="value request/event_id" /> |
1269 | + <input tal:condition="request/student|nothing" |
1270 | + type="hidden" |
1271 | + name="student" |
1272 | + tal:attributes="value request/student" /> |
1273 | + |
1274 | + <div class="gradebook"> |
1275 | + <div class="students gradebook-part"> |
1276 | + <table> |
1277 | + <thead> |
1278 | + <tr> |
1279 | + <th rowspan="2" class="name"> |
1280 | + <ul class="popup_menu"> |
1281 | + <li> |
1282 | + <span i18n:translate="">Name</span> |
1283 | + </li> |
1284 | + <li> |
1285 | + <a href="?sort_by=student" i18n:translate="">Sort by</a> |
1286 | + </li> |
1287 | + </ul> |
1288 | + <div> |
1289 | + <a class="popup_link" href="" i18n:translate="">Name</a> |
1290 | + </div> |
1291 | + </th> |
1292 | + <th i18n:translate="" class="activity-title">Day</th> |
1293 | + </tr> |
1294 | + <tr> |
1295 | + <th i18n:translate="">Period</th> |
1296 | + </tr> |
1297 | + </thead> |
1298 | + <tbody> |
1299 | + <tr tal:repeat="row table"> |
1300 | + <td colspan="2"> |
1301 | + <div> |
1302 | + <a href="" |
1303 | + tal:attributes="title row/student/title" |
1304 | + tal:content="row/student/title" /> |
1305 | + </div> |
1306 | + </td> |
1307 | + </tr> |
1308 | + </tbody> |
1309 | + </table> |
1310 | + </div> |
1311 | + <div class="grades gradebook-part"> |
1312 | + <table> |
1313 | + <thead> |
1314 | + <tr> |
1315 | + <th tal:repeat="activity activities" class="activity-title"> |
1316 | + <ul class="popup_menu"> |
1317 | + <li> |
1318 | + <span tal:content="activity/longTitle" /> |
1319 | + </li> |
1320 | + <li> |
1321 | + <a tal:attributes="href string:${request/URL}?sort_by=${activity/hash}" |
1322 | + i18n:translate="">Sort by</a> |
1323 | + </li> |
1324 | + </ul> |
1325 | + <div> |
1326 | + <a class="popup_link" |
1327 | + href="" |
1328 | + tal:attributes="title activity/longTitle; |
1329 | + href request/URL;" |
1330 | + tal:content="activity/shortTitle" /> |
1331 | + </div> |
1332 | + </th> |
1333 | + <th class="placeholder"> </th> |
1334 | + </tr> |
1335 | + <tr> |
1336 | + <th tal:repeat="activity activities"> |
1337 | + <div tal:content="activity/period" /> |
1338 | + </th> |
1339 | + <th class="placeholder"> </th> |
1340 | + </tr> |
1341 | + </thead> |
1342 | + <tbody> |
1343 | + <metal:block tal:repeat="row table"> |
1344 | + <tr> |
1345 | + <td tal:repeat="grade row/grades" |
1346 | + tal:attributes="id string:${grade/id}_cell"> |
1347 | + <span tal:condition="not: grade/editable" |
1348 | + tal:content="grade/value" /> |
1349 | + <input type="text" name="" value="" size="4" |
1350 | + tal:condition="grade/editable" |
1351 | + onkeydown="return spreadsheetBehaviour(event)" |
1352 | + tal:attributes="name string:${grade/id}; |
1353 | + id string:${grade/id}; |
1354 | + value grade/value; |
1355 | + onkeyup string:checkValid(event,'${grade/id}'); |
1356 | + onfocus string:handleCellFocus(this, '${grade/id}')"/> |
1357 | + </td> |
1358 | + <td class="placeholder"> </td> |
1359 | + </tr> |
1360 | + </metal:block> |
1361 | + </tbody> |
1362 | + </table> |
1363 | + </div> |
1364 | + <div class="totals gradebook-part"> |
1365 | + <table> |
1366 | + <thead> |
1367 | + <tr> |
1368 | + <th rowspan="2"> |
1369 | + <ul class="popup_menu"> |
1370 | + <li> |
1371 | + <span i18n:translate="">Name</span> |
1372 | + </li> |
1373 | + <li> |
1374 | + <a href="?sort_by=absences" i18n:translate="">Sort by</a> |
1375 | + </li> |
1376 | + </ul> |
1377 | + <div> |
1378 | + <a class="popup_link" href="" i18n:translate=""> |
1379 | + Abs. |
1380 | + </a> |
1381 | + </div> |
1382 | + </th> |
1383 | + <th rowspan="2"> |
1384 | + <ul class="popup_menu"> |
1385 | + <li> |
1386 | + <span i18n:translate="">Name</span> |
1387 | + </li> |
1388 | + <li> |
1389 | + <a href="?sort_by=tardies" i18n:translate="">Sort by</a> |
1390 | + </li> |
1391 | + </ul> |
1392 | + <div> |
1393 | + <a class="popup_link" href="" i18n:translate=""> |
1394 | + Tar. |
1395 | + </a> |
1396 | + </div> |
1397 | + </th> |
1398 | + <th rowspan="2"> |
1399 | + <ul class="popup_menu"> |
1400 | + <li> |
1401 | + <span i18n:translate="">Name</span> |
1402 | + </li> |
1403 | + <li> |
1404 | + <a href="?sort_by=average" i18n:translate="">Sort by</a> |
1405 | + </li> |
1406 | + </ul> |
1407 | + <div> |
1408 | + <a class="popup_link" href="" i18n:translate=""> |
1409 | + Ave. |
1410 | + </a> |
1411 | + </div> |
1412 | + </th> |
1413 | + </tr> |
1414 | + <tr> |
1415 | + </tr> |
1416 | + </thead> |
1417 | + <tbody> |
1418 | + <tr tal:repeat="row table"> |
1419 | + <td tal:content="row/absences" /> |
1420 | + <td tal:content="row/tardies" /> |
1421 | + <td tal:content="row/average" /> |
1422 | + </tr> |
1423 | + </tbody> |
1424 | + </table> |
1425 | + </div> |
1426 | + </div> |
1427 | + <div class="gradebook-controls"> |
1428 | + <div class="buttons"> |
1429 | + <input type="submit" class="button-ok" name="UPDATE_SUBMIT" value="Save" |
1430 | + onclick="setNotEdited()" |
1431 | + title="Shortcut: Alt-S" accesskey="S" |
1432 | + i18n:attributes="value; title; accesskey" /> |
1433 | + </div> |
1434 | + <div class="buttons zoom-buttons"> |
1435 | + <button type="button" class="button-neutral zoom-button" id="zoom-out" |
1436 | + title="Zoom Out" i18n:attributes="title"> |
1437 | + <span class="ui-icon ui-icon-zoomout"></span> |
1438 | + </button> |
1439 | + <button type="button" class="button-neutral zoom-button" id="zoom-normal" |
1440 | + title="Zoom Normal" i18n:attributes="title"> |
1441 | + <span class="ui-icon ui-icon-search"></span> |
1442 | + </button> |
1443 | + <button type="button" class="button-neutral zoom-button" id="zoom-in" |
1444 | + title="Zoom In" i18n:attributes="title"> |
1445 | + <span class="ui-icon ui-icon-zoomin"></span> |
1446 | + </button> |
1447 | + </div> |
1448 | + </div> |
1449 | + </form> |
1450 | +</div> |