Merge lp:~justas.sadzevicius/schooltool/timetables-section-time-boundaries into lp:schooltool/1.7

Proposed by Justas Sadzevičius
Status: Merged
Merged at revision: 2687
Proposed branch: lp:~justas.sadzevicius/schooltool/timetables-section-time-boundaries
Merge into: lp:schooltool/1.7
Diff against target: 1656 lines (+928/-262)
20 files modified
src/schooltool/app/browser/ftests/setup.py (+2/-1)
src/schooltool/app/utils.py (+42/-0)
src/schooltool/course/browser/section.py (+3/-24)
src/schooltool/course/browser/tests.py (+0/-119)
src/schooltool/level/level.py (+3/-28)
src/schooltool/level/tests/test_level.py (+2/-2)
src/schooltool/timetable/__init__.py (+133/-10)
src/schooltool/timetable/browser/__init__.py (+344/-42)
src/schooltool/timetable/browser/configure.zcml (+28/-7)
src/schooltool/timetable/browser/ftests/composite-timetables.txt (+4/-0)
src/schooltool/timetable/browser/ftests/timetable-events.txt (+6/-0)
src/schooltool/timetable/browser/ftests/timetabling.txt (+5/-5)
src/schooltool/timetable/browser/templates/confirm-timetable-delete.pt (+89/-0)
src/schooltool/timetable/browser/templates/section-timetable-add.pt (+65/-0)
src/schooltool/timetable/browser/templates/section-timetable-view.pt (+112/-0)
src/schooltool/timetable/browser/templates/timetable-edit.pt (+69/-16)
src/schooltool/timetable/configure.zcml (+8/-0)
src/schooltool/timetable/interfaces.py (+6/-0)
src/schooltool/timetable/model.py (+7/-4)
src/schooltool/timetable/tests/test_timetable.py (+0/-4)
To merge this branch: bzr merge lp:~justas.sadzevicius/schooltool/timetables-section-time-boundaries
Reviewer Review Type Date Requested Status
Gediminas Paulauskas (community) Abstain
Review via email: mp+29090@code.launchpad.net

Description of the change

Please review and merge to trunk, then release to the development PPA.

To post a comment you must log in.
Revision history for this message
Gediminas Paulauskas (menesis) wrote :

Five tests in schooltool.timetable.tests.test_model now fail with

Traceback (most recent call last):
  ...
  File "/home/menesis/src/schooltool/trunk/build/schooltool-timetables/src/schooltool/timetable/model.py", line 130, in createCalendar
    if not first <= date <= last:
TypeError: can't compare datetime.date to NoneType

review: Needs Fixing
Revision history for this message
Gediminas Paulauskas (menesis) wrote :

I have fixed the test failures and merged the branch.

I don't like that the UI changed so that everyone now has to click "Add Timetable" even when there is one timetable for the school.

This breaks Journal tests as well.

