Merge lp:~aelkner/schooltool/demo_fields into lp:schooltool/1.7

Proposed by Alan Elkner
Status: Merged
Merged at revision: 2749
Proposed branch: lp:~aelkner/schooltool/demo_fields
Merge into: lp:schooltool/1.7
Diff against target: 2683 lines (+1765/-248) (has conflicts)
27 files modified
CHANGES.txt (+6/-0)
src/schooltool/basicperson/browser/configure.zcml (+90/-8)
src/schooltool/basicperson/browser/demographics.py (+22/-19)
src/schooltool/basicperson/browser/ftests/basicperson.txt (+2/-2)
src/schooltool/basicperson/browser/ftests/demographics.txt (+87/-4)
src/schooltool/basicperson/browser/ftests/group_aware.txt (+123/-0)
src/schooltool/basicperson/browser/person.py (+124/-1)
src/schooltool/basicperson/browser/templates/demographics-view.pt (+2/-2)
src/schooltool/basicperson/browser/templates/person_form.pt (+138/-0)
src/schooltool/basicperson/configure.zcml (+15/-0)
src/schooltool/basicperson/demographics.py (+31/-3)
src/schooltool/basicperson/interfaces.py (+35/-2)
src/schooltool/basicperson/tests/test_demographics.py (+87/-0)
src/schooltool/basicperson/vocabularies.py (+30/-0)
src/schooltool/resource/browser/configure.zcml (+159/-133)
src/schooltool/resource/browser/demographics.txt (+50/-0)
src/schooltool/resource/browser/resource-booking.txt (+1/-1)
src/schooltool/resource/browser/resource.py (+339/-51)
src/schooltool/resource/browser/resources.txt (+4/-9)
src/schooltool/resource/browser/subtype_widget.pt (+8/-5)
src/schooltool/resource/browser/templates/resource_form.pt (+138/-0)
src/schooltool/resource/browser/templates/resource_view.pt (+36/-0)
src/schooltool/resource/configure.zcml (+21/-1)
src/schooltool/resource/ftesting.zcml (+1/-0)
src/schooltool/resource/interfaces.py (+23/-3)
src/schooltool/resource/resource.py (+109/-4)
src/schooltool/resource/tests/test_resource.py (+84/-0)
Text conflict in CHANGES.txt
To merge this branch: bzr merge lp:~aelkner/schooltool/demo_fields
Reviewer Review Type Date Requested Status
SchoolTool Owners Pending
Review via email: mp+45818@code.launchpad.net
To post a comment you must log in.
lp:~aelkner/schooltool/demo_fields updated
2757. By Alan Elkner <aelkner@ubuntu>

updated CHANGES.txt

2758. By Alan Elkner <aelkner@ubuntu>

moved resource limit_keys vocab to resource package

2759. By Alan Elkner <aelkner@ubuntu>

added functional tests for the limit_keys vocanulary

2760. By Alan Elkner <aelkner@ubuntu>

merged from Justus branch

2761. By Alan Elkner <aelkner@ubuntu>

added filter_keys method to field description container
used it in generateExtraFields for add/edit person views

2762. By Alan Elkner <aelkner@ubuntu>

