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

Proposed by Alan Elkner
Status: Merged
Merged at revision: 2689
Proposed branch: lp:~aelkner/schooltool/importer_fixes
Merge into: lp:schooltool/1.7
Diff against target: 1248 lines (+655/-131) (has conflicts)
13 files modified
CHANGES.txt (+6/-0)
src/schooltool/export/ftests/errant_data.txt (+192/-0)
src/schooltool/export/ftests/non_ascii.txt (+3/-3)
src/schooltool/export/ftests/test_all.py (+1/-1)
src/schooltool/export/importer.py (+390/-119)
src/schooltool/export/templates/import.pt (+3/-3)
src/schooltool/testing/analyze.py (+2/-1)
src/schooltool/testing/functional.py (+2/-1)
src/schooltool/timetable/__init__.py (+1/-0)
src/schooltool/timetable/browser/__init__.py (+14/-2)
src/schooltool/timetable/browser/templates/section-timetable-setup.pt (+5/-0)
src/schooltool/timetable/browser/tests/test_timetable.py (+31/-0)
src/schooltool/timetable/interfaces.py (+5/-1)
Text conflict in CHANGES.txt
To merge this branch: bzr merge lp:~aelkner/schooltool/importer_fixes
Reviewer Review Type Date Requested Status
Gediminas Paulauskas (community) Approve
Review via email: mp+29430@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Gediminas Paulauskas (menesis) wrote :

Very good work!

review: Approve
Revision history for this message
Gediminas Paulauskas (menesis) wrote :

Except the last change that added a checkbox to timetable setup page -- that template is no longer used after I merged Justas' timetables branch. You can see journal test failing with current trunk. Should be added to the new page instead.

Revision history for this message
Alan Elkner (aelkner) wrote :

That's cool. After you merge the rest of it, I'll re-branch trunk and re-add the change to the view. Please keep my change to the attribute and interface as that doesn't break anything. Thanks.

Revision history for this message
Alan Elkner (aelkner) wrote :

Come to think of it, I realize that it's easier for you to just throw out the whole diff for the last change, and it's easy enough for me to add back the attribute and interface change in my branch, so please disregard my last request.

Revision history for this message
Gediminas Paulauskas (menesis) wrote :

This branch was merged, and Justas has added another checkbox to the new timetable edit view.

review: Approve

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-06-15 11:17:48 +0000
3+++ CHANGES.txt 2010-07-07 23:46:41 +0000
4@@ -11,8 +11,14 @@
5 1.4.1 (unreleased)
6 ------------------
7
8+<<<<<<< TREE
9 - Copy data from previous school year (https://launchpad.net/bugs/541673)
10 - Update translations (ca, es, es_SV, sk, te, vi, zh_CN)
11+=======
12+- XLS Importer catches errors before trying to create objects.
13+- TestBrowser now strips out blank lines from printQuery
14+- TimeTable object now has consecutive_periods_as_one flag for use by journal
15+>>>>>>> MERGE-SOURCE
16
17
18 1.4.0 (2010-05-21)
19
20=== added file 'src/schooltool/export/ftests/errant_courses.xls'
21Binary files src/schooltool/export/ftests/errant_courses.xls 1970-01-01 00:00:00 +0000 and src/schooltool/export/ftests/errant_courses.xls 2010-07-07 23:46:41 +0000 differ
22=== added file 'src/schooltool/export/ftests/errant_data.txt'
23--- src/schooltool/export/ftests/errant_data.txt 1970-01-01 00:00:00 +0000
24+++ src/schooltool/export/ftests/errant_data.txt 2010-07-07 23:46:41 +0000
25@@ -0,0 +1,192 @@
26+Tests for errant import data
27+----------------------------
28+
29+This file has examples of bad data for the School Years sheet:
30+
31+ >>> browser = Browser('manager', 'schooltool')
32+ >>> browser.getLink('Manage').click()
33+ >>> browser.getLink('XLS Import').click()
34+ >>> import pkg_resources
35+ >>> browser.getControl('XLS File').add_file(
36+ ... pkg_resources.resource_stream('schooltool.export.ftests', 'errant_years.xls'),
37+ ... 'application/excel',
38+ ... 'sample_data.xls')
39+ >>> browser.getControl('Submit').click()
40+ >>> browser.printQuery("//p[@class='error']")
41+ <p class="error">School Years C2 has no date in it</p>
42+ <p class="error">School Years D2 has no date in it</p>
43+ <p class="error">School Years D3 end date cannot be before start date</p>
44+ <p class="error">School Years C5 start date overlaps another year</p>
45+ <p class="error">School Years D6 end date overlaps another year</p>
46+ <p class="error">School Years A7 not unicode or ascii string</p>
47+ <p class="error">School Years B7 not unicode or ascii string</p>
48+
49+This file has examples of bad data for the Terms sheet:
50+
51+ >>> browser.getLink('Manage').click()
52+ >>> browser.getLink('XLS Import').click()
53+ >>> import pkg_resources
54+ >>> browser.getControl('XLS File').add_file(
55+ ... pkg_resources.resource_stream('schooltool.export.ftests', 'errant_terms.xls'),
56+ ... 'application/excel',
57+ ... 'sample_data.xls')
58+ >>> browser.getControl('Submit').click()
59+ >>> browser.printQuery("//p[@class='error']")
60+ <p class="error">Terms B2 missing required text</p>
61+ <p class="error">Terms C2 missing required text</p>
62+ <p class="error">Terms D2 has no date in it</p>
63+ <p class="error">Terms E2 has no date in it</p>
64+ <p class="error">Terms A3 not unicode or ascii string</p>
65+ <p class="error">Terms A4 invalid school year</p>
66+ <p class="error">Terms E5 end date cannot be before start date</p>
67+ <p class="error">Terms D6 start date before start of school year</p>
68+ <p class="error">Terms E7 end date after end of school year</p>
69+ <p class="error">Terms D10 start date overlaps another term</p>
70+ <p class="error">Terms E11 end date overlaps another term</p>
71+ <p class="error">Terms A15 has no date in it</p>
72+ <p class="error">Terms B16 has no date in it</p>
73+ <p class="error">Terms E17 end date cannot be before start date</p>
74+
75+This file has examples of bad data for the School Timetables sheet:
76+
77+ >>> browser.getLink('Manage').click()
78+ >>> browser.getLink('XLS Import').click()
79+ >>> import pkg_resources
80+ >>> browser.getControl('XLS File').add_file(
81+ ... pkg_resources.resource_stream('schooltool.export.ftests', 'errant_timetables.xls'),
82+ ... 'application/excel',
83+ ... 'sample_data.xls')
84+ >>> browser.getControl('Submit').click()
85+ >>> browser.printQuery("//p[@class='error']")
86+ <p class="error">School Timetables B1 missing required text</p>
87+ <p class="error">School Timetables B2 missing required text</p>
88+ <p class="error">School Timetables B3 missing required text</p>
89+ <p class="error">School Timetables B4 missing required text</p>
90+ <p class="error">School Timetables B6 not unicode or ascii string</p>
91+ <p class="error">School Timetables B7 not unicode or ascii string</p>
92+ <p class="error">School Timetables B8 not unicode or ascii string</p>
93+ <p class="error">School Timetables B9 not unicode or ascii string</p>
94+ <p class="error">School Timetables B12 invalid school year</p>
95+ <p class="error">School Timetables B13 is not a valid timetable model</p>
96+ <p class="error">Schema has no day templates in A21</p>
97+ <p class="error">School Timetables A27 not unicode or ascii string</p>
98+ <p class="error">School Timetables B27 is not a valid time range</p>
99+ <p class="error">School Timetables A29 is the same day id as another in this timetable</p>
100+ <p class="error">School Timetables A40 is not defined in the 'Day Templates' section</p>
101+ <p class="error">School Timetables A42 is the same day id as another in this timetable</p>
102+ <p class="error">School Timetables A50 is not a valid integer</p>
103+ <p class="error">School Timetables A51 is not a valid integer</p>
104+ <p class="error">School Timetables A52 is not a valid weekday number (0-6)</p>
105+
106+This file has examples of bad data for the Resouces sheet:
107+
108+ >>> browser.getLink('Manage').click()
109+ >>> browser.getLink('XLS Import').click()
110+ >>> import pkg_resources
111+ >>> browser.getControl('XLS File').add_file(
112+ ... pkg_resources.resource_stream('schooltool.export.ftests', 'errant_resources.xls'),
113+ ... 'application/excel',
114+ ... 'sample_data.xls')
115+ >>> browser.getControl('Submit').click()
116+ >>> browser.printQuery("//p[@class='error']")
117+ <p class="error">Resources B2 missing required text</p>
118+ <p class="error">Resources C2 missing required text</p>
119+ <p class="error">Resources B3 must be either 'Location' or 'Resource'</p>
120+
121+This file has examples of bad data for the Persons sheet:
122+
123+ >>> browser.getLink('Manage').click()
124+ >>> browser.getLink('XLS Import').click()
125+ >>> import pkg_resources
126+ >>> browser.getControl('XLS File').add_file(
127+ ... pkg_resources.resource_stream('schooltool.export.ftests', 'errant_persons.xls'),
128+ ... 'application/excel',
129+ ... 'sample_data.xls')
130+ >>> browser.getControl('Submit').click()
131+ >>> browser.printQuery("//p[@class='error']")
132+ <p class="error">Persons B2 not unicode or ascii string</p>
133+ <p class="error">Persons C2 missing required text</p>
134+ <p class="error">Persons D2 not unicode or ascii string</p>
135+ <p class="error">Persons E2 missing required text</p>
136+ <p class="error">Persons F2 not unicode or ascii string</p>
137+ <p class="error">Persons G2 not unicode or ascii string</p>
138+ <p class="error">Persons H2 not unicode or ascii string</p>
139+ <p class="error">Persons I2 not unicode or ascii string</p>
140+ <p class="error">Persons J2 not unicode or ascii string</p>
141+ <p class="error">Persons K2 not unicode or ascii string</p>
142+ <p class="error">Persons L2 not unicode or ascii string</p>
143+ <p class="error">Persons M2 not unicode or ascii string</p>
144+ <p class="error">Persons N2 not unicode or ascii string</p>
145+ <p class="error">Persons O2 not unicode or ascii string</p>
146+ <p class="error">Persons C3 not unicode or ascii string</p>
147+ <p class="error">Persons E3 not unicode or ascii string</p>
148+
149+This file has examples of bad data for the Courses sheet:
150+
151+ >>> browser.getLink('Manage').click()
152+ >>> browser.getLink('XLS Import').click()
153+ >>> import pkg_resources
154+ >>> browser.getControl('XLS File').add_file(
155+ ... pkg_resources.resource_stream('schooltool.export.ftests', 'errant_courses.xls'),
156+ ... 'application/excel',
157+ ... 'sample_data.xls')
158+ >>> browser.getControl('Submit').click()
159+ >>> browser.printQuery("//p[@class='error']")
160+ <p class="error">Courses B2 missing required text</p>
161+ <p class="error">Courses C2 missing required text</p>
162+ <p class="error">Courses A3 not unicode or ascii string</p>
163+ <p class="error">Courses B3 not unicode or ascii string</p>
164+ <p class="error">Courses C3 not unicode or ascii string</p>
165+ <p class="error">Courses D3 not unicode or ascii string</p>
166+ <p class="error">Courses A4 invalid school year</p>
167+
168+This file has examples of bad data for the Sections sheet:
169+
170+ >>> browser.getLink('Manage').click()
171+ >>> browser.getLink('XLS Import').click()
172+ >>> import pkg_resources
173+ >>> browser.getControl('XLS File').add_file(
174+ ... pkg_resources.resource_stream('schooltool.export.ftests', 'errant_sections.xls'),
175+ ... 'application/excel',
176+ ... 'sample_data.xls')
177+ >>> browser.getControl('Submit').click()
178+ >>> browser.printQuery("//p[@class='error']")
179+ <p class="error">Sections 1 B1 missing required text</p>
180+ <p class="error">Sections 1 D1 missing required text</p>
181+ <p class="error">Sections_2 B1 not unicode or ascii string</p>
182+ <p class="error">Sections_2 D1 not unicode or ascii string</p>
183+ <p class="error">Sections_3 B1 invalid school year</p>
184+ <p class="error">Sections_4 D1 is not a valid term in the given school year</p>
185+ <p class="error">Sections_5 A11 not unicode or ascii string</p>
186+ <p class="error">Sections_5 A12 is not a valid username</p>
187+ <p class="error">Sections_5 A16 not unicode or ascii string</p>
188+ <p class="error">Sections_5 A17 is not a valid username</p>
189+ <p class="error">Sections_5 A22 not unicode or ascii string</p>
190+ <p class="error">Sections_5 B22 not unicode or ascii string</p>
191+ <p class="error">Sections_5 C22 not unicode or ascii string</p>
192+ <p class="error">Sections_5 A23 is not a valid day id for the given schema</p>
193+ <p class="error">Sections_5 C23 is not a valid resource id</p>
194+ <p class="error">Sections_5 B24 is not a valid period id for the given day</p>
195+
196+This file has examples of bad data for the Groups sheet:
197+
198+ >>> browser.getLink('Manage').click()
199+ >>> browser.getLink('XLS Import').click()
200+ >>> import pkg_resources
201+ >>> browser.getControl('XLS File').add_file(
202+ ... pkg_resources.resource_stream('schooltool.export.ftests', 'errant_groups.xls'),
203+ ... 'application/excel',
204+ ... 'sample_data.xls')
205+ >>> browser.getControl('Submit').click()
206+ >>> browser.printQuery("//p[@class='error']")
207+ <p class="error">Groups B1 missing required text</p>
208+ <p class="error">Groups B2 missing required text</p>
209+ <p class="error">Groups B3 missing required text</p>
210+ <p class="error">Groups B6 not unicode or ascii string</p>
211+ <p class="error">Groups B7 not unicode or ascii string</p>
212+ <p class="error">Groups B8 not unicode or ascii string</p>
213+ <p class="error">Groups B9 not unicode or ascii string</p>
214+ <p class="error">Groups A11 invalid school year</p>
215+ <p class="error">Groups A22 not unicode or ascii string</p>
216+ <p class="error">Groups A23 is not a valid username</p>
217+
218
219=== added file 'src/schooltool/export/ftests/errant_groups.xls'
220Binary files src/schooltool/export/ftests/errant_groups.xls 1970-01-01 00:00:00 +0000 and src/schooltool/export/ftests/errant_groups.xls 2010-07-07 23:46:41 +0000 differ
221=== added file 'src/schooltool/export/ftests/errant_persons.xls'
222Binary files src/schooltool/export/ftests/errant_persons.xls 1970-01-01 00:00:00 +0000 and src/schooltool/export/ftests/errant_persons.xls 2010-07-07 23:46:41 +0000 differ
223=== added file 'src/schooltool/export/ftests/errant_resources.xls'
224Binary files src/schooltool/export/ftests/errant_resources.xls 1970-01-01 00:00:00 +0000 and src/schooltool/export/ftests/errant_resources.xls 2010-07-07 23:46:41 +0000 differ
225=== added file 'src/schooltool/export/ftests/errant_sections.xls'
226Binary files src/schooltool/export/ftests/errant_sections.xls 1970-01-01 00:00:00 +0000 and src/schooltool/export/ftests/errant_sections.xls 2010-07-07 23:46:41 +0000 differ
227=== added file 'src/schooltool/export/ftests/errant_terms.xls'
228Binary files src/schooltool/export/ftests/errant_terms.xls 1970-01-01 00:00:00 +0000 and src/schooltool/export/ftests/errant_terms.xls 2010-07-07 23:46:41 +0000 differ
229=== added file 'src/schooltool/export/ftests/errant_timetables.xls'
230Binary files src/schooltool/export/ftests/errant_timetables.xls 1970-01-01 00:00:00 +0000 and src/schooltool/export/ftests/errant_timetables.xls 2010-07-07 23:46:41 +0000 differ
231=== added file 'src/schooltool/export/ftests/errant_years.xls'
232Binary files src/schooltool/export/ftests/errant_years.xls 1970-01-01 00:00:00 +0000 and src/schooltool/export/ftests/errant_years.xls 2010-07-07 23:46:41 +0000 differ
233=== modified file 'src/schooltool/export/ftests/non_ascii.txt'
234--- src/schooltool/export/ftests/non_ascii.txt 2010-04-27 05:08:20 +0000
235+++ src/schooltool/export/ftests/non_ascii.txt 2010-07-07 23:46:41 +0000
236@@ -16,6 +16,6 @@
237
238 We should get an error:
239
240- >>> browser.printQuery('//div[@class="error"]')
241- <div class="error">Persons A3 Username cannot contain non-ascii characters: student00...</div>
242- <div class="error">Persons A13 Username cannot contain non-ascii characters: student01...</div>
243+ >>> browser.printQuery('//p[@class="error"]')
244+ <p class="error">Persons A3 Username cannot contain non-ascii characters: student00...</p>
245+ <p class="error">Persons A13 Username cannot contain non-ascii characters: student01...</p>
246
247=== modified file 'src/schooltool/export/ftests/test_all.py'
248--- src/schooltool/export/ftests/test_all.py 2009-04-18 17:26:02 +0000
249+++ src/schooltool/export/ftests/test_all.py 2010-07-07 23:46:41 +0000
250@@ -25,7 +25,7 @@
251 from schooltool.export.ftesting import export_functional_layer
252
253 def test_suite():
254- return collect_ftests(layer=export_functional_layer, level=2)
255+ return collect_ftests(layer=export_functional_layer)
256
257
258 if __name__ == '__main__':
259
260=== modified file 'src/schooltool/export/importer.py'
261--- src/schooltool/export/importer.py 2010-04-27 05:08:20 +0000
262+++ src/schooltool/export/importer.py 2010-07-07 23:46:41 +0000
263@@ -31,6 +31,7 @@
264 from schooltool.basicperson.interfaces import IDemographicsFields
265 from schooltool.basicperson.interfaces import IDemographics
266 from schooltool.basicperson.demographics import DateFieldDescription
267+from schooltool.basicperson.person import BasicPerson
268 from schooltool.resource.resource import Resource
269 from schooltool.resource.resource import Location
270 from schooltool.group.group import Group
271@@ -62,9 +63,41 @@
272 from schooltool.common import SchoolToolMessage as _
273
274
275+ERROR_NOT_INT = _('is not a valid integer')
276+ERROR_NOT_UNICODE_OR_ASCII = _('not unicode or ascii string')
277+ERROR_MISSING_REQUIRED_TEXT = _('missing required text')
278+ERROR_NO_DATE = _('has no date in it')
279+ERROR_END_BEFORE_START = _('end date cannot be before start date')
280+ERROR_START_OVERLAP = _('start date overlaps another year')
281+ERROR_END_OVERLAP = _('end date overlaps another year')
282+ERROR_INVALID_SCHOOL_YEAR = _('invalid school year')
283+ERROR_START_BEFORE_YEAR_START = _('start date before start of school year')
284+ERROR_END_AFTER_YEAR_END = _('end date after end of school year')
285+ERROR_START_OVERLAP_TERM = _('start date overlaps another term')
286+ERROR_END_OVERLAP_TERM = _('end date overlaps another term')
287+ERROR_HAS_NO_DAYS = _("%s has no days in A%s")
288+ERROR_HAS_NO_DAY_TEMPLATES = _("%s has no day templates in A%s")
289+ERROR_TIME_RANGE = _("is not a valid time range")
290+ERROR_TIMETABLE_MODEL = _("is not a valid timetable model")
291+ERROR_DUPLICATE_DAY_ID = _("is the same day id as another in this timetable")
292+ERROR_UNKNOWN_DAY_ID = _("is not defined in the 'Day Templates' section")
293+ERROR_RESOURCE_TYPE = _("must be either 'Location' or 'Resource'")
294+ERROR_INVALID_TERM_ID = _('is not a valid term in the given school year')
295+ERROR_INVALID_COURSE_ID = _('is not a valid course in the given school year')
296+ERROR_HAS_NO_COURSES = _('%s has no courses in A%s')
297+ERROR_INVALID_PERSON_ID = _('is not a valid username')
298+ERROR_INVALID_SCHEMA_ID = _('is not a valid schema in the given school year')
299+ERROR_INVALID_DAY_ID = _('is not a valid day id for the given schema')
300+ERROR_INVALID_PERIOD_ID = _('is not a valid period id for the given day')
301+ERROR_INVALID_RESOURCE_ID = _('is not a valid resource id')
302+ERROR_UNICODE_CONVERSION = _("Username cannot contain non-ascii characters: %s")
303+ERROR_WEEKLY_DAY_ID = _('is not a valid weekday number (0-6)')
304+
305+
306 no_date = object()
307 no_data = object()
308
309+
310 class ImporterBase(object):
311
312 def __init__(self, context, request):
313@@ -79,12 +112,68 @@
314 return default
315 raise
316
317- def error(self, col, row, message):
318+ def error(self, row, col, message):
319 self.errors.append("%s %s%s %s" % (self.sheet_name,
320 chr(col + ord('A')),
321- row,
322+ row + 1,
323 message))
324
325+ def getCellAndFound(self, sheet, row, col, default=u''):
326+ try:
327+ return sheet.cell_value(rowx=row, colx=col), True
328+ except:
329+ return default, False
330+
331+ def getIntFoundValid(self, sheet, row, col, default=0):
332+ value, found = self.getCellAndFound(sheet, row, col, default)
333+ valid = True
334+ if found:
335+ if isinstance(value, float):
336+ if int(value) != value:
337+ self.error(row, col, ERROR_NOT_INT)
338+ valid = False
339+ else:
340+ value = int(value)
341+ elif not isinstance(value, int):
342+ self.error(row, col, ERROR_NOT_INT)
343+ valid = False
344+ return value, found, valid
345+
346+ def getIntFromCell(self, sheet, row, col, default=0):
347+ value, found, valid = self.getIntFoundValid(sheet, row, col, default)
348+ return value
349+
350+ def getRequiredIntFromCell(self, sheet, row, col):
351+ value, found, valid = self.getIntFoundValid(sheet, row, col)
352+ if not found:
353+ self.error(row, col, ERROR_NOT_INT)
354+ return value
355+
356+ def getTextFoundValid(self, sheet, row, col, default=u''):
357+ value, found = self.getCellAndFound(sheet, row, col, default)
358+ valid = True
359+ if found:
360+ if isinstance(value, str):
361+ try:
362+ value = unicode(value)
363+ except UnicodeError:
364+ self.error(row, col, ERROR_NOT_UNICODE_OR_ASCII)
365+ valid = False
366+ elif not isinstance(value, unicode):
367+ self.error(row, col, ERROR_NOT_UNICODE_OR_ASCII)
368+ valid = False
369+ return value, found, valid
370+
371+ def getTextFromCell(self, sheet, row, col, default=u''):
372+ value, found, valid = self.getTextFoundValid(sheet, row, col, default)
373+ return value
374+
375+ def getRequiredTextFromCell(self, sheet, row, col):
376+ value, found, valid = self.getTextFoundValid(sheet, row, col)
377+ if valid and not value:
378+ self.error(row, col, ERROR_MISSING_REQUIRED_TEXT)
379+ return value
380+
381 def getDateFromCell(self, sheet, row, col, default=no_date):
382 try:
383 dt = xlrd.xldate_as_tuple(sheet.cell_value(rowx=row, colx=col), self.wb.datemode)
384@@ -92,8 +181,8 @@
385 if default is not no_date:
386 return default
387 else:
388- self.error(col, row + 1, "has no date in it!")
389- return datetime.datetime.utcnow().date()
390+ self.error(row, col, ERROR_NO_DATE)
391+ return None
392 return datetime.datetime(*dt).date()
393
394 @property
395@@ -132,16 +221,32 @@
396 sy.__name__ = SimpleNameChooser(syc).chooseName('', sy)
397 syc[sy.__name__] = sy
398
399+ def testOverlap(self, name, date):
400+ for sy in ISchoolYearContainer(self.context).values():
401+ if name == sy.__name__:
402+ continue
403+ if date >= sy.first and date <= sy.last:
404+ return True
405+ return False
406+
407 def process(self):
408 sh = self.sheet
409 for row in range(1, sh.nrows):
410+ num_errors = len(self.errors)
411 data = {}
412- data['title'] = sh.cell_value(rowx=row, colx=0)
413- data['__name__'] = sh.cell_value(rowx=row, colx=1)
414+ data['__name__'] = self.getRequiredTextFromCell(sh, row, 0)
415+ data['title'] = self.getRequiredTextFromCell(sh, row, 1)
416 data['first'] = self.getDateFromCell(sh, row, 2)
417 data['last'] = self.getDateFromCell(sh, row, 3)
418- sy = self.createSchoolYear(data)
419- self.addSchoolYear(sy, data)
420+ if data['last'] < data['first']:
421+ self.error(row, 3, ERROR_END_BEFORE_START)
422+ elif self.testOverlap(data['__name__'], data['first']):
423+ self.error(row, 2, ERROR_START_OVERLAP)
424+ elif self.testOverlap(data['__name__'], data['last']):
425+ self.error(row, 3, ERROR_END_OVERLAP)
426+ if num_errors == len(self.errors):
427+ sy = self.createSchoolYear(data)
428+ self.addSchoolYear(sy, data)
429
430
431 class TermImporter(ImporterBase):
432@@ -171,30 +276,72 @@
433 term.__name__ = SimpleNameChooser(sy).chooseName('', term)
434 sy[term.__name__] = term
435
436+ def testBeforeYearStart(self, sy, date):
437+ return date < ISchoolYearContainer(self.context)[sy].first
438+
439+ def testAfterYearEnd(self, sy, date):
440+ return date > ISchoolYearContainer(self.context)[sy].last
441+
442+ def testOverlap(self, sy, name, date):
443+ for trm in ISchoolYearContainer(self.context)[sy].values():
444+ if trm.__name__ == name:
445+ continue
446+ if date >= trm.first and date <= trm.last:
447+ return True
448+ return False
449+
450 def process(self):
451 sh = self.sheet
452
453 for row in range(1, sh.nrows):
454 if sh.cell_value(rowx=row, colx=0) == '':
455 break
456+
457+ num_errors = len(self.errors)
458 data = {}
459- data['school_year'] = sh.cell_value(rowx=row, colx=0)
460- data['__name__'] = sh.cell_value(rowx=row, colx=1)
461- data['title'] = sh.cell_value(rowx=row, colx=2)
462+ data['school_year'] = self.getRequiredTextFromCell(sh, row, 0)
463+ data['__name__'] = self.getRequiredTextFromCell(sh, row, 1)
464+ data['title'] = self.getRequiredTextFromCell(sh, row, 2)
465 data['first'] = self.getDateFromCell(sh, row, 3)
466 data['last'] = self.getDateFromCell(sh, row, 4)
467-
468- term = self.createTerm(data)
469- self.addTerm(term, data)
470+ if num_errors < len(self.errors):
471+ continue
472+
473+ if data['school_year'] not in ISchoolYearContainer(self.context):
474+ self.error(row, 0, ERROR_INVALID_SCHOOL_YEAR)
475+ continue
476+
477+ if data['last'] < data['first']:
478+ self.error(row, 4, ERROR_END_BEFORE_START)
479+ elif self.testBeforeYearStart(data['school_year'], data['first']):
480+ self.error(row, 3, ERROR_START_BEFORE_YEAR_START)
481+ elif self.testAfterYearEnd(data['school_year'], data['last']):
482+ self.error(row, 4, ERROR_END_AFTER_YEAR_END)
483+ elif self.testOverlap(data['school_year'], data['__name__'],
484+ data['first']):
485+ self.error(row, 3, ERROR_START_OVERLAP_TERM)
486+ elif self.testOverlap(data['school_year'], data['__name__'],
487+ data['last']):
488+ self.error(row, 4, ERROR_END_OVERLAP_TERM)
489+
490+ if num_errors == len(self.errors):
491+ term = self.createTerm(data)
492+ self.addTerm(term, data)
493
494 row += 1
495 if self.getCellValue(sh, row, 0, '') == 'Holidays':
496- row += 1
497 for row in range(row + 1, sh.nrows):
498 if sh.cell_value(rowx=row, colx=0) == '':
499 break
500- holiday_region = DateRange(self.getDateFromCell(sh, row, 0),
501- self.getDateFromCell(sh, row, 1))
502+ start = self.getDateFromCell(sh, row, 0)
503+ end = self.getDateFromCell(sh, row, 1)
504+ if not start or not end:
505+ continue
506+ elif end < start:
507+ self.error(row, 4, ERROR_END_BEFORE_START)
508+ continue
509+
510+ holiday_region = DateRange(start, end)
511 for day in holiday_region:
512 for sy in ISchoolYearContainer(self.context).values():
513 for term in sy.values():
514@@ -222,35 +369,21 @@
515 def setUpSchemaDays(self, timetable, days):
516 for day in days:
517 day_id = day['id']
518- period_ids = [period
519- for period in day['periods']]
520- if len(set(period_ids)) != len(period_ids):
521- self.errors.append("Duplicate periods in schema: %s" % period_ids)
522-
523- homeroom_periods = [period
524- for period in day['homeroom_periods']]
525-
526- timetable[day_id] = TimetableSchemaDay(period_ids, homeroom_periods)
527+ timetable[day_id] = TimetableSchemaDay(day['periods'],
528+ day['homeroom_periods'])
529
530 def createSchoolTimetable(self, data):
531+ title = data['title']
532+ factory = queryUtility(ITimetableModelFactory, data['model'])
533 day_ids = [day['id'] for day in data['days']]
534
535- factory_id = data['model']
536- title = data['title']
537-
538- factory = queryUtility(ITimetableModelFactory, factory_id)
539- if factory is None:
540- self.errors.append("Incorrect timetable model factory")
541-
542- templates = data['templates']
543 template_dict = {}
544
545- for template in templates:
546- day = SchooldayTemplate()
547-
548+ for template in data['templates']:
549 used = template['id']
550
551 # parse SchoolDayPeriods
552+ day = SchooldayTemplate()
553 for tstart, duration in template['periods']:
554 slot = SchooldaySlot(tstart, duration)
555 day.add(slot)
556@@ -268,9 +401,6 @@
557 model = factory(day_ids, template_dict)
558
559 # create and set up the timetable
560- if len(set(day_ids)) != len(day_ids):
561- self.errors.append("Duplicate days in schema")
562-
563 timetable = TimetableSchema(day_ids, title=title, model=model)
564 self.setUpSchemaDays(timetable, data['days'])
565 timetable.__name__ = data['__name__']
566@@ -286,13 +416,34 @@
567 if school_timetable.__name__ not in sc:
568 sc[school_timetable.__name__] = school_timetable
569
570+ def getWeeklyDayId(self, sh, row, col):
571+ if sh.cell_value(rowx=row, colx=col) == 'default':
572+ day_id = 'default'
573+ else:
574+ day_id, found, valid = self.getIntFoundValid(sh, row, col)
575+ if valid and day_id not in range(7):
576+ self.error(row, 0, ERROR_WEEKLY_DAY_ID)
577+ return day_id
578+
579 def import_school_timetable(self, sh, row):
580+ num_errors = len(self.errors)
581 data = {}
582- data['title'] = sh.cell_value(rowx=row, colx=1)
583- data['__name__'] = sh.cell_value(rowx=row+1, colx=1)
584- data['school_year'] = sh.cell_value(rowx=row+2, colx=1)
585- data['model'] = sh.cell_value(rowx=row+3, colx=1)
586-
587+ data['title'] = self.getRequiredTextFromCell(sh, row, 1)
588+ data['__name__'] = self.getRequiredTextFromCell(sh, row+1, 1)
589+ data['school_year'] = self.getRequiredTextFromCell(sh, row+2, 1)
590+ data['model'] = self.getRequiredTextFromCell(sh, row+3, 1)
591+ if num_errors < len(self.errors):
592+ return
593+
594+ num_errors = len(self.errors)
595+ if data['school_year'] not in ISchoolYearContainer(self.context):
596+ self.error(row + 1, 1, ERROR_INVALID_SCHOOL_YEAR)
597+ if queryUtility(ITimetableModelFactory, data['model']) is None:
598+ self.error(row + 2, 1, ERROR_TIMETABLE_MODEL)
599+ if num_errors < len(self.errors):
600+ return
601+
602+ num_errors = len(self.errors)
603 row += 5
604 if self.getCellValue(sh, row, 0, '') == 'Day Templates':
605 data['templates'] = []
606@@ -300,15 +451,32 @@
607 for row in range(row, sh.nrows):
608 if sh.cell_value(rowx=row, colx=0) == '':
609 break
610- day_id = sh.cell_value(rowx=row, colx=0)
611+
612+ if data['model'] == 'WeeklyTimetableModel':
613+ day_id = self.getWeeklyDayId(sh, row, 0)
614+ else:
615+ day_id = self.getRequiredTextFromCell(sh, row, 0)
616+
617+ if day_id in [day['id'] for day in data['templates']]:
618+ self.error(row, 0, ERROR_DUPLICATE_DAY_ID)
619+
620 periods = []
621 for col in range(1, sh.ncols):
622- if sh.cell_value(rowx=row, colx=col) == '':
623+ cell = sh.cell_value(rowx=row, colx=col)
624+ if cell == '':
625 break
626- periods.append(parse_time_range(sh.cell_value(rowx=row, colx=col)))
627+ try:
628+ time_range = parse_time_range(cell)
629+ except:
630+ self.error(row, col, ERROR_TIME_RANGE)
631+ continue
632+ periods.append(time_range)
633 data['templates'].append({'id': day_id, 'periods': periods})
634 else:
635- self.errors.append("%s has no day templates in A%s" % (data['title'], row + 1))
636+ self.errors.append(ERROR_HAS_NO_DAY_TEMPLATES % (data['title'],
637+ row + 1))
638+ if num_errors < len(self.errors):
639+ return
640
641 row += 1
642 if self.getCellValue(sh, row, 0, '') == 'Days':
643@@ -322,34 +490,50 @@
644 for row in range(row, sh.nrows):
645 if sh.cell_value(rowx=row, colx=0) == '':
646 break
647- day_id = sh.cell_value(rowx=row, colx=0)
648+
649+ if data['model'] == 'WeeklyTimetableModel':
650+ day_id = self.getWeeklyDayId(sh, row, 0)
651+ else:
652+ day_id = self.getRequiredTextFromCell(sh, row, 0)
653+
654+ if day_id in [day['id'] for day in data['days']]:
655+ self.error(row, 0, ERROR_DUPLICATE_DAY_ID)
656+ if day_id not in [day['id'] for day in data['templates']]:
657+ self.error(row, 0, ERROR_UNKNOWN_DAY_ID)
658+
659 periods = []
660 for col in range(1, homeroom_start):
661- if sh.cell_value(rowx=row, colx=col) == '':
662+ cell = sh.cell_value(rowx=row, colx=col)
663+ if cell == '':
664 break
665- periods.append(sh.cell_value(rowx=row, colx=col))
666+ if cell in periods:
667+ self.error(row, col, ERROR_DUPLICATE_PERIOD)
668+ else:
669+ periods.append(cell)
670 homeroom_periods = []
671 for col in range(homeroom_start, sh.ncols):
672- if sh.cell_value(rowx=row, colx=col) == '':
673+ cell = sh.cell_value(rowx=row, colx=col)
674+ if cell == '':
675 break
676- homeroom_periods.append(sh.cell_value(rowx=row, colx=col))
677+ if cell in homeroom_periods:
678+ self.error(row, col, ERROR_DUPLICATE_HOMEROOM_PERIOD)
679+ else:
680+ homeroom_periods.append(cell)
681 data['days'].append({'id': day_id,
682 'periods': periods,
683 'homeroom_periods': homeroom_periods})
684 else:
685- self.errors.append("%s has no days in A%s" % (data['title'], row + 1))
686+ self.errors.append(ERROR_HAS_NO_DAYS % (data['title'], row + 1))
687
688 if not self.errors:
689 school_timetable = self.createSchoolTimetable(data)
690 self.addSchoolTimetable(school_timetable, data)
691
692- return row
693-
694 def process(self):
695 sh = self.sheet
696 for row in range(0, sh.nrows):
697 if sh.cell_value(rowx=row, colx=0) == 'School Timetable':
698- row = self.import_school_timetable(sh, row)
699+ self.import_school_timetable(sh, row)
700
701
702 class ResourceImporter(ImporterBase):
703@@ -379,10 +563,16 @@
704 for row in range(1, sh.nrows):
705 if sh.cell_value(rowx=row, colx=0) == '':
706 break
707+ num_errors = len(self.errors)
708 data = {}
709- data['__name__'] = sh.cell_value(rowx=row, colx=0)
710- data['type'] = sh.cell_value(rowx=row, colx=1)
711- data['title'] = sh.cell_value(rowx=row, colx=2)
712+ data['__name__'] = self.getRequiredTextFromCell(sh, row, 0)
713+ data['type'] = self.getRequiredTextFromCell(sh, row, 1)
714+ data['title'] = self.getRequiredTextFromCell(sh, row, 2)
715+ if num_errors < len(self.errors):
716+ continue
717+ if data['type'] not in ['Location', 'Resource']:
718+ self.error(row, 1, ERROR_RESOURCE_TYPE)
719+ continue
720 resource = self.createResource(data)
721 self.addResource(resource, data)
722
723@@ -404,7 +594,6 @@
724 person.setPassword(data['password'])
725
726 def createPerson(self, data):
727- from schooltool.basicperson.person import BasicPerson
728 person = BasicPerson(data['__name__'],
729 data['first_name'],
730 data['last_name'])
731@@ -426,40 +615,46 @@
732 for row in range(1, sh.nrows):
733 if sh.cell_value(rowx=row, colx=0) == '':
734 break
735+
736+ num_errors = len(self.errors)
737 data = {}
738- data['__name__'] = sh.cell_value(rowx=row, colx=0)
739- data['prefix'] = sh.cell_value(rowx=row, colx=1)
740- data['first_name'] = sh.cell_value(rowx=row, colx=2)
741- data['middle_name'] = sh.cell_value(rowx=row, colx=3)
742- data['last_name'] = sh.cell_value(rowx=row, colx=4)
743- data['suffix'] = sh.cell_value(rowx=row, colx=5)
744- data['preferred_name'] = sh.cell_value(rowx=row, colx=6)
745- data['birth_date'] = self.getDateFromCell(sh, row, 7, default=None)
746- data['gender'] = sh.cell_value(rowx=row, colx=8)
747+ data['__name__'] = self.getRequiredTextFromCell(sh, row, 0)
748+ data['prefix'] = self.getTextFromCell(sh, row, 1)
749+ data['first_name'] = self.getRequiredTextFromCell(sh, row, 2)
750+ data['middle_name'] = self.getTextFromCell(sh, row, 3)
751+ data['last_name'] = self.getRequiredTextFromCell(sh, row, 4)
752+ data['suffix'] = self.getTextFromCell(sh, row, 5)
753+ data['preferred_name'] = self.getTextFromCell(sh, row, 6)
754+ data['birth_date'] = self.getTextFromCell(sh, row, 7, default=None)
755+ data['gender'] = self.getTextFromCell(sh, row, 8)
756 if data['gender'] == '':
757 data['gender'] = None
758- data['password'] = sh.cell_value(rowx=row, colx=9)
759+ data['password'] = self.getTextFromCell(sh, row, 9)
760
761 # XXX: this has to be fixed
762 # XXX: SchoolTool should handle UTF-8
763 try:
764 data['__name__'].encode('ascii')
765 except UnicodeEncodeError:
766- self.error(0, row+1, "Username cannot contain non-ascii characters: %s" % data['__name__'])
767+ self.error(row, 0, ERROR_UNICODE_CONVERSION % data['__name__'])
768
769- person = self.createPerson(data)
770+ if num_errors == len(self.errors):
771+ person = self.createPerson(data)
772+ else:
773+ person = BasicPerson('name', 'first_name', 'last_name')
774
775 demographics = IDemographics(person)
776 for n, field in enumerate(fields.values()):
777 if isinstance(field, DateFieldDescription):
778 value = self.getDateFromCell(sh, row, n + 10, default=None)
779 else:
780- value = sh.cell_value(rowx=row, colx=n + 10)
781+ value = self.getTextFromCell(sh, row, n + 10)
782 if value == '':
783 value = None
784 demographics[field.name] = value
785
786- self.addPerson(person, data)
787+ if num_errors == len(self.errors):
788+ self.addPerson(person, data)
789
790
791 class CourseImporter(ImporterBase):
792@@ -493,11 +688,18 @@
793 for row in range(1, sh.nrows):
794 if sh.cell_value(rowx=row, colx=0) == '':
795 break
796+ num_errors = len(self.errors)
797 data = {}
798- data['school_year'] = sh.cell_value(rowx=row, colx=0)
799- data['__name__'] = sh.cell_value(rowx=row, colx=1)
800- data['title'] = sh.cell_value(rowx=row, colx=2)
801- data['description'] = sh.cell_value(rowx=row, colx=3)
802+ data['school_year'] = self.getRequiredTextFromCell(sh, row, 0)
803+ data['__name__'] = self.getRequiredTextFromCell(sh, row, 1)
804+ data['title'] = self.getRequiredTextFromCell(sh, row, 2)
805+ data['description'] = self.getTextFromCell(sh, row, 3)
806+ if num_errors < len(self.errors):
807+ continue
808+ if data['school_year'] not in ISchoolYearContainer(self.context):
809+ self.error(row, 0, ERROR_INVALID_SCHOOL_YEAR)
810+ if num_errors < len(self.errors):
811+ continue
812 course = self.createCourse(data)
813 self.addCourse(course, data)
814
815@@ -524,29 +726,49 @@
816 sc[section.__name__] = section
817
818 def import_timetable(self, sh, row, section):
819- schema_id = sh.cell_value(rowx=row, colx=1)
820- schema = ITimetableSchemaContainer(ISchoolYear(section))[schema_id]
821- course = list(section.courses)[0]
822+ schemas = ITimetableSchemaContainer(ISchoolYear(section))
823+ schema_id = self.getRequiredTextFromCell(sh, row, 1)
824+ if schema_id not in schemas:
825+ self.error(row, 0, ERROR_INVALID_SCHEMA_ID)
826+ return
827+ schema = schemas[schema_id]
828 timetable = schema.createTimetable(ITerm(section))
829 timetables = ITimetables(section).timetables
830 timetables[schema_id] = timetable
831+
832 row += 1
833+ course = list(section.courses)[0]
834+ rc = self.context['resources']
835 resources = {}
836 for row in range(row + 1, sh.nrows):
837 if sh.cell_value(rowx=row, colx=0) == '':
838 break
839- day_id = sh.cell_value(rowx=row, colx=0)
840- period_id = sh.cell_value(rowx=row, colx=1)
841- resources[day_id, period_id] = sh.cell_value(rowx=row, colx=2)
842+
843+ num_errors = len(self.errors)
844+ day_id = self.getRequiredTextFromCell(sh, row, 0)
845+ period_id = self.getRequiredTextFromCell(sh, row, 1)
846+ resource_id = self.getTextFromCell(sh, row, 2)
847+ if num_errors < len(self.errors):
848+ continue
849+
850+ if day_id not in schema.day_ids:
851+ self.error(row, 0, ERROR_INVALID_DAY_ID)
852+ elif period_id not in schema[day_id].periods:
853+ self.error(row, 1, ERROR_INVALID_PERIOD_ID)
854+ if resource_id and resource_id not in rc:
855+ self.error(row, 2, ERROR_INVALID_RESOURCE_ID)
856+ if num_errors < len(self.errors):
857+ continue
858+
859+ resources[day_id, period_id] = resource_id
860 act = TimetableActivity(title=course.title, owner=section)
861 timetable[day_id].add(period_id, act)
862- rc = self.context['resources']
863+
864 for event in ISchoolToolCalendar(section):
865 if ITimetableCalendarEvent.providedBy(event):
866 resource_id = resources[event.day_id, event.period_id]
867 resource = rc[resource_id]
868 event.bookResource(removeSecurityProxy(resource))
869- return row
870
871 def import_timetables(self, sh, row, section):
872 for row in range(row, sh.nrows):
873@@ -558,11 +780,13 @@
874
875 def import_section(self, sh, row, year, term):
876 data = {}
877- data['title'] = sh.cell_value(rowx=row, colx=1)
878- data['__name__'] = sh.cell_value(rowx=row+1, colx=1)
879- data['description'] = sh.cell_value(rowx=row+4, colx=1)
880+ data['title'] = self.getRequiredTextFromCell(sh, row, 1)
881+ data['__name__'] = self.getRequiredTextFromCell(sh, row+1, 1)
882+ data['description'] = self.getTextFromCell(sh, row+2, 1)
883+
884 section = self.createSection(data, year, term)
885 self.addSection(section, data, year, term)
886+ courses = ICourseContainer(section)
887
888 row += 4
889 if sh.cell_value(rowx=row, colx=0) == 'Courses':
890@@ -570,23 +794,39 @@
891 for row in range(row, sh.nrows):
892 if sh.cell_value(rowx=row, colx=0) == '':
893 break
894- course_id = sh.cell_value(rowx=row, colx=0)
895- course = ICourseContainer(section)[course_id]
896+ num_errors = len(self.errors)
897+
898+ course_id = self.getRequiredTextFromCell(sh, row, 0)
899+ if num_errors < len(self.errors):
900+ continue
901+ if course_id not in courses:
902+ self.error(row, 0, ERROR_INVALID_COURSE_ID)
903+ continue
904+ course = courses[course_id]
905+
906 if course not in section.courses:
907 section.courses.add(removeSecurityProxy(course))
908 else:
909- self.errors.append("%s has no courses in A%s" % (data['title'], row + 1))
910- return row
911+ self.errors.append(ERROR_HAS_NO_COURSES % (data['title'], row + 1))
912+ return
913
914 row += 1
915- pc = self.context['persons']
916+ persons = self.context['persons']
917 if sh.cell_value(rowx=row, colx=0) == 'Students':
918 row += 1
919 for row in range(row, sh.nrows):
920 if sh.cell_value(rowx=row, colx=0) == '':
921 break
922- username = sh.cell_value(rowx=row, colx=0)
923- member = pc[username]
924+ num_errors = len(self.errors)
925+
926+ username = self.getRequiredTextFromCell(sh, row, 0)
927+ if num_errors < len(self.errors):
928+ continue
929+ if username not in persons:
930+ self.error(row, 0, ERROR_INVALID_PERSON_ID)
931+ continue
932+ member = persons[username]
933+
934 if member not in section.members:
935 section.members.add(removeSecurityProxy(member))
936
937@@ -596,33 +836,48 @@
938 for row in range(row, sh.nrows):
939 if sh.cell_value(rowx=row, colx=0) == '':
940 break
941- username = sh.cell_value(rowx=row, colx=0)
942- instructor = pc[username]
943+ num_errors = len(self.errors)
944+
945+ username = self.getRequiredTextFromCell(sh, row, 0)
946+ if num_errors < len(self.errors):
947+ continue
948+ if username not in persons:
949+ self.error(row, 0, ERROR_INVALID_PERSON_ID)
950+ continue
951+ instructor = persons[username]
952+
953 if instructor not in section.instructors:
954 section.instructors.add(removeSecurityProxy(instructor))
955
956 row += 1
957 if sh.cell_value(rowx=row, colx=0) == 'School Timetable':
958- row = self.import_timetables(sh, row, section)
959-
960- return row
961+ self.import_timetables(sh, row, section)
962
963 def process(self):
964 app = ISchoolToolApplication(None)
965 schoolyears = ISchoolYearContainer(app)
966- for sheet_name in self.sheet_names:
967- sheet = self.wb.sheet_by_name(sheet_name)
968- if sheet.cell_value(rowx=0, colx=0) != 'School Year':
969- continue
970-
971- year_id = sheet.cell_value(rowx=0, colx=1)
972+ for self.sheet_name in self.sheet_names:
973+ sheet = self.wb.sheet_by_name(self.sheet_name)
974+
975+ num_errors = len(self.errors)
976+ year_id = self.getRequiredTextFromCell(sheet, 0, 1)
977+ term_id = self.getRequiredTextFromCell(sheet, 0, 3)
978+ if num_errors < len(self.errors):
979+ continue
980+
981+ if year_id not in schoolyears:
982+ self.error(0, 1, ERROR_INVALID_SCHOOL_YEAR)
983+ continue
984 year = schoolyears[year_id]
985- term_id = sheet.cell_value(rowx=0, colx=3)
986+
987+ if term_id not in year:
988+ self.error(0, 3, ERROR_INVALID_TERM_ID)
989+ continue
990 term = year[term_id]
991
992 for row in range(2, sheet.nrows):
993 if sheet.cell_value(rowx=row, colx=0) == 'Section Title':
994- row = self.import_section(sheet, row, year, term)
995+ self.import_section(sheet, row, year, term)
996
997 def import_data(self, wb):
998 self.wb = wb
999@@ -631,7 +886,7 @@
1000 if sheet_name.startswith('Section'):
1001 self.sheet_names.append(sheet_name)
1002 if self.sheet_names:
1003- return self.process()
1004+ self.process()
1005
1006
1007 class GroupImporter(ImporterBase):
1008@@ -660,11 +915,18 @@
1009 gc[group.__name__] = group
1010
1011 def import_group(self, sh, row):
1012+ num_errors = len(self.errors)
1013 data = {}
1014- data['title'] = sh.cell_value(rowx=row, colx=1)
1015- data['__name__'] = sh.cell_value(rowx=row+1, colx=1)
1016- data['school_year'] = sh.cell_value(rowx=row+2, colx=1)
1017- data['description'] = sh.cell_value(rowx=row+3, colx=1)
1018+ data['title'] = self.getRequiredTextFromCell(sh, row, 1)
1019+ data['__name__'] = self.getRequiredTextFromCell(sh, row+1, 1)
1020+ data['school_year'] = self.getRequiredTextFromCell(sh, row+2, 1)
1021+ data['description'] = self.getTextFromCell(sh, row+3, 1)
1022+ if num_errors < len(self.errors):
1023+ return
1024+ if data['school_year'] not in ISchoolYearContainer(self.context):
1025+ self.error(row, 0, ERROR_INVALID_SCHOOL_YEAR)
1026+ return
1027+
1028 group = self.createGroup(data)
1029 self.addGroup(group, data)
1030
1031@@ -675,17 +937,22 @@
1032 for row in range(row, sh.nrows):
1033 if sh.cell_value(rowx=row, colx=0) == '':
1034 break
1035- username = sh.cell_value(rowx=row, colx=0)
1036+ num_errors = len(self.errors)
1037+ username = self.getRequiredTextFromCell(sh, row, 0)
1038+ if num_errors < len(self.errors):
1039+ continue
1040+ if username not in pc:
1041+ self.error(row, 0, ERROR_INVALID_PERSON_ID)
1042+ continue
1043 member = pc[username]
1044 if member not in group.members:
1045 group.members.add(removeSecurityProxy(member))
1046- return row
1047
1048 def process(self):
1049 sh = self.sheet
1050 for row in range(0, sh.nrows):
1051 if sh.cell_value(rowx=row, colx=0) == 'Group Title':
1052- row = self.import_group(sh, row)
1053+ self.import_group(sh, row)
1054
1055
1056 class MegaImporter(BrowserView):
1057@@ -731,3 +998,7 @@
1058
1059 if self.errors:
1060 sp.rollback()
1061+
1062+ def displayErrors(self):
1063+ return self.errors[:20]
1064+
1065
1066=== modified file 'src/schooltool/export/templates/import.pt'
1067--- src/schooltool/export/templates/import.pt 2009-11-28 07:47:57 +0000
1068+++ src/schooltool/export/templates/import.pt 2010-07-07 23:46:41 +0000
1069@@ -17,14 +17,14 @@
1070 <div class="info" tal:condition="view/success">
1071 <p tal:repeat="msg view/success" tal:content="msg" />
1072 </div>
1073- <div class="error" tal:condition="view/errors"
1074- tal:repeat="err view/errors" tal:content="err" />
1075-
1076 <form method="POST" enctype="multipart/form-data" class="standalone"
1077 tal:attributes="action request/URL">
1078 <h3 i18n:translate="">
1079 Import Data
1080 </h3>
1081+ <p class="error" tal:condition="view/errors"
1082+ tal:repeat="err view/displayErrors" tal:content="err" />
1083+
1084 <p i18n:translate="">
1085 In addition to entering data through SchoolTool's web interface, you
1086 can also import data from a spreadsheet using this template:
1087
1088=== modified file 'src/schooltool/testing/analyze.py'
1089--- src/schooltool/testing/analyze.py 2009-02-08 02:01:07 +0000
1090+++ src/schooltool/testing/analyze.py 2010-07-07 23:46:41 +0000
1091@@ -41,5 +41,6 @@
1092 def printQuery(xpath, response):
1093 """Helper function to print xpath query results on an html response"""
1094 for result in queryHTML(xpath, response):
1095- print result
1096+ if result.strip():
1097+ print result
1098
1099
1100=== modified file 'src/schooltool/testing/functional.py'
1101--- src/schooltool/testing/functional.py 2010-04-27 08:01:02 +0000
1102+++ src/schooltool/testing/functional.py 2010-07-07 23:46:41 +0000
1103@@ -111,7 +111,8 @@
1104
1105 def printQuery(self, query):
1106 for item in queryHTML(query, self.contents):
1107- print item
1108+ if item.strip():
1109+ print item.strip()
1110
1111
1112 def collect_ftests(package=None, level=None, layer=None, filenames=None):
1113
1114=== modified file 'src/schooltool/timetable/__init__.py'
1115--- src/schooltool/timetable/__init__.py 2010-04-27 11:53:52 +0000
1116+++ src/schooltool/timetable/__init__.py 2010-07-07 23:46:41 +0000
1117@@ -180,6 +180,7 @@
1118 __parent__ = None
1119
1120 timezone = 'UTC'
1121+ consecutive_periods_as_one = False
1122
1123 @property
1124 def title(self):
1125
1126=== modified file 'src/schooltool/timetable/browser/__init__.py'
1127--- src/schooltool/timetable/browser/__init__.py 2010-06-07 12:43:25 +0000
1128+++ src/schooltool/timetable/browser/__init__.py 2010-07-07 23:46:41 +0000
1129@@ -484,6 +484,10 @@
1130
1131 return list(days(ttschema))
1132
1133+ @property
1134+ def consecutive_label(self):
1135+ return _('Show consecutive periods as one period in journal')
1136+
1137 def __call__(self):
1138 self.has_timetables = bool(self.ttschemas)
1139 if not self.has_timetables:
1140@@ -495,16 +499,24 @@
1141 #XXX dumb, this doesn't space course names
1142 course_title = ''.join([course.title
1143 for course in self.context.courses])
1144+ section = removeSecurityProxy(self.context)
1145+ timetable = ITimetables(section).lookup(self.term, self.ttschema)
1146+ if timetable is None:
1147+ self.consecutive_value = False
1148+ else:
1149+ self.consecutive_value = timetable.consecutive_periods_as_one
1150
1151 if 'CANCEL' in self.request:
1152 self.request.response.redirect(self.nextURL())
1153
1154 if 'SAVE' in self.request:
1155- section = removeSecurityProxy(self.context)
1156- timetable = ITimetables(section).lookup(self.term, self.ttschema)
1157 if timetable is None:
1158 timetable = self.ttschema.createTimetable(self.term)
1159 self.addTimetable(timetable)
1160+ if self.request.get('consecutive') == 'on':
1161+ timetable.consecutive_periods_as_one = True
1162+ else:
1163+ timetable.consecutive_periods_as_one = False
1164
1165 for day_id, day in timetable.items():
1166 for period_id, period in list(day.items()):
1167
1168=== modified file 'src/schooltool/timetable/browser/templates/section-timetable-setup.pt'
1169--- src/schooltool/timetable/browser/templates/section-timetable-setup.pt 2008-09-04 12:25:48 +0000
1170+++ src/schooltool/timetable/browser/templates/section-timetable-setup.pt 2010-07-07 23:46:41 +0000
1171@@ -78,6 +78,11 @@
1172 </table>
1173
1174 <div class="controls">
1175+ <input type="checkbox" id="consecutive" name="consecutive"
1176+ tal:attributes="checked view/consecutive_value " />
1177+ <span tal:content="view/consecutive_label" />
1178+ </div>
1179+ <div class="controls">
1180 <input class="button-ok" type="submit" name="SAVE" value="Save"
1181 i18n:attributes="value" />
1182 <input type="submit" class="button-cancel" name="CANCEL" value="Cancel"
1183
1184=== modified file 'src/schooltool/timetable/browser/tests/test_timetable.py'
1185--- src/schooltool/timetable/browser/tests/test_timetable.py 2010-01-10 17:31:54 +0000
1186+++ src/schooltool/timetable/browser/tests/test_timetable.py 2010-07-07 23:46:41 +0000
1187@@ -361,6 +361,37 @@
1188 >>> ITimetables(math).timetables['1']['Tue']['9:00']
1189 set([])
1190
1191+ Until now we have never checked the checkbox that corresponds to the
1192+ consecutive_periods_as_one attribute of the timetable, used in the
1193+ journal to compact consecutive periods into one column.
1194+
1195+ >>> ITimetables(math).timetables['1'].consecutive_periods_as_one
1196+ False
1197+
1198+ We'll set that option in the form and see that it is reflected in
1199+ the timetable.
1200+
1201+ >>> request = TestRequest(form={'ttschema': 'default',
1202+ ... 'term': '2005-fall',
1203+ ... 'consecutive':'on',
1204+ ... 'SAVE': 'Save'})
1205+ >>> view = SectionTimetableSetupView(context, request)
1206+ >>> result = view()
1207+
1208+ >>> ITimetables(math).timetables['1'].consecutive_periods_as_one
1209+ True
1210+
1211+ Not checking the checkbox will set the value back to False.
1212+
1213+ >>> request = TestRequest(form={'ttschema': 'default',
1214+ ... 'term': '2005-fall',
1215+ ... 'SAVE': 'Save'})
1216+ >>> view = SectionTimetableSetupView(context, request)
1217+ >>> result = view()
1218+
1219+ >>> ITimetables(math).timetables['1'].consecutive_periods_as_one
1220+ False
1221+
1222 """
1223
1224
1225
1226=== modified file 'src/schooltool/timetable/interfaces.py'
1227--- src/schooltool/timetable/interfaces.py 2010-01-19 12:09:22 +0000
1228+++ src/schooltool/timetable/interfaces.py 2010-07-07 23:46:41 +0000
1229@@ -25,7 +25,7 @@
1230 from zope.interface import Interface, Attribute, implements
1231 from zope.schema import Field, Object, Int, TextLine, List, Tuple
1232 from zope.schema import Dict, Date, Timedelta
1233-from zope.schema import Iterable
1234+from zope.schema import Iterable, Bool
1235 from zope.schema.interfaces import IField
1236 from zope.annotation.interfaces import IAnnotatable
1237 from zope.container.constraints import contains, containers
1238@@ -421,6 +421,10 @@
1239
1240 timezone = TextLine(title=u"The name of a timezone of this timetable")
1241
1242+ consecutive_periods_as_one = Bool(
1243+ title=u"Consecutive periods appear as one column in the journal",
1244+ required=False)
1245+
1246 term = Object(
1247 title=u"The term this timetable is for.",
1248 schema=ITerm)

Subscribers

People subscribed via source and target branches