Merge lp:~justas.sadzevicius/schooltool/catalogs into lp:schooltool/1.7

Proposed by Justas Sadzevičius
Status: Merged
Merged at revision: 2683
Proposed branch: lp:~justas.sadzevicius/schooltool/catalogs
Merge into: lp:schooltool/1.7
Diff against target: 2330 lines (+1430/-174)
37 files modified
src/schooltool/app/app.py (+5/-6)
src/schooltool/app/browser/app.py (+3/-4)
src/schooltool/app/browser/cal.py (+4/-5)
src/schooltool/app/browser/csvimport.py (+1/-2)
src/schooltool/app/browser/overlay.py (+3/-3)
src/schooltool/app/catalog.py (+216/-0)
src/schooltool/app/catalog.zcml (+22/-0)
src/schooltool/app/configure.zcml (+3/-1)
src/schooltool/app/interfaces.py (+40/-2)
src/schooltool/app/main.py (+12/-0)
src/schooltool/app/security.py (+7/-6)
src/schooltool/app/security.txt (+5/-2)
src/schooltool/app/tests/catalog-integration.txt (+105/-0)
src/schooltool/app/tests/test_app.py (+3/-5)
src/schooltool/app/tests/test_catalog.py (+749/-0)
src/schooltool/app/tests/test_security.py (+5/-2)
src/schooltool/basicperson/configure.zcml (+0/-5)
src/schooltool/basicperson/overrides.zcml (+4/-1)
src/schooltool/basicperson/person.py (+9/-21)
src/schooltool/commendation/browser.py (+2/-1)
src/schooltool/commendation/commendation.py (+2/-1)
src/schooltool/contact/configure.zcml (+3/-4)
src/schooltool/contact/contact.py (+12/-25)
src/schooltool/generations/__init__.py (+2/-2)
src/schooltool/generations/evolve33.py (+0/-7)
src/schooltool/generations/evolve35.py (+59/-0)
src/schooltool/generations/tests/__init__.py (+2/-0)
src/schooltool/generations/tests/test_evolve33.py (+1/-24)
src/schooltool/generations/tests/test_evolve35.py (+123/-0)
src/schooltool/person/configure.zcml (+4/-6)
src/schooltool/person/person.py (+13/-23)
src/schooltool/resource/types.py (+2/-2)
src/schooltool/skin/skin.py (+1/-3)
src/schooltool/term/browser/emergency.py (+2/-2)
src/schooltool/timetable/__init__.py (+3/-6)
src/schooltool/timetable/browser/__init__.py (+1/-2)
src/schooltool/utility/utility.py (+2/-1)
To merge this branch: bzr merge lp:~justas.sadzevicius/schooltool/catalogs
Reviewer Review Type Date Requested Status
Justas Sadzevičius Pending
Review via email: mp+26289@code.launchpad.net

Description of the change

