Merge lp:~ubuntu-branches/ubuntu/precise/schooltool.lyceum.journal/precise-201112231905 into lp:ubuntu/precise/schooltool.lyceum.journal

Proposed by Ubuntu Package Importer
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
Reviewer Review Type Date Requested Status
Ubuntu branches Pending
Review via email: mp+86826@code.launchpad.net

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://bugs.launchpad.net/udd linking to this merge proposal.

(this is an automatically generated message)

To post a comment you must log in.

Unmerged revisions

10. By Gediminas Paulauskas

* New upstream release.
* debian/patches/cells-css.patch: remove, fixed upstream.
* debian/rules: install upstream changelog.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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:&lt;script type=&quot;text/javascript&quot;&gt;" />
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:&lt;/script&gt;" />
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">&nbsp;</th>
1334+ </tr>
1335+ <tr>
1336+ <th tal:repeat="activity activities">
1337+ <div tal:content="activity/period" />
1338+ </th>
1339+ <th class="placeholder">&nbsp;</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">&nbsp;</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>

Subscribers

People subscribed via source and target branches

to all changes: