Merge lp:~justas.sadzevicius/schooltool/schooltool.section_spanning into lp:~schooltool-owners/schooltool/schooltool
- schooltool.section_spanning
- Merge into schooltool
Proposed by
Justas Sadzevičius
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~justas.sadzevicius/schooltool/schooltool.section_spanning |
Merge into: | lp:~schooltool-owners/schooltool/schooltool |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~justas.sadzevicius/schooltool/schooltool.section_spanning |
Related bugs: |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Justas Sadzevičius (justas.sadzevicius) wrote : | # |
- 2462. By Justas Sadzevičius
-
Simple section cross-term copying interface.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/schooltool/course/configure.zcml' |
2 | --- src/schooltool/course/configure.zcml 2008-10-13 13:14:27 +0000 |
3 | +++ src/schooltool/course/configure.zcml 2009-03-21 12:52:42 +0000 |
4 | @@ -178,6 +178,10 @@ |
5 | <adapter |
6 | factory=".section.PersonInstructorAdapter" /> |
7 | |
8 | + <!-- Validators --> |
9 | + <adapter factory=".section.SectionLinkContinuinityValidationSubscriber" |
10 | + name="validate_term_continuinity"/> |
11 | + |
12 | <!-- Views --> |
13 | <include package=".browser" /> |
14 | |
15 | |
16 | === modified file 'src/schooltool/course/interfaces.py' |
17 | --- src/schooltool/course/interfaces.py 2008-09-04 12:25:48 +0000 |
18 | +++ src/schooltool/course/interfaces.py 2009-03-21 12:52:42 +0000 |
19 | @@ -97,6 +97,15 @@ |
20 | size = zope.interface.Attribute( |
21 | """The number of member students in the section.""") |
22 | |
23 | + previous = zope.interface.Attribute( |
24 | + """The previous section.""") |
25 | + |
26 | + next = zope.interface.Attribute( |
27 | + """The next section.""") |
28 | + |
29 | + linked_sections = zope.interface.Attribute( |
30 | + """Chain of sections linked by previous/next with this one.""") |
31 | + |
32 | |
33 | class ISectionContainer(container.interfaces.IContainer): |
34 | """A container for Sections.""" |
35 | |
36 | === modified file 'src/schooltool/course/section.py' |
37 | --- src/schooltool/course/section.py 2008-10-08 10:52:54 +0000 |
38 | +++ src/schooltool/course/section.py 2009-03-22 13:06:04 +0000 |
39 | @@ -22,13 +22,17 @@ |
40 | $Id$ |
41 | """ |
42 | from persistent import Persistent |
43 | +import rwproperty |
44 | import zope.interface |
45 | |
46 | +from zope.event import notify |
47 | from zope.annotation.interfaces import IAttributeAnnotatable |
48 | -from zope.app.container.interfaces import IObjectRemovedEvent |
49 | +from zope.app.container.interfaces import IObjectRemovedEvent, INameChooser |
50 | from zope.app.container import btree, contained |
51 | from zope.component import adapts |
52 | from zope.interface import implements |
53 | +from zope.proxy import sameProxiedObjects |
54 | +from zope.security.proxy import removeSecurityProxy |
55 | |
56 | from schooltool.relationship import RelationshipProperty |
57 | from schooltool.app import membership |
58 | @@ -41,7 +45,9 @@ |
59 | from schooltool.common import SchoolToolMessage as _ |
60 | from schooltool.app import relationships |
61 | from schooltool.course import interfaces, booking |
62 | +from schooltool.schoolyear.subscriber import EventAdapterSubscriber |
63 | from schooltool.schoolyear.subscriber import ObjectEventAdapterSubscriber |
64 | +from schooltool.schoolyear.interfaces import ISubscriber |
65 | from schooltool.schoolyear.interfaces import ISchoolYear |
66 | from schooltool.securitypolicy.crowds import Crowd, AggregateCrowd |
67 | from schooltool.course.interfaces import ICourseContainer |
68 | @@ -51,6 +57,16 @@ |
69 | from schooltool.course.interfaces import ILearner, IInstructor |
70 | |
71 | |
72 | +class InvalidSectionLinkException(Exception): |
73 | + pass |
74 | + |
75 | + |
76 | +class SectionBeforeLinkingEvent(object): |
77 | + def __init__(self, first, second): |
78 | + self.first = first |
79 | + self.second = second |
80 | + |
81 | + |
82 | class Section(Persistent, contained.Contained): |
83 | |
84 | zope.interface.implements(interfaces.ISectionContained, |
85 | @@ -58,6 +74,9 @@ |
86 | |
87 | _location = None |
88 | |
89 | + _previous = None |
90 | + _next = None |
91 | + |
92 | def __init__(self, title="Section", description=None, schedule=None): |
93 | self.title = title |
94 | self.description = description |
95 | @@ -79,6 +98,89 @@ |
96 | size = size + len(member.members) |
97 | return size |
98 | |
99 | + def _unlinkRangeTo(self, other): |
100 | + """Unlink sections between self and the other in self.linked_sections.""" |
101 | + linked = self.linked_sections |
102 | + if other not in linked or self is other: |
103 | + return |
104 | + idx_first, idx_last = sorted([linked.index(self), linked.index(other)]) |
105 | + linked[idx_first]._next = None |
106 | + for section in linked[idx_first+1:idx_last]: |
107 | + section._previous = None |
108 | + section._next = None |
109 | + linked[idx_last]._previous = None |
110 | + |
111 | + @rwproperty.getproperty |
112 | + def previous(self): |
113 | + return self._previous |
114 | + |
115 | + @rwproperty.setproperty |
116 | + def previous(self, new): |
117 | + new = removeSecurityProxy(new) |
118 | + if new is self._previous: |
119 | + return |
120 | + if new is self: |
121 | + raise InvalidSectionLinkException( |
122 | + _('Cannot assign section as previous to itself')) |
123 | + |
124 | + notify(SectionBeforeLinkingEvent(new, self)) |
125 | + |
126 | + if new is not None: |
127 | + self._unlinkRangeTo(new) |
128 | + |
129 | + old_prev = self._previous |
130 | + self._previous = None |
131 | + if old_prev is not None: |
132 | + old_prev.next = None |
133 | + self._previous = new |
134 | + |
135 | + if new is not None: |
136 | + new.next = self |
137 | + |
138 | + @rwproperty.getproperty |
139 | + def next(self): |
140 | + return self._next |
141 | + |
142 | + @rwproperty.setproperty |
143 | + def next(self, new): |
144 | + new = removeSecurityProxy(new) |
145 | + if new is self._next: |
146 | + return |
147 | + if new is self: |
148 | + raise InvalidSectionLinkException( |
149 | + _('Cannot assign section as next to itself')) |
150 | + |
151 | + notify(SectionBeforeLinkingEvent(self, new)) |
152 | + |
153 | + if new is not None: |
154 | + self._unlinkRangeTo(new) |
155 | + |
156 | + old_next = self._next |
157 | + self._next = None |
158 | + if old_next is not None: |
159 | + old_next.previous = None |
160 | + self._next = new |
161 | + |
162 | + if new is not None: |
163 | + new.previous = self |
164 | + |
165 | + @property |
166 | + def linked_sections(self): |
167 | + sections = [self] |
168 | + |
169 | + pit = self.previous |
170 | + while pit: |
171 | + sections.insert(0, pit) |
172 | + pit = pit.previous |
173 | + |
174 | + nit = self.next |
175 | + while nit: |
176 | + sections.append(nit) |
177 | + nit = nit.next |
178 | + |
179 | + return sections |
180 | + |
181 | + |
182 | instructors = RelationshipProperty(relationships.URIInstruction, |
183 | relationships.URISection, |
184 | relationships.URIInstructor) |
185 | @@ -266,3 +368,46 @@ |
186 | section_container = ISectionContainer(self.object) |
187 | for section_id in list(section_container.keys()): |
188 | del section_container[section_id] |
189 | + |
190 | + |
191 | +class SectionLinkContinuinityValidationSubscriber(EventAdapterSubscriber): |
192 | + adapts(SectionBeforeLinkingEvent) |
193 | + implements(ISubscriber) |
194 | + |
195 | + def __call__(self): |
196 | + if (self.event.first is None or |
197 | + self.event.second is None): |
198 | + return # unlinking sections |
199 | + |
200 | + first_term = ITerm(self.event.first) |
201 | + second_term = ITerm(self.event.second) |
202 | + if sameProxiedObjects(first_term, second_term): |
203 | + raise InvalidSectionLinkException( |
204 | + _("Cannot link sections in same term")) |
205 | + |
206 | + if first_term.first > second_term.first: |
207 | + raise InvalidSectionLinkException( |
208 | + _("Sections are not in subsequent terms")) |
209 | + |
210 | + if not sameProxiedObjects(ISchoolYear(first_term), |
211 | + ISchoolYear(second_term)): |
212 | + raise InvalidSectionLinkException( |
213 | + _("Cannot link sections in different school years")) |
214 | + |
215 | + |
216 | +def copySection(section, target_term): |
217 | + """Create a copy of a section in a desired term.""" |
218 | + section_copy = Section(section.title, section.description) |
219 | + sections = ISectionContainer(target_term) |
220 | + name = section.__name__ |
221 | + if name in sections: |
222 | + name = INameChooser(sections).chooseName(name, section_copy) |
223 | + sections[name] = section_copy |
224 | + for course in section.courses: |
225 | + section_copy.courses.add(course) |
226 | + for instructor in section.instructors: |
227 | + section_copy.instructors.add(instructor) |
228 | + for member in section.members: |
229 | + section_copy.members.add(member) |
230 | + return section_copy |
231 | + |
232 | |
233 | === modified file 'src/schooltool/course/tests/test_course.py' |
234 | --- src/schooltool/course/tests/test_course.py 2007-07-14 15:18:18 +0000 |
235 | +++ src/schooltool/course/tests/test_course.py 2009-03-21 12:52:42 +0000 |
236 | @@ -25,6 +25,8 @@ |
237 | from zope.testing import doctest |
238 | from zope.interface.verify import verifyObject |
239 | |
240 | +from schooltool.relationship.tests import setUp, tearDown |
241 | + |
242 | |
243 | def doctest_CourseContainer(): |
244 | r"""Schooltool toplevel container for Courses. |
245 | @@ -46,6 +48,7 @@ |
246 | Traceback (most recent call last): |
247 | ... |
248 | InvalidItemType: ... |
249 | + |
250 | """ |
251 | |
252 | |
253 | @@ -74,9 +77,7 @@ |
254 | |
255 | To test the relationship we need to do some setup: |
256 | |
257 | - >>> from schooltool.relationship.tests import setUp, tearDown |
258 | >>> from schooltool.app.relationships import enforceCourseSectionConstraint |
259 | - >>> setUp() |
260 | >>> import zope.event |
261 | >>> old_subscribers = zope.event.subscribers[:] |
262 | >>> zope.event.subscribers.append(enforceCourseSectionConstraint) |
263 | @@ -131,16 +132,13 @@ |
264 | That's it: |
265 | |
266 | >>> zope.event.subscribers[:] = old_subscribers |
267 | - >>> tearDown() |
268 | + |
269 | """ |
270 | |
271 | |
272 | def doctest_Section(): |
273 | r"""Tests for course section groups. |
274 | |
275 | - >>> from schooltool.relationship.tests import setUp, tearDown |
276 | - >>> setUp() |
277 | - |
278 | >>> from schooltool.course.section import Section |
279 | >>> section = Section(title="section 1", description="advanced") |
280 | >>> from schooltool.course.interfaces import ISection |
281 | @@ -237,9 +235,216 @@ |
282 | US History |
283 | English |
284 | |
285 | - We're done: |
286 | - |
287 | - >>> tearDown() |
288 | + """ |
289 | + |
290 | + |
291 | +def doctest_Section_linking(): |
292 | + r"""Tests for course section linking properties (previous, next and |
293 | + linked_sections) |
294 | + |
295 | + The purpose of this test is to check that: |
296 | + * sections can be linked via Section.previous and Section.next. |
297 | + * Section.linked_sections return sections linked with the Section |
298 | + in a correct order. |
299 | + * it is impossible to create linking loops. |
300 | + |
301 | + >>> from schooltool.course.section import Section |
302 | + |
303 | + >>> def section_link_str(s): |
304 | + ... return '%s <- %s -> %s' % ( |
305 | + ... not s.previous and 'None' or s.previous.title, |
306 | + ... s.title, |
307 | + ... not s.next and 'None' or s.next.title) |
308 | + |
309 | + >>> def print_sections(sections): |
310 | + ... '''Print prev and next links for all sections in section_list''' |
311 | + ... for s in sections: |
312 | + ... print section_link_str(s) |
313 | + |
314 | + >>> def print_linked(section_list): |
315 | + ... '''Print linked sections for all sections in section_list''' |
316 | + ... for section in section_list: |
317 | + ... linked_str = ', '.join( |
318 | + ... [s.title for s in section.linked_sections]) |
319 | + ... print section.title, 'spans:', linked_str |
320 | + |
321 | + Create some sections. |
322 | + |
323 | + >>> sections = [Section('Sec0'), Section('Sec1'), Section('Sec2')] |
324 | + |
325 | + By default sections are not linked. |
326 | + |
327 | + >>> print_sections(sections) |
328 | + None <- Sec0 -> None |
329 | + None <- Sec1 -> None |
330 | + None <- Sec2 -> None |
331 | + |
332 | + And each section spans only itself. |
333 | + |
334 | + >>> print_linked(sections) |
335 | + Sec0 spans: Sec0 |
336 | + Sec1 spans: Sec1 |
337 | + Sec2 spans: Sec2 |
338 | + |
339 | + Assign s0 as previous section to s1. s0 'next' section is also updated. |
340 | + A list of linked sections updated for s0 and s1. |
341 | + |
342 | + >>> sections[1].previous = sections[0] |
343 | + |
344 | + >>> print_sections(sections) |
345 | + None <- Sec0 -> Sec1 |
346 | + Sec0 <- Sec1 -> None |
347 | + None <- Sec2 -> None |
348 | + |
349 | + >>> print_linked(sections) |
350 | + Sec0 spans: Sec0, Sec1 |
351 | + Sec1 spans: Sec0, Sec1 |
352 | + Sec2 spans: Sec2 |
353 | + |
354 | + Assign s2 as next section to s1. |
355 | + |
356 | + >>> sections[1].next = sections[2] |
357 | + |
358 | + >>> print_sections(sections) |
359 | + None <- Sec0 -> Sec1 |
360 | + Sec0 <- Sec1 -> Sec2 |
361 | + Sec1 <- Sec2 -> None |
362 | + |
363 | + >>> print_linked(sections) |
364 | + Sec0 spans: Sec0, Sec1, Sec2 |
365 | + Sec1 spans: Sec0, Sec1, Sec2 |
366 | + Sec2 spans: Sec0, Sec1, Sec2 |
367 | + |
368 | + Let's test section unlinking... |
369 | + |
370 | + >>> sections[1].previous = None |
371 | + >>> print_sections(sections) |
372 | + None <- Sec0 -> None |
373 | + None <- Sec1 -> Sec2 |
374 | + Sec1 <- Sec2 -> None |
375 | + |
376 | + >>> print_linked(sections) |
377 | + Sec0 spans: Sec0 |
378 | + Sec1 spans: Sec1, Sec2 |
379 | + Sec2 spans: Sec1, Sec2 |
380 | + |
381 | + >>> sections[2].previous = None |
382 | + >>> print_sections(sections) |
383 | + None <- Sec0 -> None |
384 | + None <- Sec1 -> None |
385 | + None <- Sec2 -> None |
386 | + |
387 | + >>> print_linked(sections) |
388 | + Sec0 spans: Sec0 |
389 | + Sec1 spans: Sec1 |
390 | + Sec2 spans: Sec2 |
391 | + |
392 | + And now some extreme cases. Try the section as next/prev to itself. |
393 | + |
394 | + >>> sections[0].previous = sections[0] |
395 | + Traceback (most recent call last): |
396 | + ... |
397 | + InvalidSectionLinkException: Cannot assign section as previous to itself |
398 | + |
399 | + >>> sections[0].next = sections[0] |
400 | + Traceback (most recent call last): |
401 | + ... |
402 | + InvalidSectionLinkException: Cannot assign section as next to itself |
403 | + |
404 | + Create a long list of linked sections. |
405 | + |
406 | + >>> sections = [Section('Sec0')] |
407 | + >>> for n in range(5): |
408 | + ... new_sec = Section('Sec%d' % (n+1)) |
409 | + ... new_sec.previous = sections[-1] |
410 | + ... sections.append(new_sec) |
411 | + |
412 | + >>> print_sections(sections) |
413 | + None <- Sec0 -> Sec1 |
414 | + Sec0 <- Sec1 -> Sec2 |
415 | + Sec1 <- Sec2 -> Sec3 |
416 | + Sec2 <- Sec3 -> Sec4 |
417 | + Sec3 <- Sec4 -> Sec5 |
418 | + Sec4 <- Sec5 -> None |
419 | + |
420 | + Try to introduce a loop by assigning a previous section. |
421 | + |
422 | + >>> sections[4].previous = sections[1] |
423 | + |
424 | + Note that sections 2 and 3 are removed from the linked list thus avoiding |
425 | + the loop. |
426 | + |
427 | + >>> print_sections(sections) |
428 | + None <- Sec0 -> Sec1 |
429 | + Sec0 <- Sec1 -> Sec4 |
430 | + None <- Sec2 -> None |
431 | + None <- Sec3 -> None |
432 | + Sec1 <- Sec4 -> Sec5 |
433 | + Sec4 <- Sec5 -> None |
434 | + |
435 | + >>> [s.title for s in sections[0].linked_sections] |
436 | + ['Sec0', 'Sec1', 'Sec4', 'Sec5'] |
437 | + |
438 | + Let's reubild the list of 5 linked sections. |
439 | + |
440 | + >>> sections[4].previous = sections[3] |
441 | + >>> sections[3].previous = sections[2] |
442 | + >>> sections[2].previous = sections[1] |
443 | + |
444 | + >>> print_linked(sections) |
445 | + Sec0 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
446 | + Sec1 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
447 | + Sec2 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
448 | + Sec3 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
449 | + Sec4 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
450 | + Sec5 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
451 | + |
452 | + |
453 | + Try to introduce a loop by assigning a next section. |
454 | + |
455 | + >>> sections[1].next = sections[4] |
456 | + |
457 | + Note that sections 2 and 3 are removed from the linked list thus avoiding |
458 | + the loop again. |
459 | + |
460 | + >>> print_sections(sections) |
461 | + None <- Sec0 -> Sec1 |
462 | + Sec0 <- Sec1 -> Sec4 |
463 | + None <- Sec2 -> None |
464 | + None <- Sec3 -> None |
465 | + Sec1 <- Sec4 -> Sec5 |
466 | + Sec4 <- Sec5 -> None |
467 | + |
468 | + >>> [s.title for s in sections[0].linked_sections] |
469 | + ['Sec0', 'Sec1', 'Sec4', 'Sec5'] |
470 | + |
471 | + Let's reubild the list of 5 linked sections. |
472 | + |
473 | + >>> sections[3].next = sections[4] |
474 | + >>> sections[2].next = sections[3] |
475 | + >>> sections[1].next = sections[2] |
476 | + |
477 | + >>> print_linked(sections) |
478 | + Sec0 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
479 | + Sec1 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
480 | + Sec2 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
481 | + Sec3 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
482 | + Sec4 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
483 | + Sec5 spans: Sec0, Sec1, Sec2, Sec3, Sec4, Sec5 |
484 | + |
485 | + And try to introduce another loop. |
486 | + |
487 | + >>> sections[0].previous = sections[3] |
488 | + |
489 | + Sections between Sec0 and Sec3 were unlinked to avoid the loop. |
490 | + |
491 | + >>> print_linked(sections) |
492 | + Sec0 spans: Sec3, Sec0 |
493 | + Sec1 spans: Sec1 |
494 | + Sec2 spans: Sec2 |
495 | + Sec3 spans: Sec3, Sec0 |
496 | + Sec4 spans: Sec4, Sec5 |
497 | + Sec5 spans: Sec4, Sec5 |
498 | |
499 | """ |
500 | |
501 | @@ -247,9 +452,6 @@ |
502 | def doctest_PersonInstructorCrowd(): |
503 | """Unit test for the PersonInstructorCrowd |
504 | |
505 | - >>> from schooltool.relationship.tests import setUp, tearDown |
506 | - >>> setUp() |
507 | - |
508 | We'll need a section, a group, and a couple of persons: |
509 | |
510 | >>> from schooltool.course.section import Section |
511 | @@ -284,18 +486,12 @@ |
512 | >>> PersonInstructorsCrowd(p2).contains(p1) |
513 | False |
514 | |
515 | - Cleanup. |
516 | - |
517 | - >>> tearDown() |
518 | """ |
519 | |
520 | |
521 | def doctest_PersonLearnerAdapter(self): |
522 | """Tests for PersonLearnerAdapter. |
523 | |
524 | - >>> from schooltool.relationship.tests import setUp, tearDown |
525 | - >>> setUp() |
526 | - |
527 | We'll need a person, a group, and a couple of sections: |
528 | |
529 | >>> from schooltool.course.section import Section |
530 | @@ -320,16 +516,14 @@ |
531 | >>> [section.title for section in learner.sections()] |
532 | ['section 1', 'section 2'] |
533 | |
534 | - Cleanup. |
535 | - |
536 | - >>> tearDown() |
537 | """ |
538 | |
539 | |
540 | def test_suite(): |
541 | - return unittest.TestSuite([ |
542 | - doctest.DocTestSuite(optionflags=doctest.ELLIPSIS), |
543 | - ]) |
544 | + optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS |
545 | + suite = doctest.DocTestSuite(optionflags=optionflags, |
546 | + setUp=setUp, tearDown=tearDown) |
547 | + return suite |
548 | |
549 | |
550 | if __name__ == '__main__': |
551 | |
552 | === added file 'src/schooltool/course/tests/test_subscribers.py' |
553 | --- src/schooltool/course/tests/test_subscribers.py 1970-01-01 00:00:00 +0000 |
554 | +++ src/schooltool/course/tests/test_subscribers.py 2009-03-22 13:06:04 +0000 |
555 | @@ -0,0 +1,262 @@ |
556 | +# |
557 | +# SchoolTool - common information systems platform for school administration |
558 | +# Copyright (c) 2005 Shuttleworth Foundation |
559 | +# |
560 | +# This program is free software; you can redistribute it and/or modify |
561 | +# it under the terms of the GNU General Public License as published by |
562 | +# the Free Software Foundation; either version 2 of the License, or |
563 | +# (at your option) any later version. |
564 | +# |
565 | +# This program is distributed in the hope that it will be useful, |
566 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
567 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
568 | +# GNU General Public License for more details. |
569 | +# |
570 | +# You should have received a copy of the GNU General Public License |
571 | +# along with this program; if not, write to the Free Software |
572 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
573 | +# |
574 | +""" |
575 | +Unit tests for course and section subscriber validation. |
576 | + |
577 | +$Id$ |
578 | +""" |
579 | + |
580 | +import unittest |
581 | +from datetime import date, timedelta |
582 | +from zope.testing import doctest |
583 | + |
584 | +from schooltool.schoolyear.testing import (setUp, tearDown, |
585 | + provideStubUtility, |
586 | + provideStubAdapter) |
587 | +from schooltool.schoolyear.ftesting import schoolyear_functional_layer |
588 | +from schooltool.app.interfaces import ISchoolToolApplication |
589 | +from schooltool.schoolyear.interfaces import ISchoolYearContainer |
590 | +from schooltool.schoolyear.schoolyear import SchoolYear |
591 | +from schooltool.term.interfaces import ITerm |
592 | +from schooltool.term.term import Term |
593 | +from schooltool.course.interfaces import ISectionContainer |
594 | +from schooltool.course.section import Section |
595 | + |
596 | + |
597 | +def setUpSchoolYear(year=2000): |
598 | + year_container = ISchoolYearContainer(ISchoolToolApplication(None)) |
599 | + sy = year_container[str(year)] = SchoolYear( |
600 | + str(year), date(year, 1, 1), date(year+1, 1, 1) - timedelta(1)) |
601 | + return sy |
602 | + |
603 | + |
604 | +def setUpTerms(schoolyear, term_count=3): |
605 | + term_delta = timedelta( |
606 | + ((schoolyear.last - schoolyear.first) / term_count).days) |
607 | + start_date = schoolyear.first |
608 | + for n in range(term_count): |
609 | + finish_date = start_date + term_delta - timedelta(1) |
610 | + schoolyear['Term%d' % (n+1)] = Term( |
611 | + 'Term %d' % (n+1), start_date, finish_date) |
612 | + start_date = finish_date + timedelta(1) |
613 | + |
614 | + |
615 | +def setUpSections(term_list, sections_per_term=1): |
616 | + for term in term_list: |
617 | + sections = ISectionContainer(term) |
618 | + for n in range(sections_per_term): |
619 | + name = 'Sec%d'%(n+1) |
620 | + sections[name] = Section(name) |
621 | + |
622 | + |
623 | +def doctest_Section_linking_terms(): |
624 | + r"""Tests for section linking term continuinity validations. |
625 | + |
626 | + Set up a school year with three terms, each containing two sections. |
627 | + |
628 | + >>> year = setUpSchoolYear(2000) |
629 | + >>> setUpTerms(year, 3) |
630 | + >>> setUpSections(year.values(), sections_per_term=2) |
631 | + |
632 | + Let's make Sec1 span the three terms. |
633 | + |
634 | + >>> s1_t1 = ISectionContainer(year['Term1'])['Sec1'] |
635 | + >>> s1_t2 = ISectionContainer(year['Term2'])['Sec1'] |
636 | + >>> s1_t3 = ISectionContainer(year['Term3'])['Sec1'] |
637 | + |
638 | + >>> s1_t2.previous = s1_t1 |
639 | + >>> s1_t2.next = s1_t3 |
640 | + |
641 | + >>> for s in s1_t2.linked_sections: |
642 | + ... print '%s, %s' % (ITerm(s).title, s.title) |
643 | + Term 1, Sec1 |
644 | + Term 2, Sec1 |
645 | + Term 3, Sec1 |
646 | + |
647 | + We cannot link a section to another section in the same term. |
648 | + |
649 | + >>> s2_t1 = ISectionContainer(year['Term1'])['Sec2'] |
650 | + >>> s2_t2 = ISectionContainer(year['Term2'])['Sec2'] |
651 | + >>> s2_t3 = ISectionContainer(year['Term3'])['Sec2'] |
652 | + |
653 | + >>> s2_t2.next = s1_t2 |
654 | + Traceback (most recent call last): |
655 | + ... |
656 | + InvalidSectionLinkException: Cannot link sections in same term |
657 | + |
658 | + >>> s2_t2.previous = s1_t2 |
659 | + Traceback (most recent call last): |
660 | + ... |
661 | + InvalidSectionLinkException: Cannot link sections in same term |
662 | + |
663 | + Cannot set previous section in the future. |
664 | + |
665 | + >>> s2_t2.previous = s1_t3 |
666 | + Traceback (most recent call last): |
667 | + ... |
668 | + InvalidSectionLinkException: Sections are not in subsequent terms |
669 | + |
670 | + Or set next section in the past. |
671 | + |
672 | + >>> s2_t2.next = s1_t1 |
673 | + Traceback (most recent call last): |
674 | + ... |
675 | + InvalidSectionLinkException: Sections are not in subsequent terms |
676 | + |
677 | + Notice that though we tried to link Sec2 with Sec1, we didn't change it's |
678 | + linked_sections, becouse all our assigments were invalid. |
679 | + |
680 | + >>> for s in s1_t2.linked_sections: |
681 | + ... print '%s, %s' % (ITerm(s).title, s.title) |
682 | + Term 1, Sec1 |
683 | + Term 2, Sec1 |
684 | + Term 3, Sec1 |
685 | + |
686 | + Let's test an unusual case: continue Section 1 from Term 1 as Section 2 |
687 | + in the last term. |
688 | + |
689 | + >>> s2_t3.previous = s1_t1 |
690 | + |
691 | + Section 2 in third term now continues Section 1. |
692 | + |
693 | + >>> for s in s1_t1.linked_sections: |
694 | + ... print '%s, %s' % (ITerm(s).title, s.title) |
695 | + Term 1, Sec1 |
696 | + Term 3, Sec2 |
697 | + |
698 | + Section 1 now spans only terms 2 and 3. |
699 | + |
700 | + >>> for s in s1_t2.linked_sections: |
701 | + ... print '%s, %s' % (ITerm(s).title, s.title) |
702 | + Term 2, Sec1 |
703 | + Term 3, Sec1 |
704 | + |
705 | + """ |
706 | + |
707 | + |
708 | +def doctest_Section_linking_schoolyears(): |
709 | + r"""Tests for section linking SchoolYear validations. |
710 | + |
711 | + Set up a school year with three terms, each containing two sections. |
712 | + |
713 | + >>> def setUpYearWithSection(year): |
714 | + ... year = setUpSchoolYear(year) |
715 | + ... setUpTerms(year, 1) |
716 | + ... setUpSections(year.values(), sections_per_term=1) |
717 | + ... return year |
718 | + |
719 | + >>> year0 = setUpYearWithSection(2000) |
720 | + >>> year1 = setUpYearWithSection(2001) |
721 | + >>> year2 = setUpYearWithSection(2002) |
722 | + |
723 | + >>> sec_year_0 = ISectionContainer(year0['Term1'])['Sec1'] |
724 | + >>> sec_year_1 = ISectionContainer(year1['Term1'])['Sec1'] |
725 | + >>> sec_year_2 = ISectionContainer(year2['Term1'])['Sec1'] |
726 | + |
727 | + We cannot link sections in the different school years. |
728 | + |
729 | + >>> sec_year_1.previous = sec_year_0 |
730 | + Traceback (most recent call last): |
731 | + ... |
732 | + InvalidSectionLinkException: |
733 | + Cannot link sections in different school years |
734 | + |
735 | + >>> sec_year_1.next = sec_year_2 |
736 | + Traceback (most recent call last): |
737 | + ... |
738 | + InvalidSectionLinkException: |
739 | + Cannot link sections in different school years |
740 | + |
741 | + """ |
742 | + |
743 | + |
744 | +def doctest_copySection(): |
745 | + r"""Test for copySection. |
746 | + |
747 | + >>> from schooltool.course.section import Section |
748 | + >>> from schooltool.course.course import Course |
749 | + >>> from schooltool.person.person import Person |
750 | + |
751 | + Create a section with a course, instructor and several members. |
752 | + |
753 | + >>> year = setUpSchoolYear(2000) |
754 | + >>> setUpTerms(year, 2) |
755 | + |
756 | + >>> section = Section('English A') |
757 | + >>> section.instructors.add(Person('teacher', 'Mr. Jones')) |
758 | + >>> section.members.add(Person('first','First')) |
759 | + >>> section.members.add(Person('second','Second')) |
760 | + >>> section.members.add(Person('third','Third')) |
761 | + >>> section.courses.add(Course(title="English")) |
762 | + >>> ISectionContainer(year['Term1'])['Sec1'] = section |
763 | + |
764 | + Let's copy it to another term. |
765 | + |
766 | + >>> from schooltool.course.section import copySection |
767 | + |
768 | + >>> new_section = copySection(section, year['Term2']) |
769 | + |
770 | + Sectoin's copy was created. |
771 | + |
772 | + >>> new_section is not section |
773 | + True |
774 | + |
775 | + Sec1 was available as section id in the new term, so it was preserved. |
776 | + |
777 | + >>> print new_section.__name__ |
778 | + Sec1 |
779 | + |
780 | + Courses, instructors and members were copied. |
781 | + |
782 | + >>> for course in new_section.courses: |
783 | + ... print course.title |
784 | + English |
785 | + |
786 | + >>> for person in new_section.instructors: |
787 | + ... print person.title |
788 | + Mr. Jones |
789 | + |
790 | + >>> for person in new_section.members: |
791 | + ... print person.title |
792 | + First |
793 | + Second |
794 | + Third |
795 | + |
796 | + If original section's __name__ is already present in the target term, an |
797 | + alternative is chosen. |
798 | + |
799 | + >>> other_section = copySection(section, year['Term2']) |
800 | + >>> print other_section.__name__ |
801 | + 1 |
802 | + |
803 | + """ |
804 | + |
805 | + |
806 | +def test_suite(): |
807 | + optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS |
808 | + suite = doctest.DocTestSuite(optionflags=optionflags, |
809 | + extraglobs={'provideAdapter': provideStubAdapter, |
810 | + 'provideUtility': provideStubUtility}, |
811 | + setUp=setUp, tearDown=tearDown) |
812 | + suite.layer = schoolyear_functional_layer |
813 | + return suite |
814 | + |
815 | + |
816 | +if __name__ == '__main__': |
817 | + unittest.main(defaultTest='test_suite') |
818 | |
819 | === modified file 'src/schooltool/relationship/tests/__init__.py' |
820 | --- src/schooltool/relationship/tests/__init__.py 2006-05-05 18:03:22 +0000 |
821 | +++ src/schooltool/relationship/tests/__init__.py 2009-03-21 12:52:42 +0000 |
822 | @@ -60,7 +60,7 @@ |
823 | return cmp(self.uri, other.uri) |
824 | |
825 | |
826 | -def setUp(): |
827 | +def setUp(test=None): |
828 | """Set up for schooltool.relationship doctests. |
829 | |
830 | Calls Zope's placelessSetUp, sets up annotations and relationships. |
831 | @@ -70,7 +70,7 @@ |
832 | setUpRelationships() |
833 | |
834 | |
835 | -def tearDown(): |
836 | +def tearDown(test=None): |
837 | """Tear down for schooltool.relationshp doctests.""" |
838 | setup.placelessTearDown() |
839 |
Section spanning core