New-style catalogs should not fall to ST in Lucid, so... merge to 1.5?

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/schooltool/app/app.py'
2--- src/schooltool/app/app.py 2010-02-09 16:54:13 +0000
3+++ src/schooltool/app/app.py 2010-05-28 11:09:37 +0000
4@@ -28,11 +28,9 @@
5 from persistent.dict import PersistentDict
6
7 from zope.location.location import LocationProxy
8-from zope.component import adapter
9-from zope.component import adapts
10+from zope.component import adapter, adapts
11 from zope.i18n import translate
12-from zope.interface import implementer
13-from zope.interface import implements, implementsOnly
14+from zope.interface import implementer, implements, implementsOnly
15
16 from zope.annotation.interfaces import IAttributeAnnotatable, IAnnotations
17 from zope.app.applicationcontrol.interfaces import IApplicationControl
18@@ -86,6 +84,8 @@
19 title = property(_title)
20
21
22+@adapter(None)
23+@implementer(ISchoolToolApplication)
24 def getSchoolToolApplication(ignore=None):
25 """Return the nearest ISchoolToolApplication.
26
27@@ -99,8 +99,7 @@
28 candidate = getSite()
29 if ISchoolToolApplication.providedBy(candidate):
30 return candidate
31- else:
32- raise ValueError("can't get a SchoolToolApplication")
33+ return None
34
35
36 class SimpleNameChooser(NameChooser):
37
38=== modified file 'src/schooltool/app/browser/app.py'
39--- src/schooltool/app/browser/app.py 2010-01-19 16:41:42 +0000
40+++ src/schooltool/app/browser/app.py 2010-05-28 11:09:37 +0000
41@@ -45,7 +45,6 @@
42 from schooltool.calendar.icalendar import convert_calendar_to_ical
43 from schooltool.common import SchoolToolMessage as _
44 from schooltool.app.browser.interfaces import IManageMenuViewletManager
45-from schooltool.app.app import getSchoolToolApplication
46 from schooltool.app.interfaces import ISchoolToolAuthenticationPlugin
47 from schooltool.app.interfaces import ISchoolToolApplication
48 from schooltool.app.interfaces import IApplicationPreferences
49@@ -63,7 +62,7 @@
50 """A view for the main application."""
51
52 def update(self):
53- prefs = IApplicationPreferences(getSchoolToolApplication())
54+ prefs = IApplicationPreferences(ISchoolToolApplication(None))
55 if prefs.frontPageCalendar:
56 url = absoluteURL(ISchoolToolCalendar(self.context),
57 self.request)
58@@ -275,7 +274,7 @@
59 def __init__(self, context, request):
60 BrowserView.__init__(self, context, request)
61
62- app = getSchoolToolApplication()
63+ app = ISchoolToolApplication(None)
64 prefs = self.schema(app)
65 initial = {}
66 for field in self.schema:
67@@ -292,7 +291,7 @@
68 except WidgetsError:
69 return # Errors will be displayed next to widgets
70
71- app = getSchoolToolApplication()
72+ app = ISchoolToolApplication(None)
73 prefs = self.schema(app)
74 for field in self.schema:
75 if field in data: # skip non-fields
76
77=== modified file 'src/schooltool/app/browser/cal.py'
78--- src/schooltool/app/browser/cal.py 2010-04-12 10:49:40 +0000
79+++ src/schooltool/app/browser/cal.py 2010-05-28 11:09:37 +0000
80@@ -77,7 +77,6 @@
81 from schooltool.app.browser.interfaces import IEventForDisplay
82 from schooltool.app.browser.interfaces import IHaveEventLegend
83 from schooltool.app.interfaces import ISchoolToolCalendarEvent
84-from schooltool.app.app import getSchoolToolApplication
85 from schooltool.app.interfaces import ISchoolToolCalendar
86 from schooltool.app.interfaces import IHaveCalendar
87 from schooltool.table.batch import IterableBatch
88@@ -934,7 +933,7 @@
89 timetable_template = ViewPageTemplateFile("templates/cal_weekly_timetable.pt")
90
91 def __call__(self):
92- app = getSchoolToolApplication()
93+ app = ISchoolToolApplication(None)
94 # XXX ttshemas were removed from app in evolve28.
95 # timetable_template can be killed now (maybe?).
96 # All related code should go to land of no return.
97@@ -2477,7 +2476,7 @@
98
99 elif "BOOK" in self.request: # and not self.update_status:
100 self.update_status = ''
101- sb = getSchoolToolApplication()
102+ sb = ISchoolToolApplication(None)
103 for res_id, resource in sb["resources"].items():
104 if 'add_item.%s' % res_id in self.request:
105 booked = self.hasBooked(resource)
106@@ -2488,7 +2487,7 @@
107
108 elif "UNBOOK" in self.request:
109 self.update_status = ''
110- sb = getSchoolToolApplication()
111+ sb = ISchoolToolApplication(None)
112 for res_id, resource in sb["resources"].items():
113 if 'remove_item.%s' % res_id in self.request:
114 booked = self.hasBooked(resource)
115@@ -2502,7 +2501,7 @@
116 @property
117 def availableResources(self):
118 """Gives us a list of all bookable resources."""
119- sb = getSchoolToolApplication()
120+ sb = ISchoolToolApplication(None)
121 calendar_owner = removeSecurityProxy(self.context.__parent__.__parent__)
122 def isBookable(resource):
123 if resource is calendar_owner:
124
125=== modified file 'src/schooltool/app/browser/csvimport.py'
126--- src/schooltool/app/browser/csvimport.py 2010-01-19 12:09:22 +0000
127+++ src/schooltool/app/browser/csvimport.py 2010-05-28 11:09:37 +0000
128@@ -27,7 +27,6 @@
129
130 from schooltool.schoolyear.interfaces import ISchoolYear
131 from schooltool.common import SchoolToolMessage as _
132-from schooltool.app.app import getSchoolToolApplication
133 from schooltool.app.app import SimpleNameChooser
134 from schooltool.app.interfaces import ISchoolToolApplication
135 from schooltool.course.interfaces import ICourseContainer
136@@ -260,7 +259,7 @@
137 def __init__(self, container, charset=None):
138 # XXX It appears that our security declarations are inadequate,
139 # because things break without this removeSecurityProxy.
140- self.app = getSchoolToolApplication()
141+ self.app = ISchoolToolApplication(None)
142 self.sections = removeSecurityProxy(container)
143 self.persons = self.app['persons']
144 self.errors = TimetableImportErrorCollection()
145
146=== modified file 'src/schooltool/app/browser/overlay.py'
147--- src/schooltool/app/browser/overlay.py 2009-11-30 18:56:52 +0000
148+++ src/schooltool/app/browser/overlay.py 2010-05-28 11:09:37 +0000
149@@ -31,8 +31,8 @@
150 from zope.viewlet.viewlet import ViewletBase
151
152 from schooltool.common import SchoolToolMessage as _
153-from schooltool.app.app import getSchoolToolApplication
154 from schooltool.app.interfaces import ISchoolToolCalendar
155+from schooltool.app.interfaces import ISchoolToolApplication
156 from schooltool.app.interfaces import IShowTimetables
157 from schooltool.person.interfaces import IPerson
158
159@@ -159,7 +159,7 @@
160 user = removeSecurityProxy(IPerson(self.request.principal, None))
161 if user is None:
162 return []
163- app = getSchoolToolApplication()
164+ app = ISchoolToolApplication(None)
165
166 result = []
167 for obj in app[container].values():
168@@ -179,7 +179,7 @@
169 """
170 user = IPerson(self.request.principal, None)
171 if user:
172- app = getSchoolToolApplication()
173+ app = ISchoolToolApplication(None)
174 calendar = ISchoolToolCalendar(app)
175 if canAccess(calendar, '__iter__'):
176 return {'title': app.title,
177
178=== added file 'src/schooltool/app/catalog.py'
179--- src/schooltool/app/catalog.py 1970-01-01 00:00:00 +0000
180+++ src/schooltool/app/catalog.py 2010-05-28 11:09:37 +0000
181@@ -0,0 +1,216 @@
182+#
183+# SchoolTool - common information systems platform for school administration
184+# Copyright (c) 2010 Shuttleworth Foundation
185+#
186+# This program is free software; you can redistribute it and/or modify
187+# it under the terms of the GNU General Public License as published by
188+# the Free Software Foundation; either version 2 of the License, or
189+# (at your option) any later version.
190+#
191+# This program is distributed in the hope that it will be useful,
192+# but WITHOUT ANY WARRANTY; without even the implied warranty of
193+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
194+# GNU General Public License for more details.
195+#
196+# You should have received a copy of the GNU General Public License
197+# along with this program; if not, write to the Free Software
198+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
199+#
200+"""
201+SchoolTool catalogs.
202+"""
203+from zope.interface import implementer, implements, implementsOnly
204+from zope.intid.interfaces import IIntIds, IIntIdAddedEvent, IIntIdRemovedEvent
205+from zope.component import adapter, queryUtility, getUtility
206+from zope.container import btree
207+from zope.container.contained import Contained
208+from zope.lifecycleevent import IObjectModifiedEvent
209+
210+from zc.catalog import extentcatalog
211+from zc.catalog import catalogindex
212+
213+from schooltool.app.interfaces import ISchoolToolApplication
214+from schooltool.app.interfaces import ICatalogStartUp
215+from schooltool.app.interfaces import ICatalogs
216+from schooltool.app.interfaces import IVersionedCatalog
217+from schooltool.app.app import ActionBase
218+from schooltool.table.catalog import FilterImplementing
219+
220+
221+APP_CATALOGS_KEY = 'schooltool.app.catalog:Catalogs'
222+
223+
224+class CatalogStartupBase(ActionBase):
225+ implementsOnly(ICatalogStartUp)
226+
227+
228+class Catalogs(btree.BTreeContainer):
229+ implements(ICatalogs)
230+
231+
232+@adapter(ISchoolToolApplication)
233+@implementer(ICatalogs)
234+def getAppCatalogs(app):
235+ if APP_CATALOGS_KEY not in app:
236+ app[APP_CATALOGS_KEY] = Catalogs()
237+ return app[APP_CATALOGS_KEY]
238+
239+
240+class VersionedCatalog(Contained):
241+ implements(IVersionedCatalog)
242+
243+ expired = False
244+ version = 0
245+ catalog = None
246+
247+ def __init__(self, catalog, version):
248+ self.catalog = catalog
249+ self.catalog.__parent__ = self
250+ self.catalog.__name__ = 'catalog'
251+ self.version = version
252+
253+ def __repr__(self):
254+ return '<%s v. %r>: %s' % (
255+ self.__class__.__name__, self.version, self.catalog)
256+
257+
258+class PrepareCatalogContainer(CatalogStartupBase):
259+
260+ def __call__(self):
261+ catalogs = ICatalogs(self.app)
262+ for entry in catalogs.values():
263+ entry.expired = True
264+
265+
266+class ExpiredCatalogCleanup(CatalogStartupBase):
267+
268+ def __call__(self):
269+ catalogs = ICatalogs(self.app)
270+ for key in list(catalogs):
271+ if catalogs[key].expired:
272+ del catalogs[key]
273+
274+
275+class CatalogFactory(CatalogStartupBase):
276+
277+ after = ('prepare-catalog-container', )
278+ before = ('expired-catalog-cleanup', )
279+
280+ version = u''
281+
282+ @classmethod
283+ def key(cls):
284+ return u'catalog:%s.%s' % (cls.__module__, cls.__name__)
285+
286+ @classmethod
287+ def get(cls, ignored=None):
288+ app = ISchoolToolApplication(None)
289+ catalogs = ICatalogs(app)
290+ entry = catalogs.get(cls.key())
291+ if entry is None:
292+ return None
293+ return entry.catalog
294+
295+ def getVersion(self):
296+ return unicode(self.version)
297+
298+ def createCatalog(self):
299+ raise NotImplementedError()
300+
301+ def setIndexes(self, catalog):
302+ raise NotImplementedError()
303+
304+ def __call__(self):
305+ app = ISchoolToolApplication(None)
306+ catalogs = ICatalogs(app)
307+ key = self.key()
308+ version = self.getVersion()
309+
310+ if key in catalogs:
311+ if catalogs[key].version == version:
312+ catalogs[key].expired = False
313+ else:
314+ del catalogs[key]
315+
316+ if key not in catalogs:
317+ catalog = self.createCatalog()
318+ catalogs[key] = VersionedCatalog(catalog, version)
319+ # XXX: if setIndexes throw, delete the catalog and rethrow
320+ self.setIndexes(catalog)
321+
322+
323+class CatalogImplementing(CatalogFactory):
324+ """Factory of catalogs containing objects implementing the given interface."""
325+
326+ interface = None # override in child classes
327+
328+ def createCatalog(self):
329+ return extentcatalog.Catalog(
330+ extentcatalog.FilterExtent(
331+ FilterImplementing(self.interface)))
332+
333+ def getVersion(self):
334+ return u'interface:%s, version:%s' % (
335+ u'%s.%s' % (self.interface.__module__, self.interface.__name__),
336+ super(CatalogImplementing, self).getVersion())
337+
338+
339+class AttributeCatalog(CatalogImplementing):
340+ """Catalog indexing specified attributes of objects implementing
341+ the given interface."""
342+
343+ attributes = ()
344+
345+ def getVersion(self):
346+ return u'attributes:%s, %s' % (
347+ tuple(sorted(self.attributes)),
348+ super(AttributeCatalog, self).getVersion())
349+
350+ def setIndexes(self, catalog):
351+ for name in self.attributes:
352+ catalog[name] = catalogindex.ValueIndex(name)
353+
354+
355+@adapter(IIntIdAddedEvent)
356+def indexDocSubscriber(event):
357+ app = ISchoolToolApplication(None, None)
358+ if app is None:
359+ return
360+ obj = event.object
361+ util = getUtility(IIntIds, context=app)
362+ obj_id = util.getId(obj)
363+ catalogs = ICatalogs(app)
364+ for entry in catalogs.values():
365+ entry.catalog.index_doc(obj_id, obj)
366+
367+
368+@adapter(IObjectModifiedEvent)
369+def reindexDocSubscriber(event):
370+ app = ISchoolToolApplication(None, None)
371+ if app is None:
372+ return
373+ obj = event.object
374+ util = queryUtility(IIntIds, context=app)
375+ if util is None:
376+ return
377+ obj_id = util.queryId(obj)
378+ if obj_id is None:
379+ return
380+ catalogs = ICatalogs(app)
381+ for entry in catalogs.values():
382+ entry.catalog.index_doc(obj_id, obj)
383+
384+
385+@adapter(IIntIdRemovedEvent)
386+def unindexDocSubscriber(event):
387+ app = ISchoolToolApplication(None, None)
388+ if app is None:
389+ return
390+ obj = event.object
391+ util = getUtility(IIntIds, context=app)
392+ obj_id = util.queryId(obj)
393+ if obj_id is None:
394+ return
395+ catalogs = ICatalogs(app)
396+ for entry in catalogs.values():
397+ entry.catalog.unindex_doc(obj_id)
398
399=== added file 'src/schooltool/app/catalog.zcml'
400--- src/schooltool/app/catalog.zcml 1970-01-01 00:00:00 +0000
401+++ src/schooltool/app/catalog.zcml 2010-05-28 11:09:37 +0000
402@@ -0,0 +1,22 @@
403+<?xml version="1.0"?>
404+<configure xmlns="http://namespaces.zope.org/zope">
405+
406+ <adapter factory=".catalog.getAppCatalogs" />
407+
408+ <subscriber
409+ for="schooltool.app.interfaces.ICatalogStartUpEvent"
410+ handler=".main.startSchoolToolCatalogs" />
411+
412+ <adapter
413+ factory=".catalog.PrepareCatalogContainer"
414+ name="prepare-catalog-container" />
415+
416+ <adapter
417+ factory=".catalog.ExpiredCatalogCleanup"
418+ name="expired-catalog-cleanup" />
419+
420+ <subscriber handler=".catalog.indexDocSubscriber" />
421+ <subscriber handler=".catalog.reindexDocSubscriber" />
422+ <subscriber handler=".catalog.unindexDocSubscriber" />
423+
424+</configure>
425
426=== modified file 'src/schooltool/app/configure.zcml'
427--- src/schooltool/app/configure.zcml 2010-01-19 16:41:42 +0000
428+++ src/schooltool/app/configure.zcml 2010-05-28 11:09:37 +0000
429@@ -169,6 +169,9 @@
430 for="schooltool.app.interfaces.IApplicationStartUpEvent"
431 handler=".main.startSchoolToolPlugins" />
432
433+ <!-- Catalog support -->
434+ <include file="catalog.zcml" />
435+
436 <!-- Timetables for content components -->
437 <class class="schooltool.app.app.SchoolToolApplication">
438 <implements interface="schooltool.timetable.interfaces.IHaveTimetables" />
439@@ -185,7 +188,6 @@
440 <class class="schooltool.resource.resource.BaseResource">
441 <implements interface="schooltool.timetable.interfaces.IHaveTimetables" />
442 </class>
443-
444
445 <!-- Core and Domain content objects -->
446
447
448=== modified file 'src/schooltool/app/interfaces.py'
449--- src/schooltool/app/interfaces.py 2010-01-19 16:41:42 +0000
450+++ src/schooltool/app/interfaces.py 2010-05-28 11:09:37 +0000
451@@ -23,7 +23,9 @@
452 import zope.schema
453
454 from zope.component.interfaces import ObjectEvent, IObjectEvent
455+from zope.container.interfaces import IContainer, IContained
456 from zope.container.interfaces import IReadContainer
457+from zope.container.constraints import contains
458 from zope.interface import implements
459 from zope.interface import Interface, Attribute
460 from zope.location.interfaces import ILocation, IContained
461@@ -72,15 +74,26 @@
462
463
464 class ICatalogSetUpEvent(IObjectEvent):
465- """The SchoolTool catalogs are being initialized.
466+ """Old style notification that the SchoolTool catalogs are being initialized.
467
468- Subscribers should set up their catalogs.
469+ Subject to deprecation. Subscribers should no longer use this to set up their
470+ catalogs.
471 """
472
473 class CatalogSetUpEvent(ObjectEvent):
474 implements(ICatalogSetUpEvent)
475
476
477+class ICatalogStartUpEvent(IObjectEvent):
478+ """The SchoolTool catalogs has started up.
479+
480+ A hook for catalog initialization.
481+ """
482+
483+class CatalogStartUpEvent(ObjectEvent):
484+ implements(ICatalogStartUpEvent)
485+
486+
487 class ISchoolToolCalendar(IEditCalendar, ILocation):
488 """A SchoolTool calendar.
489
490@@ -234,6 +247,10 @@
491 """Perform plugin specific set up."""
492
493
494+class ICatalogStartUp(IPluginAction):
495+ """Set up SchoolTool catalogs."""
496+
497+
498 class IPluginInit(IPluginAction):
499 """Perform plugin initialization when setting up the SchoolTool
500 application."""
501@@ -245,3 +262,24 @@
502
503 class ISchoolToolAuthenticationPlugin(ISchoolToolAuthentication):
504 """A plugin for local schooltool authentication utility. """
505+
506+
507+class IVersionedCatalog(IContained):
508+ """Versioned catalog entry."""
509+
510+ version = zope.schema.TextLine(
511+ title=u"Version",
512+ description=u"Current version of the catalog.")
513+
514+ expired = zope.schema.Bool(
515+ title=u"Expired",
516+ description=u"Expired catalogs should be removed.")
517+
518+ catalog = Attribute("""The catalog object.""")
519+
520+
521+class ICatalogs(IContainer):
522+ """Container of versioned catalogs."""
523+
524+ contains(IVersionedCatalog)
525+
526
527=== modified file 'src/schooltool/app/main.py'
528--- src/schooltool/app/main.py 2010-04-28 21:27:43 +0000
529+++ src/schooltool/app/main.py 2010-05-28 11:09:37 +0000
530@@ -71,12 +71,14 @@
531 from schooltool.app.interfaces import ApplicationStartUpEvent
532 from schooltool.app.interfaces import ApplicationInitializationEvent
533 from schooltool.app.interfaces import IPluginInit, IPluginStartUp
534+from schooltool.app.interfaces import ICatalogStartUp
535 from schooltool.app.interfaces import ISchoolToolInitializationUtility
536 from schooltool.app.app import SchoolToolApplication
537 from schooltool.app import pdf
538 from schooltool.person.interfaces import IPersonFactory
539 from schooltool.app.interfaces import ICookieLanguageSelector
540 from schooltool.app.interfaces import CatalogSetUpEvent
541+from schooltool.app.interfaces import CatalogStartUpEvent
542 from schooltool.utility.utility import setUpUtilities
543 from schooltool.utility.utility import UtilitySpecification
544
545@@ -448,6 +450,12 @@
546 print >> sys.stderr, "Failed to execute %s: %s" % (action, exception)
547
548
549+def startSchoolToolCatalogs(event):
550+ adapters = getAdapters((event.object, ), ICatalogStartUp)
551+ sorter = PluginActionSorter(adapters)
552+ executePluginActions(sorter())
553+
554+
555 def initializeSchoolToolPlugins(event):
556 adapters = getAdapters((event.object, ), IPluginInit)
557 sorter = PluginActionSorter(adapters)
558@@ -617,6 +625,7 @@
559 # before initializing plugins themselves or else all the
560 # initial groups, persons, resources will not get indexed
561 notify(CatalogSetUpEvent(app))
562+ notify(CatalogStartUpEvent(app))
563
564 # initialize plugins themselves
565 notify(ApplicationInitializationEvent(app))
566@@ -715,7 +724,10 @@
567 connection = db.open()
568 root = connection.root()
569 app = root[ZopePublication.root_name]
570+ setSite(app)
571+ notify(CatalogStartUpEvent(app))
572 notify(ApplicationStartUpEvent(app))
573+ setSite(None)
574 transaction.commit()
575 connection.close()
576
577
578=== modified file 'src/schooltool/app/security.py'
579--- src/schooltool/app/security.py 2010-02-09 16:54:13 +0000
580+++ src/schooltool/app/security.py 2010-05-28 11:09:37 +0000
581@@ -40,7 +40,8 @@
582 from zope.site import LocalSiteManager
583 from zope.traversing.browser.absoluteurl import absoluteURL
584 from zope.traversing.api import traverse
585-from schooltool.app.app import getSchoolToolApplication
586+
587+from schooltool.app.interfaces import ISchoolToolApplication
588 from schooltool.app.interfaces import ISchoolToolAuthentication
589 from schooltool.app.interfaces import IAsset
590 from schooltool.person.interfaces import IPerson
591@@ -95,13 +96,13 @@
592 return self.getPrincipal('sb.person.' + login)
593
594 def _checkPlainTextPassword(self, username, password):
595- app = getSchoolToolApplication()
596+ app = ISchoolToolApplication(None)
597 if username in app['persons']:
598 person = app['persons'][username]
599 return person.checkPassword(password)
600
601 def _checkHashedPassword(self, username, password):
602- app = getSchoolToolApplication()
603+ app = ISchoolToolApplication(None)
604 if username in app['persons']:
605 person = app['persons'][username]
606 return (person._hashed_password is not None
607@@ -139,7 +140,7 @@
608
609 def unauthorized(self, id, request):
610 """Signal an authorization failure."""
611- app = getSchoolToolApplication()
612+ app = ISchoolToolApplication(None)
613 app_url = absoluteURL(app, request)
614 query_string = request.getHeader('QUERY_STRING')
615 post_form_id = self.storePOSTData(request)
616@@ -162,7 +163,7 @@
617
618 Returns principals for groups and persons.
619 """
620- app = getSchoolToolApplication()
621+ app = ISchoolToolApplication(None)
622 if id.startswith(self.person_prefix):
623 username = id[len(self.person_prefix):]
624 if username in app['persons']:
625@@ -331,7 +332,7 @@
626 prefix = PersonContainerAuthenticationPlugin.group_prefix
627 if not group_principal_id.startswith(prefix):
628 return None
629- groups = IGroupContainer(getSchoolToolApplication(), None)
630+ groups = IGroupContainer(ISchoolToolApplication(None), None)
631 if groups is None:
632 return None
633 group_id = group_principal_id[len(prefix):]
634
635=== modified file 'src/schooltool/app/security.txt'
636--- src/schooltool/app/security.txt 2010-02-09 16:54:13 +0000
637+++ src/schooltool/app/security.txt 2010-05-28 11:09:37 +0000
638@@ -66,9 +66,12 @@
639 >>> verifyObject(ISite, app)
640 Traceback (most recent call last):
641 ...
642- DoesNotImplement: ...
643- ...
644+ DoesNotImplement: An object does not implement interface
645+ <InterfaceClass zope.component.interfaces.ISite>
646
647+ >>> from zope.component import provideAdapter
648+ >>> from schooltool.app.app import getSchoolToolApplication
649+ >>> provideAdapter(getSchoolToolApplication)
650
651 The SchoolToolAuthenticationUtility is an IAuthentication utility that
652 lives in that site:
653
654=== added file 'src/schooltool/app/tests/catalog-integration.txt'
655--- src/schooltool/app/tests/catalog-integration.txt 1970-01-01 00:00:00 +0000
656+++ src/schooltool/app/tests/catalog-integration.txt 2010-05-28 11:09:37 +0000
657@@ -0,0 +1,105 @@
658+Version-controlled catalogs
659+===========================
660+
661+ A Catalog in it's essence is a set of value caches (indexes).
662+Usually these are cached attributes of objects in a container or
663+objects implementing a given interface. Sometimes aggregated values
664+like persons full title are cached.
665+
666+ The main purpose of catalogs is giving access to the values without
667+the need to load the whole object into memory. Think searching persons
668+named 'M...' in a container with several thousands of them, you'll get
669+the picture.
670+
671+ Due to their nature, catalogs need to be rebuilt, changed,
672+reindexed, etc. during active development. They are commonly stored
673+in the database - that would mean evolution scripts.
674+
675+ For the above reasons, SchoolTool provides catalog version control
676+and catalog cache that gets updated when catalog versions change.
677+
678+
679+How it works
680+------------
681+
682+ To register a versioned catalog, let's first inherit the CatalogFactory and
683+implement the required methods:
684+
685+ >>> from schooltool.app.catalog import CatalogFactory
686+
687+ >>> class MyCatalog(CatalogFactory):
688+ ... version = 1
689+ ... def createCatalog(self):
690+ ... return CatalogStub('my catalog')
691+ ... def setIndexes(self, catalog):
692+ ... print 'Setting indexes for', catalog
693+
694+ Catalogs are registered as named adapters.
695+
696+ >>> provideAdapter(
697+ ... MyCatalog, name="test-catalog-integration-mycatalog")
698+
699+ Sometime at server start up catalog setup event is fired and
700+catalogs get created.
701+
702+ >>> from schooltool.app.interfaces import ISchoolToolApplication
703+ >>> from schooltool.app.interfaces import CatalogStartUpEvent
704+ >>> from zope.event import notify
705+
706+ >>> print MyCatalog.get()
707+ None
708+
709+ >>> notify(CatalogStartUpEvent(ISchoolToolApplication(None)))
710+ Setting indexes for <CatalogStub 'my catalog'>
711+
712+ >>> catalog = MyCatalog.get()
713+ >>> print catalog
714+ <CatalogStub 'my catalog'>
715+
716+ Catalog's __parent__ holds the versioning information.
717+
718+ >>> print catalog.__parent__
719+ <VersionedCatalog v. u'1'>: <CatalogStub 'my catalog'>
720+
721+ The catalog's unique name is constructed from the factory class
722+name. So if you rename the class, know that the catalogs will be
723+re-created and re-indexed.
724+
725+ >>> from schooltool.app.interfaces import ICatalogs
726+ >>> print sorted(ICatalogs(ISchoolToolApplication(None)))
727+ [u'catalog:__builtin__.MyCatalog']
728+
729+ Next time the server starts up, catalog is left as it is. We'll
730+demonstrate this by changing the catalog creation method, but leaving
731+the version unchanged.
732+
733+ >>> MyCatalog.createCatalog = lambda self: CatalogStub('my better catalog')
734+ >>> notify(CatalogStartUpEvent(ISchoolToolApplication(None)))
735+
736+ >>> print MyCatalog.get()
737+ <CatalogStub 'my catalog'>
738+
739+ But once we change the version, the catalog gets re-created.
740+
741+ >>> MyCatalog.version = 1.1
742+
743+ >>> notify(CatalogStartUpEvent(ISchoolToolApplication(None)))
744+ Setting indexes for <CatalogStub 'my better catalog'>
745+
746+ >>> catalog = MyCatalog.get()
747+ >>> print catalog.__parent__
748+ <VersionedCatalog v. u'1.1'>: <CatalogStub 'my better catalog'>
749+
750+ And of course once the catalog factory is no longer provided, catalog
751+will be deleted.
752+
753+ >>> unregisterAdapter(
754+ ... MyCatalog, name="test-catalog-integration-mycatalog")
755+
756+ >>> notify(CatalogStartUpEvent(ISchoolToolApplication(None)))
757+
758+ >>> print MyCatalog.get()
759+ None
760+
761+ >>> print sorted(ICatalogs(ISchoolToolApplication(None)))
762+ []
763
764=== modified file 'src/schooltool/app/tests/test_app.py'
765--- src/schooltool/app/tests/test_app.py 2010-02-09 19:18:22 +0000
766+++ src/schooltool/app/tests/test_app.py 2010-05-28 11:09:37 +0000
767@@ -107,13 +107,11 @@
768 >>> from zope.site import LocalSiteManager
769 >>> app.setSiteManager(LocalSiteManager(app))
770
771- If site is not a SchoolToolApplication, we get an error
772+ If site is not a SchoolToolApplication, we get None
773
774 >>> from schooltool.app.app import getSchoolToolApplication
775- >>> getSchoolToolApplication()
776- Traceback (most recent call last):
777- ...
778- ValueError: can't get a SchoolToolApplication
779+ >>> print getSchoolToolApplication()
780+ None
781
782 If current site is a SchoolToolApplication, we get it:
783
784
785=== added file 'src/schooltool/app/tests/test_catalog.py'
786--- src/schooltool/app/tests/test_catalog.py 1970-01-01 00:00:00 +0000
787+++ src/schooltool/app/tests/test_catalog.py 2010-05-28 11:09:37 +0000
788@@ -0,0 +1,749 @@
789+#
790+# SchoolTool - common information systems platform for school administration
791+# Copyright (c) 2010 Shuttleworth Foundation
792+#
793+# This program is free software; you can redistribute it and/or modify
794+# it under the terms of the GNU General Public License as published by
795+# the Free Software Foundation; either version 2 of the License, or
796+# (at your option) any later version.
797+#
798+# This program is distributed in the hope that it will be useful,
799+# but WITHOUT ANY WARRANTY; without even the implied warranty of
800+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
801+# GNU General Public License for more details.
802+#
803+# You should have received a copy of the GNU General Public License
804+# along with this program; if not, write to the Free Software
805+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
806+#
807+"""
808+Unit tests for catalogs
809+"""
810+import unittest
811+import doctest
812+
813+from zope.app.testing import setup
814+from zope.interface import implements, Interface
815+from zope.interface.verify import verifyObject
816+from zope.component import provideAdapter
817+from zope.component.hooks import getSite, setSite
818+from zope.site import SiteManagerContainer
819+from zope.site.folder import rootFolder
820+
821+from schooltool.testing.setup import ZCMLWrapper
822+from schooltool.app.interfaces import ISchoolToolApplication
823+from schooltool.app.interfaces import ICatalogs
824+from schooltool.app.catalog import getAppCatalogs
825+
826+
827+class AppStub(dict, SiteManagerContainer):
828+ implements(ISchoolToolApplication)
829+
830+
831+class CatalogStub(dict):
832+
833+ def __init__(self, name):
834+ self.name = name
835+
836+ def __repr__(self):
837+ return '<%s %r>' % (self.__class__.__name__, self.name)
838+
839+
840+def provideApplicationStub():
841+ app = AppStub()
842+ provideAdapter(
843+ lambda ignored: app,
844+ adapts=(None,),
845+ provides=ISchoolToolApplication)
846+ return app
847+
848+
849+def doctest_Catalogs():
850+ """Tests for Catalogs.
851+
852+ >>> app = provideApplicationStub()
853+
854+ There is an adapter that sets up and returns the catalogs container for the app.
855+
856+ >>> list(app.items())
857+ []
858+
859+ >>> catalogs = ICatalogs(app)
860+
861+ >>> list(app.items())
862+ [('schooltool.app.catalog:Catalogs',
863+ <schooltool.app.catalog.Catalogs object at ...>)]
864+
865+ >>> verifyObject(ICatalogs, catalogs)
866+ True
867+
868+ >>> catalogs is app.values()[0]
869+ True
870+
871+ Catalogs is a dict-like container.
872+
873+ >>> print dict(catalogs)
874+ {}
875+
876+ """
877+
878+
879+def doctest_PrepareCatalogContainer():
880+ """Tests for PrepareCatalogContainer.
881+
882+ >>> class VersionedCatalogStub(object):
883+ ... def __init__(self, expired=False):
884+ ... self.expired = expired
885+ ... def __repr__(self):
886+ ... return '<Stub expired=%s>' % self.expired
887+
888+ Let's build an app with some catalog entries.
889+
890+ >>> app = provideApplicationStub()
891+ >>> catalogs = ICatalogs(app)
892+
893+ >>> catalogs['cat-1'] = VersionedCatalogStub(expired=False)
894+ >>> catalogs['cat-2'] = VersionedCatalogStub(expired=True)
895+ >>> catalogs['cat-3'] = VersionedCatalogStub(expired=False)
896+
897+ PrepareCatalogContainer is supposed to be executed as the first action
898+ of application catalog startup.
899+
900+ >>> from schooltool.app.interfaces import ICatalogStartUp
901+ >>> from schooltool.app.catalog import PrepareCatalogContainer
902+
903+ >>> action = PrepareCatalogContainer(app)
904+ >>> verifyObject(ICatalogStartUp, action)
905+ True
906+
907+ When executed it marks all catalogs expired.
908+
909+ >>> action()
910+
911+ >>> print sorted(ICatalogs(app).items())
912+ [(u'cat-1', <Stub expired=True>),
913+ (u'cat-2', <Stub expired=True>),
914+ (u'cat-3', <Stub expired=True>)]
915+
916+ """
917+
918+
919+def doctest_ExpiredCatalogCleanup():
920+ """Tests for ExpiredCatalogCleanup.
921+
922+ >>> class VersionedCatalogStub(object):
923+ ... def __init__(self, expired=False):
924+ ... self.expired = expired
925+ ... def __repr__(self):
926+ ... return '<Stub expired=%s>' % self.expired
927+
928+ Let's build an app with some catalog entries.
929+
930+ >>> app = provideApplicationStub()
931+ >>> catalogs = ICatalogs(app)
932+
933+ >>> catalogs['cat-1'] = VersionedCatalogStub(expired=False)
934+ >>> catalogs['cat-2'] = VersionedCatalogStub(expired=True)
935+ >>> catalogs['cat-3'] = VersionedCatalogStub(expired=False)
936+
937+ ExpiredCatalogCleanup is supposed to be executed as the last action
938+ of application catalog startup.
939+
940+ >>> from schooltool.app.interfaces import ICatalogStartUp
941+ >>> from schooltool.app.catalog import ExpiredCatalogCleanup
942+
943+ >>> action = ExpiredCatalogCleanup(app)
944+ >>> verifyObject(ICatalogStartUp, action)
945+ True
946+
947+ When executed it removes expired catalogs.
948+
949+ >>> action()
950+
951+ >>> print sorted(ICatalogs(app).items())
952+ [(u'cat-1', <Stub expired=False>),
953+ (u'cat-3', <Stub expired=False>)]
954+
955+ """
956+
957+
958+def doctest_CatalogFactory():
959+ """Tests for CatalogFactory.
960+
961+ >>> app = provideApplicationStub()
962+
963+ CatalogFactory is a base class for creating and registering versioned catalogs.
964+
965+ >>> from schooltool.app.catalog import CatalogFactory
966+ >>> factory = CatalogFactory(app)
967+
968+ >>> factory.createCatalog()
969+ Traceback (most recent call last):
970+ ...
971+ NotImplementedError
972+
973+ >>> factory.setIndexes(None)
974+ Traceback (most recent call last):
975+ ...
976+ NotImplementedError
977+
978+ It is also an action to be executed on application catalog startup.
979+
980+ >>> from schooltool.app.interfaces import ICatalogStartUp
981+
982+ >>> class CatalogFactoryForTest(CatalogFactory):
983+ ... version = 1
984+ ... def createCatalog(self):
985+ ... return CatalogStub('test')
986+ ... def setIndexes(self, catalog):
987+ ... catalog['idx-1'] = 'Stub index 1'
988+
989+ >>> factory = CatalogFactoryForTest(app)
990+
991+ >>> verifyObject(ICatalogStartUp, factory)
992+ True
993+
994+ It defines some control on when it wants to be executed.
995+
996+ >>> factory.after
997+ ('prepare-catalog-container',)
998+
999+ >>> factory.before
1000+ ('expired-catalog-cleanup',)
1001+
1002+ The class itself has some helpers to fetch the catalog from app.
1003+
1004+ >>> print CatalogFactoryForTest.key()
1005+ catalog:schooltool.app.tests.test_catalog.CatalogFactoryForTest
1006+
1007+ >>> print CatalogFactoryForTest.get()
1008+ None
1009+
1010+ Well, the catalog was not set up yet. Let's build it.
1011+
1012+ >>> from schooltool.app.interfaces import IVersionedCatalog
1013+
1014+ >>> factory()
1015+
1016+ >>> catalog = CatalogFactoryForTest.get()
1017+
1018+ >>> print catalog
1019+ <CatalogStub 'test'>
1020+
1021+ >>> sorted(catalog.items())
1022+ [('idx-1', 'Stub index 1')]
1023+
1024+ Both catalog and it's version entry are stored in app's catalog container.
1025+
1026+ >>> version_entry = catalog.__parent__
1027+
1028+ >>> print version_entry
1029+ <VersionedCatalog v. u'1'>: <CatalogStub 'test'>
1030+
1031+ >>> verifyObject(IVersionedCatalog, version_entry)
1032+ True
1033+
1034+ >>> print version_entry.__parent__
1035+ <schooltool.app.catalog.Catalogs object at ...>
1036+
1037+ >>> version_entry.__parent__ is ICatalogs(app)
1038+ True
1039+
1040+ >>> print version_entry.__name__
1041+ catalog:schooltool.app.tests.test_catalog.CatalogFactoryForTest
1042+
1043+ >>> print version_entry.__name__ == CatalogFactoryForTest.key()
1044+ True
1045+
1046+ """
1047+
1048+
1049+def doctest_CatalogFactory_versioning():
1050+ """Tests for CatalogFactory handling of catalog versions.
1051+
1052+ >>> class CatalogCreatorMixin(object):
1053+ ... def createCatalog(self):
1054+ ... return CatalogStub('test-1')
1055+ ... def setIndexes(self, catalog):
1056+ ... catalog['idx-1'] = 'Stub index 1'
1057+
1058+ >>> app = provideApplicationStub()
1059+ >>> catalogs = ICatalogs(app)
1060+
1061+ Let's create a catalog first.
1062+
1063+ >>> from schooltool.app.catalog import CatalogFactory
1064+ >>> class Factory1(CatalogCreatorMixin, CatalogFactory):
1065+ ... version = 1
1066+
1067+ >>> factory1 = Factory1(app)
1068+
1069+ >>> factory1()
1070+ >>> catalog1 = Factory1.get()
1071+
1072+ >>> print catalog1
1073+ <CatalogStub 'test-1'>
1074+
1075+ >>> print sorted(catalogs.keys())
1076+ [u'catalog:schooltool.app.tests.test_catalog.Factory1']
1077+
1078+ If the version stays the same, factory leaves the catalog untouched, but
1079+ changes it's expired status.
1080+
1081+ >>> class Factory1(CatalogCreatorMixin, CatalogFactory):
1082+ ... version = 1
1083+
1084+ >>> catalog1.__parent__.expired = True
1085+
1086+ >>> factory1 = Factory1(app)
1087+ >>> factory1()
1088+
1089+ >>> Factory1.get() is catalog1
1090+ True
1091+
1092+ >>> catalog1.__parent__.expired
1093+ False
1094+
1095+ >>> print sorted(catalogs.keys())
1096+ [u'catalog:schooltool.app.tests.test_catalog.Factory1']
1097+
1098+ If we changed the version, catalog gets re-created.
1099+
1100+ >>> class Factory1(CatalogCreatorMixin, CatalogFactory):
1101+ ... version = 1.2
1102+ ... def setIndexes(self, catalog):
1103+ ... catalog['idx-1'] = 'Stub index 1 point 2'
1104+
1105+ >>> factory12 = Factory1(app)
1106+ >>> factory12()
1107+
1108+ >>> catalog12 = Factory1.get()
1109+ >>> catalog12 is catalog1
1110+ False
1111+
1112+ >>> print sorted(catalog12.items())
1113+ [('idx-1', 'Stub index 1 point 2')]
1114+
1115+ CatalogFactory actually uses the getVersion method, not the version
1116+ attribute. Let's override getVersion
1117+
1118+ >>> class Factory1(CatalogCreatorMixin, CatalogFactory):
1119+ ... version = 1.2
1120+ ... def getVersion(self):
1121+ ... return 'stable-version'
1122+
1123+ >>> factory = Factory1(app)
1124+ >>> factory()
1125+
1126+ >>> catalog_stable = Factory1.get()
1127+
1128+ >>> catalog_stable is catalog12
1129+ False
1130+
1131+ >>> print Factory1.get().__parent__
1132+ <VersionedCatalog v. 'stable-version'>: <CatalogStub 'test-1'>
1133+
1134+ >>> class Factory1(CatalogCreatorMixin, CatalogFactory):
1135+ ... version = 1.314
1136+ ... def getVersion(self):
1137+ ... return 'stable-version'
1138+
1139+ >>> factory1 = Factory1(app)
1140+ >>> factory1()
1141+
1142+ >>> Factory1.get() is catalog_stable
1143+ True
1144+
1145+ """
1146+
1147+
1148+def doctest_CatalogImplementing():
1149+ """Tests for CatalogImplementing. This is a factory of catalogs that
1150+ contain only objects implementing given interface.
1151+
1152+ Say we have two objects implementing very similar interfaces.
1153+
1154+ >>> from zope.schema import TextLine
1155+
1156+ >>> class TitledObject(object):
1157+ ... def __init__(self, title):
1158+ ... self.title = title
1159+ ... def __repr__(self):
1160+ ... return '<%s (%r)>' % (self.__class__.__name__, self.title)
1161+
1162+ >>> class IFoo(Interface):
1163+ ... title = TextLine(title=u"Title")
1164+ >>> class Foo(TitledObject):
1165+ ... implements(IFoo)
1166+
1167+ >>> class IBar(Interface):
1168+ ... title = TextLine(title=u"Title")
1169+ >>> class Bar(TitledObject):
1170+ ... implements(IBar)
1171+
1172+ >>> app = provideApplicationStub()
1173+ >>> app['objects'] = {
1174+ ... 1: Foo('one'),
1175+ ... 2: Bar('two'),
1176+ ... 3: Foo('three'),
1177+ ... }
1178+
1179+ Let's create a catalog that indexes titles of IFoo.
1180+
1181+ >>> from zope.container.contained import Contained
1182+ >>> from schooltool.app.catalog import CatalogImplementing
1183+
1184+ >>> class TitleIndex(Contained):
1185+ ... def __init__(self):
1186+ ... self.data = []
1187+ ... def index_doc(self, id, value):
1188+ ... self.data.append((id, value))
1189+
1190+ >>> class CatalogTitles(CatalogImplementing):
1191+ ... version = 1
1192+ ... interface = IFoo
1193+ ... def setIndexes(self, catalog):
1194+ ... catalog['title'] = TitleIndex()
1195+
1196+ >>> factory = CatalogTitles(app)
1197+ >>> factory()
1198+
1199+ >>> catalog = CatalogTitles.get()
1200+ >>> print catalog.__parent__
1201+ <VersionedCatalog v.
1202+ u'interface:schooltool.app.tests.test_catalog.IFoo,
1203+ version:1'>:
1204+ <zc.catalog.extentcatalog.Catalog object at ...>
1205+
1206+ This catalog only indexes objects implementing IFoo.
1207+
1208+ >>> def index_app(catalog):
1209+ ... for docid, doc in app['objects'].items():
1210+ ... catalog.index_doc(docid, doc)
1211+ ... print sorted(catalog['title'].data)
1212+
1213+ >>> index_app(CatalogTitles.get())
1214+ [(1, <Foo ('one')>), (3, <Foo ('three')>)]
1215+
1216+ Catalog indexes are untouched after app restart.
1217+
1218+ >>> factory = CatalogTitles(app)
1219+ >>> factory()
1220+
1221+ >>> CatalogTitles.get() is catalog
1222+ True
1223+
1224+ >>> print sorted(CatalogTitles.get()['title'].data)
1225+ [(1, <Foo ('one')>), (3, <Foo ('three')>)]
1226+
1227+ If we change the version, catalog will be re-created, as expected.
1228+
1229+ >>> class CatalogTitles(CatalogImplementing):
1230+ ... version = 2
1231+ ... interface = IFoo
1232+ ... def setIndexes(self, catalog):
1233+ ... catalog['title'] = TitleIndex()
1234+
1235+ >>> factory = CatalogTitles(app)
1236+ >>> factory()
1237+
1238+ >>> catalog2 = CatalogTitles.get()
1239+ >>> catalog2 is catalog
1240+ False
1241+
1242+ We trust other machinery to reindex the catalog.
1243+
1244+ >>> print sorted(CatalogTitles.get()['title'].data)
1245+ []
1246+
1247+ If we change the interface, catalog also gets re-created.
1248+
1249+ >>> index_app(CatalogTitles.get())
1250+ [(1, <Foo ('one')>), (3, <Foo ('three')>)]
1251+
1252+ >>> class CatalogTitles(CatalogImplementing):
1253+ ... version = 2
1254+ ... interface = IBar
1255+ ... def setIndexes(self, catalog):
1256+ ... catalog['title'] = TitleIndex()
1257+
1258+ >>> factory = CatalogTitles(app)
1259+ >>> factory()
1260+
1261+ >>> CatalogTitles.get() is catalog2
1262+ False
1263+
1264+ >>> index_app(CatalogTitles.get())
1265+ [(2, <Bar ('two')>)]
1266+
1267+ >>> catalog3 = CatalogTitles.get()
1268+ >>> print catalog3.__parent__
1269+ <VersionedCatalog v.
1270+ u'interface:schooltool.app.tests.test_catalog.IBar,
1271+ version:2'>:
1272+ <zc.catalog.extentcatalog.Catalog object at ...>
1273+
1274+ """
1275+
1276+
1277+def doctest_AttributeCatalog():
1278+ """Tests for AttributeCatalog. This is a factory of catalogs that
1279+ index attributes of objects implementing given interface.
1280+
1281+ Say we have an object we want to catalog.
1282+
1283+ >>> from zope.schema import TextLine
1284+
1285+ >>> class ITriplet(Interface):
1286+ ... a = TextLine(title=u"Title")
1287+ ... b = TextLine(title=u"Title")
1288+ ... c = TextLine(title=u"Title")
1289+ >>> class Triplet(object):
1290+ ... implements(ITriplet)
1291+ ... def __init__(self, a, b, c):
1292+ ... self.a, self.b, self.c = a, b, c
1293+ ... def __repr__(self):
1294+ ... return '<%s (%s:%s:%s)>' % (
1295+ ... self.__class__.__name__,
1296+ ... self.a, self.b, self.c)
1297+
1298+ >>> class Foo(object):
1299+ ... implements(Interface)
1300+ ... a = b = c = 0
1301+
1302+ >>> app = provideApplicationStub()
1303+ >>> app['objects'] = {
1304+ ... 0: Foo(),
1305+ ... 1: Triplet('one', 'eins', 'I'),
1306+ ... 2: Triplet('two', 'zwei', 'II'),
1307+ ... 3: Triplet('three', 'drei', 'III'),
1308+ ... }
1309+
1310+ Let's create a catalog that indexes few attributes of the triplet.
1311+
1312+ >>> from schooltool.app.catalog import AttributeCatalog
1313+
1314+ >>> class CatalogTriplets(AttributeCatalog):
1315+ ... version = 1
1316+ ... interface = ITriplet
1317+ ... attributes = ('a', 'c')
1318+
1319+ >>> factory = CatalogTriplets(app)
1320+ >>> factory()
1321+
1322+ And index some objects.
1323+
1324+ >>> def index_app(catalog):
1325+ ... for docid, doc in app['objects'].items():
1326+ ... catalog.index_doc(docid, doc)
1327+ ... for name, index in catalog.items():
1328+ ... print 'catalog[%r]: %s' % (
1329+ ... name,
1330+ ... sorted(index.documents_to_values.items()))
1331+
1332+ >>> index_app(CatalogTriplets.get())
1333+ catalog[u'a']: [(1, 'one'), (2, 'two'), (3, 'three')]
1334+ catalog[u'c']: [(1, 'I'), (2, 'II'), (3, 'III')]
1335+
1336+ We can see that version of the catalog also includes the list of attributes,
1337+ so the catalog will be recreated when attributes, interface or version changes.
1338+
1339+ >>> catalog = CatalogTriplets.get()
1340+ >>> print catalog.__parent__
1341+ <VersionedCatalog v.
1342+ u"attributes:('a', 'c'),
1343+ interface:schooltool.app.tests.test_catalog.ITriplet,
1344+ version:1">:
1345+ <zc.catalog.extentcatalog.Catalog object at ...>
1346+
1347+ >>> class CatalogTriplets(AttributeCatalog):
1348+ ... version = 1
1349+ ... interface = ITriplet
1350+ ... attributes = ('b')
1351+
1352+ >>> factory = CatalogTriplets(app)
1353+ >>> factory()
1354+
1355+ >>> CatalogTriplets.get() is catalog
1356+ False
1357+
1358+ >>> index_app(CatalogTriplets.get())
1359+ catalog[u'b']: [(1, 'eins'), (2, 'zwei'), (3, 'drei')]
1360+
1361+ """
1362+
1363+
1364+def doctest_catalog_subscribers():
1365+ """Tests for catalog subscribers.
1366+
1367+ Let's provide our subscribers.
1368+
1369+ >>> from zope.component import provideHandler
1370+ >>> from schooltool.app.catalog import indexDocSubscriber
1371+ >>> from schooltool.app.catalog import reindexDocSubscriber
1372+ >>> from schooltool.app.catalog import unindexDocSubscriber
1373+
1374+ >>> provideHandler(indexDocSubscriber)
1375+ >>> provideHandler(reindexDocSubscriber)
1376+ >>> provideHandler(unindexDocSubscriber)
1377+
1378+ And set up the event firing helpers.
1379+
1380+ >>> from zope.component import provideUtility, queryUtility
1381+ >>> from zope.event import notify
1382+ >>> from zope.intid.interfaces import (
1383+ ... IIntIds, IntIdAddedEvent, IntIdRemovedEvent)
1384+ >>> from zope.lifecycleevent import (
1385+ ... ObjectAddedEvent, ObjectRemovedEvent, ObjectModifiedEvent)
1386+
1387+ >>> def addAndNotify(obj, obj_id):
1388+ ... util = queryUtility(IIntIds)
1389+ ... if util is None:
1390+ ... print "No IIntIds utility, so don't fire IntIdAddedEvent"
1391+ ... return
1392+ ... util[obj] = obj_id
1393+ ... print 'firing IntIdAddedEvent'
1394+ ... notify(IntIdAddedEvent(obj, ObjectAddedEvent(obj)))
1395+
1396+ >>> def notifyModified(obj):
1397+ ... print 'firing ObjectModifiedEvent'
1398+ ... notify(ObjectModifiedEvent(obj))
1399+
1400+ >>> def notifyAndRemove(obj):
1401+ ... util = queryUtility(IIntIds)
1402+ ... if util is None:
1403+ ... print "No IIntIds utility, so don't fire IntIdRemovedEvent"
1404+ ... return
1405+ ... print 'firing IntIdRemovedEvent'
1406+ ... notify(IntIdRemovedEvent(obj, ObjectAddedEvent(obj)))
1407+ ... del util[obj]
1408+
1409+ When database is being set up, we may have no SchoolToolApplication and
1410+ no IntIds utility.
1411+
1412+ >>> class TestObj(object):
1413+ ... __parent__ = __name__ = None
1414+ ... def __init__(self, name):
1415+ ... self.name = name
1416+ ... def __repr__(self):
1417+ ... return '<%s %r>' % (self.__class__.__name__, self.name)
1418+
1419+ >>> test_one = TestObj('missing_one')
1420+ >>> addAndNotify(test_one, 1)
1421+ No IIntIds utility, so don't fire IntIdAddedEvent
1422+
1423+ >>> notifyModified(test_one)
1424+ firing ObjectModifiedEvent
1425+
1426+ >>> notifyAndRemove(test_one)
1427+ No IIntIds utility, so don't fire IntIdRemovedEvent
1428+
1429+ Having IntIds utility is not enough as catalogs live within the application
1430+ itself. Subscribers handle this case also.
1431+
1432+ >>> class IntIdsStub(dict):
1433+ ... def getId(self, obj):
1434+ ... return self[obj]
1435+ ... def queryId(self, obj):
1436+ ... return self.get(obj)
1437+
1438+ >>> provideUtility(IntIdsStub(), IIntIds)
1439+
1440+ >>> test_two = TestObj('two')
1441+ >>> addAndNotify(test_two, 2)
1442+ firing IntIdAddedEvent
1443+
1444+ >>> notifyModified(test_two)
1445+ firing ObjectModifiedEvent
1446+
1447+ >>> notifyAndRemove(test_two)
1448+ firing IntIdRemovedEvent
1449+
1450+ Let's provide an application and set up a catalog.
1451+
1452+ >>> from schooltool.app.catalog import VersionedCatalog
1453+
1454+ >>> class PrintingCatalogStub(CatalogStub):
1455+ ... def index_doc(self, doc_id, doc):
1456+ ... print 'CatalogStub(%r) indexed doc %s (%s)' % (
1457+ ... self.name, doc_id, doc)
1458+ ... def unindex_doc(self, doc_id):
1459+ ... print 'CatalogStub(%r) unindexed doc %s' % (
1460+ ... self.name, doc_id)
1461+
1462+ >>> app = provideApplicationStub()
1463+ >>> catalogs = ICatalogs(app)
1464+ >>> catalogs['demo'] = VersionedCatalog(PrintingCatalogStub('demo'), 'v1')
1465+
1466+ We can now see objects being indexed.
1467+
1468+ >>> test_three = TestObj('three')
1469+ >>> addAndNotify(test_three, 3)
1470+ firing IntIdAddedEvent
1471+ CatalogStub('demo') indexed doc 3 (<TestObj 'three'>)
1472+
1473+ >>> test_three.name='three and a half'
1474+ >>> notifyModified(test_three)
1475+ firing ObjectModifiedEvent
1476+ CatalogStub('demo') indexed doc 3 (<TestObj 'three and a half'>)
1477+
1478+ >>> notifyAndRemove(test_three)
1479+ firing IntIdRemovedEvent
1480+ CatalogStub('demo') unindexed doc 3
1481+
1482+ """
1483+
1484+
1485+def setUp(test):
1486+ setup.placefulSetUp()
1487+ provideAdapter(getAppCatalogs)
1488+
1489+
1490+def tearDown(test):
1491+ setup.placefulTearDown()
1492+
1493+
1494+def provideStubAdapter(factory, adapts=None, provides=None, name=u''):
1495+ sm = getSite().getSiteManager()
1496+ sm.registerAdapter(factory, required=adapts, provided=provides, name=name)
1497+
1498+
1499+def unregisterStubAdapter(factory, adapts=None, provides=None, name=u''):
1500+ sm = getSite().getSiteManager()
1501+ sm.unregisterAdapter(factory, required=adapts, provided=provides, name=name)
1502+
1503+
1504+def setUpIntegration(test):
1505+ setup.placefulSetUp()
1506+ zcml = ZCMLWrapper()
1507+ zcml.setUp(
1508+ namespaces={"": "http://namespaces.zope.org/zope"},
1509+ i18n_domain='schooltool')
1510+ zcml.include('zope.app.zcmlfiles')
1511+ zcml.include('schooltool.app', file='catalog.zcml')
1512+ root = rootFolder()
1513+ root['app'] = provideApplicationStub()
1514+ setup.createSiteManager(root['app'], setsite=True)
1515+ test.globs.update({
1516+ 'zcml': zcml,
1517+ 'CatalogStub': CatalogStub,
1518+ 'provideAdapter': provideStubAdapter,
1519+ 'unregisterAdapter': unregisterStubAdapter,
1520+ })
1521+
1522+
1523+def test_suite():
1524+ optionflags = (doctest.NORMALIZE_WHITESPACE |
1525+ doctest.ELLIPSIS |
1526+ doctest.REPORT_NDIFF)
1527+ return unittest.TestSuite([
1528+ doctest.DocTestSuite(setUp=setUp, tearDown=tearDown,
1529+ optionflags=optionflags),
1530+ doctest.DocFileSuite(
1531+ 'catalog-integration.txt',
1532+ setUp=setUpIntegration, tearDown=tearDown,
1533+ optionflags=optionflags),
1534+ ])
1535+
1536+if __name__ == '__main__':
1537+ unittest.main(defaultTest='test_suite')
1538
1539=== modified file 'src/schooltool/app/tests/test_security.py'
1540--- src/schooltool/app/tests/test_security.py 2010-01-21 17:31:17 +0000
1541+++ src/schooltool/app/tests/test_security.py 2010-05-28 11:09:37 +0000
1542@@ -65,10 +65,13 @@
1543
1544
1545 def test_suite():
1546+ optionflags = (doctest.ELLIPSIS |
1547+ doctest.NORMALIZE_WHITESPACE |
1548+ doctest.REPORT_NDIFF)
1549 return unittest.TestSuite([
1550 unittest.makeSuite(TestAuthSetUpSubscriber),
1551- doctest.DocTestSuite(optionflags=doctest.ELLIPSIS),
1552- doctest.DocFileSuite('../security.txt', optionflags=doctest.ELLIPSIS),
1553+ doctest.DocTestSuite(optionflags=optionflags),
1554+ doctest.DocFileSuite('../security.txt', optionflags=optionflags),
1555 ])
1556
1557
1558
1559=== modified file 'src/schooltool/basicperson/configure.zcml'
1560--- src/schooltool/basicperson/configure.zcml 2009-11-02 13:45:40 +0000
1561+++ src/schooltool/basicperson/configure.zcml 2010-05-28 11:09:37 +0000
1562@@ -64,11 +64,6 @@
1563 provides="schooltool.app.interfaces.ICalendarParentCrowd"
1564 name="schooltool.view" />
1565
1566- <subscriber
1567- for="schooltool.app.interfaces.ICatalogSetUpEvent"
1568- handler=".person.catalogSetUpSubscriber"
1569- />
1570-
1571 <utility
1572 factory=".vocabularies.groupVocabularyFactory"
1573 provides="zope.schema.interfaces.IVocabularyFactory"
1574
1575=== modified file 'src/schooltool/basicperson/overrides.zcml'
1576--- src/schooltool/basicperson/overrides.zcml 2009-12-23 20:59:28 +0000
1577+++ src/schooltool/basicperson/overrides.zcml 2010-05-28 11:09:37 +0000
1578@@ -10,9 +10,12 @@
1579 name="person_info_viewers"
1580 factory=".security.PersonInfoViewersCrowd" />
1581
1582+ <adapter factory=".person.PersonCatalog"
1583+ name="schooltool.person.person.PersonCatalog" />
1584+
1585 <adapter
1586 for="schooltool.person.interfaces.IPersonContainer"
1587- factory=".person.getPersonContainerCatalog"
1588+ factory=".person.getPersonCatalog"
1589 provides="zope.catalog.interfaces.ICatalog" />
1590
1591 <configure
1592
1593=== modified file 'src/schooltool/basicperson/person.py'
1594--- src/schooltool/basicperson/person.py 2009-12-23 20:59:28 +0000
1595+++ src/schooltool/basicperson/person.py 2010-05-28 11:09:37 +0000
1596@@ -21,11 +21,6 @@
1597 """
1598 from zope.interface import implements
1599 from zope.component import adapts
1600-from zope.catalog.interfaces import ICatalog
1601-from zope.catalog.catalog import Catalog
1602-from zope.component import getUtility
1603-
1604-from zc.catalog.catalogindex import ValueIndex
1605
1606 from schooltool.person.person import Person
1607 from schooltool.person.interfaces import IPersonFactory
1608@@ -37,12 +32,10 @@
1609 from schooltool.relationship import RelationshipProperty
1610 from schooltool.basicperson.advisor import URIAdvisor, URIAdvising, URIStudent
1611 from schooltool.basicperson.interfaces import IBasicPerson
1612+from schooltool.app.catalog import AttributeCatalog
1613 from schooltool.common import SchoolToolMessage as _
1614
1615
1616-PERSON_CATALOG_KEY = 'schooltool.basicperson'
1617-
1618-
1619 class BasicPerson(Person):
1620 implements(IBasicPerson)
1621
1622@@ -119,16 +112,11 @@
1623 PersonInstructorsCrowd(self.context).contains(principal))
1624
1625
1626-def catalogSetUp(catalog):
1627- catalog['__name__'] = ValueIndex('__name__', IBasicPerson)
1628- catalog['title'] = ValueIndex('title', IBasicPerson)
1629- catalog['first_name'] = ValueIndex('first_name', IBasicPerson)
1630- catalog['last_name'] = ValueIndex('last_name', IBasicPerson)
1631-
1632-
1633-catalogSetUpSubscriber = UtilitySetUp(
1634- Catalog, ICatalog, PERSON_CATALOG_KEY, setUp=catalogSetUp)
1635-
1636-
1637-def getPersonContainerCatalog(container):
1638- return getUtility(ICatalog, PERSON_CATALOG_KEY)
1639+class PersonCatalog(AttributeCatalog):
1640+
1641+ version = '1 - replaced catalog utility'
1642+ interface = IBasicPerson
1643+ attributes = ('__name__', 'title', 'first_name', 'last_name')
1644+
1645+
1646+getPersonCatalog = PersonCatalog.get
1647
1648=== modified file 'src/schooltool/commendation/browser.py'
1649--- src/schooltool/commendation/browser.py 2010-04-28 23:13:27 +0000
1650+++ src/schooltool/commendation/browser.py 2010-05-28 11:09:37 +0000
1651@@ -32,6 +32,7 @@
1652 from schooltool.commendation import interfaces, commendation
1653 from schooltool.commendation.interfaces import _
1654 from schooltool.app import app
1655+from schooltool.app.interfaces import ISchoolToolApplication
1656
1657
1658 # This is a simple view class that provides the information of a commendation
1659@@ -164,7 +165,7 @@
1660 self.request.get('search_scope', 'group')]
1661
1662 # Get the SchoolTool application instance and access its annotations.
1663- stapp = app.getSchoolToolApplication()
1664+ stapp = ISchoolToolApplication(None)
1665 annotations = IAnnotations(stapp)
1666 # A list comprehension that iterates through all the commendations and
1667 # applies the filter one-by-one.
1668
1669=== modified file 'src/schooltool/commendation/commendation.py'
1670--- src/schooltool/commendation/commendation.py 2010-04-28 23:13:27 +0000
1671+++ src/schooltool/commendation/commendation.py 2010-05-28 11:09:37 +0000
1672@@ -30,6 +30,7 @@
1673 from zope.security.management import queryInteraction
1674
1675 from schooltool.app import app
1676+from schooltool.app.interfaces import ISchoolToolApplication
1677 from schooltool.commendation.interfaces import ICommendation
1678 from schooltool.commendation.interfaces import ICommendations
1679 from schooltool.commendation.interfaces import ICommendationContained
1680@@ -111,7 +112,7 @@
1681 # Whereever we are, get the SchoolTool application and access its
1682 # annotations. Then add the commendation to the list of all cached
1683 # commendations.
1684- stapp = app.getSchoolToolApplication()
1685+ stapp = ISchoolToolApplication(None)
1686 annotations = IAnnotations(stapp)
1687 if CommendationsCacheKey not in annotations:
1688 annotations[CommendationsCacheKey] = persistent.list.PersistentList()
1689
1690=== modified file 'src/schooltool/contact/configure.zcml'
1691--- src/schooltool/contact/configure.zcml 2010-01-19 12:09:22 +0000
1692+++ src/schooltool/contact/configure.zcml 2010-05-28 11:09:37 +0000
1693@@ -82,10 +82,9 @@
1694 factory=".basicperson.PersonRemovedSubsciber"
1695 name="notify_person_contact_removed" />
1696
1697- <subscriber
1698- for="schooltool.app.interfaces.ICatalogSetUpEvent"
1699- handler=".contact.catalogSetUpSubscriber"
1700- />
1701+ <adapter
1702+ factory=".contact.ContactCatalog"
1703+ name="schooltool.contact.contact.ContactCatalog" />
1704
1705 <adapter
1706 for=".interfaces.IContactContainer"
1707
1708=== modified file 'src/schooltool/contact/contact.py'
1709--- src/schooltool/contact/contact.py 2010-01-19 12:09:22 +0000
1710+++ src/schooltool/contact/contact.py 2010-05-28 11:09:37 +0000
1711@@ -25,18 +25,14 @@
1712 from zope.component import adapter
1713 from zope.interface import implementer
1714 from zope.interface import implements
1715-from zope.catalog.interfaces import ICatalog
1716 from zope.intid.interfaces import IIntIds
1717 from zope.container.contained import Contained
1718 from zope.container.btree import BTreeContainer
1719 from zope.component import getUtility
1720
1721-from zc.catalog.catalogindex import ValueIndex
1722-from zc.catalog.extentcatalog import Catalog
1723-from zc.catalog.extentcatalog import FilterExtent
1724-
1725 from schooltool.app.interfaces import ISchoolToolApplication
1726 from schooltool.app.app import InitBase, StartUpBase
1727+from schooltool.app.catalog import AttributeCatalog
1728 from schooltool.utility.utility import UtilitySetUp
1729 from schooltool.contact.interfaces import IContactPersonInfo
1730 from schooltool.contact.interfaces import IContactable
1731@@ -49,7 +45,6 @@
1732 from schooltool.relationship.relationship import RelationshipSchema
1733 from schooltool.securitypolicy import crowds
1734 from schooltool.table.catalog import ConvertingIndex
1735-from schooltool.table.catalog import FilterImplementing
1736 from schooltool.table.table import simple_form_key
1737 from schooltool.person.interfaces import IPerson
1738 from schooltool.course.section import PersonInstructorsCrowd
1739@@ -57,8 +52,6 @@
1740 from schooltool.common import SchoolToolMessage as _
1741
1742
1743-CONTACT_CATALOG_KEY = 'schooltool.contact'
1744-
1745 URIContactRelationship = URIObject('http://schooltool.org/ns/contact',
1746 'Contact relationship',
1747 'The contact relationship.')
1748@@ -186,22 +179,17 @@
1749 return 'contacts.%s%x' % (simple_form_key(contact), doc_id)
1750
1751
1752-def catalogFactory():
1753- return Catalog(FilterExtent(FilterImplementing(IContact)))
1754-
1755-
1756-def catalogSetUp(catalog):
1757- catalog['form_keys'] = ConvertingIndex(converter=IUniqueFormKey)
1758- catalog['first_name'] = ValueIndex('first_name')
1759- catalog['last_name'] = ValueIndex('last_name')
1760-
1761-
1762-catalogSetUpSubscriber = UtilitySetUp(
1763- catalogFactory, ICatalog, CONTACT_CATALOG_KEY, setUp=catalogSetUp)
1764-
1765-
1766-def getContactCatalog(container):
1767- return getUtility(ICatalog, CONTACT_CATALOG_KEY)
1768+class ContactCatalog(AttributeCatalog):
1769+ version = '1 - replaced catalog utility'
1770+ interface = IContact
1771+ attributes = ('first_name', 'last_name')
1772+
1773+ def setIndexes(self, catalog):
1774+ super(ContactCatalog, self).setIndexes(catalog)
1775+ catalog['form_keys'] = ConvertingIndex(converter=IUniqueFormKey)
1776+
1777+
1778+getContactCatalog = ContactCatalog.get
1779
1780
1781 class ContactPersonInstructorsCrowd(PersonInstructorsCrowd):
1782@@ -218,4 +206,3 @@
1783 if user in section.instructors:
1784 return True
1785 return False
1786-
1787
1788=== modified file 'src/schooltool/generations/__init__.py'
1789--- src/schooltool/generations/__init__.py 2010-04-27 13:40:34 +0000
1790+++ src/schooltool/generations/__init__.py 2010-05-28 11:09:37 +0000
1791@@ -25,6 +25,6 @@
1792 from zope.app.generations.generations import SchemaManager
1793
1794 schemaManager = SchemaManager(
1795- minimum_generation=34,
1796- generation=34,
1797+ minimum_generation=35,
1798+ generation=35,
1799 package_name='schooltool.generations')
1800
1801=== modified file 'src/schooltool/generations/evolve33.py'
1802--- src/schooltool/generations/evolve33.py 2010-02-09 16:54:13 +0000
1803+++ src/schooltool/generations/evolve33.py 2010-05-28 11:09:37 +0000
1804@@ -25,13 +25,10 @@
1805 from zope.app.publication.zopepublication import ZopePublication
1806 from zope.component.hooks import getSite, setSite
1807 from zope.component import getUtility
1808-from zope.catalog.interfaces import ICatalog
1809
1810 from schooltool.app.interfaces import ISchoolToolApplication
1811 from schooltool.app.interfaces import CatalogSetUpEvent
1812 from schooltool.contact.interfaces import IContact
1813-from schooltool.contact.contact import catalogSetUpSubscriber
1814-from schooltool.contact.contact import CONTACT_CATALOG_KEY
1815 from schooltool.person.interfaces import IPerson
1816
1817
1818@@ -46,10 +43,6 @@
1819 persons = findObjectsProviding(app, IPerson)
1820 for person in persons:
1821 contact = IContact(person, None)
1822- # regsiter contact catalogs
1823- catalogSetUpSubscriber(CatalogSetUpEvent(app))
1824- util = getUtility(ICatalog, CONTACT_CATALOG_KEY)
1825- util.updateIndexes()
1826
1827 setSite(old_site)
1828
1829
1830=== added file 'src/schooltool/generations/evolve35.py'
1831--- src/schooltool/generations/evolve35.py 1970-01-01 00:00:00 +0000
1832+++ src/schooltool/generations/evolve35.py 2010-05-28 11:09:37 +0000
1833@@ -0,0 +1,59 @@
1834+#
1835+# SchoolTool - common information systems platform for school administration
1836+# Copyright (c) 2010 Shuttleworth Foundation
1837+#
1838+# This program is free software; you can redistribute it and/or modify
1839+# it under the terms of the GNU General Public License as published by
1840+# the Free Software Foundation; either version 2 of the License, or
1841+# (at your option) any later version.
1842+#
1843+# This program is distributed in the hope that it will be useful,
1844+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1845+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1846+# GNU General Public License for more details.
1847+#
1848+# You should have received a copy of the GNU General Public License
1849+# along with this program; if not, write to the Free Software
1850+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1851+#
1852+"""
1853+Upgrade SchoolTool to generation 35.
1854+
1855+Evolution script to unregister old catalog utilities.
1856+"""
1857+from zope.app.generations.utility import findObjectsProviding
1858+from zope.app.publication.zopepublication import ZopePublication
1859+from zope.component import queryUtility
1860+from zope.component.hooks import getSite, setSite
1861+from zope.traversing.api import traverse
1862+
1863+from schooltool.app.interfaces import ISchoolToolApplication
1864+from zope.catalog.interfaces import ICatalog
1865+
1866+
1867+CATALOG_KEYS = [
1868+ 'schooltool.basicperson',
1869+ 'schooltool.contact',
1870+ 'schooltool.person',
1871+ ]
1872+
1873+
1874+def evolve(context):
1875+ root = context.connection.root().get(ZopePublication.root_name, None)
1876+
1877+ old_site = getSite()
1878+ apps = findObjectsProviding(root, ISchoolToolApplication)
1879+ for app in apps:
1880+ setSite(app)
1881+ sm = app.getSiteManager()
1882+ default = traverse(app, '++etc++site/default')
1883+
1884+ for key in CATALOG_KEYS:
1885+ util = queryUtility(ICatalog, name=key, default=None)
1886+ if util is None:
1887+ continue
1888+ name = util.__name__
1889+ sm.unregisterUtility(util, ICatalog, key)
1890+ del default[name]
1891+
1892+ setSite(old_site)
1893
1894=== modified file 'src/schooltool/generations/tests/__init__.py'
1895--- src/schooltool/generations/tests/__init__.py 2010-01-19 12:09:22 +0000
1896+++ src/schooltool/generations/tests/__init__.py 2010-05-28 11:09:37 +0000
1897@@ -48,6 +48,7 @@
1898
1899
1900 def catalogSetUp(test):
1901+ """This code is deprecated, to be used only by old evolution tests."""
1902 setup.placefulSetUp()
1903 setup.setUpTraversal()
1904 setUpAnnotations()
1905@@ -91,5 +92,6 @@
1906 [IIntIdRemovedEvent])
1907
1908 def catalogTearDown(test):
1909+ """This code is deprecated, to be used only by old evolution tests."""
1910 setup.placefulTearDown()
1911 _d.clear()
1912
1913=== modified file 'src/schooltool/generations/tests/test_evolve33.py'
1914--- src/schooltool/generations/tests/test_evolve33.py 2010-02-09 19:18:22 +0000
1915+++ src/schooltool/generations/tests/test_evolve33.py 2010-05-28 11:09:37 +0000
1916@@ -24,13 +24,12 @@
1917
1918 from zope.app.testing import setup
1919
1920-from zope.component import queryUtility, provideUtility
1921+from zope.component import provideUtility
1922 from zope.component import provideAdapter
1923 from zope.interface import implements
1924 from zope.intid import IntIds
1925 from zope.intid.interfaces import IIntIds
1926 from zope.site.folder import Folder
1927-from zope.catalog.interfaces import ICatalog
1928 from zope.container.btree import BTreeContainer
1929 from zope.component.hooks import getSite, setSite
1930
1931@@ -89,28 +88,6 @@
1932 >>> print getSite()
1933 None
1934
1935- Contact catalog was registered for ISchoolToolApplication sites.
1936-
1937- >>> catalog = queryUtility(ICatalog, 'schooltool.contact')
1938- >>> print catalog
1939- None
1940-
1941- >>> setSite(app)
1942-
1943- >>> catalog = queryUtility(ICatalog, 'schooltool.contact')
1944- >>> catalog
1945- <zc.catalog.extentcatalog.Catalog object ...>
1946-
1947- Catalog indexes were set up properly.
1948-
1949- >>> sorted(i.__name__ for i in catalog.values())
1950- [u'first_name', u'form_keys', u'last_name']
1951-
1952- And contacts of existing persons got indexed.
1953-
1954- >>> sorted(catalog['first_name'].documents_to_values.values())
1955- ['Johny', 'Petey']
1956-
1957 """
1958
1959
1960
1961=== added file 'src/schooltool/generations/tests/test_evolve35.py'
1962--- src/schooltool/generations/tests/test_evolve35.py 1970-01-01 00:00:00 +0000
1963+++ src/schooltool/generations/tests/test_evolve35.py 2010-05-28 11:09:37 +0000
1964@@ -0,0 +1,123 @@
1965+#
1966+# SchoolTool - common information systems platform for school administration
1967+# Copyright (c) 2010 Shuttleworth Foundation
1968+#
1969+# This program is free software; you can redistribute it and/or modify
1970+# it under the terms of the GNU General Public License as published by
1971+# the Free Software Foundation; either version 2 of the License, or
1972+# (at your option) any later version.
1973+#
1974+# This program is distributed in the hope that it will be useful,
1975+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1976+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1977+# GNU General Public License for more details.
1978+#
1979+# You should have received a copy of the GNU General Public License
1980+# along with this program; if not, write to the Free Software
1981+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1982+#
1983+"""
1984+Unit tests for schooltool.generations.evolve35
1985+"""
1986+import unittest
1987+import doctest
1988+
1989+from zope.app.testing import setup
1990+from zope.interface import implements
1991+from zope.component import queryUtility
1992+from zope.component.hooks import getSite, setSite
1993+from zope.traversing.api import traverse
1994+from zope.site import LocalSiteManager
1995+from zope.site.folder import Folder
1996+from zope.location import Location
1997+from zope.catalog.interfaces import ICatalog
1998+
1999+from schooltool.app.interfaces import ISchoolToolApplication
2000+from schooltool.generations.tests import ContextStub
2001+from schooltool.generations.evolve35 import CATALOG_KEYS
2002+
2003+
2004+def registerLocalUtility(site, utility, name):
2005+ manager = site.getSiteManager()
2006+ default = traverse(site, '++etc++site/default')
2007+ default['storage.key:'+name] = utility
2008+ manager.registerUtility(utility, ICatalog, name)
2009+
2010+
2011+class UtilityStub(Folder):
2012+ def __init__(self, name):
2013+ super(UtilityStub, self).__init__()
2014+ self.name = name
2015+
2016+ def __repr__(self):
2017+ return '<%s (%s)>' % (self.__class__.__name__, self.name)
2018+
2019+
2020+class AppStub(Folder):
2021+ implements(ISchoolToolApplication)
2022+
2023+
2024+def doctest_evolve35():
2025+ """Test evolution to generation 35.
2026+
2027+ First, let's build ST app with local catalog utilities.
2028+
2029+ >>> context = ContextStub()
2030+ >>> context.root_folder['app'] = app = AppStub()
2031+ >>> app.setSiteManager(LocalSiteManager(app))
2032+
2033+ >>> for name in CATALOG_KEYS:
2034+ ... registerLocalUtility(app, UtilityStub(name), name)
2035+
2036+ >>> setSite(app)
2037+ >>> for name in CATALOG_KEYS:
2038+ ... print queryUtility(ICatalog, name=name, default=None)
2039+ <UtilityStub (schooltool.basicperson)>
2040+ <UtilityStub (schooltool.contact)>
2041+ <UtilityStub (schooltool.person)>
2042+
2043+ Set the site to something else and evolve.
2044+
2045+ >>> context.root_folder['frob'] = frob = Folder()
2046+ >>> frob.setSiteManager(LocalSiteManager(frob))
2047+ >>> setSite(frob)
2048+
2049+ >>> from schooltool.generations.evolve35 import evolve
2050+ >>> evolve(context)
2051+
2052+ Active site was kept.
2053+
2054+ >>> getSite() is frob
2055+ True
2056+
2057+ And catalogs in our app were unregistered.
2058+
2059+ >>> setSite(app)
2060+ >>> for name in CATALOG_KEYS:
2061+ ... print queryUtility(ICatalog, name=name, default=None)
2062+ None
2063+ None
2064+ None
2065+
2066+ """
2067+
2068+
2069+def setUp(test):
2070+ setup.placefulSetUp()
2071+ setup.setUpTraversal()
2072+
2073+
2074+def tearDown(test):
2075+ setup.placefulTearDown()
2076+
2077+
2078+def test_suite():
2079+ optionflags = (doctest.ELLIPSIS |
2080+ doctest.NORMALIZE_WHITESPACE |
2081+ doctest.REPORT_ONLY_FIRST_FAILURE)
2082+ return doctest.DocTestSuite(setUp=setUp, tearDown=tearDown,
2083+ optionflags=optionflags)
2084+
2085+
2086+if __name__ == '__main__':
2087+ unittest.main(defaultTest='test_suite')
2088
2089=== modified file 'src/schooltool/person/configure.zcml'
2090--- src/schooltool/person/configure.zcml 2010-01-19 12:09:22 +0000
2091+++ src/schooltool/person/configure.zcml 2010-05-28 11:09:37 +0000
2092@@ -91,14 +91,12 @@
2093 provides=".interfaces.IPerson"
2094 for="schooltool.person.interfaces.IPersonPreferences" />
2095
2096- <subscriber
2097- for="schooltool.app.interfaces.ICatalogSetUpEvent"
2098- handler=".person.catalogSetUpSubscriber"
2099- />
2100+ <adapter factory=".person.PersonCatalog"
2101+ name="schooltool.person.person.PersonCatalog" />
2102
2103 <adapter
2104- for=".interfaces.IPersonContainer"
2105- factory=".person.getPersonContainerCatalog"
2106+ for="schooltool.person.interfaces.IPersonContainer"
2107+ factory=".person.getPersonCatalog"
2108 provides="zope.catalog.interfaces.ICatalog" />
2109
2110 <configure
2111
2112=== modified file 'src/schooltool/person/person.py'
2113--- src/schooltool/person/person.py 2010-01-27 17:38:36 +0000
2114+++ src/schooltool/person/person.py 2010-05-28 11:09:37 +0000
2115@@ -30,16 +30,13 @@
2116 from zope.container import btree
2117 from zope.container.contained import Contained
2118 from zope.component import adapts
2119-from zope.catalog.interfaces import ICatalog
2120-from zope.catalog.catalog import Catalog
2121 from zope.component import getUtility
2122
2123-from zc.catalog.catalogindex import ValueIndex
2124-
2125-from schooltool.app.app import getSchoolToolApplication
2126+from schooltool.app.interfaces import ISchoolToolApplication
2127 from schooltool.app.interfaces import ISchoolToolCalendar
2128 from schooltool.app.membership import URIMembership, URIMember, URIGroup
2129 from schooltool.app.overlay import OverlaidCalendarsProperty
2130+from schooltool.app.catalog import AttributeCatalog
2131 from schooltool.person import interfaces
2132 from schooltool.relationship import RelationshipProperty
2133 from schooltool.securitypolicy.crowds import Crowd
2134@@ -51,8 +48,6 @@
2135 from schooltool.securitypolicy.crowds import OwnerCrowd, AggregateCrowd
2136 from schooltool.utility.utility import UtilitySetUp
2137
2138-PERSON_CATALOG_KEY = 'schooltool.person'
2139-
2140
2141 class PersonContainer(btree.BTreeContainer):
2142 """Container of persons."""
2143@@ -136,16 +131,15 @@
2144 def personAppCalendarOverlaySubscriber(person, event):
2145 """Add application calendar to overlays of all new persons.
2146 """
2147- try:
2148- app = getSchoolToolApplication()
2149- person.overlaid_calendars.add(ISchoolToolCalendar(app))
2150- except ValueError:
2151+ app = ISchoolToolApplication(None, None)
2152+ if app is None:
2153 # If we get this we are probably in the initial new-site setup
2154 # or creating a new manager during startup. This should be
2155 # safe to ignore since it will happen very infrequently
2156 # (perhaps only once) and the manager can easily add the site
2157 # calendar to his/her overlay in the overlay selection view.
2158- pass
2159+ return
2160+ person.overlaid_calendars.add(ISchoolToolCalendar(app))
2161
2162
2163 from schooltool.app.app import InitBase
2164@@ -208,14 +202,10 @@
2165 OwnerCrowd(self.context.person).contains(principal))
2166
2167
2168-def catalogSetUp(catalog):
2169- catalog['__name__'] = ValueIndex('__name__', IPerson)
2170- catalog['title'] = ValueIndex('title', IPerson)
2171-
2172-
2173-catalogSetUpSubscriber = UtilitySetUp(
2174- Catalog, ICatalog, PERSON_CATALOG_KEY, setUp=catalogSetUp)
2175-
2176-
2177-def getPersonContainerCatalog(container):
2178- return getUtility(ICatalog, PERSON_CATALOG_KEY)
2179+class PersonCatalog(AttributeCatalog):
2180+
2181+ version = '1 - replaced catalog utility'
2182+ interface = IPerson
2183+ attributes = ('__name__', 'title')
2184+
2185+getPersonCatalog = PersonCatalog.get
2186
2187=== modified file 'src/schooltool/resource/types.py'
2188--- src/schooltool/resource/types.py 2007-10-15 16:20:41 +0000
2189+++ src/schooltool/resource/types.py 2010-05-28 11:09:37 +0000
2190@@ -29,8 +29,8 @@
2191 from zc.table.column import GetterColumn
2192 from zc.table.interfaces import ISortableColumn
2193
2194+from schooltool.app.interfaces import ISchoolToolApplication
2195 from schooltool.common import SchoolToolMessage as _
2196-from schooltool.app.app import getSchoolToolApplication
2197 from schooltool.resource.interfaces import (IResource, IResourceFactoryUtility,
2198 ILocation, IEquipment,
2199 IResourceTypeInformation,
2200@@ -45,7 +45,7 @@
2201 self.interface = IResource
2202
2203 def types(self):
2204- app = getSchoolToolApplication()
2205+ app = ISchoolToolApplication(None)
2206 types = set()
2207 for resource in app['resources'].values():
2208 if self.interface.providedBy(resource):
2209
2210=== modified file 'src/schooltool/skin/skin.py'
2211--- src/schooltool/skin/skin.py 2010-01-08 14:03:27 +0000
2212+++ src/schooltool/skin/skin.py 2010-05-28 11:09:37 +0000
2213@@ -43,8 +43,6 @@
2214 from schooltool.app.interfaces import ISchoolToolApplication
2215 from schooltool.skin.interfaces import IBreadcrumbInfo
2216
2217-from schooltool.app.app import getSchoolToolApplication
2218-
2219
2220 class IJavaScriptManager(IViewletManager):
2221 """Provides a viewlet hook for the javascript link entries."""
2222@@ -141,7 +139,7 @@
2223 return ISchoolToolApplication(None)
2224
2225 def appURL(self):
2226- return absoluteURL(getSchoolToolApplication(), self.request)
2227+ return absoluteURL(ISchoolToolApplication(None), self.request)
2228
2229
2230 class TopLevelContainerNavigationViewlet(NavigationViewlet):
2231
2232=== modified file 'src/schooltool/term/browser/emergency.py'
2233--- src/schooltool/term/browser/emergency.py 2008-09-26 17:35:45 +0000
2234+++ src/schooltool/term/browser/emergency.py 2010-05-28 11:09:37 +0000
2235@@ -32,8 +32,8 @@
2236
2237 from schooltool.schoolyear.interfaces import ISchoolYear
2238 from schooltool.common import SchoolToolMessage as _
2239-from schooltool.app.app import getSchoolToolApplication
2240 from schooltool.app.cal import CalendarEvent
2241+from schooltool.app.interfaces import ISchoolToolApplication
2242 from schooltool.app.interfaces import ISchoolToolCalendar
2243 from schooltool.calendar.utils import parse_date
2244 from schooltool.term.interfaces import ITerm
2245@@ -122,7 +122,7 @@
2246 exceptionDayIds[self.replacement] = removeSecurityProxy(day_id)
2247
2248 # Post calendar events to schoolwide calendar
2249- calendar = ISchoolToolCalendar(getSchoolToolApplication())
2250+ calendar = ISchoolToolCalendar(ISchoolToolApplication(None))
2251 dtstart = datetime.datetime.combine(self.date, datetime.time())
2252 msg = _('School cancelled due to emergency.'
2253 ' Replacement day $replacement.',
2254
2255=== modified file 'src/schooltool/timetable/__init__.py'
2256--- src/schooltool/timetable/__init__.py 2010-01-19 12:09:22 +0000
2257+++ src/schooltool/timetable/__init__.py 2010-05-28 11:09:37 +0000
2258@@ -135,7 +135,6 @@
2259 from zope.interface import implementer
2260 from zope.interface import implements
2261 from zope.interface import directlyProvides
2262-
2263 from zope.annotation.interfaces import IAnnotations
2264 from zope.location.interfaces import ILocation
2265 from zope.traversing.api import getPath
2266@@ -143,10 +142,9 @@
2267 from zope.container.contained import NameChooser
2268 from zope.app.generations.utility import findObjectsProviding
2269
2270+from schooltool.app.interfaces import ISchoolToolApplication
2271+from schooltool.app.interfaces import ISchoolToolCalendar
2272 from schooltool.calendar.simple import ImmutableCalendar
2273-
2274-from schooltool.app.interfaces import ISchoolToolCalendar
2275-
2276 from schooltool.timetable.interfaces import ITimetableCalendarEvent
2277 from schooltool.timetable.interfaces import ITimetable, ITimetableWrite
2278 from schooltool.timetable.interfaces import ITimetableDay, ITimetableDayWrite
2279@@ -164,7 +162,6 @@
2280 from schooltool.timetable.interfaces import ITimetableSchema
2281 from schooltool.timetable.interfaces import Unchanged
2282 from schooltool.term.interfaces import ITerm
2283-from schooltool.app.app import getSchoolToolApplication
2284 from schooltool.app.app import InitBase
2285
2286 from schooltool.common import SchoolToolMessage as _
2287@@ -673,7 +670,7 @@
2288
2289
2290 def getAllTimetables():
2291- app = getSchoolToolApplication(None)
2292+ app = ISchoolToolApplication(None)
2293 all_timetables = []
2294 for ttowner in findObjectsProviding(app, IOwnTimetables):
2295 timetables = queryAdapter(ttowner, ITimetables, default=None)
2296
2297=== modified file 'src/schooltool/timetable/browser/__init__.py'
2298--- src/schooltool/timetable/browser/__init__.py 2010-05-03 11:24:06 +0000
2299+++ src/schooltool/timetable/browser/__init__.py 2010-05-28 11:09:37 +0000
2300@@ -32,7 +32,6 @@
2301 from zope.traversing.browser.absoluteurl import absoluteURL
2302
2303 from schooltool.app.interfaces import ISchoolToolApplication
2304-from schooltool.app.app import getSchoolToolApplication
2305 from schooltool.calendar.utils import parse_date, parse_time
2306 from schooltool.course.interfaces import ISection
2307 from schooltool.term.interfaces import ITerm
2308@@ -369,7 +368,7 @@
2309
2310 If there are no timetable schemas, None is returned.
2311 """
2312- app = getSchoolToolApplication()
2313+ app = ISchoolToolApplication(None)
2314 ttschemas = ITimetableSchemaContainer(app, None)
2315 if ttschemas is None:
2316 return None
2317
2318=== modified file 'src/schooltool/utility/utility.py'
2319--- src/schooltool/utility/utility.py 2009-11-19 02:00:46 +0000
2320+++ src/schooltool/utility/utility.py 2010-05-28 11:09:37 +0000
2321@@ -61,7 +61,8 @@
2322 if spec.override:
2323 # override existing utility
2324 name = local_utility.__name__
2325- manager.unregisterUtility(local_utility, spec.iface, name)
2326+ manager.unregisterUtility(
2327+ local_utility, spec.iface, spec.utility_name)
2328 del default[name]
2329 else:
2330 # do not register this utility; we already got it

Subscribers

People subscribed via source and target branches

to status/vote changes: