Merge lp:~aelkner/schooltool/importer_fixes into lp:schooltool/1.7
- importer_fixes
- Merge into 1.7
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gediminas Paulauskas (community) | Approve | ||
Review via email: mp+29430@code.launchpad.net |
Commit message
Description of the change
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.
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.
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.
Gediminas Paulauskas (menesis) wrote : | # |
This branch was merged, and Justas has added another checkbox to the new timetable edit view.
Preview Diff
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' |
21 | Binary 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' |
220 | Binary 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' |
222 | Binary 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' |
224 | Binary 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' |
226 | Binary 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' |
228 | Binary 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' |
230 | Binary 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' |
232 | Binary 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) |
Very good work!