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