review: Abstain

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/schooltool/app/browser/ftests/setup.py'
2--- src/schooltool/app/browser/ftests/setup.py 2009-06-24 13:58:38 +0000
3+++ src/schooltool/app/browser/ftests/setup.py 2010-07-02 14:38:26 +0000
4@@ -252,7 +252,8 @@
5
6 manager.open('http://localhost/schoolyears/2005-2006/2005-fall/sections/1')
7 manager.getLink('Schedule').click()
8-
9+ manager.getLink('Add Timetable').click()
10+ manager.getControl('Add').click()
11 manager.getControl(name="Monday.09:30-10:25").value = True
12 manager.getControl(name="Wednesday.11:35-12:20").value = True
13 manager.getControl('Save').click()
14
15=== modified file 'src/schooltool/app/utils.py'
16--- src/schooltool/app/utils.py 2009-06-24 11:55:24 +0000
17+++ src/schooltool/app/utils.py 2010-07-02 14:38:26 +0000
18@@ -22,6 +22,10 @@
19 """
20
21 import zope.schema
22+from zope.proxy import sameProxiedObjects
23+from zope.interface import implements
24+
25+from schooltool.table.table import simple_form_key
26
27
28 def vocabulary(choices):
29@@ -64,3 +68,41 @@
30 token=unicode(item.__name__).encode('punycode'),
31 title=item.title)
32 for item in items])
33+
34+
35+class TitledContainerItemSource(object):
36+ """Source of titled items in a container."""
37+ implements(zope.schema.interfaces.IIterableSource)
38+
39+ def __init__(self, context):
40+ self.context = context
41+
42+ @property
43+ def container(self):
44+ raise NotImplementedError("Don't know how to get the container from context")
45+
46+ def __len__(self):
47+ return len(self.container)
48+
49+ def __contains__(self, item):
50+ return sameProxiedObjects(item, self.container.get(item.__name__))
51+
52+ def __iter__(self):
53+ for item in self.container.values():
54+ yield self.getTerm(item)
55+
56+ def getTermByToken(self, token):
57+ terms = [self.getTerm(item) for item in self.container.values()]
58+ by_token = dict([(term.token, term) for term in terms])
59+ if token not in by_token:
60+ raise LookupError(token)
61+ return by_token[token]
62+
63+ def getTerm(self, item):
64+ return zope.schema.vocabulary.SimpleTerm(
65+ item,
66+ token=simple_form_key(item),
67+ title=self.getTitle(item))
68+
69+ def getTitle(self, item):
70+ return item.title
71
72=== modified file 'src/schooltool/course/browser/section.py'
73--- src/schooltool/course/browser/section.py 2010-04-08 08:28:24 +0000
74+++ src/schooltool/course/browser/section.py 2010-07-02 14:38:26 +0000
75@@ -60,7 +60,6 @@
76 from schooltool.course.section import Section
77 from schooltool.course.section import copySection
78 from schooltool.app.browser.app import RelationshipViewBase
79-from schooltool.timetable.browser import TimetableConflictMixin
80 from schooltool.app.interfaces import ISchoolToolApplication
81 from schooltool.table.interfaces import ITableFormatter
82
83@@ -501,23 +500,7 @@
84 return BaseEditView.update(self)
85
86
87-class ConflictDisplayMixin(TimetableConflictMixin):
88- """A mixin for use in views that display event conflicts."""
89-
90- def update(self):
91- """Set self.busy_periods."""
92- ttschema = self.getSchema()
93- term = self.getTerm()
94- if ttschema and term:
95- section_map = self.sectionMap(term, ttschema)
96- self.busy_periods = [(key, sections)
97- for key, sections in section_map.items()
98- if self.context in sections]
99- else:
100- self.busy_periods = []
101-
102-
103-class RelationshipEditConfView(RelationshipViewBase, ConflictDisplayMixin):
104+class RelationshipEditConfView(RelationshipViewBase):
105 """A relationship editing view that displays conflicts."""
106
107 __call__ = ViewPageTemplateFile('templates/edit_relationships.pt')
108@@ -530,12 +513,8 @@
109 def school_year(self):
110 return ISchoolYear(self.context)
111
112- def update(self):
113- RelationshipViewBase.update(self)
114- ConflictDisplayMixin.update(self)
115-
116-
117-class SectionInstructorView(RelationshipEditConfView, ConflictDisplayMixin):
118+
119+class SectionInstructorView(RelationshipEditConfView):
120 """View for adding instructors to a Section."""
121
122 __used_for__ = ISection
123
124=== modified file 'src/schooltool/course/browser/tests.py'
125--- src/schooltool/course/browser/tests.py 2010-04-08 05:42:14 +0000
126+++ src/schooltool/course/browser/tests.py 2010-07-02 14:38:26 +0000
127@@ -678,125 +678,6 @@
128 """
129
130
131-def doctest_ConflictDisplayMixin():
132- r"""Tests for ConflictDisplayMixin.
133-
134-# >>> app = setup.setUpSchoolToolSite()
135-#
136-# >>> class ItemStub(object):
137-# ... def __init__(self, name):
138-# ... self.__name__ = name
139-# ... self.title = name.title()
140-# >>> class RelationshipPropertyStub(object):
141-# ... items = [ItemStub('john'),
142-# ... ItemStub('pete')]
143-# ... def __iter__(self):
144-# ... return iter(self.items)
145-# ... def add(self, item):
146-# ... print "Adding: %s" % item.title
147-# ... def remove(self, item):
148-# ... print "Removing: %s" % item.title
149-#
150-# Inheriting views must implement getCollection() and
151-# getAvailableItems():
152-#
153-# >>> from schooltool.course.browser.section import ConflictDisplayMixin
154-# >>> class SchemaStub(ItemStub):
155-# ... def items(self):
156-# ... return []
157-# >>> class RelationshipView(ConflictDisplayMixin):
158-# ... def getCollection(self):
159-# ... return RelationshipPropertyStub()
160-# ... def getAvailableItems(self):
161-# ... return [ItemStub('ann'), ItemStub('frog')]
162-# ... def getTerm(self): return ItemStub('does not matter')
163-# ... def getSchema(self): return SchemaStub('does not matter')
164-#
165-# Let's add Ann to the list:
166-#
167-# >>> request = TestRequest()
168-# >>> request.form = {'add_item.ann': 'on',
169-# ... 'ADD_ITEMS': 'Apply'}
170-# >>> view = RelationshipView(None, request)
171-# >>> view.update()
172-# Adding: Ann
173-#
174-# Someone might want to cancel a change.
175-#
176-# >>> request = TestRequest()
177-# >>> request.form = {'add_item.ann': 'on', 'CANCEL': 'Cancel'}
178-# >>> view = RelationshipView(None, request)
179-# >>> view.update()
180-#
181-# No one was added, but we got redirected:
182-#
183-# >>> request.response.getStatus()
184-# 302
185-# >>> request.response.getHeader('Location')
186-# 'http://127.0.0.1'
187-#
188-# We can remove items too:
189-#
190-# >>> request.form = {'remove_item.john': 'on',
191-# ... 'remove_item.pete': 'on',
192-# ... 'REMOVE_ITEMS': 'Remove'}
193-# >>> view = RelationshipView(None, request)
194-# >>> view.update()
195-# Removing: John
196-# Removing: Pete
197-#
198-# We also use a batch for available items in this view
199-#
200-# >>> [i.title for i in view.batch]
201-# ['Ann', 'Frog']
202-#
203-# Which is searchable
204-#
205-# >>> request.form = {'SEARCH': 'ann'}
206-# >>> view = RelationshipView(None, request)
207-# >>> view.update()
208-# >>> [i.title for i in view.batch]
209-# ['Ann']
210-#
211-# The search can be cleared, ignoring any search value passed:
212-#
213-# >>> request.form = {'SEARCH': 'ann', 'CLEAR_SEARCH': 'on'}
214-# >>> view = RelationshipView(None, request)
215-# >>> view.update()
216-# >>> [i.title for i in view.batch]
217-# ['Ann', 'Frog']
218-
219- """
220-
221-
222-def doctest_ConflictDisplayMixin_no_timetables_terms():
223- r"""Tests for ConflictDisplayMixin.
224-
225- ConflictDisplayMixin should work even if there are no timetables
226- defined.
227-
228- >>> from schooltool.course.browser.section import ConflictDisplayMixin
229- >>> app = setup.setUpSchoolToolSite()
230- >>> view = ConflictDisplayMixin()
231- >>> view.getSchema = lambda: None
232- >>> view.getTerm = lambda: "I am a term"
233- >>> view.getAvailableItems = lambda: []
234-
235- >>> view.update()
236- >>> view.busy_periods
237- []
238-
239- If there are no terms, but there are timetables - it still works:
240-
241- >>> view.getSchema = lambda: "I am a schema"
242- >>> view.getTerm = lambda: None
243- >>> view.update()
244- >>> view.busy_periods
245- []
246-
247- """
248-
249-
250 def doctest_SectionInstructorView():
251 """Tests for SectionInstructorView.
252
253
254=== modified file 'src/schooltool/level/level.py'
255--- src/schooltool/level/level.py 2010-05-19 11:57:50 +0000
256+++ src/schooltool/level/level.py 2010-07-02 14:38:26 +0000
257@@ -40,6 +40,7 @@
258
259 from schooltool.app.interfaces import ISchoolToolApplication
260 from schooltool.app.app import InitBase, StartUpBase
261+from schooltool.app.utils import TitledContainerItemSource
262 from schooltool.schoolyear.interfaces import ISchoolYearContainer, ISchoolYear
263 from schooltool.schoolyear.subscriber import ObjectEventAdapterSubscriber
264 from schooltool.relationship import URIObject, RelationshipSchema
265@@ -164,43 +165,17 @@
266 del top_level_container[level_container.__name__]
267
268
269-class LevelSource(object):
270+class LevelSource(TitledContainerItemSource):
271 """Source of levels for contexts adaptable to ISchoolYear"""
272 implements(zope.schema.interfaces.IIterableSource)
273
274- def __init__(self, context):
275- self.context = context
276-
277 @property
278- def levels(self):
279+ def container(self):
280 schoolyear = ISchoolYear(self.context, None)
281 if schoolyear is None:
282 return {}
283 return interfaces.ILevelContainer(schoolyear)
284
285- def __len__(self):
286- return len(self.levels)
287-
288- def __contains__(self, level):
289- return sameProxiedObjects(level, self.levels.get(level.__name__))
290-
291- def __iter__(self):
292- for level in self.levels.values():
293- yield self.getTerm(level)
294-
295- def getTermByToken(self, token):
296- terms = [self.getTerm(level) for level in self.levels.values()]
297- by_token = dict([(term.token, term) for term in terms])
298- if token not in by_token:
299- raise LookupError(token)
300- return by_token[token]
301-
302- def getTerm(self, level):
303- return SimpleTerm(
304- level,
305- token=simple_form_key(level),
306- title=level.title)
307-
308
309 def levelSourceVocabularyFactory():
310 return LevelSource
311
312=== modified file 'src/schooltool/level/tests/test_level.py'
313--- src/schooltool/level/tests/test_level.py 2010-06-07 12:18:32 +0000
314+++ src/schooltool/level/tests/test_level.py 2010-07-02 14:38:26 +0000
315@@ -209,7 +209,7 @@
316
317 When the context cannot be adapted to ISchoolYear, the vocabulary is empty.
318
319- >>> source.levels
320+ >>> source.container
321 {}
322
323 >>> len(source)
324@@ -266,7 +266,7 @@
325 >>> verifyObject(IIterableSource, source)
326 True
327
328- >>> source.levels
329+ >>> source.container
330 <schooltool.level.level.LevelContainer ...>
331
332 >>> len(source)
333
334=== modified file 'src/schooltool/timetable/__init__.py'
335--- src/schooltool/timetable/__init__.py 2010-04-27 11:53:52 +0000
336+++ src/schooltool/timetable/__init__.py 2010-07-02 14:38:26 +0000
337@@ -125,6 +125,7 @@
338 schema. See ITimetableSchema, ITimetableSchemaDay.
339 """
340
341+import rwproperty
342 import zope.event
343 from persistent import Persistent
344 from persistent.dict import PersistentDict
345@@ -137,6 +138,8 @@
346 from zope.interface import directlyProvides
347 from zope.annotation.interfaces import IAnnotations
348 from zope.location.interfaces import ILocation
349+from zope.lifecycleevent.interfaces import IObjectAddedEvent
350+from zope.lifecycleevent.interfaces import IObjectModifiedEvent
351 from zope.traversing.api import getPath
352 from zope.container.interfaces import INameChooser
353 from zope.container.contained import NameChooser
354@@ -145,6 +148,7 @@
355 from schooltool.app.interfaces import ISchoolToolApplication
356 from schooltool.app.interfaces import ISchoolToolCalendar
357 from schooltool.calendar.simple import ImmutableCalendar
358+from schooltool.common import IDateRange, DateRange
359 from schooltool.timetable.interfaces import ITimetableCalendarEvent
360 from schooltool.timetable.interfaces import ITimetable, ITimetableWrite
361 from schooltool.timetable.interfaces import ITimetableDay, ITimetableDayWrite
362@@ -163,6 +167,8 @@
363 from schooltool.timetable.interfaces import Unchanged
364 from schooltool.term.interfaces import ITerm
365 from schooltool.app.app import InitBase
366+from schooltool.schoolyear.subscriber import ObjectEventAdapterSubscriber
367+
368
369 from schooltool.common import SchoolToolMessage as _
370
371@@ -172,6 +178,7 @@
372 # Timetabling
373 #
374
375+
376 class Timetable(Persistent):
377
378 implements(ITimetable, ITimetableWrite)
379@@ -179,6 +186,9 @@
380 __name__ = None
381 __parent__ = None
382
383+ _first = None
384+ _last = None
385+
386 timezone = 'UTC'
387
388 @property
389@@ -200,6 +210,32 @@
390 self.days = PersistentDict()
391 self.model = None
392
393+ @rwproperty.getproperty
394+ def first(self):
395+ if self._first is None:
396+ term = getattr(self, 'term', None)
397+ if term is None:
398+ return None
399+ return term.first
400+ return self._first
401+
402+ @rwproperty.setproperty
403+ def first(self, value):
404+ self._first = value
405+
406+ @rwproperty.getproperty
407+ def last(self):
408+ if self._last is None:
409+ term = getattr(self, 'term', None)
410+ if term is None:
411+ return None
412+ return term.last
413+ return self._last
414+
415+ @rwproperty.setproperty
416+ def last(self, value):
417+ self._last = value
418+
419 def keys(self):
420 return list(self.day_ids)
421
422@@ -236,6 +272,8 @@
423 raise ValueError("Timetables have different schemas")
424 for day, period, activity in other.activities():
425 self[day].add(period, activity, send_events=False)
426+ self.first = other.first
427+ self.last = other.last
428
429 def cloneEmpty(self):
430 other = Timetable(self.day_ids)
431@@ -541,16 +579,6 @@
432
433 def __setitem__(self, key, value):
434 assert ITimetable.providedBy(value)
435-
436- for k, v in self.items():
437- if (sameProxiedObjects(v.term, value.term) and
438- sameProxiedObjects(v.schooltt, value.schooltt) and
439- k != key):
440- raise DuplicateTimetableError("There already is a timetable "
441- "for term %s and section %s, but "
442- "it's key is %s!" % (v.term.__name__,
443- v.schooltt.__name__,
444- k))
445 old_value = self.get(key)
446 if old_value is not None:
447 old_value.__parent__ = None
448@@ -724,3 +752,98 @@
449 @implementer(ITerm)
450 def getTermForTimetable(timetable):
451 return ITerm(timetable.__parent__)
452+
453+
454+class TimetableOverlapError(Exception):
455+
456+ def __init__(self, schema, first, last, overlapping):
457+ self.schema = schema
458+ self.first = first
459+ self.last = last
460+ self.overlapping = overlapping
461+
462+ def _formatTitle(self, timetable):
463+ if (timetable.first is not None or
464+ timetable.last is not None):
465+ return "%s (%s-%s)" % (
466+ timetable.title, timetable.first, timetable.last)
467+ else:
468+ return timetable.title
469+
470+ def __repr__(self):
471+ return "Timetable %s overlaps other(s) (%s)" % (
472+ '%s (%s-%s)' % (self.schema.title, self.first, self.last),
473+ ", ".join(sorted(self._formatTitle(timetable)
474+ for timetable in self.overlapping)))
475+
476+ __str__ = __repr__
477+
478+
479+class TimetableOverflowError(Exception):
480+ def __init__(self, schema, first, last, term):
481+ self.schema = schema
482+ self.first = first
483+ self.last = last
484+ self.term = term
485+
486+ def __repr__(self):
487+ return "Timetable %s (%s - %s) does not fit in term %s (%s - %s)" % (
488+ self.schema.title, self.first, self.last,
489+ self.term.title, self.term.first, self.term.last)
490+
491+ __str__ = __repr__
492+
493+
494+
495+class TimetableModifiedSubscriber(ObjectEventAdapterSubscriber):
496+ adapts(IObjectModifiedEvent, ITimetable)
497+
498+ def __call__(self):
499+ if ITimetableDict.providedBy(self.object.__parent__):
500+ validateTimetable(self.object)
501+
502+
503+class TimetableAddedSubscriber(ObjectEventAdapterSubscriber):
504+ adapts(IObjectAddedEvent, ITimetable)
505+
506+ def __call__(self):
507+ if ITimetableDict.providedBy(self.object.__parent__):
508+ validateTimetable(self.object)
509+
510+
511+def validateAgainstTerm(schema, first, last, term):
512+ term_daterange = IDateRange(term)
513+ if ((first is not None and first not in term_daterange) or
514+ (last is not None and last not in term_daterange)):
515+ raise TimetableOverflowError(
516+ schema, first, last, term)
517+
518+
519+def validateAgainstOthers(schema, first, last, timetables):
520+ if first is None or last is None:
521+ return
522+
523+ daterange = DateRange(first, last)
524+ overlapping_timetables = []
525+ for other_tt in timetables:
526+ if (other_tt.schooltt is None or
527+ not sameProxiedObjects(other_tt.schooltt, schema)):
528+ continue
529+ if (other_tt.first is None or other_tt.last is None):
530+ continue
531+ if daterange.overlaps(DateRange(other_tt.first, other_tt.last)):
532+ overlapping_timetables.append(other_tt)
533+ if overlapping_timetables:
534+ raise TimetableOverlapError(
535+ schema, first, last, overlapping_timetables)
536+
537+
538+def validateTimetable(timetable):
539+ validateAgainstTerm(
540+ timetable.schooltt, timetable.first, timetable.last,
541+ timetable.term)
542+ validateAgainstOthers(
543+ timetable.schooltt, timetable.first, timetable.last,
544+ [tt for tt in timetable.__parent__.values()
545+ if tt.__name__ != timetable.__name__])
546+
547
548=== modified file 'src/schooltool/timetable/browser/__init__.py'
549--- src/schooltool/timetable/browser/__init__.py 2010-06-07 12:43:25 +0000
550+++ src/schooltool/timetable/browser/__init__.py 2010-07-02 14:38:26 +0000
551@@ -22,27 +22,44 @@
552 import datetime
553 import re
554
555+import zope.event
556+import zope.schema
557 from zope.container.interfaces import INameChooser
558 from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
559 from zope.component import adapts
560+from zope.component import getMultiAdapter
561+from zope.interface.exceptions import Invalid
562 from zope.interface import implements
563+from zope.interface import Interface
564 from zope.publisher.browser import BrowserView
565 from zope.publisher.interfaces import NotFound
566 from zope.security.proxy import removeSecurityProxy
567 from zope.traversing.browser.absoluteurl import absoluteURL
568
569+from z3c.form import form, field, button, widget, validator
570+from z3c.form.util import getSpecification
571+
572 from schooltool.app.interfaces import ISchoolToolApplication
573+from schooltool.app.utils import TitledContainerItemSource
574 from schooltool.calendar.utils import parse_date, parse_time
575 from schooltool.course.interfaces import ISection
576+from schooltool.common import DateRange
577 from schooltool.term.interfaces import ITerm
578 from schooltool.term.term import getTermForDate
579 from schooltool.timetable import SchooldaySlot
580 from schooltool.timetable import Timetable, TimetableDay
581 from schooltool.timetable import TimetableActivity
582+from schooltool.timetable import TimetableReplacedEvent
583 from schooltool.timetable.interfaces import ITimetableSchemaContainer
584 from schooltool.timetable.interfaces import ITimetable, IOwnTimetables
585-from schooltool.timetable.interfaces import ITimetables
586+from schooltool.timetable.interfaces import ITimetables, ITimetableDict
587+from schooltool.timetable import TimetableOverlapError, TimetableOverflowError
588+from schooltool.timetable import validateAgainstTerm
589+from schooltool.timetable import validateAgainstOthers
590+from schooltool.timetable.interfaces import ITimetableSchemaContainer
591 from schooltool.traverser.interfaces import ITraverserPlugin
592+from schooltool.schoolyear.interfaces import ISchoolYear
593+
594 from schooltool.common import SchoolToolMessage as _
595
596
597@@ -378,9 +395,15 @@
598 ttschema = ttschemas.values()[0]
599 return ttschema
600
601+ @property
602+ def owner(self):
603+ # XXX: make this property obsolete as soon as possible
604+ return self.context
605+
606 def getTerm(self):
607 """Return the chosen term."""
608- return ITerm(self.context)
609+ # XXX: make this method obsolete as soon as possible
610+ return ITerm(self.owner)
611
612 def getSections(self, item):
613 raise NotImplementedError(
614@@ -391,7 +414,8 @@
615 "This method should be implemented in subclasses")
616
617 def getTimetable(self):
618- timetables = ITimetables(self.context)
619+ # XXX: somewhat broken as of now.
620+ timetables = ITimetables(self.owner)
621 term = self.getTerm()
622 ttschema = self.getSchema()
623 return timetables.lookup(term, ttschema)
624@@ -530,13 +554,11 @@
625 return absoluteURL(self.context, self.request)
626
627
628-class TimetableEditView(TimetableSetupViewBase):
629-
630- __used_for__ = ITimetable
631-
632+class TimetableEditView(form.EditForm, TimetableConflictMixin):
633 template = ViewPageTemplateFile('templates/timetable-edit.pt')
634+ fields = field.Fields(ITimetable).select('first', 'last')
635
636- def getDays(self, ttschema):
637+ def getDays(self):
638 """Return the current selection.
639
640 Returns a list of dicts with the following keys
641@@ -551,6 +573,7 @@
642 for this shcema
643
644 """
645+ ttschema = self.context.schooltt
646 def days(schema):
647 for day_id, day in schema.items():
648 yield {'title': day_id,
649@@ -564,42 +587,222 @@
650
651 return list(days(ttschema))
652
653- def __call__(self):
654- self.has_timetables = bool(self.ttschemas)
655- if not self.has_timetables:
656- return self.template()
657- self.days = self.getDays(self.context.schooltt)
658- #XXX dumb, this doesn't space course names
659+ @property
660+ def timetable_dict(self):
661+ return self.context.__parent__
662+
663+ def updateActions(self):
664+ super(TimetableEditView, self).updateActions()
665+ self.actions['apply'].addClass('button-ok')
666+ self.actions['cancel'].addClass('button-cancel')
667+
668+ @button.buttonAndHandler(_('Save'), name='apply')
669+ def handleApply(self, action):
670+ data, errors = self.extractData()
671+ if errors:
672+ self.status = self.formErrorsMessage
673+ return
674+ changes = self.applyChanges(data)
675 section = removeSecurityProxy(self.context.__parent__.__parent__)
676 course_title = ''.join([course.title
677 for course in section.courses])
678-
679- if 'CANCEL' in self.request:
680- self.request.response.redirect(
681- absoluteURL(self.context, self.request))
682- if 'SAVE' in self.request:
683- for day_id, day in removeSecurityProxy(self.context).items():
684- for period_id, period in list(day.items()):
685- if '.'.join((day_id, period_id)) in self.request:
686- if not period:
687- # XXX Resource list is being copied
688- # from section as this view can't do
689- # proper resource booking
690- act = TimetableActivity(title=course_title,
691- owner=section,
692- resources=section.resources)
693- day.add(period_id, act)
694- else:
695- if period:
696- for act in list(period):
697- day.remove(period_id, act)
698-
699- # TODO: find a better place to redirect to
700- self.request.response.redirect(
701- absoluteURL(self.context,
702- self.request))
703-
704- return self.template()
705+ timetable_changed = bool(changes)
706+ for day_id, day in removeSecurityProxy(self.context).items():
707+ for period_id, period in list(day.items()):
708+ if '.'.join((day_id, period_id)) in self.request:
709+ if not period:
710+ # XXX Resource list is being copied
711+ # from section as this view can't do
712+ # proper resource booking
713+ act = TimetableActivity(title=course_title,
714+ owner=section,
715+ resources=section.resources)
716+ day.add(period_id, act)
717+ timetable_changed = True
718+ else:
719+ if period:
720+ for act in list(period):
721+ day.remove(period_id, act)
722+ timetable_changed = True
723+ self.status = self.successMessage
724+
725+ if timetable_changed:
726+ timetable = removeSecurityProxy(self.context)
727+ event = TimetableReplacedEvent(
728+ timetable.__parent__.__parent__, timetable.__name__,
729+ timetable, timetable)
730+ zope.event.notify(event)
731+ self.redirectToParent()
732+
733+ @button.buttonAndHandler(_("Cancel"), name='cancel')
734+ def handle_cancel_action(self, action):
735+ self.redirectToParent()
736+
737+ def redirectToParent(self):
738+ self.request.response.redirect(
739+ absoluteURL(self.context.__parent__,
740+ self.request))
741+
742+ @property
743+ def ttschemas(self):
744+ return ITimetableSchemaContainer(ISchoolToolApplication(None))
745+
746+ @property
747+ def has_timetables(self):
748+ return bool(self.ttschemas)
749+
750+
751+class TimetableSchemaSource(TitledContainerItemSource):
752+
753+ @property
754+ def container(self):
755+ term = ITerm(self.context)
756+ return ITimetableSchemaContainer(term, {})
757+
758+
759+def timetableSchemaSourceVocabularyFactory():
760+ return TimetableSchemaSource
761+
762+
763+class ITimetableAddForm(Interface):
764+ """Form schema for ITerm add/edit views."""
765+
766+ schooltt = zope.schema.Choice(
767+ title=_("School timetable"),
768+ source="schooltool.timetable.browser.timetable_schema_source",
769+ required=True,
770+ )
771+
772+ first = zope.schema.Date(title=_("Apply from"))
773+
774+ last = zope.schema.Date(title=_("Apply until"))
775+
776+
777+class TimetableAddView(form.AddForm, TimetableConflictMixin):
778+
779+ template = ViewPageTemplateFile('templates/section-timetable-add.pt')
780+ fields = field.Fields(ITimetableAddForm)
781+
782+ _object_added = None
783+
784+ buttons = button.Buttons(
785+ button.Button('add', title=_('Add')),
786+ button.Button('cancel', title=_('Cancel')))
787+
788+ @button.handler(buttons["add"])
789+ def handleAdd(self, action):
790+ return form.AddForm.handleAdd.func(self, action)
791+
792+ @button.handler(buttons["cancel"])
793+ def handleCancel(self, action):
794+ url = absoluteURL(self.context, self.request)
795+ self.request.response.redirect(url)
796+
797+ def updateActions(self):
798+ super(TimetableAddView, self).updateActions()
799+ self.actions['add'].addClass('button-ok')
800+ self.actions['cancel'].addClass('button-cancel')
801+
802+ @property
803+ def timetable_dict(self):
804+ return self.context
805+
806+ @property
807+ def term(self):
808+ """Return the chosen term."""
809+ return ITerm(self.context)
810+
811+ def create(self, data):
812+ schema = data['schooltt']
813+ timetable = schema.createTimetable(self.term)
814+ timetable.first = data['first']
815+ timetable.last = data['last']
816+ return timetable
817+
818+ def add(self, timetable):
819+ chooser = INameChooser(self.context)
820+ name = chooser.chooseName('', timetable)
821+ self.context[name] = timetable
822+ self._object_added = timetable
823+
824+ def nextURL(self):
825+ if self._object_added is not None:
826+ return absoluteURL(self._object_added, self.request)
827+ return absoluteURL(self.context, self.request)
828+
829+
830+TimetableAdd_default_first = widget.ComputedWidgetAttribute(
831+ lambda adapter: adapter.view.term.first,
832+ view=TimetableAddView,
833+ field=ITimetableAddForm['first']
834+ )
835+
836+
837+TimetableAdd_default_last = widget.ComputedWidgetAttribute(
838+ lambda adapter: adapter.view.term.last,
839+ view=TimetableAddView,
840+ field=ITimetableAddForm['last']
841+ )
842+
843+
844+class TimetableFormValidator(validator.InvariantsValidator):
845+
846+ def _formatTitle(self, object):
847+ if object is None:
848+ return None
849+ def dateTitle(date):
850+ if date is None:
851+ return '...'
852+ formatter = getMultiAdapter((date, self.request), name='mediumDate')
853+ return formatter()
854+ return u"%s (%s - %s)" % (
855+ object.title, dateTitle(object.first), dateTitle(object.last))
856+
857+ def validateObject(self, timetable):
858+ errors = super(TimetableFormValidator, self).validateObject(timetable)
859+ try:
860+ dr = DateRange(timetable.first, timetable.last)
861+ timetable_dict = self.view.timetable_dict
862+ term = ITerm(timetable_dict)
863+ try:
864+ other_timetables = timetable_dict.values()
865+ if getattr(timetable, '__name__', None) is not None:
866+ other_timetables = [tt for tt in other_timetables
867+ if tt.__name__ != timetable.__name__]
868+ validateAgainstOthers(
869+ timetable.schooltt, timetable.first, timetable.last,
870+ other_timetables)
871+ except TimetableOverlapError, e:
872+ for tt in e.overlapping:
873+ errors += (Invalid(
874+ u"%s %s" % (
875+ _("Timetable conflicts with another:"),
876+ self._formatTitle(tt))), )
877+ try:
878+ validateAgainstTerm(
879+ timetable.schooltt, timetable.first, timetable.last,
880+ term)
881+ except TimetableOverflowError, e:
882+ errors += (Invalid(u"%s %s" % (
883+ _("Timetable does not fit in term"),
884+ self._formatTitle(term))), )
885+ except ValueError, e:
886+ errors += (Invalid(_("Timetable must begin before it ends.")), )
887+ except validator.NoInputData:
888+ return errors
889+ return errors
890+
891+
892+validator.WidgetsValidatorDiscriminators(
893+ TimetableFormValidator,
894+ view=TimetableEditView,
895+ schema=getSpecification(ITimetable, force=True))
896+
897+
898+validator.WidgetsValidatorDiscriminators(
899+ TimetableFormValidator,
900+ view=TimetableAddView,
901+ schema=getSpecification(ITimetableAddForm, force=True))
902
903
904 class SpecialDayView(BrowserView):
905@@ -711,7 +914,6 @@
906 self.request.response.redirect(
907 absoluteURL(self.context, self.request))
908
909-
910 def timeplustd(self, t, td):
911 """Add a timedelta to time.
912
913@@ -755,3 +957,103 @@
914 def __call__(self):
915 self.update()
916 return self.template()
917+
918+
919+class SectionTimetablesViewBase(TimetableSetupViewBase):
920+
921+ def formatTimetableForTemplate(self, timetable):
922+ timetable = removeSecurityProxy(timetable)
923+ has_activities = False
924+ days = []
925+ for day_id, day in timetable.items():
926+ periods = []
927+ for period, activities in day.items():
928+ periods.append({
929+ 'title': period,
930+ 'activities': " / ".join(
931+ sorted([a.title for a in activities])),
932+ })
933+ has_activities |= bool(len(activities))
934+ days.append({
935+ 'title': day_id,
936+ 'periods': periods,
937+ })
938+ return {
939+ 'timetable': timetable,
940+ 'has_activities': has_activities,
941+ 'days': days,
942+ }
943+
944+
945+class SectionTimetablesView(SectionTimetablesViewBase):
946+
947+ __used_for__ = ITimetableDict
948+ template = ViewPageTemplateFile('templates/section-timetable-view.pt')
949+
950+ @property
951+ def owner(self):
952+ # XXX: make this property obsolete as soon as possible
953+ return self.context.__parent__
954+
955+ @property
956+ def term(self):
957+ return ITerm(self.owner)
958+
959+ @property
960+ def school_year(self):
961+ return ISchoolYear(self.term)
962+
963+ @property
964+ def timetables(self):
965+ timetables = sorted(self.context.values(),
966+ key=lambda tt: (tt.first, tt.title))
967+ result = []
968+ for timetable in timetables:
969+ result.append(self.formatTimetableForTemplate(timetable))
970+ return result
971+
972+ def hasTimetables(self):
973+ return bool(self.context)
974+
975+ def __call__(self):
976+ return self.template()
977+
978+
979+class SectionTimetableDeleteView(SectionTimetablesViewBase):
980+
981+ __used_for__ = ITimetableDict
982+ template = ViewPageTemplateFile('templates/confirm-timetable-delete.pt')
983+
984+ def owner(self):
985+ # XXX: make this property obsolete as soon as possible
986+ return self.context.__parent__
987+
988+ @property
989+ def term(self):
990+ return ITerm(self.owner)
991+
992+ @property
993+ def school_year(self):
994+ return ISchoolYear(self.term)
995+
996+ @property
997+ def timetable(self):
998+ name = self.request['timetable']
999+ if name not in self.context:
1000+ return None
1001+ timetable = self.context[name]
1002+ return self.formatTimetableForTemplate(timetable)
1003+
1004+ def nextURL(self):
1005+ return absoluteURL(self.context, self.request)
1006+
1007+ def __call__(self):
1008+ timetable = self.timetable
1009+ if 'CONFIRM' in self.request and timetable is not None:
1010+ del self.context[timetable['timetable'].__name__]
1011+ self.request.response.redirect(self.nextURL())
1012+ elif 'CANCEL' in self.request:
1013+ self.request.response.redirect(self.nextURL())
1014+ else:
1015+ return self.template()
1016+
1017
1018=== modified file 'src/schooltool/timetable/browser/configure.zcml'
1019--- src/schooltool/timetable/browser/configure.zcml 2009-12-30 17:38:37 +0000
1020+++ src/schooltool/timetable/browser/configure.zcml 2010-07-02 14:38:26 +0000
1021@@ -43,7 +43,7 @@
1022 manager="schooltool.skin.IActionMenuManager"
1023 template="templates/actionsViewlet.pt"
1024 permission="schooltool.edit"
1025- link="schedule.html"
1026+ link="timetables"
1027 title="Schedule"
1028 order="10"
1029 />
1030@@ -117,22 +117,43 @@
1031 menu="zmi_views" title="View"
1032 />
1033
1034+ <zope:adapter
1035+ factory=".TimetableFormValidator" />
1036+
1037 <page
1038 name="index.html"
1039 for="schooltool.timetable.interfaces.ITimetableDict"
1040- class="schooltool.timetable.browser.event.TimetableContainerView"
1041- template="templates/timetable_list.pt"
1042- permission="schooltool.view"
1043- menu="zmi_views" title="View"
1044- />
1045+ class="schooltool.timetable.browser.SectionTimetablesView"
1046+ permission="schooltool.edit" />
1047+
1048+ <page
1049+ name="confirm-delete.html"
1050+ for="schooltool.timetable.interfaces.ITimetableDict"
1051+ class="schooltool.timetable.browser.SectionTimetableDeleteView"
1052+ permission="schooltool.edit" />
1053+
1054+ <zope:adapter
1055+ factory=".TimetableAdd_default_first"
1056+ name="default"
1057+ />
1058+
1059+ <zope:adapter
1060+ factory=".TimetableAdd_default_last"
1061+ name="default"
1062+ />
1063
1064 <page
1065 name="addTimetable.html"
1066 for="schooltool.timetable.interfaces.ITimetableDict"
1067- class=".TimetableAddForm"
1068+ class=".TimetableAddView"
1069 permission="schooltool.edit"
1070 />
1071
1072+ <zope:utility
1073+ factory=".timetableSchemaSourceVocabularyFactory"
1074+ provides="zope.schema.interfaces.IVocabularyFactory"
1075+ name="schooltool.timetable.browser.timetable_schema_source" />
1076+
1077 <!-- ttwizard -->
1078 <page
1079 name="add.html"
1080
1081=== modified file 'src/schooltool/timetable/browser/ftests/composite-timetables.txt'
1082--- src/schooltool/timetable/browser/ftests/composite-timetables.txt 2009-12-22 10:51:56 +0000
1083+++ src/schooltool/timetable/browser/ftests/composite-timetables.txt 2010-07-02 14:38:26 +0000
1084@@ -113,6 +113,8 @@
1085 >>> manager.getLink('Birding').click()
1086 >>> manager.getLink('Birding1').click()
1087 >>> manager.getLink('Schedule').click()
1088+ >>> manager.getLink('Add Timetable').click()
1089+ >>> manager.getControl('Add').click()
1090 >>> manager.getControl('C').click()
1091 >>> manager.getControl('Save').click()
1092
1093@@ -121,6 +123,8 @@
1094 >>> manager.getLink('Birding').click()
1095 >>> manager.getLink('Birding2').click()
1096 >>> manager.getLink('Schedule').click()
1097+ >>> manager.getLink('Add Timetable').click()
1098+ >>> manager.getControl('Add').click()
1099 >>> manager.getControl('D').click()
1100 >>> manager.getControl('Save').click()
1101
1102
1103=== modified file 'src/schooltool/timetable/browser/ftests/timetable-events.txt'
1104--- src/schooltool/timetable/browser/ftests/timetable-events.txt 2009-01-27 08:11:56 +0000
1105+++ src/schooltool/timetable/browser/ftests/timetable-events.txt 2010-07-02 14:38:26 +0000
1106@@ -128,6 +128,8 @@
1107 >>> manager.getControl('Add').click()
1108
1109 >>> manager.getLink('Schedule').click()
1110+ >>> manager.getLink('Add Timetable').click()
1111+ >>> manager.getControl('Add').click()
1112 >>> manager.getControl(name="Monday.13:35-14:20").value = True
1113 >>> manager.getControl(name="Tuesday.09:30-10:25").value = True
1114 >>> manager.getControl(name="Wednesday.09:30-10:25").value = True
1115@@ -141,6 +143,8 @@
1116 >>> manager.getControl('Add').click()
1117
1118 >>> manager.getLink('Schedule').click()
1119+ >>> manager.getLink('Add Timetable').click()
1120+ >>> manager.getControl('Add').click()
1121 >>> manager.getControl(name="Monday.11:35-12:20").value = True
1122 >>> manager.getControl(name="Wednesday.12:45-13:30").value = True
1123 >>> manager.getControl(name="Thursday.10:30-11:25").value = True
1124@@ -154,6 +158,8 @@
1125 >>> manager.getControl('Add').click()
1126
1127 >>> manager.getLink('Schedule').click()
1128+ >>> manager.getLink('Add Timetable').click()
1129+ >>> manager.getControl('Add').click()
1130 >>> manager.getControl(name="Tuesday.14:30-15:15").value = True
1131 >>> manager.getControl(name="Wednesday.14:30-15:15").value = True
1132 >>> manager.getControl(name="Thursday.11:35-12:20").value = True
1133
1134=== modified file 'src/schooltool/timetable/browser/ftests/timetabling.txt'
1135--- src/schooltool/timetable/browser/ftests/timetabling.txt 2009-12-21 05:15:58 +0000
1136+++ src/schooltool/timetable/browser/ftests/timetabling.txt 2010-07-02 14:38:26 +0000
1137@@ -314,13 +314,13 @@
1138 >>> manager.getLink('Swimming - Frog').click()
1139 >>> manager.open(manager.url + '/timetables')
1140
1141- >>> manager.getLink('2005-fall.default')
1142- <Link text='2005-fall.default'
1143+ >>> manager.getLink('edit periods')
1144+ <Link text='(edit periods)'
1145 url='.../sections/1/timetables/1'>
1146
1147 We can have a look at the timetable too, and see a few timetable activities:
1148
1149- >>> manager.getLink('2005-fall.default').click()
1150+ >>> manager.getLink('edit periods').click()
1151
1152 >>> print analyze.queryHTML('//table/tr[2]', manager.contents)[0]
1153 <tr><td>
1154@@ -423,9 +423,9 @@
1155 >>> manager.getControl('No').click()
1156
1157 >>> manager.open('http://localhost/schoolyears/2005/2005-fall/sections/1/timetables/addTimetable.html')
1158- >>> manager.getControl(name='ttschema').value = ['singleperiod']
1159+ >>> manager.getControl(name='form.widgets.schooltt:list').value = ['singleperiod-']
1160 >>> manager.getControl('Add').click()
1161- >>> manager.getLink('2005-fall.singleperiod').click()
1162+ >>> manager.getControl('Save').click()
1163
1164 The activities should show up in the calendar of the users that are in
1165 this section too:
1166
1167=== added file 'src/schooltool/timetable/browser/templates/confirm-timetable-delete.pt'
1168--- src/schooltool/timetable/browser/templates/confirm-timetable-delete.pt 1970-01-01 00:00:00 +0000
1169+++ src/schooltool/timetable/browser/templates/confirm-timetable-delete.pt 2010-07-02 14:38:26 +0000
1170@@ -0,0 +1,89 @@
1171+<html metal:use-macro="view/@@standard_macros/page" i18n:domain="schooltool">
1172+<head>
1173+ <title metal:fill-slot="title">
1174+ Delete items
1175+ </title>
1176+</head>
1177+<body>
1178+
1179+<span metal:fill-slot="zonki">
1180+ <img src="zonki.png" alt="SchoolTool" i18n:attributes="alt"
1181+ tal:attributes="src context/++resource++zonki-question.png" />
1182+</span>
1183+
1184+<div metal:fill-slot="content-header" i18n:translate="">
1185+ <h1>Are you sure you want to unschedule this timetable?</h1>
1186+</div>
1187+
1188+
1189+<metal:block metal:fill-slot="body">
1190+
1191+ <form method="post"
1192+ tal:define="timetable view/timetable"
1193+ tal:attributes="action string:${context/@@absolute_url}/confirm-delete.html">
1194+
1195+ <tal:block condition="view/timetable">
1196+ <h2 i18n:translate="">
1197+ Timetable "<tal:block i18n:name="title" content="timetable/timetable/schooltt/title"/>"
1198+ </h2>
1199+ <table class="timetable">
1200+ <tr>
1201+ <tal:block repeat="day timetable/days">
1202+ <th class="day" tal:content="day/title">Day N</th>
1203+ </tal:block>
1204+ </tr>
1205+ <tal:block condition="not:timetable/has_activities">
1206+ <tr>
1207+ <td tal:attributes="colspan python:len(timetable['days'])">
1208+ <p i18n:translate="">No periods selected.</p>
1209+ </td>
1210+ </tr>
1211+ </tal:block>
1212+ <tal:block condition="timetable/has_activities">
1213+ <tr>
1214+ <tal:block repeat="day timetable/days">
1215+ <td >
1216+ <tal:block tal:repeat="period day/periods">
1217+ <ul>
1218+ <li>
1219+ <p tal:condition="period/activities">
1220+ <tal:block content="period/title" />:
1221+ <tal:block content="period/activities"/>
1222+ </p>
1223+ </li>
1224+ </ul>
1225+ </tal:block>
1226+ </td>
1227+ </tal:block>
1228+ </tr>
1229+ </tal:block>
1230+ <tr>
1231+ <td tal:attributes="colspan python:len(timetable['days'])">
1232+
1233+ <p i18n:translate="">
1234+ Scheduled from
1235+ <tal:block i18n:name="from" content="timetable/timetable/first/@@mediumDate" />
1236+ until
1237+ <tal:block i18n:name="until" content="timetable/timetable/last/@@mediumDate" />
1238+ </p>
1239+ </td>
1240+ </tr>
1241+ </table>
1242+
1243+ </tal:block>
1244+
1245+ <div class="controls">
1246+ <tal:block condition="view/timetable">
1247+ <input type="submit" class="button-cancel" name="CONFIRM" value="Confirm"
1248+ i18n:attributes="value"/>
1249+ <input type="hidden" name="timetable"
1250+ tal:attributes="value timetable/timetable/__name__" />
1251+ </tal:block>
1252+ <input type="submit" class="button-cancel" name="CANCEL" value="Cancel"
1253+ i18n:attributes="value" />
1254+ </div>
1255+ </form>
1256+
1257+
1258+</metal:block>
1259+</body></html>
1260
1261=== added file 'src/schooltool/timetable/browser/templates/section-timetable-add.pt'
1262--- src/schooltool/timetable/browser/templates/section-timetable-add.pt 1970-01-01 00:00:00 +0000
1263+++ src/schooltool/timetable/browser/templates/section-timetable-add.pt 2010-07-02 14:38:26 +0000
1264@@ -0,0 +1,65 @@
1265+<html metal:use-macro="view/@@standard_macros/page" i18n:domain="schooltool">
1266+<head>
1267+ <title metal:fill-slot="title" i18n:translate="">
1268+ Scheduling for <span i18n:name="section"
1269+ tal:define="section context/__parent__"
1270+ tal:replace="section/label" />
1271+ </title>
1272+</head>
1273+<body>
1274+
1275+<h1 metal:fill-slot="content-header" i18n:translate="">
1276+ Scheduling for <span i18n:name="section"
1277+ tal:define="section context/__parent__"
1278+ tal:replace="section/label" />
1279+</h1>
1280+
1281+<metal:block metal:fill-slot="body">
1282+
1283+ <div tal:define="term view/term">
1284+ <div i18n:translate="">For term:
1285+ <strong i18n:name="term_title" tal:content="term/title" />
1286+ (from
1287+ <tal:block i18n:name="starts" tal:content="term/first/@@mediumDate"/>
1288+ to
1289+ <tal:block i18n:name="ends" tal:content="term/last/@@mediumDate"/>)
1290+ </div>
1291+
1292+ </div>
1293+
1294+
1295+ <div class="status" tal:condition="view/status">
1296+ <ul class="errors"
1297+ tal:condition="view/widgets/errors"
1298+ metal:define-macro="form-errors">
1299+ <li tal:repeat="error view/widgets/errors">
1300+ <tal:block condition="error/widget">
1301+ <span tal:replace="error/widget/label" />:
1302+ </tal:block>
1303+ <span tal:replace="structure error/render">Error Type</span>
1304+ </li>
1305+ </ul>
1306+ </div>
1307+
1308+<form action="." method="post" enctype="multipart/form-data"
1309+ metal:define-macro="form"
1310+ tal:attributes="method view/method;
1311+ enctype view/enctype;
1312+ acceptCharset view/acceptCharset;
1313+ accept view/accept;
1314+ action view/action;
1315+ name view/name;
1316+ id view/id">
1317+
1318+ <div metal:use-macro="macro:widget-rows">
1319+ </div>
1320+
1321+ <div metal:use-macro="macro:form-buttons">
1322+ </div>
1323+
1324+</form>
1325+
1326+
1327+</metal:block>
1328+</body>
1329+</html>
1330
1331=== added file 'src/schooltool/timetable/browser/templates/section-timetable-view.pt'
1332--- src/schooltool/timetable/browser/templates/section-timetable-view.pt 1970-01-01 00:00:00 +0000
1333+++ src/schooltool/timetable/browser/templates/section-timetable-view.pt 2010-07-02 14:38:26 +0000
1334@@ -0,0 +1,112 @@
1335+<html metal:use-macro="view/@@standard_macros/page" i18n:domain="schooltool">
1336+<head>
1337+ <title metal:fill-slot="title" i18n:translate="">
1338+ Scheduling for <span i18n:name="section"
1339+ tal:replace="view/owner/label" />
1340+ </title>
1341+</head>
1342+<body>
1343+
1344+<div metal:fill-slot="content-header" i18n:translate="">
1345+
1346+<h1>
1347+ Time table schedule of
1348+ <tal:loop tal:repeat="course view/owner/courses" i18n:name="course">
1349+ <a tal:replace="structure course/@@title" />
1350+ </tal:loop>
1351+ <a i18n:name="section"
1352+ tal:attributes="href view/owner/@@absolute_url"
1353+ tal:content="view/owner/label" />,
1354+
1355+</h1>
1356+
1357+<p i18n:translate="">
1358+ For more information, see
1359+ <a tal:attributes="href view/term/@@absolute_url">school days</a> and
1360+ <a tal:attributes="href string:${view/school_year/@@absolute_url}/school_timetables">available timetables</a>
1361+ in
1362+ <tal:block i18n:name="term" tal:replace="structure view/term/@@title"/> of
1363+ <tal:block i18n:name="schoolyear" tal:replace="structure view/school_year/@@title" />
1364+</p>
1365+
1366+</div>
1367+
1368+<metal:block metal:fill-slot="body">
1369+
1370+
1371+<p tal:condition="not:view/hasTimetables"
1372+ i18n:translate="">There are no timetables assigned.</p>
1373+
1374+<tal:if tal:condition="view/hasTimetables">
1375+
1376+ <tal:block repeat="timetable view/timetables">
1377+ <div>
1378+
1379+ <h2 i18n:translate="">
1380+ Timetable "<tal:block i18n:name="title" content="timetable/timetable/schooltt/title"/>"
1381+ <a tal:attributes="href timetable/timetable/@@absolute_url">(edit periods)</a>
1382+ </h2>
1383+ <table class="timetable">
1384+ <tr>
1385+ <tal:block repeat="day timetable/days">
1386+ <th class="day" tal:content="day/title">Day N</th>
1387+ </tal:block>
1388+ </tr>
1389+ <tal:block condition="not:timetable/has_activities">
1390+ <tr>
1391+ <td tal:attributes="colspan python:len(timetable['days'])">
1392+ <p i18n:translate="">No periods selected.</p>
1393+ </td>
1394+ </tr>
1395+ </tal:block>
1396+ <tal:block condition="timetable/has_activities">
1397+ <tr>
1398+ <tal:block repeat="day timetable/days">
1399+ <td >
1400+ <tal:block tal:repeat="period day/periods">
1401+ <ul>
1402+ <li>
1403+ <p tal:condition="period/activities">
1404+ <tal:block content="period/title" />:
1405+ <tal:block content="period/activities"/>
1406+ </p>
1407+ </li>
1408+ </ul>
1409+ </tal:block>
1410+ </td>
1411+ </tal:block>
1412+ </tr>
1413+ </tal:block>
1414+ <tr>
1415+ <td tal:attributes="colspan python:len(timetable['days'])">
1416+
1417+ <p i18n:translate="">
1418+ Scheduled from
1419+ <tal:block i18n:name="from" content="timetable/timetable/first/@@mediumDate" />
1420+ until
1421+ <tal:block i18n:name="until" content="timetable/timetable/last/@@mediumDate" />
1422+ </p>
1423+ </td>
1424+ </tr>
1425+ </table>
1426+
1427+ <div>
1428+ <form method="post"
1429+ tal:attributes="action string:${context/@@absolute_url}/confirm-delete.html">
1430+ <input type="hidden" name="timetable" tal:attributes="value timetable/timetable/__name__" />
1431+ <div class="controls">
1432+ <input type="submit" class="button-cancel" name="DELETE" value="Remove"
1433+ i18n:attributes="value" />
1434+ </div>
1435+ </form>
1436+
1437+ </div>
1438+ <br>
1439+ </div>
1440+ </tal:block>
1441+
1442+</tal:if>
1443+
1444+</metal:block>
1445+</body>
1446+</html>
1447
1448=== modified file 'src/schooltool/timetable/browser/templates/timetable-edit.pt'
1449--- src/schooltool/timetable/browser/templates/timetable-edit.pt 2008-09-04 12:25:48 +0000
1450+++ src/schooltool/timetable/browser/templates/timetable-edit.pt 2010-07-02 14:38:26 +0000
1451@@ -23,25 +23,53 @@
1452
1453 <div tal:define="term view/getTerm;
1454 ttschemas view/ttschemas/values">
1455- <div i18n:translate="">For term:</div>
1456- <strong tal:content="term/title" />
1457-
1458- <div i18n:translate="">Using timetable:</div>
1459-
1460- <strong tal:repeat="ttschema ttschemas/sortby:title"
1461+ <div i18n:translate="">For term:
1462+ <strong i18n:name="term_title" tal:content="term/title" />
1463+ (from
1464+ <tal:block i18n:name="starts" tal:content="term/first/@@mediumDate"/>
1465+ to
1466+ <tal:block i18n:name="ends" tal:content="term/last/@@mediumDate"/>)
1467+ </div>
1468+
1469+ <div i18n:translate="">Using timetable:
1470+ <strong i18n:name="schema_titles"
1471+ tal:repeat="ttschema ttschemas/sortby:title"
1472 tal:content="ttschema/title" />
1473- </div>
1474-
1475-<form tal:attributes="action request/URL" class="plain" method="post">
1476+ </div>
1477+ </div>
1478+
1479+
1480+ <div class="status" tal:condition="view/status">
1481+ <ul class="errors"
1482+ tal:condition="view/widgets/errors"
1483+ metal:define-macro="form-errors">
1484+ <li tal:repeat="error view/widgets/errors">
1485+ <tal:block condition="error/widget">
1486+ <span tal:replace="error/widget/label" />:
1487+ </tal:block>
1488+ <span tal:replace="structure error/render">Error Type</span>
1489+ </li>
1490+ </ul>
1491+ </div>
1492+
1493+<form action="." method="post" enctype="multipart/form-data"
1494+ metal:define-macro="form"
1495+ tal:attributes="method view/method;
1496+ enctype view/enctype;
1497+ acceptCharset view/acceptCharset;
1498+ accept view/accept;
1499+ action view/action;
1500+ name view/name;
1501+ id view/id">
1502
1503 <table class="timetable">
1504 <tr>
1505- <tal:block repeat="day view/days">
1506+ <tal:block repeat="day view/getDays">
1507 <th class="day" tal:content="day/title">Day N</th>
1508 </tal:block>
1509 </tr>
1510 <tr>
1511- <tal:block repeat="day view/days">
1512+ <tal:block repeat="day view/getDays">
1513 <td >
1514 <ul>
1515 <tal:block tal:repeat="period day/periods">
1516@@ -62,16 +90,41 @@
1517 </td>
1518 </tal:block>
1519 </tr>
1520+ <tr>
1521+ <td tal:attributes="colspan python:len(view.getDays())">
1522+
1523+
1524+ <span i18n:translate="">Apply from:</span>
1525+
1526+ <tal:block define="widget python:view.widgets['first']">
1527+ <metal:block define-macro="render-widget">
1528+ <div class="row" tal:attributes="id string:${widget/id}-row">
1529+ <div class="widget" tal:content="structure widget/render">
1530+ </div>
1531+ <div class="error" tal:condition="widget/error">
1532+ <span tal:replace="structure widget/error/render">error</span>
1533+ </div>
1534+ </div>
1535+ </metal:block>
1536+ </tal:block>
1537+
1538+ <span i18n:translate="">Apply until:</span>
1539+
1540+ <tal:block define="widget python:view.widgets['last']">
1541+ <div metal:use-macro="template/macros/render-widget">
1542+ </div>
1543+ </tal:block>
1544+
1545+ </td>
1546+ </tr>
1547 </table>
1548
1549- <div class="controls">
1550- <input class="button-ok" type="submit" name="SAVE" value="Save"
1551- i18n:attributes="value" />
1552- <input type="submit" class="button-cancel" name="CANCEL" value="Cancel"
1553- i18n:attributes="value cancel-button" />
1554+ <div metal:use-macro="macro:form-buttons">
1555 </div>
1556
1557+
1558 </form>
1559+
1560 </tal:if>
1561
1562 </metal:block>
1563
1564=== modified file 'src/schooltool/timetable/configure.zcml'
1565--- src/schooltool/timetable/configure.zcml 2010-05-26 12:35:03 +0000
1566+++ src/schooltool/timetable/configure.zcml 2010-07-02 14:38:26 +0000
1567@@ -14,6 +14,14 @@
1568
1569 <!-- Subscribers -->
1570
1571+ <adapter
1572+ factory="schooltool.timetable.TimetableModifiedSubscriber"
1573+ name="validate_timetable_modified" />
1574+
1575+ <adapter
1576+ factory="schooltool.timetable.TimetableAddedSubscriber"
1577+ name="validate_timetable_modified" />
1578+
1579 <subscriber
1580 for=".interfaces.IHaveTimetables"
1581 provides=".interfaces.ITimetableSource"
1582
1583=== modified file 'src/schooltool/timetable/interfaces.py'
1584--- src/schooltool/timetable/interfaces.py 2010-01-19 12:09:22 +0000
1585+++ src/schooltool/timetable/interfaces.py 2010-07-02 14:38:26 +0000
1586@@ -415,6 +415,12 @@
1587
1588 title = TextLine(title=u"Title", required=True)
1589
1590+ first = Date(
1591+ title=u"Apply timetable from")
1592+
1593+ last = Date(
1594+ title=u"Apply timetable until")
1595+
1596 model = Object(
1597 title=u"A timetable model this timetable should be used with.",
1598 schema=ITimetableModel)
1599
1600=== modified file 'src/schooltool/timetable/model.py'
1601--- src/schooltool/timetable/model.py 2008-06-09 18:04:16 +0000
1602+++ src/schooltool/timetable/model.py 2010-07-02 14:38:26 +0000
1603@@ -43,6 +43,7 @@
1604 from persistent.dict import PersistentDict
1605 from zope.interface import implements, classProvides
1606 from zope.traversing.api import getPath
1607+from zope.proxy import sameProxiedObjects
1608
1609 from schooltool.app.cal import CalendarEvent
1610 from schooltool.calendar.simple import ImmutableCalendar
1611@@ -122,9 +123,9 @@
1612 events = []
1613 day_id_gen = self._dayGenerator()
1614 if first is None:
1615- first = term.first
1616+ first = timetable.first
1617 if last is None:
1618- last = term.last
1619+ last = timetable.last
1620 for date in term:
1621 if not first <= date <= last:
1622 # must call getDayId to keep track of days
1623@@ -410,7 +411,8 @@
1624 if (cal_event.activity == event.activity and
1625 cal_event.period_id == event.period_id and
1626 cal_event.day_id == event.day_id and
1627- cal_event.activity.timetable is event.activity.timetable):
1628+ sameProxiedObjects(cal_event.activity.timetable,
1629+ event.activity.timetable)):
1630 section_calendar.removeEvent(cal_event)
1631
1632
1633@@ -418,7 +420,8 @@
1634 section_calendar = ISchoolToolCalendar(event.object)
1635 for cal_event in list(section_calendar):
1636 if ITimetableCalendarEvent.providedBy(cal_event):
1637- if (cal_event.activity.timetable is event.old_timetable):
1638+ if sameProxiedObjects(cal_event.activity.timetable,
1639+ event.old_timetable):
1640 section_calendar.removeEvent(cal_event)
1641
1642
1643
1644=== modified file 'src/schooltool/timetable/tests/test_timetable.py'
1645--- src/schooltool/timetable/tests/test_timetable.py 2010-01-10 17:31:54 +0000
1646+++ src/schooltool/timetable/tests/test_timetable.py 2010-07-02 14:38:26 +0000
1647@@ -830,10 +830,6 @@
1648 td['1'] = TimetableStub(term1, schooltt1)
1649 td['1'] = TimetableStub(term1, schooltt1)
1650 td['1'] = TimetableStub(term2, schooltt1)
1651-
1652- self.assertRaises(DuplicateTimetableError, td.__setitem__, '2',
1653- TimetableStub(term2, schooltt1))
1654-
1655 td['2'] = TimetableStub(term1, schooltt1)
1656 td['3'] = TimetableStub(term1, schooltt2)
1657

Subscribers

People subscribed via source and target branches