Merge lp:~justas.sadzevicius/schooltool/timetables-section-time-boundaries into lp:schooltool/1.7
- timetables-section-time-boundaries
- Merge into 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 | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gediminas Paulauskas (community) | Abstain | ||
Review via email: mp+29090@code.launchpad.net |
Commit message
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 : | # |
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 |
Five tests in schooltool. timetable. tests.test_ model now fail with
Traceback (most recent call last): menesis/ src/schooltool/ trunk/build/ schooltool- timetables/ src/schooltool/ timetable/ model.py" , line 130, in createCalendar
...
File "/home/
if not first <= date <= last:
TypeError: can't compare datetime.date to NoneType