created group-aware (teacher, student, adinistrator) person add views

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CHANGES.txt'
2--- CHANGES.txt 2010-11-23 22:58:23 +0000
3+++ CHANGES.txt 2011-01-17 05:56:49 +0000
4@@ -12,7 +12,13 @@
5 ------------------
6
7 - Fixed Section meeting time misalignment in calendar (https://launchpad.net/bugs/611797)
8+<<<<<<< TREE
9 - Added Bengali and Faroese translations, updated many others
10+=======
11+- Added BooFieldDescription for boolean demographic fields
12+- Added limit_keys list to IFieldDescription and filter_key method to IDemographicsFields
13+- Created resource demographics and changed resource views to have demos
14+>>>>>>> MERGE-SOURCE
15
16
17 1.5.2 (2010-10-04)
18
19=== modified file 'src/schooltool/basicperson/browser/configure.zcml'
20--- src/schooltool/basicperson/browser/configure.zcml 2010-09-22 09:40:18 +0000
21+++ src/schooltool/basicperson/browser/configure.zcml 2011-01-17 05:56:49 +0000
22@@ -3,13 +3,6 @@
23 xmlns:zope="http://namespaces.zope.org/zope"
24 i18n_domain="schooltool">
25
26- <zope:class class=".demographics.CustomEnumTextWidget">
27- <require
28- permission="zope.Public"
29- interface=".demographics.IEnumTextWidget"
30- />
31- </zope:class>
32-
33 <page
34 name="index.html"
35 for="schooltool.basicperson.interfaces.IBasicPerson"
36@@ -171,6 +164,13 @@
37 menu="schooltool_actions" title="New Date Field" />
38
39 <page
40+ name="addBool.html"
41+ for="schooltool.basicperson.interfaces.IDemographicsFields"
42+ class=".demographics.BoolFieldDescriptionAddView"
43+ permission="schooltool.edit"
44+ menu="schooltool_actions" title="New Yes/No Field" />
45+
46+ <page
47 name="addEnum.html"
48 for="schooltool.basicperson.interfaces.IDemographicsFields"
49 class=".demographics.EnumFieldDescriptionAddView"
50@@ -193,6 +193,13 @@
51
52 <page
53 name="edit.html"
54+ for="schooltool.basicperson.demographics.BoolFieldDescription"
55+ class=".demographics.FieldDescriptionEditView"
56+ permission="schooltool.edit"
57+ menu="schooltool_actions" title="Edit" />
58+
59+ <page
60+ name="edit.html"
61 for="schooltool.basicperson.demographics.EnumFieldDescription"
62 class=".demographics.EnumFieldDescriptionEditView"
63 permission="schooltool.edit"
64@@ -214,6 +221,13 @@
65
66 <page
67 name="index.html"
68+ for="schooltool.basicperson.demographics.BoolFieldDescription"
69+ class=".demographics.BoolFieldDescriptionView"
70+ permission="schooltool.view"
71+ menu="schooltool_actions" title="View" />
72+
73+ <page
74+ name="index.html"
75 for="schooltool.basicperson.demographics.EnumFieldDescription"
76 class=".demographics.EnumFieldDescriptionView"
77 permission="schooltool.view"
78@@ -229,7 +243,22 @@
79 name="absolute_url"/>
80
81 <zope:adapter
82- factory=".demographics.CustomEnumDataConverter" />
83+ factory="z3c.form.browser.checkbox.CheckBoxFieldWidget"
84+ for="schooltool.basicperson.interfaces.FilterKeyList
85+ z3c.form.interfaces.IFormLayer"
86+ provides="z3c.form.interfaces.IFieldWidget" />
87+
88+ <zope:adapter
89+ factory=".demographics.CustomEnumDataConverter"
90+ for="schooltool.basicperson.interfaces.EnumValueList
91+ z3c.form.interfaces.ITextAreaWidget"
92+ />
93+
94+ <zope:adapter
95+ factory="z3c.form.browser.textarea.TextAreaFieldWidget"
96+ for="schooltool.basicperson.interfaces.EnumValueList
97+ z3c.form.interfaces.IFormLayer"
98+ />
99
100 <resourceDirectory
101 name="jstree"
102@@ -241,4 +270,57 @@
103 file="resources/jstree/jquery.min.js"
104 layer=".skin.IBasicPersonLayer" />
105
106+ <!-- Group-aware add views -->
107+ <page
108+ name="addTeacher.html"
109+ for="schooltool.person.interfaces.IPersonContainer"
110+ class=".person.TeacherAddView"
111+ permission="schooltool.edit"
112+ />
113+ <page
114+ name="addStudent.html"
115+ for="schooltool.person.interfaces.IPersonContainer"
116+ class=".person.StudentAddView"
117+ permission="schooltool.edit"
118+ />
119+ <page
120+ name="addAdministrator.html"
121+ for="schooltool.person.interfaces.IPersonContainer"
122+ class=".person.AdministratorAddView"
123+ permission="schooltool.edit"
124+ />
125+
126+ <configure package="schooltool.skin">
127+ <navigationViewlet
128+ name="add_teacher"
129+ for="schooltool.person.interfaces.IPersonContainer"
130+ manager="schooltool.skin.IActionMenuManager"
131+ template="templates/actionsViewlet.pt"
132+ class="schooltool.skin.skin.ActionMenuViewlet"
133+ permission="schooltool.edit"
134+ link="addTeacher.html"
135+ title="Add Teacher"
136+ />
137+ <navigationViewlet
138+ name="add_student"
139+ for="schooltool.person.interfaces.IPersonContainer"
140+ manager="schooltool.skin.IActionMenuManager"
141+ template="templates/actionsViewlet.pt"
142+ class="schooltool.skin.skin.ActionMenuViewlet"
143+ permission="schooltool.edit"
144+ link="addStudent.html"
145+ title="Add Student"
146+ />
147+ <navigationViewlet
148+ name="add_administrator"
149+ for="schooltool.person.interfaces.IPersonContainer"
150+ manager="schooltool.skin.IActionMenuManager"
151+ template="templates/actionsViewlet.pt"
152+ class="schooltool.skin.skin.ActionMenuViewlet"
153+ permission="schooltool.edit"
154+ link="addAdministrator.html"
155+ title="Add Administrator"
156+ />
157+ </configure>
158+
159 </configure>
160
161=== modified file 'src/schooltool/basicperson/browser/demographics.py'
162--- src/schooltool/basicperson/browser/demographics.py 2010-08-30 16:09:34 +0000
163+++ src/schooltool/basicperson/browser/demographics.py 2011-01-17 05:56:49 +0000
164@@ -29,41 +29,28 @@
165 from zope.container.interfaces import INameChooser
166 from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
167
168-from z3c.form.browser.textarea import TextAreaWidget
169 from z3c.form.interfaces import ITextAreaWidget
170 from z3c.form import form, field, button
171 from z3c.form.converter import BaseDataConverter, FormatterValidationError
172-from z3c.form.widget import FieldWidget
173
174 from schooltool.app.interfaces import ISchoolToolApplication
175 from schooltool.basicperson.demographics import TextFieldDescription
176 from schooltool.basicperson.demographics import DateFieldDescription
177+from schooltool.basicperson.demographics import BoolFieldDescription
178 from schooltool.basicperson.demographics import EnumFieldDescription
179 from schooltool.basicperson.interfaces import IEnumFieldDescription
180 from schooltool.basicperson.interfaces import IDemographicsFields
181 from schooltool.basicperson.interfaces import IFieldDescription
182+from schooltool.basicperson.interfaces import EnumValueList
183
184 from schooltool.common import format_message
185 from schooltool.common import SchoolToolMessage as _
186
187
188-class IEnumTextWidget(ITextAreaWidget):
189- """Marker interface for custom enum widget."""
190-
191-
192-class CustomEnumTextWidget(TextAreaWidget):
193- implements(IEnumTextWidget)
194-
195-
196-def CustomEnumFieldTextWidget(field, request):
197- """IFieldWidget factory for CustomEnumTextWidget."""
198- return FieldWidget(field, CustomEnumTextWidget(request))
199-
200-
201 class CustomEnumDataConverter(BaseDataConverter):
202 """A special data converter for iso enums."""
203
204- adapts(IList, CustomEnumTextWidget)
205+ adapts(EnumValueList, ITextAreaWidget)
206
207 def toWidgetValue(self, value):
208 """See interfaces.IDataConverter"""
209@@ -110,6 +97,8 @@
210 class DemographicsView(BrowserView):
211 """A Demographics List view."""
212
213+ title = _('Demographics Container')
214+
215 def demographics(self):
216 pos = 0
217 for demo in list(self.context.values()):
218@@ -195,10 +184,21 @@
219 self._fd = fd
220 return fd
221
222+
223+class BoolFieldDescriptionAddView(FieldDescriptionAddView):
224+
225+ def create(self, data):
226+ fd = BoolFieldDescription(data['title'],
227+ str(data['name']),
228+ data['required'])
229+ form.applyChanges(self, fd, data)
230+ self._fd = fd
231+ return fd
232+
233+
234 class EnumFieldDescriptionAddView(FieldDescriptionAddView):
235
236 fields = field.Fields(IEnumFieldDescription)
237- fields['items'].widgetFactory = CustomEnumFieldTextWidget
238
239 def create(self, data):
240 fd = EnumFieldDescription(data['title'],
241@@ -234,7 +234,6 @@
242 class EnumFieldDescriptionEditView(FieldDescriptionEditView):
243
244 fields = field.Fields(IEnumFieldDescription).omit('name')
245- fields['items'].widgetFactory = CustomEnumFieldTextWidget
246
247
248 class FieldDescriptionView(form.DisplayForm):
249@@ -255,8 +254,12 @@
250 """Display form for a date field description."""
251
252
253+class BoolFieldDescriptionView(FieldDescriptionView):
254+ """Display form for a bool field description."""
255+
256+
257 class EnumFieldDescriptionView(FieldDescriptionView):
258 """Display form for an enum field description."""
259
260 fields = field.Fields(IEnumFieldDescription)
261- fields['items'].widgetFactory = CustomEnumFieldTextWidget
262+
263
264=== modified file 'src/schooltool/basicperson/browser/ftests/basicperson.txt'
265--- src/schooltool/basicperson/browser/ftests/basicperson.txt 2010-11-15 16:05:34 +0000
266+++ src/schooltool/basicperson/browser/ftests/basicperson.txt 2011-01-17 05:56:49 +0000
267@@ -36,7 +36,7 @@
268 >>> manager.getLink('John')
269 <Link text='John' url='http://localhost/persons/john'>
270
271- >>> manager.getLink('Administrator', index=1)
272+ >>> manager.getLink('Administrator', index=2)
273 <Link text='Administrator' url='http://localhost/persons/manager'>
274
275 You can filter the list:
276@@ -49,7 +49,7 @@
277 >>> manager.getLink('John')
278 <Link text='John' url='http://localhost/persons/john'>
279
280- >>> manager.getLink('Administrator', index=1)
281+ >>> manager.getLink('Administrator', index=2)
282 Traceback (most recent call last):
283 ...
284 LinkNotFoundError
285
286=== modified file 'src/schooltool/basicperson/browser/ftests/demographics.txt'
287--- src/schooltool/basicperson/browser/ftests/demographics.txt 2010-05-21 15:45:10 +0000
288+++ src/schooltool/basicperson/browser/ftests/demographics.txt 2011-01-17 05:56:49 +0000
289@@ -147,6 +147,8 @@
290 >>> manager.getLink('Demographics').click()
291 >>> manager.getLink('ethnicity').click()
292 >>> manager.getLink('Edit').click()
293+ >>> print manager.getControl(name='form.widgets.limit_keys:list').value
294+ []
295 >>> print manager.getControl('List of values').value
296 American Indian or Alaska Native
297 Asian
298@@ -154,15 +156,33 @@
299 Native Hawaiian or Other Pacific Islander
300 White
301
302-Let's make the field required, and add Martians to it:
303+Let's make the field required, and add Martians to it. Also, we'll put limit
304+keys into the empty list so that we can test that they get saved:
305
306+ >>> manager.getControl(name='form.widgets.limit_keys:list').value = ['students', 'teachers']
307 >>> values = manager.getControl('List of values').value.splitlines()
308 >>> manager.getControl('List of values').value = "\n".join(values + ['Martians'])
309 >>> manager.getControl('yes').click()
310 >>> manager.getControl('Apply').click()
311
312-Now if we go to the add person form, we should see the additional
313-value in the select box, and have the field required:
314+Calling the edit form back up, we see that our changes took:
315+
316+ >>> manager.getLink('Manage').click()
317+ >>> manager.getLink('Demographics').click()
318+ >>> manager.getLink('ethnicity').click()
319+ >>> manager.getLink('Edit').click()
320+ >>> print manager.getControl(name='form.widgets.limit_keys:list').value
321+ ['students', 'teachers']
322+ >>> print manager.getControl('List of values').value
323+ American Indian or Alaska Native
324+ Asian
325+ Black or African American
326+ Native Hawaiian or Other Pacific Islander
327+ White
328+ Martians
329+
330+When we go to the add person form, we should still not see the additional
331+value in the select box as the group is not yet set:
332
333 >>> manager.getLink('Manage').click()
334 >>> manager.getLink('Persons').click()
335@@ -183,9 +203,42 @@
336 Confirm password
337 ID
338 Language
339- Ethnicity *
340 Citizenship
341
342+However, if we make john a teacher, it will show up when we edit him.
343+
344+ >>> manager.getLink('Manage').click()
345+ >>> manager.getLink('Persons').click()
346+ >>> manager.getLink('John').click()
347+ >>> manager.getLink('Groups').click()
348+ >>> manager.getControl(name='add_item.teachers').value = 'checked'
349+ >>> manager.getControl('Add').click()
350+
351+ >>> manager.getLink('Manage').click()
352+ >>> manager.getLink('Persons').click()
353+ >>> manager.getLink('John').click()
354+ >>> manager.getLink('Edit').click()
355+ >>> manager.getControl('Ethnicity').displayOptions
356+ ['American Indian or Alaska Native',
357+ 'Asian',
358+ 'Black or African American',
359+ 'Native Hawaiian or Other Pacific Islander',
360+ 'White',
361+ 'Martians']
362+
363+When we change the demo to not have a limit_key, then it will show when we add
364+a new person:
365+
366+ >>> manager.getLink('Manage').click()
367+ >>> manager.getLink('Demographics').click()
368+ >>> manager.getLink('ethnicity').click()
369+ >>> manager.getLink('Edit').click()
370+ >>> manager.getControl(name='form.widgets.limit_keys:list').value = []
371+ >>> manager.getControl('Apply').click()
372+
373+ >>> manager.getLink('Manage').click()
374+ >>> manager.getLink('Persons').click()
375+ >>> manager.getLink('New Person').click()
376 >>> manager.getControl('Ethnicity').displayOptions
377 ['American Indian or Alaska Native',
378 'Asian',
379@@ -217,6 +270,36 @@
380 'Martians',
381 'N\xc3\xb6rds']
382
383+Boolean fields are available, so we'll create one called 'certified' and test
384+the values in the display view:
385+
386+ >>> manager.getLink('Manage').click()
387+ >>> manager.getLink('Demographics').click()
388+ >>> manager.getLink('New Yes/No Field').click()
389+ >>> manager.getControl('Title').value = "Certified"
390+ >>> manager.getControl('ID').value = "certified"
391+ >>> manager.getControl(name='form.widgets.required:list').value = ['true']
392+ >>> manager.getControl('Add').click()
393+
394+ >>> manager.printQuery('//span[@id="form-widgets-title"]')
395+ <span id="form-widgets-title" class="text-widget required textline-field">Certified</span>
396+ >>> manager.printQuery('//span[@id="form-widgets-name"]')
397+ <span id="form-widgets-name" class="text-widget required pythonidentifier-field">certified</span>
398+ >>> manager.printQuery('//span[@id="form-widgets-required"]//span')
399+ <span class="selected-option">yes</span>
400+
401+Now when we add a new person, the new 'Certified' field appears as a boolean
402+radio field:
403+
404+ >>> manager.getLink('Manage').click()
405+ >>> manager.getLink('Persons').click()
406+ >>> manager.getLink('New Person').click()
407+
408+ >>> manager.printQuery('//input[@id="form-widgets-certified-0"]')
409+ <input id="form-widgets-certified-0" ... value="true" type="radio" />
410+ >>> manager.printQuery('//input[@id="form-widgets-certified-1"]')
411+ <input id="form-widgets-certified-1" ... value="false" type="radio" />
412+
413 TODO
414 Test values being unset if you remove a field
415 Test export
416
417=== added file 'src/schooltool/basicperson/browser/ftests/group_aware.txt'
418--- src/schooltool/basicperson/browser/ftests/group_aware.txt 1970-01-01 00:00:00 +0000
419+++ src/schooltool/basicperson/browser/ftests/group_aware.txt 2011-01-17 05:56:49 +0000
420@@ -0,0 +1,123 @@
421+Group-aware add views
422+=====================
423+
424+A manager logs in
425+
426+ >>> manager = Browser('manager', 'schooltool')
427+
428+ >>> from schooltool.app.browser.ftests import setup
429+ >>> setup.setUpBasicSchool()
430+
431+Let's change some of the default demographics fields to be group-specific:
432+
433+ >>> manager.getLink('Manage').click()
434+ >>> manager.getLink('Demographics').click()
435+ >>> manager.getLink('ethnicity').click()
436+ >>> manager.getLink('Edit').click()
437+ >>> manager.getControl(name='form.widgets.limit_keys:list').value = ['students']
438+ >>> manager.getControl('Apply').click()
439+
440+ >>> manager.getLink('Manage').click()
441+ >>> manager.getLink('Demographics').click()
442+ >>> manager.getLink('language').click()
443+ >>> manager.getLink('Edit').click()
444+ >>> manager.getControl(name='form.widgets.limit_keys:list').value = ['teachers']
445+ >>> manager.getControl('Apply').click()
446+
447+ >>> manager.getLink('Manage').click()
448+ >>> manager.getLink('Demographics').click()
449+ >>> manager.getLink('citizenship').click()
450+ >>> manager.getLink('Edit').click()
451+ >>> manager.getControl(name='form.widgets.limit_keys:list').value = ['administrators']
452+ >>> manager.getControl('Apply').click()
453+
454+Adding a student will have only student demos:
455+
456+ >>> manager.getLink('Manage').click()
457+ >>> manager.getLink('Persons').click()
458+ >>> manager.getLink('Add Student').click()
459+ >>> manager.printQuery('//fieldset[@id="demo-data"]/div[@class="row"]/div[@class="label"]//span/text()')
460+ ID
461+ Ethnicity
462+ Place of birth
463+
464+ >>> manager.getControl('First name').value = "John"
465+ >>> manager.getControl('Last name').value = "Petterson"
466+ >>> manager.getControl('Gender').value = ['male']
467+ >>> manager.getControl('Birth date').value = '1980-01-01'
468+ >>> manager.getControl('Username').value = "john"
469+ >>> manager.getControl('Password').value = "pwd"
470+ >>> manager.getControl('Confirm password').value = "pwd"
471+
472+ >>> manager.getControl('ID').value = "001122"
473+ >>> manager.getControl('Ethnicity').displayValue = ['Black or African American']
474+ >>> manager.getControl('Place of birth').value = "South Hampton"
475+ >>> manager.getControl('Add').click()
476+
477+ >>> manager.printQuery('//span[@id="form-widgets-ID"]/text()')
478+ 001122
479+ >>> manager.printQuery('//span[@id="form-widgets-ethnicity"]/span/text()')
480+ Black or African American
481+ >>> manager.printQuery('//span[@id="form-widgets-placeofbirth"]/text()')
482+ South Hampton
483+
484+Adding a teacher will have only teacher demos:
485+
486+ >>> manager.getLink('Manage').click()
487+ >>> manager.getLink('Persons').click()
488+ >>> manager.getLink('Add Teacher').click()
489+ >>> manager.printQuery('//fieldset[@id="demo-data"]/div[@class="row"]/div[@class="label"]//span/text()')
490+ ID
491+ Language
492+ Place of birth
493+
494+ >>> manager.getControl('First name').value = "Bob"
495+ >>> manager.getControl('Last name').value = "Hall"
496+ >>> manager.getControl('Gender').value = ['male']
497+ >>> manager.getControl('Birth date').value = '1980-01-01'
498+ >>> manager.getControl('Username').value = "bhall"
499+ >>> manager.getControl('Password').value = "pwd"
500+ >>> manager.getControl('Confirm password').value = "pwd"
501+
502+ >>> manager.getControl('ID').value = "987654"
503+ >>> manager.getControl('Language').value = "French"
504+ >>> manager.getControl('Place of birth').value = "Boston"
505+ >>> manager.getControl('Add').click()
506+
507+ >>> manager.printQuery('//span[@id="form-widgets-ID"]/text()')
508+ 987654
509+ >>> manager.printQuery('//span[@id="form-widgets-language"]/text()')
510+ French
511+ >>> manager.printQuery('//span[@id="form-widgets-placeofbirth"]/text()')
512+ Boston
513+
514+Adding an administrator will have only administrator demos:
515+
516+ >>> manager.getLink('Manage').click()
517+ >>> manager.getLink('Persons').click()
518+ >>> manager.getLink('Add Administrator').click()
519+ >>> manager.printQuery('//fieldset[@id="demo-data"]/div[@class="row"]/div[@class="label"]//span/text()')
520+ ID
521+ Place of birth
522+ Citizenship
523+
524+ >>> manager.getControl('First name').value = "Jane"
525+ >>> manager.getControl('Last name').value = "Brown"
526+ >>> manager.getControl('Gender').value = ['male']
527+ >>> manager.getControl('Birth date').value = '1980-01-01'
528+ >>> manager.getControl('Username').value = "jbrown"
529+ >>> manager.getControl('Password').value = "pwd"
530+ >>> manager.getControl('Confirm password').value = "pwd"
531+
532+ >>> manager.getControl('ID').value = "123456"
533+ >>> manager.getControl('Place of birth').value = "Miami"
534+ >>> manager.getControl('Citizenship').value = 'Spain'
535+ >>> manager.getControl('Add').click()
536+
537+ >>> manager.printQuery('//span[@id="form-widgets-ID"]/text()')
538+ 123456
539+ >>> manager.printQuery('//span[@id="form-widgets-placeofbirth"]/text()')
540+ Miami
541+ >>> manager.printQuery('//span[@id="form-widgets-citizenship"]/text()')
542+ Spain
543+
544
545=== modified file 'src/schooltool/basicperson/browser/person.py'
546--- src/schooltool/basicperson/browser/person.py 2010-11-15 16:06:20 +0000
547+++ src/schooltool/basicperson/browser/person.py 2011-01-17 05:56:49 +0000
548@@ -37,7 +37,9 @@
549
550 from schooltool.app.browser.app import RelationshipViewBase
551 from schooltool.app.interfaces import ISchoolToolApplication
552+from schooltool.group.interfaces import IGroupContainer
553 from schooltool.person.interfaces import IPersonFactory
554+from schooltool.schoolyear.interfaces import ISchoolYearContainer
555
556 from schooltool.basicperson.interfaces import IDemographicsFields
557 from schooltool.basicperson.interfaces import IBasicPerson
558@@ -135,7 +137,11 @@
559 def generateExtraFields(self):
560 field_descriptions = IDemographicsFields(ISchoolToolApplication(None))
561 fields = field.Fields()
562- for field_desc in field_descriptions.values():
563+ if IBasicPerson.providedBy(self.context):
564+ limit_keys = [group.__name__ for group in self.context.groups]
565+ else:
566+ limit_keys = []
567+ for field_desc in field_descriptions.filter_keys(limit_keys):
568 fields += field_desc.makeField()
569 return fields
570
571@@ -458,3 +464,120 @@
572 def getCollection(self):
573 return self.context.advisees
574
575+
576+############### Base class of all group-aware add views ################
577+class PersonAddViewBase(PersonAddFormBase):
578+
579+ id = 'person-form'
580+ template = ViewPageTemplateFile('templates/person_form.pt')
581+
582+ def makeRows(self, fields, cols=1):
583+ rows = []
584+ while fields:
585+ rows.append(fields[:cols])
586+ fields = fields[cols:]
587+ return rows
588+
589+ def makeFieldSet(self, fieldset_id, legend, fields, cols=1):
590+ result = {
591+ 'id': fieldset_id,
592+ 'legend': legend,
593+ }
594+ result['rows'] = self.makeRows(fields, cols)
595+ return result
596+
597+ def fieldsets(self):
598+ result = []
599+ sources = [
600+ (self.base_id, self.base_legend, list(self.getBaseFields())),
601+ (self.demo_id, self.demo_legend, list(self.getDemoFields())),
602+ ]
603+ for fieldset_id, legend, fields in sources:
604+ result.append(self.makeFieldSet(fieldset_id, legend, fields, 2))
605+ return result
606+
607+ def getDemoFields(self):
608+ fields = field.Fields()
609+ dfs = IDemographicsFields(ISchoolToolApplication(None))
610+ for field_desc in dfs.filter_key(self.group_id):
611+ fields += field_desc.makeField()
612+ return fields
613+
614+ def getBaseFields(self):
615+ return field.Fields(IPersonAddForm).omit('group')
616+
617+ def groupViewURL(self):
618+ return '%s/%s.html' % (absoluteURL(self.context, self.request),
619+ self.group_id)
620+
621+ def updateActions(self):
622+ super(PersonAddViewBase, self).updateActions()
623+ self.actions['add'].addClass('button-ok')
624+ self.actions['cancel'].addClass('button-cancel')
625+
626+ @button.buttonAndHandler(_('Add'))
627+ def handleAdd(self, action):
628+ super(PersonAddViewBase, self).handleAdd.func(self, action)
629+
630+ @button.buttonAndHandler(_('Cancel'))
631+ def handle_cancel_action(self, action):
632+ self.request.response.redirect(self.groupViewURL())
633+
634+ def create(self, data):
635+ username = data.get('username')
636+ first_name = data.get('first_name')
637+ last_name = data.get('last_name')
638+ person = self._factory(username, first_name, last_name)
639+ data.pop('confirm')
640+ form.applyChanges(self, person, data)
641+ group = None
642+ syc = ISchoolYearContainer(ISchoolToolApplication(None))
643+ active_schoolyear = syc.getActiveSchoolYear()
644+ if active_schoolyear is not None:
645+ group = IGroupContainer(active_schoolyear).get(self.group_id)
646+ if group is not None:
647+ person.groups.add(group)
648+ self._person = person
649+ return person
650+
651+ def update(self):
652+ self.fields = self.getBaseFields()
653+ self.fields += self.getDemoFields()
654+ self.updateWidgets()
655+ self.updateActions()
656+ self.actions.execute()
657+
658+ def updateWidgets(self):
659+ super(PersonAddViewBase, self).updateWidgets()
660+
661+
662+############### Group-aware add views ################
663+class TeacherAddView(PersonAddViewBase):
664+
665+ group_id = 'teachers'
666+ base_id = 'base-data'
667+ base_legend = _('Teacher identification')
668+ demo_id = 'demo-data'
669+ demo_legend = _('Teacher demographics')
670+ label = _('Add new teacher')
671+
672+
673+class StudentAddView(PersonAddViewBase):
674+
675+ group_id = 'students'
676+ base_id = 'base-data'
677+ base_legend = _('Student identification')
678+ demo_id = 'demo-data'
679+ demo_legend = _('Student demographics')
680+ label = _('Add new student')
681+
682+
683+class AdministratorAddView(PersonAddViewBase):
684+
685+ group_id = 'administrators'
686+ base_id = 'base-data'
687+ base_legend = _('Administrator identification')
688+ demo_id = 'demo-data'
689+ demo_legend = _('Administrator demographics')
690+ label = _('Add new administrator')
691+
692
693=== modified file 'src/schooltool/basicperson/browser/templates/demographics-view.pt'
694--- src/schooltool/basicperson/browser/templates/demographics-view.pt 2009-10-25 01:13:33 +0000
695+++ src/schooltool/basicperson/browser/templates/demographics-view.pt 2011-01-17 05:56:49 +0000
696@@ -1,10 +1,10 @@
697 <tal:dummy condition="view/update"> </tal:dummy>
698 <html metal:use-macro="view/@@standard_macros/page" i18n:domain="schooltool">
699 <head>
700- <title metal:fill-slot="title" i18n:translate="">Demographics Container</title>
701+ <title metal:fill-slot="title" tal:content="view/title" />
702 </head>
703 <body>
704-<h1 metal:fill-slot="content-header" i18n:translate="">Demographics Container</h1>
705+<h1 metal:fill-slot="content-header" tal:content="view/title" />
706
707 <metal:block metal:fill-slot="body">
708
709
710=== added file 'src/schooltool/basicperson/browser/templates/person_form.pt'
711--- src/schooltool/basicperson/browser/templates/person_form.pt 1970-01-01 00:00:00 +0000
712+++ src/schooltool/basicperson/browser/templates/person_form.pt 2011-01-17 05:56:49 +0000
713@@ -0,0 +1,138 @@
714+<html metal:use-macro="view/@@standard_macros/page" i18n:domain="schooltool">
715+ <body>
716+ <metal:nothing metal:fill-slot="content-header" />
717+ <metal:block metal:fill-slot="body">
718+ <form action="." method="post" enctype="multipart/form-data" class="standalone"
719+ metal:define-macro="form"
720+ tal:attributes="method view/method;
721+ enctype view/enctype;
722+ acceptCharset view/acceptCharset;
723+ accept view/accept;
724+ action view/action;
725+ name view/name;
726+ id view/id">
727+ <metal:subform define-macro="subform">
728+ <div class="viewspace" metal:define-slot="viewspace">
729+ <metal:label define-slot="label">
730+ <h3 metal:define-macro="form-label"
731+ tal:condition="view/label|nothing"
732+ tal:content="view/label">
733+ Form Label
734+ </h3>
735+ </metal:label>
736+ <metal:info define-slot="info">
737+ <div class="required-info"
738+ metal:define-macro="required-info">
739+ <!--<span class="required">*</span>
740+ &ndash; required -->
741+ </div>
742+ </metal:info>
743+ <metal:header define-slot="header">
744+ <div class="status"
745+ tal:condition="view/status"
746+ metal:define-macro="form-header">
747+ <div class="summary"
748+ i18n:translate=""
749+ tal:content="view/status">
750+ Form status summary
751+ </div>
752+ <ul class="errors"
753+ tal:define="errors view/errors|view/widgets/errors"
754+ tal:condition="errors"
755+ metal:define-macro="form-errors">
756+ <li tal:repeat="error errors">
757+ <tal:block condition="error/widget">
758+ <span tal:replace="error/widget/label" />:
759+ </tal:block>
760+ <span tal:replace="structure error/render">Error Type</span>
761+ </li>
762+ </ul>
763+ </div>
764+ </metal:header>
765+ <div metal:define-slot="extra-info" tal:replace="nothing">
766+ </div>
767+ <div metal:define-slot="main">
768+ <metal:widget-rows define-macro="widget-rows">
769+ <tal:loop repeat="fieldset view/fieldsets">
770+ <fieldset tal:attributes="id fieldset/id">
771+ <legend tal:content="fieldset/legend">legend</legend>
772+ <tal:loop repeat="row fieldset/rows">
773+ <tal:block repeat="widget_id row">
774+ <metal:block tal:define="widget python:view.widgets[widget_id]">
775+ <div id="" class="row"
776+ tal:attributes="id string:${widget/id}-row"
777+ tal:condition="python:widget.mode != 'hidden'">
778+ <metal:widget-row define-macro="widget-row">
779+ <div class="label">
780+ <label tal:attributes="for widget/id">
781+ <span i18n:translate=""
782+ tal:content="widget/label">label</span>
783+ <span class="required"
784+ tal:condition="widget/required">*</span>
785+ </label>
786+ </div>
787+ <p class="hint" tal:content="widget/field/description">Description of this field.</p>
788+ <div class="widget" tal:content="structure widget/render">
789+ <input type="text" size="24" value="" />
790+ </div>
791+ <div class="error"
792+ tal:condition="widget/error">
793+ <span tal:replace="structure widget/error/render">error</span>
794+ </div>
795+ </metal:widget-row>
796+ </div>
797+ <input type="hidden" value=""
798+ tal:condition="python:widget.mode == 'hidden'"
799+ tal:replace="structure widget/render" />
800+ </metal:block>
801+ </tal:block>
802+ <br class="clearboth" />
803+ </tal:loop>
804+ <div metal:define-slot="extra-widgets" tal:replace="nothing">
805+ </div>
806+ </fieldset>
807+ </tal:loop>
808+ </metal:widget-rows>
809+ <metal:groups define-macro="groups">
810+ <fieldset tal:condition="view/groups|nothing"
811+ tal:repeat="view view/groups">
812+ <legend tal:condition="view/label"
813+ tal:content="view/label">Label</legend>
814+ <metal:group-header define-slot="group-header">
815+ <div class="status"
816+ tal:condition="view/widgets/errors">
817+ <div metal:use-macro="template/macros/errors" />
818+ </div>
819+ </metal:group-header>
820+ <metal:group-rows define-slot="group-rows">
821+ <div metal:use-macro="template/macros/widget-rows" />
822+ </metal:group-rows>
823+ </fieldset>
824+ </metal:groups>
825+ </div>
826+ <metal:above-buttons define-slot="above-buttons">
827+ <tal:block
828+ repeat="form view/subforms"
829+ content="structure form/render"
830+ condition="view/subforms|nothing" />
831+ </metal:above-buttons>
832+ </div>
833+ <metal:buttons define-slot="buttons">
834+ <div metal:define-macro="form-buttons">
835+ <div class="buttons controls"
836+ metal:define-slot="bottom-buttons">
837+ <tal:block condition="view/actions|nothing">
838+ <input tal:repeat="action view/actions/values"
839+ tal:replace="structure action/render"
840+ />
841+ </tal:block>
842+ </div>
843+ </div>
844+ </metal:buttons>
845+ <metal:bottom define-slot="bottom">
846+ </metal:bottom>
847+ </metal:subform>
848+ </form>
849+ </metal:block>
850+ </body>
851+</html>
852
853=== modified file 'src/schooltool/basicperson/configure.zcml'
854--- src/schooltool/basicperson/configure.zcml 2010-08-08 12:06:54 +0000
855+++ src/schooltool/basicperson/configure.zcml 2011-01-17 05:56:49 +0000
856@@ -52,6 +52,13 @@
857 set_schema="schooltool.basicperson.interfaces.IFieldDescription" />
858 </class>
859
860+ <class class=".demographics.BoolFieldDescription">
861+ <allow interface="zope.location.interfaces.ILocation" />
862+ <allow interface="schooltool.basicperson.interfaces.IFieldDescription" />
863+ <require permission="schooltool.edit"
864+ set_schema="schooltool.basicperson.interfaces.IFieldDescription" />
865+ </class>
866+
867 <class class=".demographics.EnumFieldDescription">
868 <allow interface="zope.location.interfaces.ILocation" />
869 <allow interface="schooltool.basicperson.interfaces.IEnumFieldDescription" />
870@@ -74,6 +81,14 @@
871 provides="zope.schema.interfaces.IVocabularyFactory"
872 name="schooltool.basicperson.advisor_vocabulary" />
873
874+ <utility
875+ factory=".vocabularies.LimitKeyVocabFactory"
876+ provides="zope.schema.interfaces.IVocabularyFactory"
877+ name="schooltool.basicperson.limit_keys_vocabulary" />
878+
879+ <adapter factory=".vocabularies.getLimitKeyVocabularyForFieldDescription" />
880+ <adapter factory=".vocabularies.getLimitKeyVocabularyForPersonFields" />
881+
882 <include package="schooltool.basicperson.browser" />
883
884 <adapter factory=".demographics.getPersonDemographics" />
885
886=== modified file 'src/schooltool/basicperson/demographics.py'
887--- src/schooltool/basicperson/demographics.py 2010-08-30 09:15:43 +0000
888+++ src/schooltool/basicperson/demographics.py 2011-01-17 05:56:49 +0000
889@@ -24,7 +24,7 @@
890
891 from zope.schema._field import Choice
892 from zope.schema._field import Date
893-from zope.schema import TextLine
894+from zope.schema import TextLine, Bool
895 from zope.location.location import Location
896 from zope.location.location import locate
897 from zope.interface import Interface
898@@ -113,6 +113,25 @@
899 class DemographicsFields(OrderedContainer):
900 implements(IDemographicsFields)
901
902+ def filter_key(self, key):
903+ """Return the subset of fields whose limited_keys list is either
904+ empty, or it contains the key passed."""
905+ result = []
906+ for field in self.values():
907+ if not field.limit_keys or key in field.limit_keys:
908+ result.append(field)
909+ return result
910+
911+ def filter_keys(self, keys):
912+ """Return the subset of fields whose limited_keys list is either
913+ empty, or it contains one of the keys passed."""
914+ result = []
915+ for field in self.values():
916+ limit_keys = field.limit_keys
917+ if not limit_keys or [key for key in keys if key in limit_keys]:
918+ result.append(field)
919+ return result
920+
921
922 def setUpDefaultDemographics(app):
923 dfs = DemographicsFields()
924@@ -152,9 +171,11 @@
925
926 class FieldDescription(Persistent, Location):
927 implements(IFieldDescription)
928+ limit_keys = []
929
930- def __init__(self, name, title, required=False):
931- self.name, self.title,self.required = name, title, required
932+ def __init__(self, name, title, required=False, limit_keys=[]):
933+ self.name, self.title, self.required, self.limit_keys = (name,
934+ title, required, limit_keys)
935
936 def setUpField(self, form_field):
937 form_field.required = self.required
938@@ -202,3 +223,10 @@
939
940 def makeField(self):
941 return self.setUpField(TextLine(title=unicode(self.title)))
942+
943+
944+class BoolFieldDescription(FieldDescription):
945+
946+ def makeField(self):
947+ return self.setUpField(Bool(title=unicode(self.title)))
948+
949
950=== modified file 'src/schooltool/basicperson/interfaces.py'
951--- src/schooltool/basicperson/interfaces.py 2010-08-08 12:06:54 +0000
952+++ src/schooltool/basicperson/interfaces.py 2011-01-17 05:56:49 +0000
953@@ -21,7 +21,7 @@
954 """
955 from zope.container.interfaces import IContainer
956 from zope.container.interfaces import IOrderedContainer
957-from zope.schema import Date, Choice, TextLine, Bool
958+from zope.schema import Date, Choice, TextLine, Bool, List, Set
959 from zope.configuration.fields import PythonIdentifier
960 from zope.interface import Interface, Attribute
961
962@@ -113,6 +113,18 @@
963 class IDemographicsFields(IOrderedContainer):
964 """Demographics field storage."""
965
966+ def filter_key(key):
967+ """Return the subset of fields whose limited_keys list is either
968+ empty, or it contains the key passed"""
969+
970+ def filter_keys(keys):
971+ """Return the subset of fields whose limited_keys list is either
972+ empty, or it contains one of the keys passed"""
973+
974+
975+class FilterKeyList(List):
976+ """Marker field to pin widgets on."""
977+
978
979 class IFieldDescription(Interface):
980 """Demographics field."""
981@@ -129,9 +141,30 @@
982 title = _(u"Required"),
983 description = _(u"Whether this Field is required or not"))
984
985+ limit_keys = FilterKeyList(
986+ title = _(u"Limit keys"),
987+ description = _(u"An optional list of limit keys for this field"),
988+ value_type=Choice(
989+ title=_(u"Limit key"),
990+ source="schooltool.basicperson.limit_keys_vocabulary",
991+ required=True,
992+ ),
993+ required=False)
994+
995+
996+class EnumValueList(List):
997+ """Marker field to pin custom validation on."""
998+
999
1000 class IEnumFieldDescription(IFieldDescription):
1001 """Enumeration demographics field."""
1002
1003- items = List(
1004+ items = EnumValueList(
1005 title = _('List of values'))
1006+
1007+
1008+class IFieldFilterVocabulary(IVocabularyTokenized):
1009+ """Marker interface for vocabularies that give keys that are used
1010+ to filder demographics fields for the context.
1011+ """
1012+
1013
1014=== modified file 'src/schooltool/basicperson/tests/test_demographics.py'
1015--- src/schooltool/basicperson/tests/test_demographics.py 2010-01-10 17:31:54 +0000
1016+++ src/schooltool/basicperson/tests/test_demographics.py 2011-01-17 05:56:49 +0000
1017@@ -31,6 +31,7 @@
1018 from schooltool.basicperson.demographics import setUpDefaultDemographics
1019 from schooltool.basicperson.demographics import IDemographicsForm
1020 from schooltool.basicperson.demographics import TextFieldDescription
1021+from schooltool.basicperson.demographics import BoolFieldDescription
1022 from schooltool.basicperson.demographics import PersonDemographicsData
1023 from schooltool.schoolyear.testing import (setUp, tearDown,
1024 provideStubUtility,
1025@@ -140,6 +141,65 @@
1026 """
1027
1028
1029+def doctest_DemographicsFields():
1030+ """Tests for DemographicsFields
1031+
1032+ DemographicsFields is a class that contains demo fields that themselves
1033+ may or may not be limited to a group or groups.
1034+
1035+ >>> dfs = IDemographicsFields(ISchoolToolApplication(None))
1036+ >>> dfs['email'] = TextFieldDescription("email", "Email")
1037+ >>> dfs['supervisor'] = TextFieldDescription("supervisor", "Supervisor",
1038+ ... limit_keys=['teachers'])
1039+ >>> dfs['advisor'] = TextFieldDescription("advisor", "Advisor",
1040+ ... limit_keys=['students'])
1041+ >>> dfs['phone'] = TextFieldDescription("phone", "Phone",
1042+ ... limit_keys=['teachers', 'students'])
1043+
1044+
1045+ When we pass the filter_key method a key that does not belong
1046+ to any of the limit_keys lists, then it will only return those
1047+ fields that have empty limit_keys lists.
1048+
1049+ >>> [f.__name__ for f in dfs.filter_key('anything')]
1050+ [u'ID', u'ethnicity', u'language', u'placeofbirth', u'citizenship',
1051+ u'email']
1052+
1053+ When we pass 'teachers', it picks up the additional fields that are for
1054+ teachers.
1055+
1056+ >>> [f.__name__ for f in dfs.filter_key('teachers')]
1057+ [u'ID', u'ethnicity', u'language', u'placeofbirth', u'citizenship',
1058+ u'email', u'supervisor', u'phone']
1059+
1060+ When we pass 'students', it picks up the additional fields that are for
1061+ students.
1062+
1063+ >>> [f.__name__ for f in dfs.filter_key('students')]
1064+ [u'ID', u'ethnicity', u'language', u'placeofbirth', u'citizenship',
1065+ u'email', u'advisor', u'phone']
1066+
1067+ We also have a filter_keys method to return fields whose keys are in the
1068+ list passed.
1069+
1070+ >>> [f.__name__ for f in dfs.filter_keys([])]
1071+ [u'ID', u'ethnicity', u'language', u'placeofbirth', u'citizenship',
1072+ u'email']
1073+
1074+ >>> [f.__name__ for f in dfs.filter_keys(['students'])]
1075+ [u'ID', u'ethnicity', u'language', u'placeofbirth', u'citizenship',
1076+ u'email', u'advisor', u'phone']
1077+
1078+ >>> [f.__name__ for f in dfs.filter_keys(['teachers'])]
1079+ [u'ID', u'ethnicity', u'language', u'placeofbirth', u'citizenship',
1080+ u'email', u'supervisor', u'phone']
1081+
1082+ >>> [f.__name__ for f in dfs.filter_keys(['students', 'teachers'])]
1083+ [u'ID', u'ethnicity', u'language', u'placeofbirth', u'citizenship',
1084+ u'email', u'supervisor', u'advisor', u'phone']
1085+ """
1086+
1087+
1088 def doctest_EnumFieldDescription():
1089 """Tests for EnumFieldDescription
1090
1091@@ -194,6 +254,33 @@
1092 """
1093
1094
1095+def doctest_BoolFieldDescription():
1096+ """Tests for BoolFieldDescription
1097+
1098+ Boolean field description is a class that defines a boolean field shown
1099+ in person add/edit form.
1100+
1101+ >>> fd = BoolFieldDescription("ID", "ID")
1102+ >>> fields = fd.makeField()
1103+ >>> len(fields)
1104+ 1
1105+
1106+ >>> field = fields['ID']
1107+ >>> field
1108+ <Field 'ID'>
1109+
1110+ >>> field.interface
1111+ <InterfaceClass schooltool.basicperson.demographics.IDemographicsForm>
1112+
1113+ >>> field.field
1114+ <zope.schema._bootstrapfields.Bool object at ...>
1115+
1116+ >>> field.__name__
1117+ 'ID'
1118+
1119+ """
1120+
1121+
1122 def doctest_DateFieldDescription():
1123 """Tests for DateFieldDescription
1124
1125
1126=== modified file 'src/schooltool/basicperson/vocabularies.py'
1127--- src/schooltool/basicperson/vocabularies.py 2010-08-08 12:06:54 +0000
1128+++ src/schooltool/basicperson/vocabularies.py 2011-01-17 05:56:49 +0000
1129@@ -20,9 +20,12 @@
1130 Sources and vocabularies for form fields.
1131 """
1132 from zope.interface import implements
1133+from zope.interface import implementer
1134+from zope.component import adapter
1135 from zope.schema.interfaces import ITitledTokenizedTerm
1136
1137 from schooltool.app.interfaces import ISchoolToolApplication
1138+from schooltool.app.utils import vocabulary
1139 from schooltool.person.interfaces import IPerson
1140 from schooltool.group.interfaces import IGroupContainer
1141 from schooltool.group.interfaces import IGroup
1142@@ -31,6 +34,11 @@
1143 from schooltool.basicperson.browser.person import GroupTerm
1144 from schooltool.basicperson.interfaces import IGroupVocabulary
1145 from schooltool.basicperson.interfaces import IBasicPersonVocabulary
1146+from schooltool.basicperson.interfaces import IDemographicsFields
1147+from schooltool.basicperson.interfaces import IFieldDescription
1148+from schooltool.basicperson.interfaces import IFieldFilterVocabulary
1149+
1150+from schooltool.common import SchoolToolMessage as _
1151
1152
1153 class Term(object):
1154@@ -115,3 +123,25 @@
1155
1156 def advisorVocabularyFactory():
1157 return AdvisorVocabulary
1158+
1159+
1160+def LimitKeyVocabFactory():
1161+ return IFieldFilterVocabulary
1162+
1163+
1164+@adapter(IFieldDescription)
1165+@implementer(IFieldFilterVocabulary)
1166+def getLimitKeyVocabularyForFieldDescription(field_description):
1167+ field_container = field_description.__parent__
1168+ return IFieldFilterVocabulary(field_container)
1169+
1170+
1171+@adapter(IDemographicsFields)
1172+@implementer(IFieldFilterVocabulary)
1173+def getLimitKeyVocabularyForPersonFields(person_field_description_container):
1174+ return vocabulary([
1175+ ('students', _('Students')),
1176+ ('teachers', _('Teachers')),
1177+ ('administrators', _('Administrators')),
1178+ ])
1179+
1180
1181=== modified file 'src/schooltool/resource/browser/configure.zcml'
1182--- src/schooltool/resource/browser/configure.zcml 2009-11-23 14:48:29 +0000
1183+++ src/schooltool/resource/browser/configure.zcml 2011-01-17 05:56:49 +0000
1184@@ -1,8 +1,31 @@
1185 <?xml version="1.0"?>
1186 <configure xmlns="http://namespaces.zope.org/browser"
1187+ xmlns:z3c="http://namespaces.zope.org/z3c"
1188 xmlns:zope="http://namespaces.zope.org/zope"
1189 i18n_domain="schooltool">
1190
1191+ <zope:class class=".resource.ResourceSubTypeWidget">
1192+ <require
1193+ permission="zope.Public"
1194+ interface="z3c.form.interfaces.ITextWidget"
1195+ />
1196+ </zope:class>
1197+
1198+ <zope:adapter
1199+ factory=".resource.ResourceSubTypeFieldWidget"
1200+ provides="z3c.form.interfaces.IFieldWidget"
1201+ for="schooltool.resource.interfaces.ResourceSubType
1202+ z3c.form.interfaces.IFormLayer"
1203+ />
1204+
1205+ <z3c:widgetTemplate
1206+ mode="input"
1207+ widget=".resource.IResourceSubTypeWidget"
1208+ layer="z3c.form.interfaces.IFormLayer"
1209+ template="subtype_widget.pt"
1210+ />
1211+
1212+
1213 <!-- Resource Traverser -->
1214 <zope:view
1215 for="..interfaces.IBaseResource"
1216@@ -58,6 +81,29 @@
1217 />
1218
1219 <configure package="schooltool.skin">
1220+ <navigationViewlet
1221+ name="resources"
1222+ for="*"
1223+ manager="schooltool.app.browser.interfaces.IManageMenuViewletManager"
1224+ template="templates/navigationViewlet.pt"
1225+ class="schooltool.skin.TopLevelContainerNavigationViewlet"
1226+ permission="schooltool.view"
1227+ link="resources"
1228+ title="Resources"
1229+ order="60"
1230+ />
1231+
1232+ <navigationViewlet
1233+ name="resource_demographics"
1234+ for="*"
1235+ manager="schooltool.app.browser.interfaces.IManageMenuViewletManager"
1236+ template="templates/navigationViewlet.pt"
1237+ class="schooltool.skin.skin.NavigationViewlet"
1238+ permission="schooltool.edit"
1239+ link="resource_demographics"
1240+ title="Resource Demographics"
1241+ order="65"
1242+ />
1243
1244 <page
1245 name="index.html"
1246@@ -67,147 +113,14 @@
1247 menu="zmi_views"
1248 title="View"
1249 />
1250-
1251- <addform
1252- label="Add a Resource"
1253- name="addSchoolToolResource.html"
1254- schema="schooltool.resource.interfaces.IResource"
1255- fields="title description notes"
1256- set_before_add="title description notes"
1257- content_factory="schooltool.resource.resource.Resource"
1258- template="templates/simple_add.pt"
1259- class="schooltool.app.browser.app.BaseAddView"
1260- permission="schooltool.edit">
1261- <widget field="description" height="5" />
1262- </addform>
1263-
1264- <editform
1265- for="schooltool.resource.interfaces.IResource"
1266- name="edit.html"
1267- label="Edit Resource Information"
1268- schema="schooltool.resource.interfaces.IResource"
1269- fields="title description notes"
1270- permission="schooltool.edit"
1271- template="templates/simple_edit.pt"
1272- class="schooltool.resource.browser.resource.ResourceEditView"
1273- menu="schooltool_actions"
1274- title="Edit Info">
1275- <widget field="description" height="5" />
1276- </editform>
1277-
1278- <!-- Location forms -->
1279- <addform
1280- label="Add a Location"
1281- name="addSchoolToolLocation.html"
1282- schema="schooltool.resource.interfaces.ILocation"
1283- fields="title type description capacity notes"
1284- set_before_add="title type description capacity notes"
1285- content_factory="schooltool.resource.resource.Location"
1286- template="templates/simple_add.pt"
1287- class="schooltool.app.browser.app.BaseAddView"
1288- permission="schooltool.edit">
1289- <widget field="description" height="5" />
1290- <widget field="type"
1291- class="schooltool.resource.browser.resource.ResourceSubTypeWidget"
1292- utility="location"/>
1293- </addform>
1294-
1295- <editform
1296- for="schooltool.resource.interfaces.ILocation"
1297- name="edit.html"
1298- label="Edit Location Information"
1299- schema="schooltool.resource.interfaces.ILocation"
1300- fields="title type description capacity notes"
1301- permission="schooltool.edit"
1302- template="templates/simple_edit.pt"
1303- class="schooltool.resource.browser.resource.ResourceEditView"
1304- menu="schooltool_actions"
1305- title="Edit Info">
1306- <widget field="description" height="5" />
1307- <widget field="type"
1308- class="schooltool.resource.browser.resource.ResourceSubTypeWidget"
1309- utility="location"/>
1310- </editform>
1311-
1312- <!-- Equipment forms -->
1313- <addform
1314- label="Add an Equipment"
1315- name="addSchoolToolEquipment.html"
1316- schema="schooltool.resource.interfaces.IEquipment"
1317- fields="title description type manufacturer model serialNumber
1318- purchaseDate notes"
1319- set_before_add="title description notes type manufacturer model
1320- serialNumber purchaseDate"
1321- content_factory="schooltool.resource.resource.Equipment"
1322- template="templates/simple_add.pt"
1323- class="schooltool.app.browser.app.BaseAddView"
1324- permission="schooltool.edit">
1325- <widget field="description" height="5" />
1326- <widget field="type"
1327- class="schooltool.resource.browser.resource.ResourceSubTypeWidget"
1328- utility="equipment"/>
1329- </addform>
1330-
1331- <editform
1332- for="schooltool.resource.interfaces.IEquipment"
1333- name="edit.html"
1334- label="Edit Equipment Information"
1335- schema="schooltool.resource.interfaces.IEquipment"
1336- fields="title description type manufacturer model serialNumber
1337- purchaseDate notes"
1338- permission="schooltool.edit"
1339- template="templates/simple_edit.pt"
1340- class="schooltool.resource.browser.resource.ResourceEditView"
1341- menu="schooltool_actions"
1342- title="Edit Info">
1343- <widget field="description" height="5" />
1344- <widget field="type"
1345- class="schooltool.resource.browser.resource.ResourceSubTypeWidget"
1346- utility="equipment"/>
1347- </editform>
1348-
1349- <navigationViewlet
1350- name="resources"
1351- for="*"
1352- manager="schooltool.app.browser.interfaces.IManageMenuViewletManager"
1353- template="templates/navigationViewlet.pt"
1354- class="schooltool.skin.TopLevelContainerNavigationViewlet"
1355- permission="schooltool.view"
1356- link="resources"
1357- title="Resources"
1358- order="60"
1359- />
1360-
1361 </configure>
1362
1363-
1364 <containerViews
1365 for="..interfaces.IResourceContainer"
1366 contents="schooltool.view"
1367 add="schooltool.edit"
1368 />
1369
1370- <menuItem
1371- menu="schooltool_actions"
1372- title="New Resource"
1373- for="..interfaces.IResourceContainer"
1374- action="+/addSchoolToolResource.html"
1375- />
1376-
1377- <menuItem
1378- menu="schooltool_actions"
1379- title="New Location"
1380- for="..interfaces.IResourceContainer"
1381- action="+/addSchoolToolLocation.html"
1382- />
1383-
1384- <menuItem
1385- menu="schooltool_actions"
1386- title="New Equipment"
1387- for="..interfaces.IResourceContainer"
1388- action="+/addSchoolToolEquipment.html"
1389- />
1390-
1391 <page
1392 name="index.html"
1393 for="..interfaces.IResource"
1394@@ -302,4 +215,117 @@
1395 order="5"
1396 />
1397
1398+ <zope:adapterTraverserPlugin
1399+ for="schooltool.app.interfaces.ISchoolToolApplication"
1400+ layer="zope.publisher.interfaces.http.IHTTPRequest"
1401+ name="resource_demographics"
1402+ adapter="schooltool.resource.interfaces.IResourceDemographicsFields"
1403+ permission="zope.Public"
1404+ />
1405+
1406+ <zope:configure package="schooltool.basicperson.browser">
1407+ <page
1408+ name="index.html"
1409+ for="schooltool.resource.interfaces.IResourceDemographicsFields"
1410+ class="schooltool.resource.browser.resource.ResourceDemographicsView"
1411+ template="templates/demographics-view.pt"
1412+ permission="schooltool.edit"
1413+ />
1414+ </zope:configure>
1415+
1416+ <zope:adapter
1417+ factory=".resource.ResourceDemographicsFieldsAbsoluteURLAdapter"
1418+ provides="zope.traversing.browser.interfaces.IAbsoluteURL" />
1419+
1420+ <zope:adapter
1421+ factory=".resource.ResourceDemographicsFieldsAbsoluteURLAdapter"
1422+ provides="zope.interface.Interface"
1423+ name="absolute_url"/>
1424+
1425+ <menuItem
1426+ menu="schooltool_actions"
1427+ title="Edit Info"
1428+ for="schooltool.resource.interfaces.IResource"
1429+ action="edit.html"
1430+ permission="schooltool.edit"
1431+ />
1432+ <page
1433+ for="schooltool.resource.interfaces.IResource"
1434+ name="edit.html"
1435+ class=".resource.ResourceEditView"
1436+ permission="schooltool.edit"
1437+ layer="schooltool.skin.ISchoolToolLayer"
1438+ />
1439+ <menuItem
1440+ menu="schooltool_actions"
1441+ title="Edit Info"
1442+ for="schooltool.resource.interfaces.ILocation"
1443+ action="edit.html"
1444+ permission="schooltool.edit"
1445+ />
1446+ <page
1447+ for="schooltool.resource.interfaces.ILocation"
1448+ name="edit.html"
1449+ class=".resource.LocationEditView"
1450+ permission="schooltool.edit"
1451+ layer="schooltool.skin.ISchoolToolLayer"
1452+ />
1453+ <menuItem
1454+ menu="schooltool_actions"
1455+ title="Edit Info"
1456+ for="schooltool.resource.interfaces.IEquipment"
1457+ action="edit.html"
1458+ permission="schooltool.edit"
1459+ />
1460+ <page
1461+ for="schooltool.resource.interfaces.IEquipment"
1462+ name="edit.html"
1463+ class=".resource.EquipmentEditView"
1464+ permission="schooltool.edit"
1465+ layer="schooltool.skin.ISchoolToolLayer"
1466+ />
1467+
1468+ <menuItem
1469+ menu="schooltool_actions"
1470+ title="New Resource"
1471+ for="schooltool.resource.interfaces.IResourceContainer"
1472+ action="addResource.html"
1473+ permission="schooltool.edit"
1474+ />
1475+ <page
1476+ name="addResource.html"
1477+ for="schooltool.resource.interfaces.IResourceContainer"
1478+ class=".resource.ResourceAddView"
1479+ permission="schooltool.edit"
1480+ layer="schooltool.skin.ISchoolToolLayer"
1481+ />
1482+ <menuItem
1483+ menu="schooltool_actions"
1484+ title="New Location"
1485+ for="schooltool.resource.interfaces.IResourceContainer"
1486+ action="addLocation.html"
1487+ permission="schooltool.edit"
1488+ />
1489+ <page
1490+ name="addLocation.html"
1491+ for="schooltool.resource.interfaces.IResourceContainer"
1492+ class=".resource.LocationAddView"
1493+ permission="schooltool.edit"
1494+ layer="schooltool.skin.ISchoolToolLayer"
1495+ />
1496+ <menuItem
1497+ menu="schooltool_actions"
1498+ title="New Equipment"
1499+ for="schooltool.resource.interfaces.IResourceContainer"
1500+ action="addEquipment.html"
1501+ permission="schooltool.edit"
1502+ />
1503+ <page
1504+ name="addEquipment.html"
1505+ for="schooltool.resource.interfaces.IResourceContainer"
1506+ class=".resource.EquipmentAddView"
1507+ permission="schooltool.edit"
1508+ layer="schooltool.skin.ISchoolToolLayer"
1509+ />
1510+
1511 </configure>
1512
1513=== added file 'src/schooltool/resource/browser/demographics.txt'
1514--- src/schooltool/resource/browser/demographics.txt 1970-01-01 00:00:00 +0000
1515+++ src/schooltool/resource/browser/demographics.txt 2011-01-17 05:56:49 +0000
1516@@ -0,0 +1,50 @@
1517+Resource demographics
1518+=====================
1519+
1520+A manager logs in.
1521+
1522+ >>> manager = Browser('manager', 'schooltool')
1523+
1524+There's a link in the manage tab that brings up the Resource Demographics
1525+container view.
1526+
1527+ >>> manager.getLink('Manage').click()
1528+ >>> manager.getLink('Resource Demographics').click()
1529+ >>> manager.printQuery("id('content-header')//h1")
1530+ <h1>Resource Demographics Container</h1>
1531+
1532+We'll create some demographics fields.
1533+
1534+ >>> manager.getLink('New Text Field').click()
1535+
1536+ >>> manager.printQuery("id('column-center')//input[@type='checkbox']")
1537+ <input ... value="resource" type="checkbox" />
1538+ <input ... value="location" type="checkbox" />
1539+ <input ... value="equipment" type="checkbox" />
1540+
1541+ >>> manager.getControl('Title').value = "Room Number"
1542+ >>> manager.getControl('ID').value = "room"
1543+ >>> manager.getControl(name='form.widgets.required:list').value = ['true']
1544+ >>> manager.getControl(name='form.widgets.limit_keys:list').value = ['location']
1545+ >>> manager.getControl('Add').click()
1546+
1547+ >>> manager.getLink('Edit').click()
1548+ >>> print manager.getControl(name='form.widgets.limit_keys:list').value
1549+ ['location']
1550+
1551+ >>> manager.getLink('Manage').click()
1552+ >>> manager.getLink('Resource Demographics').click()
1553+ >>> manager.getLink('New Yes/No Field').click()
1554+ >>> manager.getControl('Title').value = "Certified"
1555+ >>> manager.getControl('ID').value = "certified"
1556+ >>> manager.getControl(name='form.widgets.required:list').value = ['true']
1557+ >>> manager.getControl('Add').click()
1558+
1559+Now we see them in the container view.
1560+
1561+ >>> manager.getLink('Manage').click()
1562+ >>> manager.getLink('Resource Demographics').click()
1563+ >>> manager.printQuery("id('column-center')//a")
1564+ <a href="http://localhost/resource_demographics/room">room</a>
1565+ <a href="http://localhost/resource_demographics/certified">certified</a>
1566+
1567
1568=== modified file 'src/schooltool/resource/browser/resource-booking.txt'
1569--- src/schooltool/resource/browser/resource-booking.txt 2008-09-05 12:13:50 +0000
1570+++ src/schooltool/resource/browser/resource-booking.txt 2011-01-17 05:56:49 +0000
1571@@ -15,7 +15,7 @@
1572 >>> manager.getLink('Resources').click()
1573 >>> manager.getLink('New Location').click()
1574 >>> manager.getControl('Title').value = 'mud'
1575- >>> manager.getControl(name='field.type.newSubType').value = 'Computer Lab'
1576+ >>> manager.getControl(name='form.widgets.type.newSubType').value = 'Computer Lab'
1577 >>> manager.getControl('Add').click()
1578 >>> manager.getLink('Computer Lab').click()
1579 >>> 'mud' in manager.contents
1580
1581=== modified file 'src/schooltool/resource/browser/resource.py'
1582--- src/schooltool/resource/browser/resource.py 2010-08-30 16:09:34 +0000
1583+++ src/schooltool/resource/browser/resource.py 2011-01-17 05:56:49 +0000
1584@@ -22,22 +22,38 @@
1585 $Id$
1586 """
1587
1588+import z3c.form
1589+import z3c.form.browser.text
1590+from z3c.form import form, field, button, subform, validator, widget
1591+from z3c.form.interfaces import DISPLAY_MODE, HIDDEN_MODE, NO_VALUE, IActionHandler
1592 from zc.table import table
1593+
1594 from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
1595+from zope.component import adapter, adapts
1596 from zope.component import getUtilitiesFor
1597 from zope.component import queryAdapter
1598 from zope.component import queryMultiAdapter
1599 from zope.component import queryUtility
1600-from zope.formlib import form
1601+from zope.component import getMultiAdapter
1602+from zope.container.interfaces import INameChooser
1603+from zope.event import notify
1604+from zope.formlib import form as oldform
1605+from zope.interface import implementer, implements, implementsOnly
1606+from zope.lifecycleevent import ObjectCreatedEvent
1607+from zope.publisher.browser import BrowserView
1608+from zope.publisher.interfaces.browser import IBrowserRequest
1609 from zope.session.interfaces import ISession
1610-from zope.app.form.browser import widget
1611 from zope.traversing.browser.absoluteurl import absoluteURL
1612+from zope.traversing.browser.interfaces import IAbsoluteURL
1613
1614 from schooltool.app.browser.app import BaseEditView
1615+from schooltool.app.interfaces import ISchoolToolApplication
1616+from schooltool.basicperson.browser.demographics import DemographicsView
1617 from schooltool.resource.interfaces import IBookingCalendar
1618 from schooltool.resource.interfaces import (IBaseResourceContained,
1619 IResourceContainer, IResourceTypeInformation, IResourceSubTypes,
1620- IResource, IEquipment, ILocation)
1621+ IResource, IEquipment, ILocation, IResourceDemographicsFields)
1622+from schooltool.resource.resource import Resource, Location, Equipment
1623 from schooltool.table.interfaces import IFilterWidget
1624 from schooltool.table.table import url_cell_formatter
1625 from schooltool.table.table import CheckboxColumn
1626@@ -48,7 +64,7 @@
1627 from schooltool.common import SchoolToolMessage as _
1628
1629
1630-class ResourceContainerView(form.FormBase):
1631+class ResourceContainerView(oldform.FormBase):
1632 """A Resource Container view."""
1633
1634 __used_for__ = IResourceContainer
1635@@ -56,20 +72,20 @@
1636 index_title = _("Resource index")
1637
1638 prefix = "resources"
1639- form_fields = form.Fields()
1640- searchActions = form.Actions(
1641- form.Action('Search', success='handle_search_action'),)
1642+ form_fields = oldform.Fields()
1643+ searchActions = oldform.Actions(
1644+ oldform.Action('Search', success='handle_search_action'),)
1645
1646- actions = form.Actions(
1647- form.Action('Delete', success='handle_delete_action'),
1648- form.Action('Book', success='handle_book_action'))
1649+ actions = oldform.Actions(
1650+ oldform.Action('Delete', success='handle_delete_action'),
1651+ oldform.Action('Book', success='handle_book_action'))
1652 template = ViewPageTemplateFile("resourcecontainer.pt")
1653 delete_template = ViewPageTemplateFile('container_delete.pt')
1654
1655 resourceType = None
1656
1657 def __init__(self, context, request):
1658- form.FormBase.__init__(self, context, request)
1659+ oldform.FormBase.__init__(self, context, request)
1660 self.resourceType = self.request.get('SEARCH_TYPE','|').split('|')[0]
1661 self.filter_widget = queryMultiAdapter((self.getResourceUtility(),
1662 self.request), IFilterWidget)
1663@@ -185,12 +201,28 @@
1664 """Resource Type Filter"""
1665
1666
1667-class ResourceSubTypeWidget(widget.SimpleInputWidget):
1668-
1669- __call__ = ViewPageTemplateFile('subtype_widget.pt')
1670+class IResourceSubTypeWidget(z3c.form.interfaces.ITextWidget):
1671+ pass
1672+
1673+
1674+class ResourceSubTypeWidget(z3c.form.browser.text.TextWidget):
1675+ implementsOnly(IResourceSubTypeWidget)
1676+
1677+ utility = None
1678+
1679+ def __init__(self, request, utility=None):
1680+ super(ResourceSubTypeWidget, self).__init__(request)
1681+ self.utility = utility
1682+
1683+ def freeTextValue(self):
1684+ if self.value in self.subTypes():
1685+ return ''
1686+ return self.value
1687
1688 def subTypes(self):
1689- util = queryUtility(IResourceFactoryUtility, name=self.utility, default=None)
1690+ util = queryUtility(IResourceFactoryUtility,
1691+ name=self.utility,
1692+ default=None)
1693 if IResourceSubTypes.providedBy(util):
1694 subtypes = util
1695 else:
1696@@ -202,42 +234,26 @@
1697 def hasInput(self):
1698 return self.request.get(self.name,None) != '' or self.request.get(self.name+'.newSubType',None)
1699
1700- def getInputValue(self):
1701- subType = self.request.get(self.name)
1702- newSubType = self.request.get(self.name+'.newSubType')
1703- return newSubType or subType
1704-
1705-
1706-class ResourceView(form.DisplayFormBase):
1707- """A Resource info view."""
1708-
1709- __used_for__ = IResource
1710-
1711- form_fields = form.Fields(IResource)
1712-
1713- template = ViewPageTemplateFile("resource.pt")
1714-
1715- def __init__(self, context, request):
1716- self.context = context
1717- self.request = request
1718-
1719-
1720-class LocationView(ResourceView):
1721- """A location info view."""
1722- __used_for__ = ILocation
1723- form_fields = form.Fields(ILocation)
1724-
1725-
1726-class EquipmentView(ResourceView):
1727- """A equipment info view."""
1728- __used_for__ = IEquipment
1729- form_fields = form.Fields(IEquipment)
1730-
1731-
1732-class ResourceEditView(BaseEditView):
1733- """A view for editing resource info."""
1734-
1735- __used_for__ = IBaseResourceContained
1736+ def extract(self, default=NO_VALUE):
1737+ subType = self.request.get(self.name, default)
1738+ newSubType = self.request.get(self.name+'.newSubType', default)
1739+ if subType and subType != default:
1740+ return subType
1741+ return newSubType or default
1742+
1743+
1744+def ResourceSubTypeFieldWidget(field, request):
1745+ utility_name = 'resource'
1746+ if not field.interface is None:
1747+ # XXX: this is what we get for using named utilities
1748+ if issubclass(field.interface, IEquipment):
1749+ utility_name = 'equipment'
1750+ elif issubclass(field.interface, ILocation):
1751+ utility_name = 'location'
1752+ return widget.FieldWidget(
1753+ field,
1754+ ResourceSubTypeWidget(request, utility=utility_name)
1755+ )
1756
1757
1758 class ResourceContainerFilterWidget(PersonFilterWidget):
1759@@ -291,3 +307,275 @@
1760 results = filter_widget.filter(results)
1761
1762 return results
1763+
1764+
1765+class ResourceDemographicsFieldsAbsoluteURLAdapter(BrowserView):
1766+
1767+ adapts(IResourceDemographicsFields, IBrowserRequest)
1768+ implements(IAbsoluteURL)
1769+
1770+ def __str__(self):
1771+ app = ISchoolToolApplication(None)
1772+ url = str(getMultiAdapter((app, self.request), name='absolute_url'))
1773+ return url + '/resource_demographics'
1774+
1775+ __call__ = __str__
1776+
1777+
1778+class ResourceDemographicsView(DemographicsView):
1779+
1780+ title = _('Resource Demographics Container')
1781+
1782+
1783+########## Base class of all resource views (uses self.resource_type) #########
1784+class ResourceFieldGenerator(object):
1785+
1786+ def makeRows(self, fields, cols=1):
1787+ rows = []
1788+ while fields:
1789+ rows.append(fields[:cols])
1790+ fields = fields[cols:]
1791+ return rows
1792+
1793+ def makeFieldSet(self, fieldset_id, legend, fields, cols=1):
1794+ result = {
1795+ 'id': fieldset_id,
1796+ 'legend': legend,
1797+ }
1798+ result['rows'] = self.makeRows(fields, cols)
1799+ return result
1800+
1801+ def fieldsets(self):
1802+ result = []
1803+ sources = [
1804+ (self.base_id, self.base_legend, list(self.getBaseFields())),
1805+ (self.demo_id, self.demo_legend, list(self.getDemoFields())),
1806+ ]
1807+ for fieldset_id, legend, fields in sources:
1808+ result.append(self.makeFieldSet(fieldset_id, legend, fields, 2))
1809+ return result
1810+
1811+ def getDemoFields(self):
1812+ fields = field.Fields()
1813+ dfs = IResourceDemographicsFields(ISchoolToolApplication(None))
1814+ for field_desc in dfs.filter_key(self.resource_type):
1815+ fields += field_desc.makeField()
1816+ return fields
1817+
1818+
1819+############### Resource view (group determined by base class) #################
1820+class BaseResourceView(form.Form, ResourceFieldGenerator):
1821+
1822+ template = ViewPageTemplateFile('templates/resource_view.pt')
1823+ mode = DISPLAY_MODE
1824+ id = 'resource-view'
1825+
1826+ @property
1827+ def label(self):
1828+ return self.context.title
1829+
1830+ def update(self):
1831+ self.fields = self.getBaseFields()
1832+ self.fields += self.getDemoFields()
1833+ self.subforms = []
1834+ super(BaseResourceView, self).update()
1835+
1836+ def __call__(self):
1837+ self.update()
1838+ return self.render()
1839+
1840+ def updateWidgets(self):
1841+ super(BaseResourceView, self).updateWidgets()
1842+ for widget in self.widgets:
1843+ if not self.widgets[widget].value:
1844+ self.widgets[widget].mode = HIDDEN_MODE
1845+
1846+
1847+class ResourceView(BaseResourceView):
1848+ """A location info view."""
1849+
1850+ resource_type = 'resource'
1851+
1852+ def getBaseFields(self):
1853+ return field.Fields(IResource)
1854+
1855+
1856+class LocationView(BaseResourceView):
1857+ """A location info view."""
1858+
1859+ resource_type = 'location'
1860+
1861+ def getBaseFields(self):
1862+ return field.Fields(ILocation)
1863+
1864+
1865+class EquipmentView(BaseResourceView):
1866+ """A equipment info view."""
1867+
1868+ resource_type = 'equipment'
1869+
1870+ def getBaseFields(self):
1871+ return field.Fields(IEquipment)
1872+
1873+
1874+############### Base classes of all resource add/edit views ################
1875+class BaseResourceAddView(form.AddForm, ResourceFieldGenerator):
1876+
1877+ id = 'resource-form'
1878+ template = ViewPageTemplateFile('templates/resource_form.pt')
1879+
1880+ def getBaseFields(self):
1881+ return field.Fields(IResource)
1882+
1883+ def groupViewURL(self):
1884+ return '%s/%s.html' % (absoluteURL(self.context, self.request),
1885+ self.group_id)
1886+
1887+ def updateActions(self):
1888+ super(BaseResourceAddView, self).updateActions()
1889+ self.actions['add'].addClass('button-ok')
1890+ self.actions['cancel'].addClass('button-cancel')
1891+
1892+ @button.buttonAndHandler(_('Add'))
1893+ def handleAdd(self, action):
1894+ super(BaseResourceAddView, self).handleAdd.func(self, action)
1895+
1896+ @button.buttonAndHandler(_('Cancel'))
1897+ def handle_cancel_action(self, action):
1898+ self.request.response.redirect(self.groupViewURL())
1899+
1900+ def createAndAdd(self, data):
1901+ resource = self._factory()
1902+ resource.title = data.get('title')
1903+ chooser = INameChooser(self.context)
1904+ resource.__name__ = chooser.chooseName('', resource)
1905+ form.applyChanges(self, resource, data)
1906+ notify(ObjectCreatedEvent(resource))
1907+ self.context[resource.__name__] = resource
1908+ return resource
1909+
1910+ def update(self):
1911+ self.fields = self.getBaseFields()
1912+ self.fields += self.getDemoFields()
1913+ self.updateWidgets()
1914+ self.updateActions()
1915+ self.actions.execute()
1916+
1917+ def updateWidgets(self):
1918+ super(BaseResourceAddView, self).updateWidgets()
1919+
1920+ def nextURL(self):
1921+ return absoluteURL(self.context, self.request)
1922+
1923+
1924+class BaseResourceEditView(form.EditForm, ResourceFieldGenerator):
1925+
1926+ id = 'resource-form'
1927+ template = ViewPageTemplateFile('templates/resource_form.pt')
1928+
1929+ def update(self):
1930+ self.fields = self.getBaseFields()
1931+ self.fields += self.getDemoFields()
1932+ super(BaseResourceEditView, self).update()
1933+
1934+ @button.buttonAndHandler(_('Apply'))
1935+ def handle_apply_action(self, action):
1936+ data, errors = self.extractData()
1937+ if errors:
1938+ self.status = self.formErrorsMessage
1939+ return
1940+ self.applyChanges(data)
1941+ url = absoluteURL(self.context, self.request)
1942+ self.request.response.redirect(url)
1943+
1944+ @button.buttonAndHandler(_('Cancel'))
1945+ def handle_cancel_action(self, action):
1946+ url = absoluteURL(self.context, self.request)
1947+ self.request.response.redirect(url)
1948+
1949+ @property
1950+ def label(self):
1951+ return _(u'Change information for ${fullname}',
1952+ mapping={'fullname': self.context.title})
1953+
1954+ def updateActions(self):
1955+ super(BaseResourceEditView, self).updateActions()
1956+ self.actions['apply'].addClass('button-ok')
1957+ self.actions['cancel'].addClass('button-cancel')
1958+
1959+
1960+############### Resource add/edit views ################
1961+class BaseResourceForm(object):
1962+
1963+ resource_type = 'resource'
1964+ base_id = 'base-data'
1965+ base_legend = _('Resource identification')
1966+ demo_id = 'demo-data'
1967+ demo_legend = _('Resource demographics')
1968+
1969+ def getBaseFields(self):
1970+ return field.Fields(IResource).omit('type')
1971+
1972+
1973+class ResourceAddView(BaseResourceForm, BaseResourceAddView):
1974+
1975+ label = _('Add new resource')
1976+ _factory = Resource
1977+
1978+
1979+class ResourceEditView(BaseResourceForm, BaseResourceEditView):
1980+
1981+ label = _('Edit resource')
1982+
1983+
1984+############### Location add/edit views ################
1985+class BaseLocationForm(object):
1986+
1987+ resource_type = 'location'
1988+ base_id = 'base-data'
1989+ base_legend = _('Location identification')
1990+ demo_id = 'demo-data'
1991+ demo_legend = _('Location demographics')
1992+
1993+ def getBaseFields(self):
1994+ fields = field.Fields(ILocation).select('title', 'type', 'description',
1995+ 'capacity', 'notes')
1996+ return fields
1997+
1998+
1999+class LocationAddView(BaseLocationForm, BaseResourceAddView):
2000+
2001+ label = _('Add new location')
2002+ _factory = Location
2003+
2004+
2005+class LocationEditView(BaseLocationForm, BaseResourceEditView):
2006+
2007+ label = _('Edit location')
2008+
2009+
2010+############### Equipment add/edit views ################
2011+class BaseEquipmentForm(object):
2012+
2013+ resource_type = 'equipment'
2014+ base_id = 'base-data'
2015+ base_legend = _('Equipment identification')
2016+ demo_id = 'demo-data'
2017+ demo_legend = _('Equipment demographics')
2018+
2019+ def getBaseFields(self):
2020+ fields = field.Fields(IEquipment).select('title', 'type', 'description',
2021+ 'manufacturer', 'model', 'serialNumber', 'purchaseDate', 'notes')
2022+ return fields
2023+
2024+
2025+class EquipmentAddView(BaseEquipmentForm, BaseResourceAddView):
2026+
2027+ label = _('Add new equipment')
2028+ _factory = Equipment
2029+
2030+
2031+class EquipmentEditView(BaseEquipmentForm, BaseResourceEditView):
2032+
2033+ label = _('Edit equipment')
2034+
2035
2036=== modified file 'src/schooltool/resource/browser/resources.txt'
2037--- src/schooltool/resource/browser/resources.txt 2008-09-05 12:13:50 +0000
2038+++ src/schooltool/resource/browser/resources.txt 2011-01-17 05:56:49 +0000
2039@@ -7,11 +7,7 @@
2040 Set up
2041 ------
2042
2043- >>> from zope.testbrowser.testing import Browser
2044- >>> manager = Browser()
2045- >>> manager.addHeader('Authorization', 'Basic manager:schooltool')
2046- >>> manager.handleErrors = False
2047- >>> manager.open('http://localhost/')
2048+ >>> manager = Browser('manager', 'schooltool')
2049
2050 Creating Resources
2051 ------------------
2052@@ -32,8 +28,7 @@
2053
2054 >>> manager.getLink('New Location').click()
2055 >>> manager.getControl('Title').value = 'Room 101'
2056- >>> manager.getControl(name='field.type.newSubType').value = 'class room'
2057- >>> manager.getControl('Type').value = ['']
2058+ >>> manager.getControl(name='form.widgets.type.newSubType').value = 'class room'
2059 >>> manager.getControl('Capacity').value = '10'
2060 >>> manager.getControl('Add').click()
2061 >>> manager.getLink('class room').click()
2062@@ -44,7 +39,7 @@
2063
2064 >>> manager.getLink('New Equipment').click()
2065 >>> manager.getControl('Title').value = 'Projector 1'
2066- >>> manager.getControl(name='field.type.newSubType').value = 'Projector'
2067+ >>> manager.getControl(name='form.widgets.type.newSubType').value = 'Projector'
2068 >>> manager.getControl('Manufacturer').value = 'Sony'
2069 >>> manager.getControl('Model').value = 'RS-1'
2070 >>> manager.getControl('Serial Number').value = 'ABC123-987'
2071@@ -60,7 +55,7 @@
2072
2073 >>> manager.getLink('New Equipment').click()
2074 >>> manager.getControl('Title').value = 'Projector 2'
2075- >>> manager.getControl('Type').value = ['Projector']
2076+ >>> manager.getControl(name='form.widgets.type').value = ['Projector']
2077 >>> manager.getControl('Manufacturer').value = 'Sony'
2078 >>> manager.getControl('Model').value = 'RS-1'
2079 >>> manager.getControl('Serial Number').value = 'ABC123-9876'
2080
2081=== modified file 'src/schooltool/resource/browser/subtype_widget.pt'
2082--- src/schooltool/resource/browser/subtype_widget.pt 2007-03-17 15:25:31 +0000
2083+++ src/schooltool/resource/browser/subtype_widget.pt 2011-01-17 05:56:49 +0000
2084@@ -1,13 +1,16 @@
2085+<html xmlns="http://www.w3.org/1999/xhtml"
2086+ xmlns:tal="http://xml.zope.org/namespaces/tal"
2087+ tal:omit-tag="">
2088 <input type="text"
2089 tal:attributes="id string:${view/name}.newSubType;
2090 name string:${view/name}.newSubType;
2091- value: view/_getFormValue"
2092- />
2093-
2094+ value view/freeTextValue" />
2095 <select tal:attributes="id string:${view/name};
2096 name string:${view/name}">
2097- <option value="">Choose One</option>
2098+ <option value="" i18n:translate="">Choose One</option>
2099 <option tal:repeat="type view/subTypes"
2100- tal:attributes="value type"
2101+ tal:attributes="value type;
2102+ selected python:type==view.value and 'selected' or None"
2103 tal:content="type">subtype</option>
2104 </select>
2105+</html>
2106
2107=== added directory 'src/schooltool/resource/browser/templates'
2108=== added file 'src/schooltool/resource/browser/templates/resource_form.pt'
2109--- src/schooltool/resource/browser/templates/resource_form.pt 1970-01-01 00:00:00 +0000
2110+++ src/schooltool/resource/browser/templates/resource_form.pt 2011-01-17 05:56:49 +0000
2111@@ -0,0 +1,138 @@
2112+<html metal:use-macro="view/@@standard_macros/page" i18n:domain="schooltool">
2113+ <body>
2114+ <metal:nothing metal:fill-slot="content-header" />
2115+ <metal:block metal:fill-slot="body">
2116+ <form action="." method="post" enctype="multipart/form-data" class="standalone"
2117+ metal:define-macro="form"
2118+ tal:attributes="method view/method;
2119+ enctype view/enctype;
2120+ acceptCharset view/acceptCharset;
2121+ accept view/accept;
2122+ action view/action;
2123+ name view/name;
2124+ id view/id">
2125+ <metal:subform define-macro="subform">
2126+ <div class="viewspace" metal:define-slot="viewspace">
2127+ <metal:label define-slot="label">
2128+ <h3 metal:define-macro="form-label"
2129+ tal:condition="view/label|nothing"
2130+ tal:content="view/label">
2131+ Form Label
2132+ </h3>
2133+ </metal:label>
2134+ <metal:info define-slot="info">
2135+ <div class="required-info"
2136+ metal:define-macro="required-info">
2137+ <!--<span class="required">*</span>
2138+ &ndash; required -->
2139+ </div>
2140+ </metal:info>
2141+ <metal:header define-slot="header">
2142+ <div class="status"
2143+ tal:condition="view/status"
2144+ metal:define-macro="form-header">
2145+ <div class="summary"
2146+ i18n:translate=""
2147+ tal:content="view/status">
2148+ Form status summary
2149+ </div>
2150+ <ul class="errors"
2151+ tal:define="errors view/errors|view/widgets/errors"
2152+ tal:condition="errors"
2153+ metal:define-macro="form-errors">
2154+ <li tal:repeat="error errors">
2155+ <tal:block condition="error/widget">
2156+ <span tal:replace="error/widget/label" />:
2157+ </tal:block>
2158+ <span tal:replace="structure error/render">Error Type</span>
2159+ </li>
2160+ </ul>
2161+ </div>
2162+ </metal:header>
2163+ <div metal:define-slot="extra-info" tal:replace="nothing">
2164+ </div>
2165+ <div metal:define-slot="main">
2166+ <metal:widget-rows define-macro="widget-rows">
2167+ <tal:loop repeat="fieldset view/fieldsets">
2168+ <fieldset tal:condition="fieldset/rows"
2169+ tal:attributes="id fieldset/id">
2170+ <legend tal:content="fieldset/legend">legend</legend>
2171+ <tal:loop repeat="row fieldset/rows">
2172+ <tal:block repeat="widget_id row">
2173+ <metal:block tal:define="widget python:view.widgets[widget_id]">
2174+ <div id="" class="row"
2175+ tal:attributes="id string:${widget/id}-row"
2176+ tal:condition="python:widget.mode != 'hidden'">
2177+ <metal:widget-row define-macro="widget-row">
2178+ <div class="label">
2179+ <label tal:attributes="for widget/id">
2180+ <span i18n:translate=""
2181+ tal:content="widget/label">label</span>
2182+ <span class="required"
2183+ tal:condition="widget/required">*</span>
2184+ </label>
2185+ </div>
2186+ <p class="hint" tal:content="widget/field/description">Description of this field.</p>
2187+ <div class="widget" tal:content="structure widget/render">
2188+ <input type="text" size="24" value="" />
2189+ </div>
2190+ <div class="error"
2191+ tal:condition="widget/error">
2192+ <span tal:replace="structure widget/error/render">error</span>
2193+ </div>
2194+ </metal:widget-row>
2195+ </div>
2196+ <input type="hidden" value=""
2197+ tal:condition="python:widget.mode == 'hidden'"
2198+ tal:replace="structure widget/render" />
2199+ </metal:block>
2200+ </tal:block>
2201+ </tal:loop>
2202+ <div metal:define-slot="extra-widgets" tal:replace="nothing">
2203+ </div>
2204+ </fieldset>
2205+ </tal:loop>
2206+ </metal:widget-rows>
2207+ <metal:groups define-macro="groups">
2208+ <fieldset tal:condition="view/groups|nothing"
2209+ tal:repeat="view view/groups">
2210+ <legend tal:condition="view/label"
2211+ tal:content="view/label">Label</legend>
2212+ <metal:group-header define-slot="group-header">
2213+ <div class="status"
2214+ tal:condition="view/widgets/errors">
2215+ <div metal:use-macro="template/macros/errors" />
2216+ </div>
2217+ </metal:group-header>
2218+ <metal:group-rows define-slot="group-rows">
2219+ <div metal:use-macro="template/macros/widget-rows" />
2220+ </metal:group-rows>
2221+ </fieldset>
2222+ </metal:groups>
2223+ </div>
2224+ <metal:above-buttons define-slot="above-buttons">
2225+ <tal:block
2226+ repeat="form view/subforms"
2227+ content="structure form/render"
2228+ condition="view/subforms|nothing" />
2229+ </metal:above-buttons>
2230+ </div>
2231+ <metal:buttons define-slot="buttons">
2232+ <div metal:define-macro="form-buttons">
2233+ <div class="buttons controls"
2234+ metal:define-slot="bottom-buttons">
2235+ <tal:block condition="view/actions|nothing">
2236+ <input tal:repeat="action view/actions/values"
2237+ tal:replace="structure action/render"
2238+ />
2239+ </tal:block>
2240+ </div>
2241+ </div>
2242+ </metal:buttons>
2243+ <metal:bottom define-slot="bottom">
2244+ </metal:bottom>
2245+ </metal:subform>
2246+ </form>
2247+ </metal:block>
2248+ </body>
2249+</html>
2250
2251=== added file 'src/schooltool/resource/browser/templates/resource_view.pt'
2252--- src/schooltool/resource/browser/templates/resource_view.pt 1970-01-01 00:00:00 +0000
2253+++ src/schooltool/resource/browser/templates/resource_view.pt 2011-01-17 05:56:49 +0000
2254@@ -0,0 +1,36 @@
2255+<html metal:use-macro="view/@@standard_macros/page">
2256+
2257+ <body>
2258+ <metal:nothing metal:fill-slot="content-header" />
2259+ <metal:block metal:fill-slot="body">
2260+ <tal:block repeat="widget view/widgets/values">
2261+ <div id="" class="row"
2262+ tal:attributes="id string:${widget/id}-row"
2263+ tal:condition="python:widget.mode != 'hidden'">
2264+ <metal:widget-row define-macro="widget-row">
2265+ <div class="label">
2266+ <label tal:attributes="for widget/id">
2267+ <span i18n:translate=""
2268+ tal:content="widget/label">label</span>
2269+ <span class="required"
2270+ tal:condition="widget/required">*</span>
2271+ </label>
2272+ </div>
2273+ <p class="hint" tal:content="widget/field/description">Description of this field.</p>
2274+ <div class="widget" tal:content="structure widget/render">
2275+ <input type="text" size="24" value="" />
2276+ </div>
2277+ <div class="error"
2278+ tal:condition="widget/error">
2279+ <span tal:replace="structure widget/error/render">error</span>
2280+ </div>
2281+ </metal:widget-row>
2282+ </div>
2283+ <input type="hidden" value=""
2284+ tal:condition="python:widget.mode == 'hidden'"
2285+ tal:replace="structure widget/render" />
2286+ </tal:block>
2287+ </metal:block>
2288+ </body>
2289+</html>
2290+
2291
2292=== modified file 'src/schooltool/resource/configure.zcml'
2293--- src/schooltool/resource/configure.zcml 2010-01-19 12:09:22 +0000
2294+++ src/schooltool/resource/configure.zcml 2011-01-17 05:56:49 +0000
2295@@ -57,13 +57,19 @@
2296 factory="schooltool.app.app.SimpleNameChooser"
2297 provides="zope.container.interfaces.INameChooser" />
2298
2299- <!-- Application hook -->
2300+ <!-- Application Init -->
2301 <adapter
2302 for="schooltool.app.interfaces.ISchoolToolApplication"
2303 factory=".resource.ResourceInit"
2304 name="schooltool.resources"
2305 />
2306
2307+ <!-- Application StartUp -->
2308+ <adapter
2309+ factory=".resource.ResourceStartUp"
2310+ name="schooltool.resources.startup"
2311+ />
2312+
2313 <!-- sample data -->
2314 <configure
2315 xmlns:zcml="http://namespaces.zope.org/zcml"
2316@@ -120,6 +126,9 @@
2317 name="booking"
2318 />
2319
2320+ <adapter factory=".resource.getResourceDemographics" />
2321+ <adapter factory=".resource.DemographicsFormAdapter" />
2322+ <adapter factory=".resource.getResourceDemographicsFields" />
2323
2324 <class class=".booking.ResourceBookingCalendar">
2325 <require permission="schooltool.view"
2326@@ -133,6 +142,17 @@
2327 />
2328 </class>
2329
2330+ <!-- resource demos -->
2331+
2332+ <class class=".resource.ResourceDemographicsFields">
2333+ <implements interface="schooltool.resource.interfaces.IResourceDemographicsFields" />
2334+ <require
2335+ permission="schooltool.edit"
2336+ interface="schooltool.resource.interfaces.IResourceDemographicsFields"
2337+ set_schema="schooltool.resource.interfaces.IResourceDemographicsFields" />
2338+ </class>
2339+
2340+ <adapter factory=".resource.getLimitKeyVocabularyForResourceFields" />
2341
2342 <subscriber
2343 for="schooltool.resource.interfaces.IBookingCalendar
2344
2345=== modified file 'src/schooltool/resource/ftesting.zcml'
2346--- src/schooltool/resource/ftesting.zcml 2010-04-08 06:26:02 +0000
2347+++ src/schooltool/resource/ftesting.zcml 2011-01-17 05:56:49 +0000
2348@@ -8,6 +8,7 @@
2349 <include package="schooltool.term" />
2350 <include package="schooltool.schoolyear" />
2351 <include package="schooltool.term" file="time_machine.zcml" />
2352+ <include package="schooltool.basicperson" />
2353
2354 <browser:defaultSkin name="SchoolTool" />
2355
2356
2357=== modified file 'src/schooltool/resource/interfaces.py'
2358--- src/schooltool/resource/interfaces.py 2010-02-22 10:39:32 +0000
2359+++ src/schooltool/resource/interfaces.py 2011-01-17 05:56:49 +0000
2360@@ -21,15 +21,22 @@
2361 """
2362
2363 import zope.schema
2364+from zope.configuration.fields import PythonIdentifier
2365 from zope.container.interfaces import IContainer, IContained
2366+from zope.container.interfaces import IContainer, IOrderedContainer
2367 from zope.container.constraints import contains, containers
2368 from zope.interface import Interface, Attribute
2369
2370+from schooltool.basicperson.interfaces import IDemographics, IDemographicsFields
2371 from schooltool.calendar.interfaces import ICalendarEvent
2372 from schooltool.calendar.interfaces import ICalendar
2373 from schooltool.common import SchoolToolMessage as _
2374
2375
2376+class ResourceSubType(zope.schema.TextLine):
2377+ """Sub-type of the resource is a free-form text line."""
2378+
2379+
2380 class IBaseResource(Interface):
2381 """Resource."""
2382
2383@@ -37,7 +44,7 @@
2384 title=_("Title"),
2385 description=_("Title of the resource."))
2386
2387- type = zope.schema.TextLine(
2388+ type = ResourceSubType(
2389 title=_("Resource Type"),
2390 description=_("Type of resource"),
2391 required=True)
2392@@ -73,7 +80,7 @@
2393 class ILocation(IBaseResource):
2394 """Location."""
2395
2396- type = zope.schema.TextLine(
2397+ type = ResourceSubType(
2398 title=_("Location Type"),
2399 description=_("Type of location (i.e. computer lab, class room, etc.)"),
2400 required=True)
2401@@ -88,7 +95,7 @@
2402 class IEquipment(IBaseResource):
2403 """Equipment."""
2404
2405- type = zope.schema.TextLine(
2406+ type = ResourceSubType(
2407 title=_("Equipment Type"),
2408 description=_("Type of equipment (i.e. camcorder, computer, etc.)"),
2409 required=True)
2410@@ -160,3 +167,16 @@
2411
2412 class IBookingTimetableEvent(ICalendarEvent):
2413 """Event that represents a possible booking in a timetable slot."""
2414+
2415+
2416+class IResourceDemographics(IDemographics):
2417+ """Demographics data storage for a resource
2418+
2419+ Stores any kind of data defined by field descriptions that are set
2420+ up for the resource container.
2421+ """
2422+
2423+
2424+class IResourceDemographicsFields(IDemographicsFields):
2425+ """Demographics field storage for resource demos."""
2426+
2427
2428=== modified file 'src/schooltool/resource/resource.py'
2429--- src/schooltool/resource/resource.py 2010-01-19 12:09:22 +0000
2430+++ src/schooltool/resource/resource.py 2011-01-17 05:56:49 +0000
2431@@ -24,23 +24,42 @@
2432 __docformat__ = 'restructuredtext'
2433
2434 from persistent import Persistent
2435-from zope.component import adapts
2436-from zope.interface import implements
2437+from persistent.dict import PersistentDict
2438+
2439+from zope.component import adapts, adapter
2440+from zope.interface import implements, implementer
2441+from zope.interface import Interface
2442 from zope.annotation.interfaces import IAttributeAnnotatable
2443 from zope.container.contained import Contained
2444 from zope.container.btree import BTreeContainer
2445+from zope.container.ordered import OrderedContainer
2446+from zope.location.location import Location
2447+from zope.schema import TextLine, Bool, Date, Choice
2448+from zope.schema.vocabulary import SimpleVocabulary
2449+from zope.schema.vocabulary import SimpleTerm
2450
2451 from schooltool.app.app import Asset
2452-from schooltool.app.app import InitBase
2453+from schooltool.app.app import InitBase, StartUpBase
2454+from schooltool.app.utils import vocabulary
2455 from schooltool.app.security import LeaderCrowd
2456-from schooltool.securitypolicy.crowds import TeachersCrowd
2457 from schooltool.app.interfaces import ICalendarParentCrowd
2458+from schooltool.app.interfaces import ISchoolToolApplication
2459+from schooltool.app.utils import vocabulary
2460+from schooltool.basicperson.demographics import PersonDemographicsData
2461+from schooltool.basicperson.demographics import DemographicsFields
2462+from schooltool.basicperson.demographics import IDemographicsForm
2463+from schooltool.basicperson.interfaces import IFieldFilterVocabulary
2464 from schooltool.resource import interfaces
2465+from schooltool.securitypolicy.crowds import TeachersCrowd
2466 from schooltool.securitypolicy.crowds import ConfigurableCrowd, AggregateCrowd
2467 from schooltool.securitypolicy.crowds import AuthenticatedCrowd
2468 from schooltool.common import SchoolToolMessage as _
2469
2470
2471+RESOURCE_DEMO_FIELDS_KEY = 'schooltool.resource.demographics_fields'
2472+RESOURCE_DEMO_DATA_KEY = 'schooltool.resource.demographics_data'
2473+
2474+
2475 class ResourceContainer(BTreeContainer):
2476 """Container of resources."""
2477
2478@@ -93,6 +112,18 @@
2479
2480 def __call__(self):
2481 self.app['resources'] = ResourceContainer()
2482+ self.app[RESOURCE_DEMO_FIELDS_KEY] = ResourceDemographicsFields()
2483+ self.app[RESOURCE_DEMO_DATA_KEY] = ResourceDemographicsDataContainer()
2484+
2485+
2486+class ResourceStartUp(StartUpBase):
2487+
2488+ def __call__(self):
2489+ if RESOURCE_DEMO_FIELDS_KEY not in self.app:
2490+ self.app[RESOURCE_DEMO_FIELDS_KEY] = ResourceDemographicsFields()
2491+ if RESOURCE_DEMO_DATA_KEY not in self.app:
2492+ self.app[RESOURCE_DEMO_DATA_KEY] = \
2493+ ResourceDemographicsDataContainer()
2494
2495
2496 class ResourceViewersCrowd(ConfigurableCrowd):
2497@@ -127,3 +158,77 @@
2498
2499 def crowdFactories(self):
2500 return [LeaderCrowd, TeachersCrowd]
2501+
2502+
2503+################### Demographics #################
2504+class ResourceDemographicsFields(DemographicsFields):
2505+ """Storage for demographics fields for all resources."""
2506+ implements(interfaces.IResourceDemographicsFields)
2507+
2508+
2509+@implementer(interfaces.IResourceDemographicsFields)
2510+@adapter(ISchoolToolApplication)
2511+def getResourceDemographicsFields(app):
2512+ return app[RESOURCE_DEMO_FIELDS_KEY]
2513+
2514+
2515+class ResourceDemographicsDataContainer(BTreeContainer):
2516+ """Storage for demographics information for all resources."""
2517+
2518+
2519+class ResourceDemographicsData(PersonDemographicsData):
2520+ """Storage for demographics information for a resource."""
2521+ implements(interfaces.IResourceDemographics)
2522+
2523+ def isValidKey(self, key):
2524+ app = ISchoolToolApplication(None)
2525+ demographics_fields = interfaces.IResourceDemographicsFields(app)
2526+ return key in demographics_fields
2527+
2528+
2529+@adapter(interfaces.IBaseResource)
2530+@implementer(interfaces.IResourceDemographics)
2531+def getResourceDemographics(resource):
2532+ app = ISchoolToolApplication(None)
2533+ rdc = app[RESOURCE_DEMO_DATA_KEY]
2534+ demographics = rdc.get(resource.__name__, None)
2535+ if demographics is None:
2536+ rdc[resource.__name__] = demographics = ResourceDemographicsData()
2537+ return demographics
2538+
2539+
2540+@adapter(interfaces.IResourceDemographicsFields)
2541+@implementer(IFieldFilterVocabulary)
2542+def getLimitKeyVocabularyForResourceFields(resource_field_description_container):
2543+ return vocabulary([
2544+ ('resource', _('Resource')),
2545+ ('location', _('Location')),
2546+ ('equipment', _('Equipment')),
2547+ ])
2548+
2549+
2550+class DemographicsFormAdapter(object):
2551+ implements(IDemographicsForm)
2552+ adapts(interfaces.IBaseResource)
2553+
2554+ def __init__(self, context):
2555+ self.__dict__['context'] = context
2556+ self.__dict__['demographics'] = interfaces.IResourceDemographics(
2557+ self.context)
2558+
2559+ def __setattr__(self, name, value):
2560+ self.demographics[name] = value
2561+
2562+ def __getattr__(self, name):
2563+ return self.demographics.get(name, None)
2564+
2565+
2566+@adapter(interfaces.IResourceDemographicsFields)
2567+@implementer(IFieldFilterVocabulary)
2568+def getLimitKeyVocabularyForResourceFields(resource_field_description_container):
2569+ return vocabulary([
2570+ ('resource', _('Resource')),
2571+ ('location', _('Location')),
2572+ ('equipment', _('Equipment')),
2573+ ])
2574+
2575
2576=== modified file 'src/schooltool/resource/tests/test_resource.py'
2577--- src/schooltool/resource/tests/test_resource.py 2010-01-10 17:31:54 +0000
2578+++ src/schooltool/resource/tests/test_resource.py 2011-01-17 05:56:49 +0000
2579@@ -68,6 +68,7 @@
2580
2581 """
2582
2583+
2584 def doctest_Location():
2585 r"""Test for Location
2586
2587@@ -84,6 +85,7 @@
2588
2589 """
2590
2591+
2592 def doctest_Equipment():
2593 r"""Test for Equipment
2594
2595@@ -105,6 +107,88 @@
2596 >>> projector.purchaseDate
2597 """
2598
2599+
2600+def doctest_ResourceDemographics():
2601+ r"""Test Resource Demographics objects and adapters
2602+
2603+ >>> from schooltool.resource import interfaces, resource
2604+ >>> from schooltool.basicperson import demographics
2605+
2606+ First we need to set up a mock app and register its adapter:
2607+
2608+ >>> from zope.component import provideAdapter
2609+ >>> from zope.interface import implements
2610+ >>> from schooltool.app.interfaces import ISchoolToolApplication
2611+
2612+ >>> class MockSchoolToolApplication(dict):
2613+ ... implements(ISchoolToolApplication)
2614+ >>> app = MockSchoolToolApplication()
2615+ >>> provideAdapter(lambda context: app, (None,), ISchoolToolApplication)
2616+
2617+ We need to do what the AppInit adapter would otherwise do:
2618+
2619+ >>> resource.ResourceInit(app)()
2620+ >>> resource.RESOURCE_DEMO_FIELDS_KEY in app
2621+ True
2622+ >>> resource.RESOURCE_DEMO_DATA_KEY in app
2623+ True
2624+
2625+ There's an adapter for the resource demo fields container:
2626+
2627+ >>> provideAdapter(resource.getResourceDemographicsFields)
2628+ >>> dfs = interfaces.IResourceDemographicsFields(app)
2629+ >>> interfaces.IResourceDemographicsFields.providedBy(dfs)
2630+ True
2631+ >>> len(dfs)
2632+ 0
2633+
2634+ We'll add some demo fields to the container, some that are limited to a
2635+ specific resource type or types:
2636+
2637+ >>> dfs['ID'] = demographics.TextFieldDescription("ID", "Identifier")
2638+ >>> dfs['square_ft'] = demographics.TextFieldDescription("square_ft",
2639+ ... "Square Feet", limit_keys=['location'])
2640+ >>> dfs['warranty'] = demographics.TextFieldDescription("warranty",
2641+ ... "Warranty", limit_keys=['equiptment'])
2642+ >>> dfs['creation_date'] = demographics.DateFieldDescription(
2643+ ... "creation_date", "Creation Date",
2644+ ... limit_keys=['location', 'equiptment'])
2645+
2646+ When we pass the filter_key method a key that does not
2647+ belong to any of the limit_keys lists, then it will only return
2648+ those fields that have empty limit_keys lists.
2649+
2650+ >>> [f.__name__ for f in dfs.filter_key('anything')]
2651+ [u'ID']
2652+
2653+ When we pass 'location', it picks up the additional fields that are for
2654+ location type resources.
2655+
2656+ >>> [f.__name__ for f in dfs.filter_key('location')]
2657+ [u'ID', u'square_ft', u'creation_date']
2658+
2659+ When we pass 'equiptment', it picks up the additional fields that are for
2660+ equiptment type resources.
2661+
2662+ >>> [f.__name__ for f in dfs.filter_key('equiptment')]
2663+ [u'ID', u'warranty', u'creation_date']
2664+
2665+ Finally there's an adapter that adapts a resource it's demo data:
2666+
2667+ >>> provideAdapter(resource.getResourceDemographics)
2668+
2669+ Now we will create a resource and see what we get when we adapt it to
2670+ IDemographics:
2671+
2672+ >>> sample = resource.Resource('Sample Resource')
2673+ >>> sample.__name__ = 'sample'
2674+ >>> demos = interfaces.IResourceDemographics(sample)
2675+ >>> interfaces.IResourceDemographics.providedBy(demos)
2676+ True
2677+ >>> len(demos)
2678+ 0
2679+ """
2680+
2681 def test_suite():
2682 return unittest.TestSuite([
2683 doctest.DocTestSuite(optionflags=doctest.ELLIPSIS),

Subscribers

People subscribed via source and target branches