Merge lp:~salgado/launchpad/remove-crap into lp:launchpad
- remove-crap
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Jonathan Lange |
Approved revision: | no longer in the source branch. |
Merged at revision: | not available |
Proposed branch: | lp:~salgado/launchpad/remove-crap |
Merge into: | lp:launchpad |
Diff against target: |
3508 lines (+0/-3401) 17 files modified
lib/canonical/launchpad/components/cal.py (+0/-89) lib/canonical/launchpad/components/crowd.py (+0/-80) lib/canonical/launchpad/doc/crowd.txt (+0/-124) lib/canonical/launchpad/interfaces/launchpad.py (+0/-20) lib/canonical/launchpad/zcml/configure.zcml (+0/-1) lib/canonical/launchpad/zcml/crowd.zcml (+0/-17) lib/schoolbell/README.txt (+0/-7) lib/schoolbell/__init__.py (+0/-38) lib/schoolbell/browser.py (+0/-71) lib/schoolbell/icalendar.py (+0/-1127) lib/schoolbell/interfaces.py (+0/-396) lib/schoolbell/mixins.py (+0/-314) lib/schoolbell/simple.py (+0/-96) lib/schoolbell/tests/__init__.py (+0/-1) lib/schoolbell/tests/test_icalendar.py (+0/-782) lib/schoolbell/tests/test_schoolbell.py (+0/-74) lib/schoolbell/utils.py (+0/-164) |
To merge this branch: | bzr merge lp:~salgado/launchpad/remove-crap |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jonathan Lange (community) | Approve | ||
Review via email: mp+21549@code.launchpad.net |
Commit message
Description of the change
Just remove some crap that was no longer used:
lib/schoolbell/
lib/canonical
lib/canonical
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files:
lib/canonical
lib/canonical
== Pyflakes notices ==
lib/canonical/
25: 'UnexpectedForm
25: 'UnsafeFormGetS
25: 'NotFoundError' imported but unused
25: 'IBasicLaunchpa
25: 'IOpenLaunchBag' imported but unused
25: 'ILaunchpadRoot' imported but unused
25: 'ILaunchBag' imported but unused
== Pylint notices ==
lib/canonical/
17: [F0401] Unable to import 'lazr.restful.
Preview Diff
1 | === removed file 'lib/canonical/launchpad/components/cal.py' |
2 | --- lib/canonical/launchpad/components/cal.py 2009-07-17 00:26:05 +0000 |
3 | +++ lib/canonical/launchpad/components/cal.py 1970-01-01 00:00:00 +0000 |
4 | @@ -1,89 +0,0 @@ |
5 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
6 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
7 | - |
8 | -""" |
9 | -Calendaring for Launchpad |
10 | - |
11 | -This package contains various components that don't fit into database/ |
12 | -or browser/. |
13 | -""" |
14 | - |
15 | -__metaclass__ = type |
16 | - |
17 | -from zope.interface import implements |
18 | -from zope.component import getUtility |
19 | - |
20 | -from canonical.launchpad import _ |
21 | -from schoolbell.interfaces import ICalendar |
22 | -from canonical.launchpad.interfaces import ( |
23 | - ILaunchBag, ILaunchpadCalendar, ILaunchpadMergedCalendar, |
24 | - ICalendarSubscriptionSubset) |
25 | - |
26 | -from schoolbell.mixins import CalendarMixin, EditableCalendarMixin |
27 | -from schoolbell.icalendar import convert_calendar_to_ical |
28 | - |
29 | - |
30 | -def calendarFromCalendarOwner(calendarowner): |
31 | - """Adapt ICalendarOwner to ICalendar.""" |
32 | - return calendarowner.calendar |
33 | - |
34 | - |
35 | -############# Merged Calendar ############# |
36 | - |
37 | - |
38 | -class MergedCalendar(CalendarMixin, EditableCalendarMixin): |
39 | - implements(ILaunchpadCalendar, ILaunchpadMergedCalendar) |
40 | - |
41 | - def __init__(self): |
42 | - self.id = None |
43 | - self.revision = 0 |
44 | - self.owner = getUtility(ILaunchBag).user |
45 | - if self.owner is None: |
46 | - # The merged calendar can not be accessed when the user is |
47 | - # not logged in. However this object still needs to be |
48 | - # instantiable when not logged in, so that the user gets |
49 | - # redirected to the login page when trying to access the |
50 | - # calendar, rather than seeing an error page. |
51 | - return |
52 | - self.subscriptions = ICalendarSubscriptionSubset(self.owner) |
53 | - self.title = _('Merged Calendar for %s') % self.owner.displayname |
54 | - |
55 | - def __iter__(self): |
56 | - for calendar in self.subscriptions: |
57 | - for event in calendar: |
58 | - yield event |
59 | - |
60 | - def expand(self, first, last): |
61 | - for calendar in self.subscriptions: |
62 | - for event in calendar.expand(first, last): |
63 | - yield event |
64 | - |
65 | - def addEvent(self, event): |
66 | - calendar = self.owner.getOrCreateCalendar() |
67 | - calendar.addEvent(event) |
68 | - |
69 | - def removeEvent(self, event): |
70 | - calendar = event.calendar |
71 | - calendar.removeEvent(event) |
72 | - |
73 | - |
74 | -############# iCalendar export ################### |
75 | - |
76 | -class ViewICalendar: |
77 | - """Publish an object implementing the ICalendar interface in |
78 | - the iCalendar format. This allows desktop calendar clients to |
79 | - display the events.""" |
80 | - __used_for__ = ICalendar |
81 | - |
82 | - def __init__(self, context, request): |
83 | - self.context = context |
84 | - self.request = request |
85 | - |
86 | - def __call__(self): |
87 | - result = convert_calendar_to_ical(self.context) |
88 | - result = '\r\n'.join(result) |
89 | - |
90 | - self.request.response.setHeader('Content-Type', 'text/calendar') |
91 | - self.request.response.setHeader('Content-Length', len(result)) |
92 | - |
93 | - return result |
94 | |
95 | === removed file 'lib/canonical/launchpad/components/crowd.py' |
96 | --- lib/canonical/launchpad/components/crowd.py 2009-06-25 05:30:52 +0000 |
97 | +++ lib/canonical/launchpad/components/crowd.py 1970-01-01 00:00:00 +0000 |
98 | @@ -1,80 +0,0 @@ |
99 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
100 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
101 | - |
102 | -__metaclass__ = type |
103 | - |
104 | -from zope.interface import implements |
105 | - |
106 | -from canonical.launchpad.interfaces import ICrowd, IPerson, ITeam |
107 | - |
108 | - |
109 | -class CrowdOfOnePerson: |
110 | - implements(ICrowd) |
111 | - __used_for__ = IPerson |
112 | - |
113 | - def __init__(self, person): |
114 | - self.person = person |
115 | - |
116 | - def __contains__(self, person_or_team): |
117 | - return person_or_team.id == self.person.id |
118 | - |
119 | - def __add__(self, crowd): |
120 | - return CrowdsAddedTogether(crowd, self) |
121 | - |
122 | - |
123 | -class CrowdOfOneTeam: |
124 | - implements(ICrowd) |
125 | - __used_for__ = ITeam |
126 | - |
127 | - def __init__(self, team): |
128 | - self.team = team |
129 | - |
130 | - def __contains__(self, person_or_team): |
131 | - if person_or_team.id == self.team.id: |
132 | - return True |
133 | - return person_or_team.inTeam(self.team) |
134 | - |
135 | - def __add__(self, crowd): |
136 | - return CrowdsAddedTogether(crowd, self) |
137 | - |
138 | - |
139 | -class CrowdsAddedTogether: |
140 | - |
141 | - implements(ICrowd) |
142 | - |
143 | - def __init__(self, *crowds): |
144 | - self.crowds = crowds |
145 | - |
146 | - def __contains__(self, person_or_team): |
147 | - for crowd in self.crowds: |
148 | - if person_or_team in crowd: |
149 | - return True |
150 | - return False |
151 | - |
152 | - def __add__(self, crowd): |
153 | - return CrowdsAddedTogether(crowd, *self.crowds) |
154 | - |
155 | - |
156 | -# XXX ddaa 2005-04-01: This shouldn't be in components |
157 | -class AnyPersonCrowd: |
158 | - |
159 | - implements(ICrowd) |
160 | - |
161 | - def __contains__(self, person_or_team): |
162 | - return IPerson.providedBy(person_or_team) |
163 | - |
164 | - def __add__(self, crowd): |
165 | - return CrowdsAddedTogether(crowd, self) |
166 | - |
167 | -# XXX ddaa 2005-04-01: This shouldn't be in components |
168 | -class EmptyCrowd: |
169 | - |
170 | - implements(ICrowd) |
171 | - |
172 | - def __contains__(self, person_or_team): |
173 | - return False |
174 | - |
175 | - def __add__(self, crowd): |
176 | - return crowd |
177 | - |
178 | - |
179 | |
180 | === removed file 'lib/canonical/launchpad/doc/crowd.txt' |
181 | --- lib/canonical/launchpad/doc/crowd.txt 2009-08-13 15:12:16 +0000 |
182 | +++ lib/canonical/launchpad/doc/crowd.txt 1970-01-01 00:00:00 +0000 |
183 | @@ -1,124 +0,0 @@ |
184 | - |
185 | - |
186 | -A person is adaptable to ICrowd. |
187 | - |
188 | - >>> from canonical.launchpad.interfaces import IPersonSet, ICrowd |
189 | - >>> from zope.component import getUtility |
190 | - >>> personset = getUtility(IPersonSet) |
191 | - >>> mark = personset.getByName('mark') |
192 | - >>> print mark.name |
193 | - mark |
194 | - >>> mark_crowd = ICrowd(mark) |
195 | - |
196 | -The person is in that crowd. |
197 | - |
198 | - >>> mark in mark_crowd |
199 | - True |
200 | - |
201 | - |
202 | -A team is adaptable to ICrowd, but it gets a different adapter than that used |
203 | -for a person. |
204 | - |
205 | - >>> vcs_imports = personset.getByName('vcs-imports') |
206 | - >>> print vcs_imports.name |
207 | - vcs-imports |
208 | - >>> vcs_imports_crowd = ICrowd(vcs_imports) |
209 | - |
210 | -The team is in that crowd. |
211 | - |
212 | - >>> vcs_imports in vcs_imports_crowd |
213 | - True |
214 | - |
215 | -mark is not in that crowd, because he is not in the vcs-imports team. |
216 | - |
217 | - >>> mark in vcs_imports_crowd |
218 | - False |
219 | - |
220 | -lifeless is in the vcs-imports team. So, lifeless is in that crowd. |
221 | - |
222 | - >>> lifeless = personset.getByName('lifeless') |
223 | - >>> print lifeless.name |
224 | - lifeless |
225 | - >>> lifeless in vcs_imports_crowd |
226 | - True |
227 | - |
228 | -Adding mark_crowd to the vcs_imports_crowd gives us a crowd that contains |
229 | -mark and the vcs-imports team and lifeless, but not stevea. |
230 | -This tests ICrowd(team).__add__. |
231 | - |
232 | - >>> combined_crowd = vcs_imports_crowd + mark_crowd |
233 | - >>> stevea = personset.getByName('stevea') |
234 | - >>> stevea not in combined_crowd |
235 | - True |
236 | - >>> lifeless in combined_crowd |
237 | - True |
238 | - >>> mark in combined_crowd |
239 | - True |
240 | - >>> vcs_imports in combined_crowd |
241 | - True |
242 | - |
243 | -Now, to try it the other way around: adding vcs_imports_crowd to mark_crowd. |
244 | -This tests ICrowd(person).__add__. |
245 | - |
246 | - >>> combined_crowd = mark_crowd + vcs_imports_crowd |
247 | - >>> lifeless in combined_crowd |
248 | - True |
249 | - >>> mark in combined_crowd |
250 | - True |
251 | - >>> vcs_imports in combined_crowd |
252 | - True |
253 | - |
254 | -There is an AnyPersonCrowd that contains any person or team. |
255 | - |
256 | - >>> from canonical.launchpad.components.crowd import AnyPersonCrowd |
257 | - >>> mark in AnyPersonCrowd() |
258 | - True |
259 | - >>> vcs_imports in AnyPersonCrowd() |
260 | - True |
261 | - >>> vcs_imports_crowd in AnyPersonCrowd() |
262 | - False |
263 | - |
264 | -Adding an AnyPersonCrowd to some other crowd works as expected. |
265 | - |
266 | - >>> combined_crowd = mark_crowd + AnyPersonCrowd() |
267 | - >>> mark in combined_crowd |
268 | - True |
269 | - >>> stevea in combined_crowd |
270 | - True |
271 | - |
272 | -Same goes for the other way around. |
273 | - |
274 | - >>> combined_crowd = AnyPersonCrowd() + mark_crowd |
275 | - >>> mark in combined_crowd |
276 | - True |
277 | - >>> stevea in combined_crowd |
278 | - True |
279 | - |
280 | - |
281 | -The EmptyCrowd doens't contain anything. |
282 | - |
283 | - >>> from canonical.launchpad.components.crowd import EmptyCrowd |
284 | - >>> mark in EmptyCrowd() |
285 | - False |
286 | - >>> vcs_imports_crowd in EmptyCrowd() |
287 | - False |
288 | - |
289 | -Adding a crowd to EmptyCrowd, and vice versa, gives you essentially that crowd. |
290 | - |
291 | - >>> combined_crowd = EmptyCrowd() + vcs_imports_crowd |
292 | - >>> vcs_imports in combined_crowd |
293 | - True |
294 | - >>> mark in combined_crowd |
295 | - False |
296 | - >>> lifeless in combined_crowd |
297 | - True |
298 | - |
299 | - >>> combined_crowd = vcs_imports_crowd + EmptyCrowd() |
300 | - >>> vcs_imports in combined_crowd |
301 | - True |
302 | - >>> mark in combined_crowd |
303 | - False |
304 | - >>> lifeless in combined_crowd |
305 | - True |
306 | - |
307 | - |
308 | |
309 | === modified file 'lib/canonical/launchpad/interfaces/launchpad.py' |
310 | --- lib/canonical/launchpad/interfaces/launchpad.py 2010-02-24 23:18:40 +0000 |
311 | +++ lib/canonical/launchpad/interfaces/launchpad.py 2010-03-17 12:06:32 +0000 |
312 | @@ -34,7 +34,6 @@ |
313 | 'IAuthServerApplication', |
314 | 'IBasicLaunchpadRequest', |
315 | 'IBazaarApplication', |
316 | - 'ICrowd', |
317 | 'IFeedsApplication', |
318 | 'IHasAppointedDriver', |
319 | 'IHasAssignee', |
320 | @@ -244,25 +243,6 @@ |
321 | """ |
322 | |
323 | |
324 | -class ICrowd(Interface): |
325 | - |
326 | - def __contains__(person_or_team_or_anything): |
327 | - """Return True if person_or_team_or_anything is in the crowd. |
328 | - |
329 | - Note that a particular crowd can choose to answer 'True' to this |
330 | - question, if that is what it is supposed to do. So, crowds that |
331 | - contain other crowds will want to allow the other crowds the |
332 | - opportunity to answer __contains__ before that crowd does. |
333 | - """ |
334 | - |
335 | - def __add__(crowd): |
336 | - """Return a new ICrowd that is this crowd added to the given crowd. |
337 | - |
338 | - The returned crowd contains the person or teams in |
339 | - both this crowd and the given crowd. |
340 | - """ |
341 | - |
342 | - |
343 | class IPrivateMaloneApplication(ILaunchpadApplication): |
344 | """Private application root for malone.""" |
345 | |
346 | |
347 | === modified file 'lib/canonical/launchpad/zcml/configure.zcml' |
348 | --- lib/canonical/launchpad/zcml/configure.zcml 2010-02-18 17:00:54 +0000 |
349 | +++ lib/canonical/launchpad/zcml/configure.zcml 2010-03-17 12:06:32 +0000 |
350 | @@ -12,7 +12,6 @@ |
351 | <include file="account.zcml" /> |
352 | <include file="batchnavigator.zcml" /> |
353 | <include file="binaryandsourcepackagename.zcml" /> |
354 | - <include file="crowd.zcml" /> |
355 | <include file="datetime.zcml" /> |
356 | <include file="decoratedresultset.zcml" /> |
357 | <include file="emailaddress.zcml" /> |
358 | |
359 | === removed file 'lib/canonical/launchpad/zcml/crowd.zcml' |
360 | --- lib/canonical/launchpad/zcml/crowd.zcml 2009-07-13 18:15:02 +0000 |
361 | +++ lib/canonical/launchpad/zcml/crowd.zcml 1970-01-01 00:00:00 +0000 |
362 | @@ -1,17 +0,0 @@ |
363 | -<!-- Copyright 2009 Canonical Ltd. This software is licensed under the |
364 | - GNU Affero General Public License version 3 (see the file LICENSE). |
365 | ---> |
366 | - |
367 | -<configure xmlns="http://namespaces.zope.org/zope"> |
368 | - <adapter |
369 | - for="canonical.launchpad.interfaces.IPerson" |
370 | - provides="canonical.launchpad.interfaces.ICrowd" |
371 | - factory="canonical.launchpad.components.crowd.CrowdOfOnePerson" |
372 | - /> |
373 | - <adapter |
374 | - for="canonical.launchpad.interfaces.ITeam" |
375 | - provides="canonical.launchpad.interfaces.ICrowd" |
376 | - factory="canonical.launchpad.components.crowd.CrowdOfOneTeam" |
377 | - /> |
378 | -</configure> |
379 | - |
380 | |
381 | === removed symlink 'lib/mercurial' |
382 | === target was u'../sourcecode/mercurial/mercurial' |
383 | === removed directory 'lib/schoolbell' |
384 | === removed file 'lib/schoolbell/README.txt' |
385 | --- lib/schoolbell/README.txt 2005-10-31 18:29:12 +0000 |
386 | +++ lib/schoolbell/README.txt 1970-01-01 00:00:00 +0000 |
387 | @@ -1,7 +0,0 @@ |
388 | -SchoolBell |
389 | -========== |
390 | - |
391 | -SchoolBell is a calendaring library for Zope 3. |
392 | - |
393 | -See the docstring of __init__.py for a list of features and shortcomings. |
394 | - |
395 | |
396 | === removed file 'lib/schoolbell/__init__.py' |
397 | --- lib/schoolbell/__init__.py 2005-10-31 18:29:12 +0000 |
398 | +++ lib/schoolbell/__init__.py 1970-01-01 00:00:00 +0000 |
399 | @@ -1,38 +0,0 @@ |
400 | -""" |
401 | -Calendaring for Zope 3 applications. |
402 | - |
403 | -SchoolBell is a calendaring library for Zope 3. Its main features are |
404 | -(currently most of these features are science fiction): |
405 | - |
406 | -- It can parse and generate iCalendar files. Only a subset of the iCalendar |
407 | - spec is supported, however it is a sensible subset that should be enough for |
408 | - interoperation with desktop calendaring applications like Apple's iCal, |
409 | - Mozilla Calendar, Evolution, and KOrganizer. |
410 | - |
411 | -- It has browser views for presenting calendars in various ways (daily, weekly, |
412 | - monthly, yearly views). |
413 | - |
414 | -- It is storage independent -- your application could store the calendar in |
415 | - ZODB, in a relational database, or elsewhere, as long as the storage |
416 | - component provides the necessary interface. A default content component |
417 | - that stores data in ZODB is provided. |
418 | - |
419 | -- You can also generate calendars on the fly from other data (e.g. a bug |
420 | - tracking system). These calendars can be read-only (simpler) or read-write. |
421 | - |
422 | -- You can display several calendars in a single view by using calendar |
423 | - composition. |
424 | - |
425 | -- It supports recurring events (daily, weekly, monthly and yearly). |
426 | - |
427 | -Things that are not currently supported: |
428 | - |
429 | -- Timezone handling (UTC times are converted into server's local time in the |
430 | - iCalendar parser, but that's all). |
431 | - |
432 | -- All-day events (that is, events that only specify the date but not the time). |
433 | - |
434 | -- Informing the user when uploaded iCalendar files use features that are not |
435 | - supported by SchoolBell. |
436 | - |
437 | -""" |
438 | |
439 | === removed file 'lib/schoolbell/browser.py' |
440 | --- lib/schoolbell/browser.py 2005-10-31 18:29:12 +0000 |
441 | +++ lib/schoolbell/browser.py 1970-01-01 00:00:00 +0000 |
442 | @@ -1,71 +0,0 @@ |
443 | -r""" |
444 | -Browser views for schoolbell. |
445 | - |
446 | -iCalendar views |
447 | ---------------- |
448 | - |
449 | -CalendarICalendarView can export calendars in iCalendar format |
450 | - |
451 | - >>> from datetime import datetime, timedelta |
452 | - >>> from schoolbell.simple import ImmutableCalendar, SimpleCalendarEvent |
453 | - >>> event = SimpleCalendarEvent(datetime(2004, 12, 16, 11, 46, 16), |
454 | - ... timedelta(hours=1), "doctests", |
455 | - ... location=u"Matar\u00f3", |
456 | - ... unique_id="12345678-5432@example.com") |
457 | - >>> calendar = ImmutableCalendar([event]) |
458 | - |
459 | - >>> from zope.publisher.browser import TestRequest |
460 | - >>> view = CalendarICalendarView() |
461 | - >>> view.context = calendar |
462 | - >>> view.request = TestRequest() |
463 | - >>> output = view.show() |
464 | - |
465 | - >>> lines = output.splitlines(True) |
466 | - >>> from pprint import pprint |
467 | - >>> pprint(lines) |
468 | - ['BEGIN:VCALENDAR\r\n', |
469 | - 'VERSION:2.0\r\n', |
470 | - 'PRODID:-//SchoolTool.org/NONSGML SchoolBell//EN\r\n', |
471 | - 'BEGIN:VEVENT\r\n', |
472 | - 'UID:12345678-5432@example.com\r\n', |
473 | - 'SUMMARY:doctests\r\n', |
474 | - 'LOCATION:Matar\xc3\xb3\r\n', |
475 | - 'DTSTART:20041216T114616\r\n', |
476 | - 'DURATION:PT1H\r\n', |
477 | - 'DTSTAMP:...\r\n', |
478 | - 'END:VEVENT\r\n', |
479 | - 'END:VCALENDAR'] |
480 | - |
481 | -XXX: Should the last line also end in '\r\n'? Go read RFC 2445 and experiment |
482 | -with calendaring clients. |
483 | - |
484 | -Register the iCalendar read view in ZCML as |
485 | - |
486 | - <browser:page |
487 | - for="schoolbell.interfaces.ICalendar" |
488 | - name="calendar.ics" |
489 | - permission="zope.Public" |
490 | - class="schoolbell.browser.CalendarICalendarView" |
491 | - attribute="show" |
492 | - /> |
493 | - |
494 | -""" |
495 | - |
496 | -from schoolbell.icalendar import convert_calendar_to_ical |
497 | - |
498 | -__metaclass__ = type |
499 | - |
500 | - |
501 | -class CalendarICalendarView: |
502 | - """RFC 2445 (ICalendar) view for calendars.""" |
503 | - |
504 | - def show(self): |
505 | - data = "\r\n".join(convert_calendar_to_ical(self.context)) |
506 | - request = self.request |
507 | - if request is not None: |
508 | - request.response.setHeader('Content-Type', |
509 | - 'text/calendar; charset=UTF-8') |
510 | - request.response.setHeader('Content-Length', len(data)) |
511 | - |
512 | - return data |
513 | - |
514 | |
515 | === removed file 'lib/schoolbell/icalendar.py' |
516 | --- lib/schoolbell/icalendar.py 2010-02-09 01:31:05 +0000 |
517 | +++ lib/schoolbell/icalendar.py 1970-01-01 00:00:00 +0000 |
518 | @@ -1,1127 +0,0 @@ |
519 | -r""" |
520 | -iCalendar parsing and generating. |
521 | - |
522 | -iCalendar (RFC 2445) is a big and hard-to-read specification. This module |
523 | -supports only a subset of it: VEVENT components with a limited set of |
524 | -attributes and a limited recurrence model. The subset should be sufficient |
525 | -for interoperation with desktop calendaring applications like Apple's iCal, |
526 | -Mozilla Calendar, Evolution and KOrganizer. |
527 | - |
528 | -If you have a calendar, you can convert it to an iCalendar file like this: |
529 | - |
530 | - >>> from datetime import datetime, timedelta |
531 | - >>> from schoolbell.simple import ImmutableCalendar, SimpleCalendarEvent |
532 | - >>> event = SimpleCalendarEvent(datetime(2004, 12, 16, 10, 58, 47), |
533 | - ... timedelta(hours=1), "doctests", |
534 | - ... location=u"Matar\u00f3", |
535 | - ... unique_id="12345678-5432@example.com") |
536 | - >>> calendar = ImmutableCalendar([event]) |
537 | - |
538 | - >>> ical_file_as_string = "\r\n".join(convert_calendar_to_ical(calendar)) |
539 | - |
540 | -The returned string is in UTF-8. |
541 | - |
542 | - >>> event.location.encode("UTF-8") in ical_file_as_string |
543 | - True |
544 | - |
545 | -You can also parse iCalendar files back into calendars: |
546 | - |
547 | - >>> event_iterator = read_icalendar(ical_file_as_string) |
548 | - >>> new_calendar = ImmutableCalendar(event_iterator) |
549 | - >>> [e.title for e in new_calendar] |
550 | - [u'doctests'] |
551 | - |
552 | -""" |
553 | - |
554 | -import datetime |
555 | -import calendar |
556 | -import re |
557 | -import pytz |
558 | -from cStringIO import StringIO |
559 | -from schoolbell.simple import SimpleCalendarEvent |
560 | - |
561 | -_utc_tz = pytz.timezone('UTC') |
562 | - |
563 | -def convert_event_to_ical(event): |
564 | - r"""Convert an ICalendarEvent to iCalendar VEVENT component. |
565 | - |
566 | - Returns a list of strings (without newlines) in UTF-8. |
567 | - |
568 | - >>> from datetime import datetime, timedelta |
569 | - >>> event = SimpleCalendarEvent(datetime(2004, 12, 16, 10, 7, 29), |
570 | - ... timedelta(hours=1), "iCal rendering", |
571 | - ... location="Big room", |
572 | - ... unique_id="12345678-5432@example.com") |
573 | - >>> lines = convert_event_to_ical(event) |
574 | - >>> print "\n".join(lines) |
575 | - BEGIN:VEVENT |
576 | - UID:12345678-5432@example.com |
577 | - SUMMARY:iCal rendering |
578 | - LOCATION:Big room |
579 | - DTSTART:20041216T100729 |
580 | - DURATION:PT1H |
581 | - DTSTAMP:... |
582 | - END:VEVENT |
583 | - |
584 | - """ |
585 | - dtstamp = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") |
586 | - result = [ |
587 | - "BEGIN:VEVENT", |
588 | - "UID:%s" % ical_text(event.unique_id), |
589 | - "SUMMARY:%s" % ical_text(event.title)] |
590 | - if event.description: |
591 | - result.append("DESCRIPTION:%s" % ical_text(event.description)) |
592 | - if event.location: |
593 | - result.append("LOCATION:%s" % ical_text(event.location)) |
594 | -### if event.recurrence is not None: # TODO |
595 | -### start = event.dtstart |
596 | -### result.extend(event.recurrence.iCalRepresentation(start)) |
597 | - result += [ |
598 | - "DTSTART:%s" % ical_datetime(event.dtstart), |
599 | - "DURATION:%s" % ical_duration(event.duration), |
600 | - "DTSTAMP:%s" % dtstamp, |
601 | - "END:VEVENT", |
602 | - ] |
603 | - return result |
604 | - |
605 | - |
606 | -def convert_calendar_to_ical(calendar): |
607 | - r"""Convert an ICalendar to iCalendar VCALENDAR component. |
608 | - |
609 | - Returns a list of strings (without newlines) in UTF-8. They should be |
610 | - joined with '\r\n' to get a valid iCalendar file. |
611 | - |
612 | - >>> from schoolbell.simple import ImmutableCalendar |
613 | - >>> from schoolbell.simple import SimpleCalendarEvent |
614 | - >>> from datetime import datetime, timedelta |
615 | - >>> event = SimpleCalendarEvent(datetime(2004, 12, 16, 10, 7, 29), |
616 | - ... timedelta(hours=1), "iCal rendering", |
617 | - ... location="Big room", |
618 | - ... unique_id="12345678-5432@example.com") |
619 | - >>> calendar = ImmutableCalendar([event]) |
620 | - >>> lines = convert_calendar_to_ical(calendar) |
621 | - >>> print "\n".join(lines) |
622 | - BEGIN:VCALENDAR |
623 | - VERSION:2.0 |
624 | - PRODID:-//SchoolTool.org/NONSGML SchoolBell//EN |
625 | - BEGIN:VEVENT |
626 | - UID:12345678-5432@example.com |
627 | - SUMMARY:iCal rendering |
628 | - LOCATION:Big room |
629 | - DTSTART:20041216T100729 |
630 | - DURATION:PT1H |
631 | - DTSTAMP:... |
632 | - END:VEVENT |
633 | - END:VCALENDAR |
634 | - |
635 | - Empty calendars are not allowed by RFC 2445, so we have to invent a dummy |
636 | - event: |
637 | - |
638 | - >>> lines = convert_calendar_to_ical(ImmutableCalendar()) |
639 | - >>> print "\n".join(lines) |
640 | - BEGIN:VCALENDAR |
641 | - VERSION:2.0 |
642 | - PRODID:-//SchoolTool.org/NONSGML SchoolBell//EN |
643 | - BEGIN:VEVENT |
644 | - UID:... |
645 | - SUMMARY:Empty calendar |
646 | - DTSTART:19700101T000000 |
647 | - DURATION:P0D |
648 | - DTSTAMP:... |
649 | - END:VEVENT |
650 | - END:VCALENDAR |
651 | - |
652 | - """ |
653 | - header = [ |
654 | - "BEGIN:VCALENDAR", |
655 | - "VERSION:2.0", |
656 | - "PRODID:-//SchoolTool.org/NONSGML SchoolBell//EN", |
657 | - ] |
658 | - footer = [ |
659 | - "END:VCALENDAR" |
660 | - ] |
661 | - events = [] |
662 | - for event in calendar: |
663 | - events += convert_event_to_ical(event) |
664 | - if not events: |
665 | - placeholder = SimpleCalendarEvent(datetime.datetime(1970, 1, 1), |
666 | - datetime.timedelta(0), |
667 | - "Empty calendar") |
668 | - events += convert_event_to_ical(placeholder) |
669 | - return header + events + footer |
670 | - |
671 | - |
672 | -def ical_text(value): |
673 | - r"""Format value according to iCalendar TEXT escaping rules. |
674 | - |
675 | - Converts Unicode strings to UTF-8 as well. |
676 | - |
677 | - >>> ical_text('Foo') |
678 | - 'Foo' |
679 | - >>> ical_text(u'Matar\u00f3') |
680 | - 'Matar\xc3\xb3' |
681 | - >>> ical_text('\\') |
682 | - '\\\\' |
683 | - >>> ical_text(';') |
684 | - '\\;' |
685 | - >>> ical_text(',') |
686 | - '\\,' |
687 | - >>> ical_text('\n') |
688 | - '\\n' |
689 | - """ |
690 | - return (value.encode('UTF-8') |
691 | - .replace('\\', '\\\\') |
692 | - .replace(';', '\\;') |
693 | - .replace(',', '\\,') |
694 | - .replace('\n', '\\n')) |
695 | - |
696 | - |
697 | -def ical_datetime(value): |
698 | - """Format a datetime as an iCalendar DATETIME value. |
699 | - |
700 | - >>> from datetime import datetime |
701 | - >>> from pytz import timezone |
702 | - >>> ical_datetime(datetime(2004, 12, 16, 10, 45, 07)) |
703 | - '20041216T104507' |
704 | - >>> ical_datetime(datetime(2004, 12, 16, 10, 45, 07, |
705 | - ... tzinfo=timezone('Australia/Perth'))) |
706 | - '20041216T024507Z' |
707 | - |
708 | - """ |
709 | - if value.tzinfo: |
710 | - value = value.astimezone(_utc_tz) |
711 | - return value.strftime('%Y%m%dT%H%M%SZ') |
712 | - return value.strftime('%Y%m%dT%H%M%S') |
713 | - |
714 | - |
715 | -def ical_duration(value): |
716 | - """Format a timedelta as an iCalendar DURATION value. |
717 | - |
718 | - >>> from datetime import timedelta |
719 | - >>> ical_duration(timedelta(11)) |
720 | - 'P11D' |
721 | - >>> ical_duration(timedelta(-14)) |
722 | - '-P14D' |
723 | - >>> ical_duration(timedelta(1, 7384)) |
724 | - 'P1DT2H3M4S' |
725 | - >>> ical_duration(timedelta(1, 7380)) |
726 | - 'P1DT2H3M' |
727 | - >>> ical_duration(timedelta(1, 7200)) |
728 | - 'P1DT2H' |
729 | - >>> ical_duration(timedelta(0, 7200)) |
730 | - 'PT2H' |
731 | - >>> ical_duration(timedelta(0, 7384)) |
732 | - 'PT2H3M4S' |
733 | - >>> ical_duration(timedelta(0, 184)) |
734 | - 'PT3M4S' |
735 | - >>> ical_duration(timedelta(0, 22)) |
736 | - 'PT22S' |
737 | - >>> ical_duration(timedelta(0, 3622)) |
738 | - 'PT1H0M22S' |
739 | - """ |
740 | - sign = "" |
741 | - if value.days < 0: |
742 | - sign = "-" |
743 | - timepart = "" |
744 | - if value.seconds: |
745 | - timepart = "T" |
746 | - hours = value.seconds // 3600 |
747 | - minutes = value.seconds % 3600 // 60 |
748 | - seconds = value.seconds % 60 |
749 | - if hours: |
750 | - timepart += "%dH" % hours |
751 | - if minutes or (hours and seconds): |
752 | - timepart += "%dM" % minutes |
753 | - if seconds: |
754 | - timepart += "%dS" % seconds |
755 | - if value.days == 0 and timepart: |
756 | - return "%sP%s" % (sign, timepart) |
757 | - else: |
758 | - return "%sP%dD%s" % (sign, abs(value.days), timepart) |
759 | - |
760 | - |
761 | -def read_icalendar(icalendar_text): |
762 | - """Read an iCalendar file and return calendar events. |
763 | - |
764 | - Returns an iterator over calendar events. |
765 | - |
766 | - `icalendar_text` can be a file object or a string. It is assumed that |
767 | - the iCalendar file contains UTF-8 text. |
768 | - |
769 | - Unsuppored features of the iCalendar file (e.g. VTODO components, complex |
770 | - recurrence rules, unknown properties) are silently ignored. |
771 | - """ |
772 | - if isinstance(icalendar_text, str): |
773 | - icalendar_text = StringIO(icalendar_text) |
774 | - reader = ICalReader(icalendar_text) |
775 | - for vevent in reader.iterEvents(): |
776 | - # TODO: ignore empty calendar placeholder |
777 | - |
778 | - # Currently SchoolBell does not support all-day events, so we must |
779 | - # convert them into ordinary events that last 24 hours |
780 | - dtstart = vevent.dtstart |
781 | - if not isinstance(dtstart, datetime.datetime): |
782 | - dtstart = datetime.datetime.combine(dtstart, |
783 | - datetime.time(0)) |
784 | - |
785 | - yield SimpleCalendarEvent(dtstart, vevent.duration, vevent.summary, |
786 | - location=vevent.location, |
787 | - unique_id=vevent.uid, |
788 | - recurrence=vevent.rrule) |
789 | - |
790 | - |
791 | -# |
792 | -# The rest of this module could use some review and refactoring |
793 | -# |
794 | - |
795 | -class ICalReader: |
796 | - """An object which reads in an iCalendar file. |
797 | - |
798 | - The `iterEvents` method returns an iterator over all VEvent objects |
799 | - corresponding to the events in the iCalendar file. |
800 | - |
801 | - Short grammar of iCalendar files (RFC 2445 is the full spec): |
802 | - |
803 | - contentline = name *(";" param ) ":" value CRLF |
804 | - ; content line first must be unfolded by replacing CRLF followed by a |
805 | - ; single WSP with an empty string |
806 | - name = x-name / iana-token |
807 | - x-name = "X-" [vendorid "-"] 1*(ALPHA / DIGIT / "-") |
808 | - iana-token = 1*(ALPHA / DIGIT / "-") |
809 | - vendorid = 3*(ALPHA / DIGIT) |
810 | - param = param-name "=" param-value *("," param-value) |
811 | - param-name = iana-token / x-token |
812 | - param-value = paramtext / quoted-string |
813 | - paramtext = *SAFE-CHAR |
814 | - value = *VALUE-CHAR |
815 | - quoted-string = DQUOTE *QSAFE-CHAR DQUOTE |
816 | - |
817 | - NON-US-ASCII = %x80-F8 |
818 | - QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-US-ASCII |
819 | - ; Any character except CTLs and DQUOTE |
820 | - SAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-7E |
821 | - / NON-US-ASCII |
822 | - ; Any character except CTLs, DQUOTE, ";", ":", "," |
823 | - VALUE-CHAR = WSP / %x21-7E / NON-US-ASCII ; anything except CTLs |
824 | - CR = %x0D |
825 | - LF = %x0A |
826 | - CRLF = CR LF |
827 | - CTL = %x00-08 / %x0A-1F / %x7F |
828 | - ALPHA = %x41-5A / %x61-7A ; A-Z / a-z |
829 | - DIGIT = %x30-39 ; 0-9 |
830 | - DQUOTE = %x22 ; Quotation Mark |
831 | - WSP = SPACE / HTAB |
832 | - SPACE = %x20 |
833 | - HTAB = %x09 |
834 | - """ |
835 | - |
836 | - def __init__(self, file, charset='UTF-8'): |
837 | - self.file = file |
838 | - self.charset = charset |
839 | - |
840 | - def _parseRow(record_str): |
841 | - """Parse a single content line. |
842 | - |
843 | - A content line consists of a property name (optionally followed by a |
844 | - number of parameters) and a value, separated by a colon. Parameters |
845 | - (if present) are separated from the property name and from each other |
846 | - with semicolons. Parameters are of the form name=value; value |
847 | - can be double-quoted. |
848 | - |
849 | - Returns a tuple (name, value, param_dict). Case-insensitive values |
850 | - (i.e. property names, parameter names, unquoted parameter values) are |
851 | - uppercased. |
852 | - |
853 | - Raises ICalParseError on syntax errors. |
854 | - |
855 | - >>> ICalReader._parseRow('foo:bar') |
856 | - ('FOO', 'bar', {}) |
857 | - >>> ICalReader._parseRow('foo;value=bar:BAZFOO') |
858 | - ('FOO', 'BAZFOO', {'VALUE': 'BAR'}) |
859 | - """ |
860 | - |
861 | - it = iter(record_str) |
862 | - getChar = it.next |
863 | - |
864 | - def err(msg): |
865 | - raise ICalParseError("%s in line:\n%s" % (msg, record_str)) |
866 | - |
867 | - try: |
868 | - c = getChar() |
869 | - # name |
870 | - key = '' |
871 | - while c.isalnum() or c == '-': |
872 | - key += c |
873 | - c = getChar() |
874 | - if not key: |
875 | - err("Missing property name") |
876 | - key = key.upper() |
877 | - # optional parameters |
878 | - params = {} |
879 | - while c == ';': |
880 | - c = getChar() |
881 | - # param name |
882 | - param = '' |
883 | - while c.isalnum() or c == '-': |
884 | - param += c |
885 | - c = getChar() |
886 | - if not param: |
887 | - err("Missing parameter name") |
888 | - param = param.upper() |
889 | - # = |
890 | - if c != '=': |
891 | - err("Expected '='") |
892 | - # value (or rather a list of values) |
893 | - pvalues = [] |
894 | - while True: |
895 | - c = getChar() |
896 | - if c == '"': |
897 | - c = getChar() |
898 | - pvalue = '' |
899 | - while c >= ' ' and c not in ('\177', '"'): |
900 | - pvalue += c |
901 | - c = getChar() |
902 | - # value is case-sensitive in this case |
903 | - if c != '"': |
904 | - err("Expected '\"'") |
905 | - c = getChar() |
906 | - else: |
907 | - # unquoted value |
908 | - pvalue = '' |
909 | - while c >= ' ' and c not in ('\177', '"', ';', ':', |
910 | - ','): |
911 | - pvalue += c |
912 | - c = getChar() |
913 | - pvalue = pvalue.upper() |
914 | - pvalues.append(pvalue) |
915 | - if c != ',': |
916 | - break |
917 | - if len(pvalues) > 1: |
918 | - params[param] = pvalues |
919 | - else: |
920 | - params[param] = pvalues[0] |
921 | - # colon and value |
922 | - if c != ':': |
923 | - err("Expected ':'") |
924 | - value = ''.join(it) |
925 | - except StopIteration: |
926 | - err("Syntax error") |
927 | - else: |
928 | - return (key, value, params) |
929 | - |
930 | - _parseRow = staticmethod(_parseRow) |
931 | - |
932 | - def _iterRow(self): |
933 | - """A generator that returns one record at a time, as a tuple of |
934 | - (name, value, params). |
935 | - """ |
936 | - record = [] |
937 | - for line in self.file.readlines(): |
938 | - if line[0] in '\t ': |
939 | - line = line[1:] |
940 | - elif record: |
941 | - row = "".join(record).decode(self.charset) |
942 | - yield self._parseRow(row) |
943 | - record = [] |
944 | - if line.endswith('\r\n'): |
945 | - record.append(line[:-2]) |
946 | - elif line.endswith('\n'): |
947 | - # strictly speaking this is a violation of RFC 2445 |
948 | - record.append(line[:-1]) |
949 | - else: |
950 | - # strictly speaking this is a violation of RFC 2445 |
951 | - record.append(line) |
952 | - if record: |
953 | - row = "".join(record).decode(self.charset) |
954 | - yield self._parseRow(row) |
955 | - |
956 | - def iterEvents(self): |
957 | - """Iterate over all VEVENT objects in an ICalendar file.""" |
958 | - iterator = self._iterRow() |
959 | - |
960 | - # Check that the stream begins with BEGIN:VCALENDAR |
961 | - try: |
962 | - key, value, params = iterator.next() |
963 | - if (key, value, params) != ('BEGIN', 'VCALENDAR', {}): |
964 | - raise ICalParseError('This is not iCalendar') |
965 | - except StopIteration: |
966 | - # The file is empty. Mozilla produces a 0-length file when |
967 | - # publishing an empty calendar. Let's accept it as a valid |
968 | - # calendar that has no events. I'm not sure if a 0-length |
969 | - # file is a valid text/calendar object according to RFC 2445. |
970 | - raise |
971 | - component_stack = ['VCALENDAR'] |
972 | - |
973 | - # Extract all VEVENT components |
974 | - obj = None |
975 | - for key, value, params in iterator: |
976 | - if key == "BEGIN": |
977 | - if obj is not None: |
978 | - # Subcomponents terminate the processing of a VEVENT |
979 | - # component. We can get away with this now, because we're |
980 | - # not interested in alarms and RFC 2445 specifies, that all |
981 | - # properties inside a VEVENT component ought to precede any |
982 | - # VALARM subcomponents. |
983 | - obj.validate() |
984 | - yield obj |
985 | - obj = None |
986 | - if not component_stack and value != "VCALENDAR": |
987 | - raise ICalParseError("Text outside VCALENDAR component") |
988 | - if value == "VEVENT": |
989 | - obj = VEvent() |
990 | - component_stack.append(value) |
991 | - elif key == "END": |
992 | - if obj is not None and value == "VEVENT": |
993 | - obj.validate() |
994 | - yield obj |
995 | - obj = None |
996 | - if not component_stack or component_stack[-1] != value: |
997 | - raise ICalParseError("Mismatched BEGIN/END") |
998 | - component_stack.pop() |
999 | - elif obj is not None: |
1000 | - obj.add(key, value, params) |
1001 | - elif not component_stack: |
1002 | - raise ICalParseError("Text outside VCALENDAR component") |
1003 | - if component_stack: |
1004 | - raise ICalParseError("Unterminated components") |
1005 | - |
1006 | - |
1007 | -def parse_text(value): |
1008 | - r"""Parse iCalendar TEXT value. |
1009 | - |
1010 | - >>> parse_text('Foo') |
1011 | - 'Foo' |
1012 | - >>> parse_text('\\\\') |
1013 | - '\\' |
1014 | - >>> parse_text('\\;') |
1015 | - ';' |
1016 | - >>> parse_text('\\,') |
1017 | - ',' |
1018 | - >>> parse_text('\\n') |
1019 | - '\n' |
1020 | - >>> parse_text('A string with\\; some\\\\ characters\\nin\\Nit') |
1021 | - 'A string with; some\\ characters\nin\nit' |
1022 | - >>> parse_text('Unterminated \\') |
1023 | - Traceback (most recent call last): |
1024 | - ... |
1025 | - IndexError: string index out of range |
1026 | - |
1027 | - """ |
1028 | - if '\\' not in value: |
1029 | - return value |
1030 | - out = [] |
1031 | - prev = 0 |
1032 | - while True: |
1033 | - idx = value.find('\\', prev) |
1034 | - if idx == -1: |
1035 | - break |
1036 | - out.append(value[prev:idx]) |
1037 | - if value[idx + 1] in 'nN': |
1038 | - out.append('\n') |
1039 | - else: |
1040 | - out.append(value[idx + 1]) |
1041 | - prev = idx + 2 |
1042 | - out.append(value[prev:]) |
1043 | - return "".join(out) |
1044 | - |
1045 | - |
1046 | -def parse_date(value): |
1047 | - """Parse iCalendar DATE value. Returns a date instance. |
1048 | - |
1049 | - >>> parse_date('20030405') |
1050 | - datetime.date(2003, 4, 5) |
1051 | - >>> parse_date('20030405T060708') |
1052 | - Traceback (most recent call last): |
1053 | - ... |
1054 | - ValueError: Invalid iCalendar date: '20030405T060708' |
1055 | - >>> parse_date('') |
1056 | - Traceback (most recent call last): |
1057 | - ... |
1058 | - ValueError: Invalid iCalendar date: '' |
1059 | - >>> parse_date('yyyymmdd') |
1060 | - Traceback (most recent call last): |
1061 | - ... |
1062 | - ValueError: Invalid iCalendar date: 'yyyymmdd' |
1063 | - """ |
1064 | - if len(value) != 8: |
1065 | - raise ValueError('Invalid iCalendar date: %r' % value) |
1066 | - try: |
1067 | - y, m, d = int(value[0:4]), int(value[4:6]), int(value[6:8]) |
1068 | - except ValueError: |
1069 | - raise ValueError('Invalid iCalendar date: %r' % value) |
1070 | - else: |
1071 | - return datetime.date(y, m, d) |
1072 | - |
1073 | - |
1074 | -def parse_date_time(value): |
1075 | - """Parse iCalendar DATE-TIME value. Returns a datetime instance. |
1076 | - |
1077 | - A simple usage example: |
1078 | - |
1079 | - >>> parse_date_time('20030405T060708') |
1080 | - datetime.datetime(2003, 4, 5, 6, 7, 8) |
1081 | - |
1082 | - Examples of invalid arguments: |
1083 | - |
1084 | - >>> parse_date_time('20030405T060708A') |
1085 | - Traceback (most recent call last): |
1086 | - ... |
1087 | - ValueError: Invalid iCalendar date-time: '20030405T060708A' |
1088 | - >>> parse_date_time('') |
1089 | - Traceback (most recent call last): |
1090 | - ... |
1091 | - ValueError: Invalid iCalendar date-time: '' |
1092 | - |
1093 | - For timezone tests see tests.test_icalendar.TestParseDateTime. |
1094 | - |
1095 | - """ |
1096 | - datetime_rx = re.compile(r'(\d{4})(\d{2})(\d{2})' |
1097 | - r'T(\d{2})(\d{2})(\d{2})(Z?)$') |
1098 | - match = datetime_rx.match(value) |
1099 | - if match is None: |
1100 | - raise ValueError('Invalid iCalendar date-time: %r' % value) |
1101 | - y, m, d, hh, mm, ss, utc = match.groups() |
1102 | - dt = datetime.datetime(int(y), int(m), int(d), |
1103 | - int(hh), int(mm), int(ss)) |
1104 | - if utc: |
1105 | - # In the future we might want to get the timezone from the iCalendar |
1106 | - # file, but for now using the local timezone of the server should |
1107 | - # be adequate. |
1108 | - timetuple = dt.timetuple() |
1109 | - ticks = calendar.timegm(timetuple) |
1110 | - dt = datetime.datetime.fromtimestamp(ticks) |
1111 | - |
1112 | - return dt |
1113 | - |
1114 | - |
1115 | -def parse_duration(value): |
1116 | - """Parse iCalendar DURATION value. Returns a timedelta instance. |
1117 | - |
1118 | - >>> parse_duration('+P11D') |
1119 | - datetime.timedelta(11) |
1120 | - >>> parse_duration('-P2W') |
1121 | - datetime.timedelta(-14) |
1122 | - >>> parse_duration('P1DT2H3M4S') |
1123 | - datetime.timedelta(1, 7384) |
1124 | - >>> parse_duration('P1DT2H3M') |
1125 | - datetime.timedelta(1, 7380) |
1126 | - >>> parse_duration('P1DT2H') |
1127 | - datetime.timedelta(1, 7200) |
1128 | - >>> parse_duration('PT2H') |
1129 | - datetime.timedelta(0, 7200) |
1130 | - >>> parse_duration('PT2H3M4S') |
1131 | - datetime.timedelta(0, 7384) |
1132 | - >>> parse_duration('PT3M4S') |
1133 | - datetime.timedelta(0, 184) |
1134 | - >>> parse_duration('PT22S') |
1135 | - datetime.timedelta(0, 22) |
1136 | - >>> parse_duration('') |
1137 | - Traceback (most recent call last): |
1138 | - ... |
1139 | - ValueError: Invalid iCalendar duration: '' |
1140 | - >>> parse_duration('xyzzy') |
1141 | - Traceback (most recent call last): |
1142 | - ... |
1143 | - ValueError: Invalid iCalendar duration: 'xyzzy' |
1144 | - >>> parse_duration('P') |
1145 | - Traceback (most recent call last): |
1146 | - ... |
1147 | - ValueError: Invalid iCalendar duration: 'P' |
1148 | - >>> parse_duration('P1WT2H') |
1149 | - Traceback (most recent call last): |
1150 | - ... |
1151 | - ValueError: Invalid iCalendar duration: 'P1WT2H' |
1152 | - """ |
1153 | - date_part = r'(\d+)D' |
1154 | - time_part = r'T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?' |
1155 | - datetime_part = '(?:%s)?(?:%s)?' % (date_part, time_part) |
1156 | - weeks_part = r'(\d+)W' |
1157 | - duration_rx = re.compile(r'([-+]?)P(?:%s|%s)$' |
1158 | - % (weeks_part, datetime_part)) |
1159 | - match = duration_rx.match(value) |
1160 | - if match is None: |
1161 | - raise ValueError('Invalid iCalendar duration: %r' % value) |
1162 | - sign, weeks, days, hours, minutes, seconds = match.groups() |
1163 | - if weeks: |
1164 | - value = datetime.timedelta(weeks=int(weeks)) |
1165 | - else: |
1166 | - if (days is None and hours is None |
1167 | - and minutes is None and seconds is None): |
1168 | - raise ValueError('Invalid iCalendar duration: %r' |
1169 | - % value) |
1170 | - value = datetime.timedelta(days=int(days or 0), |
1171 | - hours=int(hours or 0), |
1172 | - minutes=int(minutes or 0), |
1173 | - seconds=int(seconds or 0)) |
1174 | - if sign == '-': |
1175 | - value = -value |
1176 | - return value |
1177 | - |
1178 | - |
1179 | -def parse_period(value): |
1180 | - """Parse iCalendar PERIOD value. Returns a Period instance. |
1181 | - |
1182 | - >>> p = parse_period('20030405T060708/20030405T060709') |
1183 | - >>> print repr(p).replace('),', '),\\n ') |
1184 | - Period(datetime.datetime(2003, 4, 5, 6, 7, 8), |
1185 | - datetime.datetime(2003, 4, 5, 6, 7, 9)) |
1186 | - >>> parse_period('20030405T060708/PT1H1M1S') |
1187 | - Period(datetime.datetime(2003, 4, 5, 6, 7, 8), datetime.timedelta(0, 3661)) |
1188 | - >>> parse_period('xyzzy') |
1189 | - Traceback (most recent call last): |
1190 | - ... |
1191 | - ValueError: Invalid iCalendar period: 'xyzzy' |
1192 | - >>> parse_period('foo/foe') |
1193 | - Traceback (most recent call last): |
1194 | - ... |
1195 | - ValueError: Invalid iCalendar period: 'foo/foe' |
1196 | - """ |
1197 | - parts = value.split('/') |
1198 | - if len(parts) != 2: |
1199 | - raise ValueError('Invalid iCalendar period: %r' % value) |
1200 | - try: |
1201 | - start = parse_date_time(parts[0]) |
1202 | - try: |
1203 | - end_or_duration = parse_date_time(parts[1]) |
1204 | - except ValueError: |
1205 | - end_or_duration = parse_duration(parts[1]) |
1206 | - except ValueError: |
1207 | - raise ValueError('Invalid iCalendar period: %r' % value) |
1208 | - else: |
1209 | - return Period(start, end_or_duration) |
1210 | - |
1211 | - |
1212 | -## TODO: implement recurrences |
1213 | - |
1214 | -## def _parse_recurrence_weekly(args): |
1215 | -## """Parse iCalendar weekly recurrence rule. |
1216 | -## |
1217 | -## args is a mapping from attribute names in RRULE to their string values. |
1218 | -## |
1219 | -## >>> _parse_recurrence_weekly({}) |
1220 | -## WeeklyRecurrenceRule(1, None, None, (), ()) |
1221 | -## >>> _parse_recurrence_weekly({'BYDAY': 'WE'}) |
1222 | -## WeeklyRecurrenceRule(1, None, None, (), (2,)) |
1223 | -## >>> _parse_recurrence_weekly({'BYDAY': 'MO,WE,SU'}) |
1224 | -## WeeklyRecurrenceRule(1, None, None, (), (0, 2, 6)) |
1225 | -## |
1226 | -## """ |
1227 | -## from schooltool.cal import WeeklyRecurrenceRule |
1228 | -## weekdays = [] |
1229 | -## days = args.get('BYDAY', None) |
1230 | -## if days is not None: |
1231 | -## for day in days.split(','): |
1232 | -## weekdays.append(ical_weekdays.index(day)) |
1233 | -## return WeeklyRecurrenceRule(weekdays=weekdays) |
1234 | -## |
1235 | -## |
1236 | -## def _parse_recurrence_monthly(args): |
1237 | -## """Parse iCalendar monthly recurrence rule. |
1238 | -## |
1239 | -## args is a mapping from attribute names in RRULE to their string values. |
1240 | -## |
1241 | -## Month-day recurrency is the default: |
1242 | -## |
1243 | -## >>> _parse_recurrence_monthly({}) |
1244 | -## MonthlyRecurrenceRule(1, None, None, (), 'monthday') |
1245 | -## |
1246 | -## 3rd Tuesday in a month: |
1247 | -## |
1248 | -## >>> _parse_recurrence_monthly({'BYDAY': '3TU'}) |
1249 | -## MonthlyRecurrenceRule(1, None, None, (), 'weekday') |
1250 | -## |
1251 | -## Last Wednesday: |
1252 | -## |
1253 | -## >>> _parse_recurrence_monthly({'BYDAY': '-1WE'}) |
1254 | -## MonthlyRecurrenceRule(1, None, None, (), 'lastweekday') |
1255 | -## """ |
1256 | -## from schooltool.cal import MonthlyRecurrenceRule |
1257 | -## if 'BYDAY' in args: |
1258 | -## if args['BYDAY'][0] == '-': |
1259 | -## monthly = 'lastweekday' |
1260 | -## else: |
1261 | -## monthly = 'weekday' |
1262 | -## else: |
1263 | -## monthly = 'monthday' |
1264 | -## return MonthlyRecurrenceRule(monthly=monthly) |
1265 | -## |
1266 | -## |
1267 | -## def parse_recurrence_rule(value): |
1268 | -## """Parse iCalendar RRULE entry. |
1269 | -## |
1270 | -## Returns the corresponding subclass of RecurrenceRule. |
1271 | -## |
1272 | -## params is a mapping from attribute names in RRULE to their string values, |
1273 | -## |
1274 | -## A trivial example of a daily recurrence: |
1275 | -## |
1276 | -## >>> parse_recurrence_rule('FREQ=DAILY') |
1277 | -## DailyRecurrenceRule(1, None, None, ()) |
1278 | -## |
1279 | -## A slightly more complex example: |
1280 | -## |
1281 | -## >>> parse_recurrence_rule('FREQ=DAILY;INTERVAL=5;COUNT=7') |
1282 | -## DailyRecurrenceRule(5, 7, None, ()) |
1283 | -## |
1284 | -## An example that includes use of UNTIL: |
1285 | -## |
1286 | -## >>> parse_recurrence_rule('FREQ=DAILY;UNTIL=20041008T000000') |
1287 | -## DailyRecurrenceRule(1, None, datetime.datetime(2004, 10, 8, 0, 0), ()) |
1288 | -## >>> parse_recurrence_rule('FREQ=DAILY;UNTIL=20041008') |
1289 | -## DailyRecurrenceRule(1, None, datetime.datetime(2004, 10, 8, 0, 0), ()) |
1290 | -## |
1291 | -## Of course, other recurrence frequencies may be used: |
1292 | -## |
1293 | -## >>> parse_recurrence_rule('FREQ=WEEKLY;BYDAY=MO,WE,SU') |
1294 | -## WeeklyRecurrenceRule(1, None, None, (), (0, 2, 6)) |
1295 | -## >>> parse_recurrence_rule('FREQ=MONTHLY') |
1296 | -## MonthlyRecurrenceRule(1, None, None, (), 'monthday') |
1297 | -## >>> parse_recurrence_rule('FREQ=YEARLY') |
1298 | -## YearlyRecurrenceRule(1, None, None, ()) |
1299 | -## |
1300 | -## You have to provide a valid recurrence frequency, or you will get an error: |
1301 | -## |
1302 | -## >>> parse_recurrence_rule('') |
1303 | -## Traceback (most recent call last): |
1304 | -## ... |
1305 | -## ValueError: Invalid frequency of recurrence: None |
1306 | -## >>> parse_recurrence_rule('FREQ=bogus') |
1307 | -## Traceback (most recent call last): |
1308 | -## ... |
1309 | -## ValueError: Invalid frequency of recurrence: 'bogus' |
1310 | -## |
1311 | -## Unknown keys in params are ignored silently: |
1312 | -## |
1313 | -## >>> parse_recurrence_rule('FREQ=DAILY;WHATEVER=IGNORED') |
1314 | -## DailyRecurrenceRule(1, None, None, ()) |
1315 | -## |
1316 | -## """ |
1317 | -## from schooltool.cal import DailyRecurrenceRule, YearlyRecurrenceRule |
1318 | -## |
1319 | -## # split up the given value into parameters |
1320 | -## params = {} |
1321 | -## if value: |
1322 | -## for pair in value.split(';'): |
1323 | -## k, v = pair.split('=', 1) |
1324 | -## params[k] = v |
1325 | -## |
1326 | -## # parse common recurrency attributes |
1327 | -## interval = int(params.pop('INTERVAL', '1')) |
1328 | -## count = params.pop('COUNT', None) |
1329 | -## if count is not None: |
1330 | -## count = int(count) |
1331 | -## until = params.pop('UNTIL', None) |
1332 | -## if until is not None: |
1333 | -## if len(until) == 8: |
1334 | -## until = datetime.datetime.combine(parse_date(until), |
1335 | -## datetime.time(0, 0)) |
1336 | -## else: |
1337 | -## until = parse_date_time(until) |
1338 | -## |
1339 | -## # instantiate the corresponding recurrence rule |
1340 | -## freq = params.pop('FREQ', None) |
1341 | -## if freq == 'DAILY': |
1342 | -## rule = DailyRecurrenceRule() |
1343 | -## elif freq == 'WEEKLY': |
1344 | -## rule = _parse_recurrence_weekly(params) |
1345 | -## elif freq == 'MONTHLY': |
1346 | -## rule = _parse_recurrence_monthly(params) |
1347 | -## elif freq == 'YEARLY': |
1348 | -## rule = YearlyRecurrenceRule() |
1349 | -## else: |
1350 | -## raise ValueError('Invalid frequency of recurrence: %r' % freq) |
1351 | -## |
1352 | -## return rule.replace(interval=interval, count=count, until=until) |
1353 | -## |
1354 | - |
1355 | -def parse_recurrence_rule(value): |
1356 | - return None # XXX |
1357 | - |
1358 | - |
1359 | -class VEvent: |
1360 | - """iCalendar event. |
1361 | - |
1362 | - Life cycle: when a VEvent is created, a number of properties should be |
1363 | - added to it using the add method. Then validate should be called. |
1364 | - After that you can start using query methods (getOne, hasProp, iterDates). |
1365 | - |
1366 | - Events are classified into two kinds: |
1367 | - - normal events |
1368 | - - all-day events |
1369 | - |
1370 | - All-day events are identified by their DTSTART property having a DATE value |
1371 | - instead of the default DATE-TIME. All-day events should satisfy the |
1372 | - following requirements (otherwise an exception will be raised): |
1373 | - - DURATION property (if defined) should be an integral number of days |
1374 | - - DTEND property (if defined) should have a DATE value |
1375 | - - any RDATE and EXDATE properties should only contain DATE values |
1376 | - |
1377 | - The first two requirements are stated in RFC 2445; I'm not so sure about |
1378 | - the third one. |
1379 | - """ |
1380 | - |
1381 | - default_type = { |
1382 | - # Default value types for some properties |
1383 | - 'DTSTAMP': 'DATE-TIME', |
1384 | - 'DTSTART': 'DATE-TIME', |
1385 | - 'CREATED': 'DATE-TIME', |
1386 | - 'DTEND': 'DATE-TIME', |
1387 | - 'DURATION': 'DURATION', |
1388 | - 'LAST-MODIFIED': 'DATE-TIME', |
1389 | - 'PRIORITY': 'INTEGER', |
1390 | - 'RECURRENCE-ID': 'DATE-TIME', |
1391 | - 'SEQUENCE': 'INTEGER', |
1392 | - 'URL': 'URI', |
1393 | - 'ATTACH': 'URI', |
1394 | - 'EXDATE': 'DATE-TIME', |
1395 | - 'EXRULE': 'RECUR', |
1396 | - 'RDATE': 'DATE-TIME', |
1397 | - 'RRULE': 'RECUR', |
1398 | - 'LOCATION': 'TEXT', |
1399 | - 'UID': 'TEXT', |
1400 | - } |
1401 | - |
1402 | - converters = { |
1403 | - 'INTEGER': int, |
1404 | - 'DATE': parse_date, |
1405 | - 'DATE-TIME': parse_date_time, |
1406 | - 'DURATION': parse_duration, |
1407 | - 'PERIOD': parse_period, |
1408 | - 'TEXT': parse_text, |
1409 | - 'RECUR': parse_recurrence_rule, |
1410 | - } |
1411 | - |
1412 | - singleton_properties = set([ |
1413 | - 'DTSTAMP', |
1414 | - 'DTSTART', |
1415 | - 'UID', |
1416 | - 'CLASS', |
1417 | - 'CREATED', |
1418 | - 'DESCRIPTION', |
1419 | - 'DTEND', |
1420 | - 'DURATION', |
1421 | - 'GEO', |
1422 | - 'LAST-MODIFIED', |
1423 | - 'LOCATION', |
1424 | - 'ORGANIZER', |
1425 | - 'PRIORITY', |
1426 | - 'RECURRENCE-ID', |
1427 | - 'SEQUENCE', |
1428 | - 'STATUS', |
1429 | - 'SUMMARY', |
1430 | - 'TRANSP', |
1431 | - 'URL', |
1432 | - ]) |
1433 | - |
1434 | - rdate_types = set(['DATE', 'DATE-TIME', 'PERIOD']) |
1435 | - exdate_types = set(['DATE', 'DATE-TIME']) |
1436 | - |
1437 | - def __init__(self): |
1438 | - self._props = {} |
1439 | - |
1440 | - def add(self, property, value, params=None): |
1441 | - """Add a property. |
1442 | - |
1443 | - Property name is case insensitive. Params should be a dict from |
1444 | - uppercased parameter names to their values. |
1445 | - |
1446 | - Multiple calls to add with the same property name override the |
1447 | - value. This is sufficient for now, but will have to be changed |
1448 | - soon. |
1449 | - """ |
1450 | - if params is None: |
1451 | - params = {} |
1452 | - key = property.upper() |
1453 | - if key in self._props: |
1454 | - if key in self.singleton_properties: |
1455 | - raise ICalParseError("Property %s cannot occur more than once" |
1456 | - % key) |
1457 | - self._props[key].append((value, params)) |
1458 | - else: |
1459 | - self._props[key] = [(value, params)] |
1460 | - |
1461 | - def validate(self): |
1462 | - """Check that this VEvent has all the necessary properties. |
1463 | - |
1464 | - Also sets the following attributes: |
1465 | - uid The unique id of this event |
1466 | - summary Textual summary of this event |
1467 | - all_day_event True if this is an all-day event |
1468 | - dtstart start of the event (inclusive) |
1469 | - dtend end of the event (not inclusive) |
1470 | - duration length of the event |
1471 | - location location of the event |
1472 | - rrule recurrency rule |
1473 | - rdates a list of recurrence dates or periods |
1474 | - exdates a list of exception dates |
1475 | - """ |
1476 | - if not self.hasProp('UID'): |
1477 | - raise ICalParseError("VEVENT must have a UID property") |
1478 | - if not self.hasProp('DTSTART'): |
1479 | - raise ICalParseError("VEVENT must have a DTSTART property") |
1480 | - if self._getType('DTSTART') not in ('DATE', 'DATE-TIME'): |
1481 | - raise ICalParseError("DTSTART property should have a DATE or" |
1482 | - " DATE-TIME value") |
1483 | - if self.hasProp('DTEND'): |
1484 | - if self._getType('DTEND') != self._getType('DTSTART'): |
1485 | - raise ICalParseError("DTEND property should have the same type" |
1486 | - " as DTSTART") |
1487 | - if self.hasProp('DURATION'): |
1488 | - raise ICalParseError("VEVENT cannot have both a DTEND" |
1489 | - " and a DURATION property") |
1490 | - if self.hasProp('DURATION'): |
1491 | - if self._getType('DURATION') != 'DURATION': |
1492 | - raise ICalParseError("DURATION property should have type" |
1493 | - " DURATION") |
1494 | - |
1495 | - self.uid = self.getOne('UID') |
1496 | - self.summary = self.getOne('SUMMARY') |
1497 | - |
1498 | - self.all_day_event = self._getType('DTSTART') == 'DATE' |
1499 | - self.dtstart = self.getOne('DTSTART') |
1500 | - if self.hasProp('DURATION'): |
1501 | - self.duration = self.getOne('DURATION') |
1502 | - self.dtend = self.dtstart + self.duration |
1503 | - else: |
1504 | - self.dtend = self.getOne('DTEND', None) |
1505 | - if self.dtend is None: |
1506 | - self.dtend = self.dtstart |
1507 | - if self.all_day_event: |
1508 | - self.dtend += datetime.date.resolution |
1509 | - self.duration = self.dtend - self.dtstart |
1510 | - |
1511 | - self.location = self.getOne('LOCATION', None) |
1512 | - |
1513 | - if self.dtstart > self.dtend: |
1514 | - raise ICalParseError("Event start time should precede end time") |
1515 | - elif self.all_day_event and self.dtstart == self.dtend: |
1516 | - raise ICalParseError("Event start time should precede end time") |
1517 | - |
1518 | - self.rdates = self._extractListOfDates('RDATE', self.rdate_types, |
1519 | - self.all_day_event) |
1520 | - self.exdates = self._extractListOfDates('EXDATE', self.exdate_types, |
1521 | - self.all_day_event) |
1522 | - |
1523 | - self.rrule = self.getOne('RRULE', None) |
1524 | - if self.rrule is not None and self.exdates: |
1525 | - if self._getType('EXDATE') == 'DATE-TIME': |
1526 | - exceptions = [dt.date() for dt in self.exdates] |
1527 | - else: |
1528 | - exceptions = self.exdates |
1529 | - self.rrule = self.rrule.replace(exceptions=exceptions) |
1530 | - |
1531 | - def _extractListOfDates(self, key, accepted_types, all_day_event): |
1532 | - """Parse a comma separated list of values. |
1533 | - |
1534 | - If all_day_event is True, only accepts DATE values. Otherwise accepts |
1535 | - all value types listed in 'accepted_types'. |
1536 | - """ |
1537 | - dates = [] |
1538 | - default_type = self.default_type[key] |
1539 | - for value, params in self._props.get(key, []): |
1540 | - value_type = params.get('VALUE', default_type) |
1541 | - if value_type not in accepted_types: |
1542 | - raise ICalParseError('Invalid value type for %s: %s' |
1543 | - % (key, value_type)) |
1544 | - if all_day_event and value_type != 'DATE': |
1545 | - raise ICalParseError('I do not understand how to interpret ' |
1546 | - '%s values in %s for all-day events.' |
1547 | - % (value_type, key)) |
1548 | - converter = self.converters.get(value_type) |
1549 | - dates.extend(map(converter, value.split(','))) |
1550 | - return dates |
1551 | - |
1552 | - def _getType(self, property): |
1553 | - """Return the type of the property value.""" |
1554 | - key = property.upper() |
1555 | - values = self._props[key] |
1556 | - assert len(values) == 1 |
1557 | - value, params = values[0] |
1558 | - default_type = self.default_type.get(key, 'TEXT') |
1559 | - return params.get('VALUE', default_type) |
1560 | - |
1561 | - def getOne(self, property, default=None): |
1562 | - """Return the value of a property as an appropriate Python object. |
1563 | - |
1564 | - Only call getOne for properties that do not occur more than once. |
1565 | - """ |
1566 | - try: |
1567 | - values = self._props[property.upper()] |
1568 | - assert len(values) == 1 |
1569 | - value, params = values[0] |
1570 | - except KeyError: |
1571 | - return default |
1572 | - else: |
1573 | - converter = self.converters.get(self._getType(property)) |
1574 | - if converter is None: |
1575 | - return value |
1576 | - else: |
1577 | - return converter(value) |
1578 | - |
1579 | - def hasProp(self, property): |
1580 | - """Return True if this VEvent has a named property.""" |
1581 | - return property.upper() in self._props |
1582 | - |
1583 | - def iterDates(self): |
1584 | - """Iterate over all dates within this event. |
1585 | - |
1586 | - This is only valid for all-day events at the moment. |
1587 | - """ |
1588 | - if not self.all_day_event: |
1589 | - raise ValueError('iterDates is only defined for all-day events') |
1590 | - |
1591 | - # Find out the set of start dates |
1592 | - start_set = {self.dtstart: None} |
1593 | - for rdate in self.rdates: |
1594 | - start_set[rdate] = rdate |
1595 | - for exdate in self.exdates: |
1596 | - if exdate in start_set: |
1597 | - del start_set[exdate] |
1598 | - |
1599 | - # Find out the set of all dates |
1600 | - date_set = set(start_set) |
1601 | - duration = self.duration.days |
1602 | - for d in start_set: |
1603 | - for n in range(1, duration): |
1604 | - d += datetime.date.resolution |
1605 | - date_set.add(d) |
1606 | - |
1607 | - # Yield all dates in chronological order |
1608 | - dates = list(date_set) |
1609 | - dates.sort() |
1610 | - for d in dates: |
1611 | - yield d |
1612 | - |
1613 | - |
1614 | -class Period: |
1615 | - """A period of time""" |
1616 | - |
1617 | - def __init__(self, start, end_or_duration): |
1618 | - self.start = start |
1619 | - self.end_or_duration = end_or_duration |
1620 | - if isinstance(end_or_duration, datetime.timedelta): |
1621 | - self.duration = end_or_duration |
1622 | - self.end = self.start + self.duration |
1623 | - else: |
1624 | - self.end = end_or_duration |
1625 | - self.duration = self.end - self.start |
1626 | - if self.start > self.end: |
1627 | - raise ValueError("Start time is greater than end time") |
1628 | - |
1629 | - def __repr__(self): |
1630 | - return "Period(%r, %r)" % (self.start, self.end_or_duration) |
1631 | - |
1632 | - def __cmp__(self, other): |
1633 | - if not isinstance(other, Period): |
1634 | - raise NotImplementedError('Cannot compare Period with %r' % other) |
1635 | - return cmp((self.start, self.end), (other.start, other.end)) |
1636 | - |
1637 | - def overlaps(self, other): |
1638 | - if self.start > other.start: |
1639 | - return other.overlaps(self) |
1640 | - if self.start <= other.start < self.end: |
1641 | - return True |
1642 | - return False |
1643 | - |
1644 | -class ICalParseError(Exception): |
1645 | - """Invalid syntax in an iCalendar file.""" |
1646 | |
1647 | === removed file 'lib/schoolbell/interfaces.py' |
1648 | --- lib/schoolbell/interfaces.py 2005-11-17 21:44:26 +0000 |
1649 | +++ lib/schoolbell/interfaces.py 1970-01-01 00:00:00 +0000 |
1650 | @@ -1,396 +0,0 @@ |
1651 | -""" |
1652 | -Interface definitions for SchoolBell. |
1653 | - |
1654 | -There are two interfaces for calendars: `ICalendar` for read-only calendars, |
1655 | -and `IEditCalendar` for read-write calendars. |
1656 | - |
1657 | -Semantically calendars are unordered sets of events. Events themselves |
1658 | -(`ICalendarEvent`) are immutable and comparable. If you have an editable |
1659 | -calendar, and want to change an event in it, you need to create a new event |
1660 | -object and put it into the calendar: |
1661 | - |
1662 | - calendar.removeEvent(event) |
1663 | - replacement_event = event.replace(title=u"New title", ...) |
1664 | - calendar.addEvent(replacement_event) |
1665 | - |
1666 | -Calendars have globally unique IDs. If you are changing an event in the |
1667 | -fashion demonstrated above, you should preserve its unique_id attribute. |
1668 | - |
1669 | -""" |
1670 | - |
1671 | -from zope.interface import Interface |
1672 | -from zope.schema import Text, TextLine, Int, Datetime, Date, List, Set, Choice |
1673 | -from zope.schema import Field, Object |
1674 | - |
1675 | - |
1676 | -class ICalendar(Interface): |
1677 | - """Calendar. |
1678 | - |
1679 | - A calendar is a set of calendar events (see ICalendarEvent). Recurring |
1680 | - events are listed only once. |
1681 | - """ |
1682 | - |
1683 | - def __iter__(): |
1684 | - """Return an iterator over all events in this calendar. |
1685 | - |
1686 | - The order of events is not defined. |
1687 | - """ |
1688 | - |
1689 | - def find(unique_id): |
1690 | - """Return an event with the given unique id. |
1691 | - |
1692 | - Raises a KeyError if there is no event with this id. |
1693 | - """ |
1694 | - |
1695 | - def expand(first, last): |
1696 | - """Return an iterator over all expanded events in a given time period. |
1697 | - |
1698 | - "Expanding" here refers to expanding recurring events, that is, |
1699 | - creating objects for all occurrences of recurring events. If a |
1700 | - recurring event has occurreces that overlap the specified time |
1701 | - interval, every such occurrence is represented as a new calendar event |
1702 | - with the `dtstart` attribute replaced with the date and time of that |
1703 | - occurrence. These events provide IExpandedCalendarEvent and have an |
1704 | - additional attribute which points to the original event. |
1705 | - |
1706 | - `first` and `last` are datetime.datetimes and define a half-open |
1707 | - time interval. |
1708 | - |
1709 | - The order of returned events is not defined. |
1710 | - """ |
1711 | - |
1712 | - |
1713 | -class IEditCalendar(ICalendar): |
1714 | - """Editable calendar. |
1715 | - |
1716 | - Calendar events are read-only, so to change an event you need to remove |
1717 | - the old event, and add a replacement event in the calendar. |
1718 | - """ |
1719 | - |
1720 | - def clear(): |
1721 | - """Remove all events.""" |
1722 | - |
1723 | - def addEvent(event): |
1724 | - """Add an event to the calendar. |
1725 | - |
1726 | - Raises ValueError if an event with the same unique_id already exists |
1727 | - in the calendar. |
1728 | - |
1729 | - Returns the newly added event (which may be a copy of the argument, |
1730 | - e.g. if the calendar needs its events to be instances of a particular |
1731 | - class). |
1732 | - |
1733 | - It is perhaps not a good idea to add calendar events that have no |
1734 | - occurrences into calendars (see ICalendarEvent.hasOccurrences), as they |
1735 | - will be invisible in date-based of calendar views. |
1736 | - |
1737 | - Do not call addEvent while iterating over the calendar. |
1738 | - """ |
1739 | - |
1740 | - def removeEvent(event): |
1741 | - """Remove event from the calendar. |
1742 | - |
1743 | - Raises ValueError if event is not present in the calendar. |
1744 | - |
1745 | - Do not call removeEvent while iterating over the calendar. |
1746 | - """ |
1747 | - |
1748 | - def update(calendar): |
1749 | - """Add all events from another calendar. |
1750 | - |
1751 | - cal1.update(cal2) |
1752 | - |
1753 | - is equivalent to |
1754 | - |
1755 | - for event in cal2: |
1756 | - cal1.addEvent(event) |
1757 | - """ |
1758 | - |
1759 | - |
1760 | -class IRecurrenceRule(Interface): |
1761 | - """Base interface of the recurrence rules. |
1762 | - |
1763 | - Recurrence rules are stored as attributes of ICalendarEvent. They |
1764 | - are also immutable and comparable. To modify the recurrence |
1765 | - rule of an event, you need to create a new recurrence rule, and a new |
1766 | - event: |
1767 | - |
1768 | - replacement_rule = event.recurrence.replace(count=3, until=None) |
1769 | - replacement_event = event.replace(recurrence=replacement_rule) |
1770 | - calendar.removeEvent(event) |
1771 | - calendar.addEvent(replacement_event) |
1772 | - |
1773 | - """ |
1774 | - |
1775 | - interval = Int( |
1776 | - title=u"Interval", |
1777 | - min=1, |
1778 | - description=u""" |
1779 | - Interval of recurrence (a positive integer). |
1780 | - |
1781 | - For example, to indicate that an event occurs every second day, |
1782 | - you would create a DailyRecurrenceRule witl interval equal to 2. |
1783 | - """) |
1784 | - |
1785 | - count = Int( |
1786 | - title=u"Count", |
1787 | - required=False, |
1788 | - description=u""" |
1789 | - Number of times the event is repeated. |
1790 | - |
1791 | - Can be None or an integer value. If count is not None then |
1792 | - until must be None. If both count and until are None the |
1793 | - event repeats forever. |
1794 | - """) |
1795 | - |
1796 | - until = Date( |
1797 | - title=u"Until", |
1798 | - required=False, |
1799 | - description=u""" |
1800 | - The date of the last recurrence of the event. |
1801 | - |
1802 | - Can be None or a datetime.date instance. If until is not None |
1803 | - then count must be None. If both count and until are None the |
1804 | - event repeats forever. |
1805 | - """) |
1806 | - |
1807 | - exceptions = List( |
1808 | - title=u"Exceptions", |
1809 | - value_type=Date(), |
1810 | - description=u""" |
1811 | - A list of days when this event does not occur. |
1812 | - |
1813 | - Values in this list must be instances of datetime.date. |
1814 | - """) |
1815 | - |
1816 | - def replace(**kw): |
1817 | - """Return a copy of this recurrence rule with new specified fields.""" |
1818 | - |
1819 | - def __eq__(other): |
1820 | - """See if self == other.""" |
1821 | - |
1822 | - def __ne__(other): |
1823 | - """See if self != other.""" |
1824 | - |
1825 | - def apply(event, enddate=None): |
1826 | - """Apply this rule to an event. |
1827 | - |
1828 | - This is a generator that returns the dates on which the event should |
1829 | - recur. Be careful when iterating over these dates -- rules that do not |
1830 | - have either 'until' or 'count' attributes will go on forever. |
1831 | - |
1832 | - The optional enddate attribute can be used to set a range on the dates |
1833 | - generated by this function (inclusive). |
1834 | - """ |
1835 | - |
1836 | - def iCalRepresentation(dtstart): |
1837 | - """Return the rule in iCalendar format. |
1838 | - |
1839 | - Returns a list of strings. XXX more details, please |
1840 | - |
1841 | - dtstart is a datetime representing the date that the recurring |
1842 | - event starts on. |
1843 | - """ |
1844 | - |
1845 | - |
1846 | -class IDailyRecurrenceRule(IRecurrenceRule): |
1847 | - """Daily recurrence.""" |
1848 | - |
1849 | - |
1850 | -class IYearlyRecurrenceRule(IRecurrenceRule): |
1851 | - """Yearly recurrence.""" |
1852 | - |
1853 | - |
1854 | -class IWeeklyRecurrenceRule(IRecurrenceRule): |
1855 | - """Weekly recurrence.""" |
1856 | - |
1857 | - weekdays = Set( |
1858 | - title=u"Weekdays", |
1859 | - value_type=Int(min=0, max=6), |
1860 | - description=u""" |
1861 | - A set of weekdays when this event occurs. |
1862 | - |
1863 | - Weekdays are represented as integers from 0 (Monday) to 6 (Sunday). |
1864 | - This is what the `calendar` and `datetime` modules use. |
1865 | - |
1866 | - The event repeats on the weekday of the first occurence even |
1867 | - if that weekday is not in this set. |
1868 | - """) |
1869 | - |
1870 | - |
1871 | -class IMonthlyRecurrenceRule(IRecurrenceRule): |
1872 | - """Monthly recurrence.""" |
1873 | - |
1874 | - monthly = Choice( |
1875 | - title=u"Type", |
1876 | - values=('monthday', 'weekday', 'lastweekday'), |
1877 | - description=u""" |
1878 | - Specification of a monthly occurence. |
1879 | - |
1880 | - Can be one of three values: 'monthday', 'weekday', 'lastweekday'. |
1881 | - |
1882 | - 'monthday' specifies that the event recurs on the same day of month |
1883 | - (e.g., 25th day of a month). |
1884 | - |
1885 | - 'weekday' specifies that the event recurs on the same week |
1886 | - within a month on the same weekday, indexed from the |
1887 | - first (e.g. 3rd Friday of a month). |
1888 | - |
1889 | - 'lastweekday' specifies that the event recurs on the same week |
1890 | - within a month on the same weekday, indexed from the |
1891 | - end of month (e.g. 2nd last Friday of a month). |
1892 | - """) |
1893 | - |
1894 | - |
1895 | -class ICalendarEvent(Interface): |
1896 | - """A calendar event. |
1897 | - |
1898 | - Calendar events are immutable and comparable. |
1899 | - |
1900 | - Events are compared in chronological order, so lists of events can be |
1901 | - sorted. If two events start at the same time, they are ordered according |
1902 | - to their titles. |
1903 | - |
1904 | - While `unique_id` is a globally unique ID of a calendar event, you can |
1905 | - have several calendar event objects with the same value of `unique_id`, |
1906 | - and they will not be equal if any their attributes are different. |
1907 | - Semantically these objects are different versions of the same calendar |
1908 | - event. |
1909 | - |
1910 | - If you need to modify a calendar event in a calendar, you should do |
1911 | - the following: |
1912 | - |
1913 | - calendar.removeEvent(event) |
1914 | - replacement_event = event.replace(title=u"New title", ...) |
1915 | - calendar.addEvent(replacement_event) |
1916 | - |
1917 | - """ |
1918 | - |
1919 | - id = Int( |
1920 | - title=u"An internal ID for the event", |
1921 | - required=True, readonly=True, |
1922 | - description=u"""An ID for the event, guaranteed to be unique locally |
1923 | - but not globally.""") |
1924 | - |
1925 | - calendar = Object( |
1926 | - title=u"Calendar", |
1927 | - schema=ICalendar, |
1928 | - description=u""" |
1929 | - The calendar this event belongs to. |
1930 | - """) |
1931 | - |
1932 | - unique_id = TextLine( |
1933 | - title=u"UID", |
1934 | - description=u""" |
1935 | - A globally unique id for this calendar event. |
1936 | - |
1937 | - iCalendar (RFC 2445) recommeds using the RFC 822 addr-spec syntax |
1938 | - for unique IDs. Put the current timestamp and a random number |
1939 | - on the left of the @ sign, and put the hostname on the right. |
1940 | - """) |
1941 | - |
1942 | - dtstart = Datetime( |
1943 | - title=u"Starting date and time", |
1944 | - description=u"""Format: yyyy-mm-dd hh:mm""" |
1945 | - ) |
1946 | - |
1947 | - duration = Field(title=u"Duration") |
1948 | - # The duration of the event (datetime.timedelta). |
1949 | - # You can compute the event end date/time by adding duration to dtstart. |
1950 | - # zope.schema does not have TimeInterval. |
1951 | - |
1952 | - title = TextLine(title=u"Name") |
1953 | - |
1954 | - description = Text(title=u"Description") |
1955 | - |
1956 | - location = TextLine( |
1957 | - title=u"Location", |
1958 | - required=False, |
1959 | - description=u"""Where the event will take place.""") |
1960 | - |
1961 | - recurrence = Object( |
1962 | - title=u"Recurrence", |
1963 | - schema=IRecurrenceRule, |
1964 | - required=False, |
1965 | - description=u""" |
1966 | - The recurrence rule, if this is a recurring event, otherwise None. |
1967 | - """) |
1968 | - |
1969 | - def replace(**kw): |
1970 | - """Return a calendar event with new specified fields. |
1971 | - |
1972 | - This is useful for editing calendars. For example, to change the |
1973 | - title and location of an event in a calendar, you would do |
1974 | - |
1975 | - calendar.removeEvent(event) |
1976 | - replacement_event = event.replace(title=u"New title", |
1977 | - location=None) |
1978 | - calendar.addEvent(replacement_event) |
1979 | - |
1980 | - """ |
1981 | - |
1982 | - def __eq__(other): |
1983 | - """See if self == other.""" |
1984 | - |
1985 | - def __ne__(other): |
1986 | - """See if self != other.""" |
1987 | - |
1988 | - def __lt__(other): |
1989 | - """See if self < other.""" |
1990 | - |
1991 | - def __gt__(other): |
1992 | - """See if self > other.""" |
1993 | - |
1994 | - def __le__(other): |
1995 | - """See if self <= other.""" |
1996 | - |
1997 | - def __ge__(other): |
1998 | - """See if self >= other.""" |
1999 | - |
2000 | - def hasOccurrences(): |
2001 | - """Does the event have any occurrences? |
2002 | - |
2003 | - Normally all events have at least one occurrence. However if you have |
2004 | - a repeating event that repeats a finite number of times, and all those |
2005 | - repetitions are listed as exceptions, then hasOccurrences() will return |
2006 | - False. There are other corner cases as well (e.g. a recurring event |
2007 | - with until date that is earlier than dtstart). |
2008 | - """ |
2009 | - |
2010 | - |
2011 | -class IExpandedCalendarEvent(ICalendarEvent): |
2012 | - """A single occurrence of a recurring calendar event. |
2013 | - |
2014 | - The original event is stored in the `original` attribute. The `dtstart` |
2015 | - attribute contains the date and time of this occurrence and may differ |
2016 | - from the `dtstart` attribute of the original event. All other attributes |
2017 | - are the same. |
2018 | - """ |
2019 | - |
2020 | - dtstart = Datetime( |
2021 | - title=u"Start", |
2022 | - description=u""" |
2023 | - Date and time when this occurrence of the event starts. |
2024 | - """) |
2025 | - |
2026 | - original = Object( |
2027 | - title=u"Original", |
2028 | - schema=ICalendarEvent, |
2029 | - description=u""" |
2030 | - The recurring event that generated this occurrence. |
2031 | - """) |
2032 | - |
2033 | - def replace(**kw): |
2034 | - """Return a calendar event with new specified fields. |
2035 | - |
2036 | - expanded_event.replace(**kw) |
2037 | - |
2038 | - is (almost) equivalent to |
2039 | - |
2040 | - expanded_event.original.replace(**kw) |
2041 | - |
2042 | - In other words, the returned event will not provide |
2043 | - IExpandedCalendarEvent and its dtstart attribute will be the date and |
2044 | - time of the original event rather than this specific occurrence. |
2045 | - """ |
2046 | - |
2047 | |
2048 | === removed file 'lib/schoolbell/mixins.py' |
2049 | --- lib/schoolbell/mixins.py 2005-10-31 18:29:12 +0000 |
2050 | +++ lib/schoolbell/mixins.py 1970-01-01 00:00:00 +0000 |
2051 | @@ -1,314 +0,0 @@ |
2052 | -""" |
2053 | -Mixins for implementing calendars. |
2054 | -""" |
2055 | - |
2056 | -__metaclass__ = type |
2057 | - |
2058 | - |
2059 | -class CalendarMixin: |
2060 | - """Mixin for implementing ICalendar methods. |
2061 | - |
2062 | - You do not have to use this mixin, however it might make implementation |
2063 | - easier, albeit potentially slower. |
2064 | - |
2065 | - A class that uses this mixin must already implement ICalendar.__iter__. |
2066 | - |
2067 | - >>> from schoolbell.interfaces import ICalendar |
2068 | - >>> from zope.interface import implements |
2069 | - >>> class MyCalendar(CalendarMixin): |
2070 | - ... implements(ICalendar) |
2071 | - ... def __iter__(self): |
2072 | - ... return iter([]) |
2073 | - >>> from zope.interface.verify import verifyObject |
2074 | - >>> verifyObject(ICalendar, MyCalendar()) |
2075 | - True |
2076 | - |
2077 | - """ |
2078 | - |
2079 | - def find(self, unique_id): |
2080 | - """Find a calendar event with a given UID. |
2081 | - |
2082 | - This particular implementation simply performs a linear search by |
2083 | - iterating over all events and looking at their UIDs. |
2084 | - |
2085 | - >>> from schoolbell.interfaces import ICalendar |
2086 | - >>> from zope.interface import implements |
2087 | - |
2088 | - >>> class Event: |
2089 | - ... def __init__(self, uid): |
2090 | - ... self.unique_id = uid |
2091 | - |
2092 | - >>> class MyCalendar(CalendarMixin): |
2093 | - ... implements(ICalendar) |
2094 | - ... def __iter__(self): |
2095 | - ... return iter([Event(uid) for uid in 'a', 'b']) |
2096 | - >>> cal = MyCalendar() |
2097 | - |
2098 | - >>> cal.find('a').unique_id |
2099 | - 'a' |
2100 | - >>> cal.find('b').unique_id |
2101 | - 'b' |
2102 | - >>> cal.find('c') |
2103 | - Traceback (most recent call last): |
2104 | - ... |
2105 | - KeyError: 'c' |
2106 | - |
2107 | - """ |
2108 | - for event in self: |
2109 | - if event.unique_id == unique_id: |
2110 | - return event |
2111 | - raise KeyError(unique_id) |
2112 | - |
2113 | - def expand(self, first, last): |
2114 | - """Return an iterator over all expanded events in a given time period. |
2115 | - |
2116 | - See ICalendar for more details. |
2117 | - |
2118 | - >>> from datetime import datetime, timedelta |
2119 | - >>> from schoolbell.interfaces import ICalendar |
2120 | - >>> from zope.interface import implements |
2121 | - |
2122 | - >>> class Event: |
2123 | - ... def __init__(self, dtstart, duration, title): |
2124 | - ... self.dtstart = dtstart |
2125 | - ... self.duration = duration |
2126 | - ... self.title = title |
2127 | - |
2128 | - >>> class MyCalendar(CalendarMixin): |
2129 | - ... implements(ICalendar) |
2130 | - ... def __iter__(self): |
2131 | - ... return iter([Event(datetime(2004, 12, 14, 12, 30), |
2132 | - ... timedelta(hours=1), 'a'), |
2133 | - ... Event(datetime(2004, 12, 15, 16, 30), |
2134 | - ... timedelta(hours=1), 'b'), |
2135 | - ... Event(datetime(2004, 12, 15, 14, 30), |
2136 | - ... timedelta(hours=1), 'c'), |
2137 | - ... Event(datetime(2004, 12, 16, 17, 30), |
2138 | - ... timedelta(hours=1), 'd'), |
2139 | - ... ]) |
2140 | - >>> cal = MyCalendar() |
2141 | - |
2142 | - We will define a convenience function for showing all events returned |
2143 | - by expand: |
2144 | - |
2145 | - >>> def show(first, last): |
2146 | - ... events = cal.expand(first, last) |
2147 | - ... print '[%s]' % ', '.join([e.title for e in events]) |
2148 | - |
2149 | - Events that fall inside the interval |
2150 | - |
2151 | - >>> show(datetime(2004, 12, 1), datetime(2004, 12, 31)) |
2152 | - [a, b, c, d] |
2153 | - |
2154 | - >>> show(datetime(2004, 12, 15), datetime(2004, 12, 16)) |
2155 | - [b, c] |
2156 | - |
2157 | - Events that fall partially in the interval |
2158 | - |
2159 | - >>> show(datetime(2004, 12, 15, 17, 0), |
2160 | - ... datetime(2004, 12, 16, 18, 0)) |
2161 | - [b, d] |
2162 | - |
2163 | - Corner cases: if event.dtstart + event.duration == last, or |
2164 | - event.dtstart == first, the event is not included. |
2165 | - |
2166 | - >>> show(datetime(2004, 12, 15, 15, 30), |
2167 | - ... datetime(2004, 12, 15, 16, 30)) |
2168 | - [] |
2169 | - |
2170 | - TODO: recurring events |
2171 | - |
2172 | - """ |
2173 | - for event in self: |
2174 | - # TODO: recurring events |
2175 | - dtstart = event.dtstart |
2176 | - dtend = dtstart + event.duration |
2177 | - if dtend > first and dtstart < last: |
2178 | - yield event |
2179 | - |
2180 | - |
2181 | -class EditableCalendarMixin: |
2182 | - """Mixin for implementing some IEditCalendar methods. |
2183 | - |
2184 | - This mixin implements `clear` and `update` by using `addEvent` and |
2185 | - `removeEvent`. |
2186 | - |
2187 | - >>> class Event: |
2188 | - ... def __init__(self, uid): |
2189 | - ... self.unique_id = uid |
2190 | - |
2191 | - >>> class SampleCalendar(EditableCalendarMixin): |
2192 | - ... def __init__(self): |
2193 | - ... self._events = {} |
2194 | - ... def __iter__(self): |
2195 | - ... return self._events.itervalues() |
2196 | - ... def addEvent(self, event): |
2197 | - ... self._events[event.unique_id] = event |
2198 | - ... def removeEvent(self, event): |
2199 | - ... del self._events[event.unique_id] |
2200 | - |
2201 | - >>> cal = SampleCalendar() |
2202 | - >>> cal.addEvent(Event('a')) |
2203 | - >>> cal.addEvent(Event('b')) |
2204 | - >>> cal.addEvent(Event('c')) |
2205 | - >>> len(list(cal)) |
2206 | - 3 |
2207 | - |
2208 | - >>> cal2 = SampleCalendar() |
2209 | - >>> cal2.update(cal) |
2210 | - >>> len(list(cal2)) |
2211 | - 3 |
2212 | - |
2213 | - >>> cal.clear() |
2214 | - >>> list(cal) |
2215 | - [] |
2216 | - |
2217 | - """ |
2218 | - |
2219 | - def update(self, calendar): |
2220 | - """Add all events from another calendar to this calendar.""" |
2221 | - for event in calendar: |
2222 | - self.addEvent(event) |
2223 | - |
2224 | - def clear(self): |
2225 | - """Remove all events from the calendar.""" |
2226 | - for event in list(self): |
2227 | - self.removeEvent(event) |
2228 | - |
2229 | - |
2230 | -class CalendarEventMixin: |
2231 | - """Mixin for implementing ICalendarEvent comparison methods. |
2232 | - |
2233 | - Calendar events are equal iff all their attributes are equal. We can get a |
2234 | - list of those attributes easily because ICalendarEvent is a schema. |
2235 | - |
2236 | - >>> from schoolbell.interfaces import ICalendarEvent |
2237 | - >>> from zope.schema import getFieldNames |
2238 | - >>> all_attrs = getFieldNames(ICalendarEvent) |
2239 | - >>> 'unique_id' in all_attrs |
2240 | - True |
2241 | - >>> '__eq__' not in all_attrs |
2242 | - True |
2243 | - |
2244 | - We will create a bunch of Event objects that differ in exactly one |
2245 | - attribute and compare them. |
2246 | - |
2247 | - >>> class Event(CalendarEventMixin): |
2248 | - ... def __init__(self, **kw): |
2249 | - ... for attr in all_attrs: |
2250 | - ... setattr(self, attr, '%s_default_value' % attr) |
2251 | - ... for attr, value in kw.items(): |
2252 | - ... setattr(self, attr, value) |
2253 | - |
2254 | - >>> e1 = Event() |
2255 | - >>> for attr in all_attrs: |
2256 | - ... e2 = Event() |
2257 | - ... setattr(e2, attr, 'some other value') |
2258 | - ... assert e1 != e2, 'change in %s was not noticed' % attr |
2259 | - |
2260 | - If you have two events with the same values in all ICalendarEvent |
2261 | - attributes, they are equal |
2262 | - |
2263 | - >>> e1 = Event() |
2264 | - >>> e2 = Event() |
2265 | - >>> e1 == e2 |
2266 | - True |
2267 | - |
2268 | - even if they have extra attributes |
2269 | - |
2270 | - >>> e1 = Event() |
2271 | - >>> e1.annotation = 'a' |
2272 | - >>> e2 = Event() |
2273 | - >>> e2.annotation = 'b' |
2274 | - >>> e1 == e2 |
2275 | - True |
2276 | - |
2277 | - Events are ordered by their date and time, title and, finally, UID (to |
2278 | - break any ties and provide a stable consistent ordering). |
2279 | - |
2280 | - >>> from datetime import datetime |
2281 | - |
2282 | - >>> e1 = Event(dtstart=datetime(2004, 12, 15)) |
2283 | - >>> e2 = Event(dtstart=datetime(2004, 12, 16)) |
2284 | - >>> e1 < e2 |
2285 | - True |
2286 | - |
2287 | - >>> e1 = Event(dtstart=datetime(2004, 12, 15), title="A") |
2288 | - >>> e2 = Event(dtstart=datetime(2004, 12, 15), title="B") |
2289 | - >>> e1 < e2 |
2290 | - True |
2291 | - |
2292 | - >>> e1 = Event(dtstart=datetime(2004, 12, 1), title="A", unique_id="X") |
2293 | - >>> e2 = Event(dtstart=datetime(2004, 12, 1), title="A", unique_id="Y") |
2294 | - >>> e1 < e2 |
2295 | - True |
2296 | - |
2297 | - """ |
2298 | - |
2299 | - def __eq__(self, other): |
2300 | - """Check whether two calendar events are equal.""" |
2301 | - return (self.unique_id, self.dtstart, self.duration, self.title, |
2302 | - self.location, self.recurrence) \ |
2303 | - == (other.unique_id, other.dtstart, other.duration, other.title, |
2304 | - other.location, other.recurrence) |
2305 | - |
2306 | - def __ne__(self, other): |
2307 | - return not self.__eq__(other) |
2308 | - |
2309 | - def __lt__(self, other): |
2310 | - return (self.dtstart, self.title, self.unique_id) \ |
2311 | - < (other.dtstart, other.title, other.unique_id) |
2312 | - |
2313 | - def __gt__(self, other): |
2314 | - return (self.dtstart, self.title, self.unique_id) \ |
2315 | - > (other.dtstart, other.title, other.unique_id) |
2316 | - |
2317 | - def __le__(self, other): |
2318 | - return (self.dtstart, self.title, self.unique_id) \ |
2319 | - <= (other.dtstart, other.title, other.unique_id) |
2320 | - |
2321 | - def __ge__(self, other): |
2322 | - return (self.dtstart, self.title, self.unique_id) \ |
2323 | - >= (other.dtstart, other.title, other.unique_id) |
2324 | - |
2325 | - def hasOccurrences(self): |
2326 | - raise NotImplementedError # TODO |
2327 | - |
2328 | - def replace(self, **kw): |
2329 | - r"""Return a copy of this event with some attributes replaced. |
2330 | - |
2331 | - >>> from schoolbell.interfaces import ICalendarEvent |
2332 | - >>> from zope.schema import getFieldNames |
2333 | - >>> all_attrs = getFieldNames(ICalendarEvent) |
2334 | - >>> class Event(CalendarEventMixin): |
2335 | - ... def __init__(self, **kw): |
2336 | - ... for attr in all_attrs: |
2337 | - ... setattr(self, attr, '%s_default_value' % attr) |
2338 | - ... for attr, value in kw.items(): |
2339 | - ... setattr(self, attr, value) |
2340 | - |
2341 | - >>> from datetime import datetime, timedelta |
2342 | - >>> e1 = Event(dtstart=datetime(2004, 12, 15, 18, 57), |
2343 | - ... duration=timedelta(minutes=15), |
2344 | - ... title='Work on schoolbell.simple', |
2345 | - ... location=None) |
2346 | - |
2347 | - >>> e2 = e1.replace(location=u'Matar\u00f3') |
2348 | - >>> e2 == e1 |
2349 | - False |
2350 | - >>> e2.title == e1.title |
2351 | - True |
2352 | - >>> e2.location |
2353 | - u'Matar\xf3' |
2354 | - |
2355 | - >>> e3 = e2.replace(location=None) |
2356 | - >>> e3 == e1 |
2357 | - True |
2358 | - |
2359 | - """ |
2360 | - # The import is here to avoid cyclic dependencies |
2361 | - from schoolbell.simple import SimpleCalendarEvent |
2362 | - for attr in ['dtstart', 'duration', 'title', 'description', |
2363 | - 'location', 'unique_id', 'recurrence']: |
2364 | - kw.setdefault(attr, getattr(self, attr)) |
2365 | - return SimpleCalendarEvent(**kw) |
2366 | |
2367 | === removed file 'lib/schoolbell/simple.py' |
2368 | --- lib/schoolbell/simple.py 2005-10-31 18:29:12 +0000 |
2369 | +++ lib/schoolbell/simple.py 1970-01-01 00:00:00 +0000 |
2370 | @@ -1,96 +0,0 @@ |
2371 | -""" |
2372 | -Simple calendar events and calendars. |
2373 | -""" |
2374 | - |
2375 | -import datetime |
2376 | -import random |
2377 | -import email.Utils |
2378 | -from zope.interface import implements |
2379 | -from schoolbell.interfaces import ICalendar, ICalendarEvent |
2380 | -from schoolbell.mixins import CalendarEventMixin, CalendarMixin |
2381 | - |
2382 | -__metaclass__ = type |
2383 | - |
2384 | - |
2385 | -class SimpleCalendarEvent(CalendarEventMixin): |
2386 | - """A simple implementation of ICalendarEvent. |
2387 | - |
2388 | - >>> from datetime import datetime, timedelta |
2389 | - >>> from zope.interface.verify import verifyObject |
2390 | - >>> e = SimpleCalendarEvent(datetime(2004, 12, 15, 18, 57), |
2391 | - ... timedelta(minutes=15), |
2392 | - ... 'Work on schoolbell.simple') |
2393 | - >>> verifyObject(ICalendarEvent, e) |
2394 | - True |
2395 | - |
2396 | - If you do not specify a unique ID, a random one is generated |
2397 | - |
2398 | - >>> e.unique_id is not None |
2399 | - True |
2400 | - |
2401 | - """ |
2402 | - |
2403 | - implements(ICalendarEvent) |
2404 | - |
2405 | - def __init__(self, dtstart, duration, title, description=None, location=None, unique_id=None, |
2406 | - recurrence=None): |
2407 | - self.dtstart = dtstart |
2408 | - self.duration = duration |
2409 | - self.title = title |
2410 | - self.description=description |
2411 | - self.location = location |
2412 | - self.recurrence = recurrence |
2413 | - self.unique_id = unique_id |
2414 | - if not self.unique_id: |
2415 | - self.unique_id = new_unique_id() |
2416 | - |
2417 | - |
2418 | -class ImmutableCalendar(CalendarMixin): |
2419 | - """A simple read-only calendar. |
2420 | - |
2421 | - >>> from datetime import datetime, timedelta |
2422 | - >>> from zope.interface.verify import verifyObject |
2423 | - >>> e = SimpleCalendarEvent(datetime(2004, 12, 15, 18, 57), |
2424 | - ... timedelta(minutes=15), |
2425 | - ... 'Work on schoolbell.simple') |
2426 | - >>> calendar = ImmutableCalendar([e]) |
2427 | - >>> verifyObject(ICalendar, calendar) |
2428 | - True |
2429 | - |
2430 | - >>> [e.title for e in calendar] |
2431 | - ['Work on schoolbell.simple'] |
2432 | - |
2433 | - """ |
2434 | - |
2435 | - implements(ICalendar) |
2436 | - |
2437 | - def __init__(self, events=()): |
2438 | - self._events = tuple(events) |
2439 | - |
2440 | - def __iter__(self): |
2441 | - return iter(self._events) |
2442 | - |
2443 | - |
2444 | -def new_unique_id(): |
2445 | - """Generate a new unique ID for a calendar event. |
2446 | - |
2447 | - UID is randomly generated and follows RFC 822 addr-spec: |
2448 | - |
2449 | - >>> uid = new_unique_id() |
2450 | - >>> '@' in uid |
2451 | - True |
2452 | - |
2453 | - Note that it does not have the angle brackets |
2454 | - |
2455 | - >>> '<' not in uid |
2456 | - True |
2457 | - >>> '>' not in uid |
2458 | - True |
2459 | - |
2460 | - """ |
2461 | - more_uniqueness = '%d.%d' % (datetime.datetime.now().microsecond, |
2462 | - random.randrange(10 ** 6, 10 ** 7)) |
2463 | - # generate an rfc-822 style id and strip angle brackets |
2464 | - unique_id = email.Utils.make_msgid(more_uniqueness)[1:-1] |
2465 | - return unique_id |
2466 | - |
2467 | |
2468 | === removed directory 'lib/schoolbell/tests' |
2469 | === removed file 'lib/schoolbell/tests/__init__.py' |
2470 | --- lib/schoolbell/tests/__init__.py 2005-10-31 18:29:12 +0000 |
2471 | +++ lib/schoolbell/tests/__init__.py 1970-01-01 00:00:00 +0000 |
2472 | @@ -1,1 +0,0 @@ |
2473 | -"""Unit tests for SchoolBell""" |
2474 | |
2475 | === removed file 'lib/schoolbell/tests/test_icalendar.py' |
2476 | --- lib/schoolbell/tests/test_icalendar.py 2005-10-31 18:29:12 +0000 |
2477 | +++ lib/schoolbell/tests/test_icalendar.py 1970-01-01 00:00:00 +0000 |
2478 | @@ -1,782 +0,0 @@ |
2479 | -""" |
2480 | -Unit tests for schoolbell.icalendar |
2481 | -""" |
2482 | - |
2483 | -import unittest |
2484 | -import difflib |
2485 | -import time |
2486 | -import os |
2487 | -from pprint import pformat |
2488 | -from textwrap import dedent |
2489 | -from datetime import date, timedelta, datetime |
2490 | -from StringIO import StringIO |
2491 | - |
2492 | -from zope.testing import doctest |
2493 | - |
2494 | - |
2495 | -def diff(old, new, oldlabel="expected output", newlabel="actual output"): |
2496 | - """Display a unified diff between old text and new text.""" |
2497 | - old = old.splitlines() |
2498 | - new = new.splitlines() |
2499 | - |
2500 | - diff = ['--- %s' % oldlabel, '+++ %s' % newlabel] |
2501 | - |
2502 | - def dump(tag, x, lo, hi): |
2503 | - for i in xrange(lo, hi): |
2504 | - diff.append(tag + x[i]) |
2505 | - |
2506 | - differ = difflib.SequenceMatcher(a=old, b=new) |
2507 | - for tag, alo, ahi, blo, bhi in differ.get_opcodes(): |
2508 | - if tag == 'replace': |
2509 | - dump('-', old, alo, ahi) |
2510 | - dump('+', new, blo, bhi) |
2511 | - elif tag == 'delete': |
2512 | - dump('-', old, alo, ahi) |
2513 | - elif tag == 'insert': |
2514 | - dump('+', new, blo, bhi) |
2515 | - elif tag == 'equal': |
2516 | - dump(' ', old, alo, ahi) |
2517 | - else: |
2518 | - raise AssertionError('unknown tag %r' % tag) |
2519 | - return "\n".join(diff) |
2520 | - |
2521 | - |
2522 | -class TimezoneTestMixin: |
2523 | - """A mixin for tests that fiddle with timezones.""" |
2524 | - |
2525 | - def setUp(self): |
2526 | - self.have_tzset = hasattr(time, 'tzset') |
2527 | - self.touched_tz = False |
2528 | - self.old_tz = os.getenv('TZ') |
2529 | - |
2530 | - def tearDown(self): |
2531 | - if self.touched_tz: |
2532 | - self.setTZ(self.old_tz) |
2533 | - |
2534 | - def setTZ(self, tz): |
2535 | - self.touched_tz = True |
2536 | - if tz is None: |
2537 | - os.unsetenv('TZ') |
2538 | - else: |
2539 | - os.putenv('TZ', tz) |
2540 | - time.tzset() |
2541 | - |
2542 | - |
2543 | -class TestParseDateTime(TimezoneTestMixin, unittest.TestCase): |
2544 | - |
2545 | - def test_timezones(self): |
2546 | - # The simple tests are in the doctest of parse_date_time. |
2547 | - from schoolbell.icalendar import parse_date_time |
2548 | - |
2549 | - if not self.have_tzset: |
2550 | - return # Do not run this test on Windows |
2551 | - |
2552 | - self.setTZ('UTC') |
2553 | - dt = parse_date_time('20041029T125031Z') |
2554 | - self.assertEquals(dt, datetime(2004, 10, 29, 12, 50, 31)) |
2555 | - |
2556 | - self.setTZ('EET-2EEST') |
2557 | - dt = parse_date_time('20041029T095031Z') # daylight savings |
2558 | - self.assertEquals(dt, datetime(2004, 10, 29, 12, 50, 31)) |
2559 | - dt = parse_date_time('20041129T095031Z') # no daylight savings |
2560 | - self.assertEquals(dt, datetime(2004, 11, 29, 11, 50, 31)) |
2561 | - |
2562 | - |
2563 | -class TestPeriod(unittest.TestCase): |
2564 | - |
2565 | - def test(self): |
2566 | - from schoolbell.icalendar import Period |
2567 | - dt1 = datetime(2001, 2, 3, 14, 30, 5) |
2568 | - dt2 = datetime(2001, 2, 3, 16, 35, 20) |
2569 | - td = dt2 - dt1 |
2570 | - p1 = Period(dt1, dt2) |
2571 | - self.assertEquals(p1.start, dt1) |
2572 | - self.assertEquals(p1.end, dt2) |
2573 | - self.assertEquals(p1.duration, td) |
2574 | - |
2575 | - p2 = Period(dt1, td) |
2576 | - self.assertEquals(p2.start, dt1) |
2577 | - self.assertEquals(p2.end, dt2) |
2578 | - self.assertEquals(p2.duration, td) |
2579 | - |
2580 | - self.assertEquals(p1, p2) |
2581 | - |
2582 | - p = Period(dt1, timedelta(0)) |
2583 | - self.assertEquals(p.start, dt1) |
2584 | - self.assertEquals(p.end, dt1) |
2585 | - self.assertEquals(p.duration, timedelta(0)) |
2586 | - |
2587 | - self.assertRaises(ValueError, Period, dt2, dt1) |
2588 | - self.assertRaises(ValueError, Period, dt1, -td) |
2589 | - |
2590 | - def test_overlap(self): |
2591 | - from schoolbell.icalendar import Period |
2592 | - p1 = Period(datetime(2004, 1, 1, 12, 0), timedelta(hours=1)) |
2593 | - p2 = Period(datetime(2004, 1, 1, 11, 30), timedelta(hours=1)) |
2594 | - p3 = Period(datetime(2004, 1, 1, 12, 30), timedelta(hours=1)) |
2595 | - p4 = Period(datetime(2004, 1, 1, 11, 0), timedelta(hours=3)) |
2596 | - |
2597 | - self.assert_(p1.overlaps(p2)) |
2598 | - self.assert_(p2.overlaps(p1)) |
2599 | - |
2600 | - self.assert_(p1.overlaps(p3)) |
2601 | - self.assert_(p3.overlaps(p1)) |
2602 | - |
2603 | - self.assert_(not p2.overlaps(p3)) |
2604 | - self.assert_(not p3.overlaps(p2)) |
2605 | - |
2606 | - self.assert_(p1.overlaps(p4)) |
2607 | - self.assert_(p4.overlaps(p1)) |
2608 | - |
2609 | - self.assert_(p1.overlaps(p1)) |
2610 | - |
2611 | - |
2612 | -class TestVEvent(unittest.TestCase): |
2613 | - |
2614 | - def test_add(self): |
2615 | - from schoolbell.icalendar import VEvent, ICalParseError |
2616 | - vevent = VEvent() |
2617 | - value, params = 'bar', {'VALUE': 'TEXT'} |
2618 | - vevent.add('foo', value, params) |
2619 | - self.assertEquals(vevent._props, {'FOO': [(value, params)]}) |
2620 | - value2 = 'guug' |
2621 | - vevent.add('fie', value2) |
2622 | - self.assertEquals(vevent._props, {'FOO': [(value, params)], |
2623 | - 'FIE': [(value2, {})]}) |
2624 | - vevent.add('fie', value, params) |
2625 | - self.assertEquals(vevent._props, {'FOO': [(value, params)], |
2626 | - 'FIE': [(value2, {}), |
2627 | - (value, params)]}) |
2628 | - |
2629 | - # adding a singleton property twice |
2630 | - vevent.add('uid', '1') |
2631 | - self.assertRaises(ICalParseError, vevent.add, 'uid', '2') |
2632 | - |
2633 | - def test_hasProp(self): |
2634 | - from schoolbell.icalendar import VEvent |
2635 | - vevent = VEvent() |
2636 | - vevent.add('foo', 'bar', {}) |
2637 | - self.assert_(vevent.hasProp('foo')) |
2638 | - self.assert_(vevent.hasProp('Foo')) |
2639 | - self.assert_(not vevent.hasProp('baz')) |
2640 | - |
2641 | - def test__getType(self): |
2642 | - from schoolbell.icalendar import VEvent |
2643 | - vevent = VEvent() |
2644 | - vevent.add('x-explicit', '', {'VALUE': 'INTEGER'}) |
2645 | - vevent.add('dtstart', 'implicit type', {}) |
2646 | - vevent.add('x-default', '', {}) |
2647 | - self.assertEquals(vevent._getType('x-explicit'), 'INTEGER') |
2648 | - self.assertEquals(vevent._getType('dtstart'), 'DATE-TIME') |
2649 | - self.assertEquals(vevent._getType('x-default'), 'TEXT') |
2650 | - self.assertEquals(vevent._getType('X-Explicit'), 'INTEGER') |
2651 | - self.assertEquals(vevent._getType('DtStart'), 'DATE-TIME') |
2652 | - self.assertEquals(vevent._getType('X-Default'), 'TEXT') |
2653 | - self.assertRaises(KeyError, vevent._getType, 'nonexistent') |
2654 | - |
2655 | - def test_getOne(self): |
2656 | - from schoolbell.icalendar import VEvent |
2657 | - vevent = VEvent() |
2658 | - |
2659 | - vevent.add('foo', 'bar', {}) |
2660 | - self.assertEquals(vevent.getOne('foo'), 'bar') |
2661 | - self.assertEquals(vevent.getOne('Foo'), 'bar') |
2662 | - self.assertEquals(vevent.getOne('baz'), None) |
2663 | - self.assertEquals(vevent.getOne('baz', 'quux'), 'quux') |
2664 | - self.assertEquals(vevent.getOne('dtstart', 'quux'), 'quux') |
2665 | - |
2666 | - vevent.add('int-foo', '42', {'VALUE': 'INTEGER'}) |
2667 | - vevent.add('int-bad', 'xyzzy', {'VALUE': 'INTEGER'}) |
2668 | - self.assertEquals(vevent.getOne('int-foo'), 42) |
2669 | - self.assertEquals(vevent.getOne('Int-Foo'), 42) |
2670 | - self.assertRaises(ValueError, vevent.getOne, 'int-bad') |
2671 | - |
2672 | - vevent.add('date-foo', '20030405', {'VALUE': 'DATE'}) |
2673 | - vevent.add('date-bad1', '20030405T1234', {'VALUE': 'DATE'}) |
2674 | - vevent.add('date-bad2', '2003', {'VALUE': 'DATE'}) |
2675 | - vevent.add('date-bad3', '200301XX', {'VALUE': 'DATE'}) |
2676 | - self.assertEquals(vevent.getOne('date-Foo'), date(2003, 4, 5)) |
2677 | - self.assertRaises(ValueError, vevent.getOne, 'date-bad1') |
2678 | - self.assertRaises(ValueError, vevent.getOne, 'date-bad2') |
2679 | - self.assertRaises(ValueError, vevent.getOne, 'date-bad3') |
2680 | - |
2681 | - vevent.add('datetime-foo1', '20030405T060708', {'VALUE': 'DATE-TIME'}) |
2682 | - vevent.add('datetime-foo2', '20030405T060708', {'VALUE': 'DATE-TIME'}) |
2683 | - vevent.add('datetime-bad1', '20030405T010203444444', |
2684 | - {'VALUE': 'DATE-TIME'}) |
2685 | - vevent.add('datetime-bad2', '2003', {'VALUE': 'DATE-TIME'}) |
2686 | - self.assertEquals(vevent.getOne('datetime-foo1'), |
2687 | - datetime(2003, 4, 5, 6, 7, 8)) |
2688 | - self.assertEquals(vevent.getOne('Datetime-Foo2'), |
2689 | - datetime(2003, 4, 5, 6, 7, 8)) |
2690 | - self.assertRaises(ValueError, vevent.getOne, 'datetime-bad1') |
2691 | - self.assertRaises(ValueError, vevent.getOne, 'datetime-bad2') |
2692 | - |
2693 | - vevent.add('dur-foo1', '+P11D', {'VALUE': 'DURATION'}) |
2694 | - vevent.add('dur-foo2', '-P2W', {'VALUE': 'DURATION'}) |
2695 | - vevent.add('dur-foo3', 'P1DT2H3M4S', {'VALUE': 'DURATION'}) |
2696 | - vevent.add('dur-foo4', 'PT2H', {'VALUE': 'DURATION'}) |
2697 | - vevent.add('dur-bad1', 'xyzzy', {'VALUE': 'DURATION'}) |
2698 | - self.assertEquals(vevent.getOne('dur-foo1'), timedelta(days=11)) |
2699 | - self.assertEquals(vevent.getOne('Dur-Foo2'), -timedelta(weeks=2)) |
2700 | - self.assertEquals(vevent.getOne('Dur-Foo3'), |
2701 | - timedelta(days=1, hours=2, minutes=3, seconds=4)) |
2702 | - self.assertEquals(vevent.getOne('DUR-FOO4'), timedelta(hours=2)) |
2703 | - self.assertRaises(ValueError, vevent.getOne, 'dur-bad1') |
2704 | - |
2705 | - vevent.add('unknown', 'magic', {'VALUE': 'UNKNOWN-TYPE'}) |
2706 | - self.assertEquals(vevent.getOne('unknown'), 'magic') |
2707 | - |
2708 | - def test_iterDates(self): |
2709 | - from schoolbell.icalendar import VEvent |
2710 | - vevent = VEvent() |
2711 | - vevent.all_day_event = True |
2712 | - vevent.dtstart = date(2003, 1, 2) |
2713 | - vevent.dtend = date(2003, 1, 5) |
2714 | - vevent.duration = timedelta(days=3) |
2715 | - vevent.rdates = [] |
2716 | - vevent.exdates = [] |
2717 | - self.assertEquals(list(vevent.iterDates()), |
2718 | - [date(2003, 1, 2), date(2003, 1, 3), date(2003, 1, 4)]) |
2719 | - |
2720 | - vevent.all_day_event = False; |
2721 | - self.assertRaises(ValueError, list, vevent.iterDates()) |
2722 | - |
2723 | - def test_iterDates_with_rdate_exdate(self): |
2724 | - from schoolbell.icalendar import VEvent |
2725 | - vevent = VEvent() |
2726 | - vevent.all_day_event = True |
2727 | - vevent.dtstart = date(2003, 1, 5) |
2728 | - vevent.dtend = date(2003, 1, 6) |
2729 | - vevent.duration = timedelta(days=1) |
2730 | - vevent.rdates = [date(2003, 1, 2), date(2003, 1, 8), date(2003, 1, 8)] |
2731 | - vevent.exdates = [] |
2732 | - expected = [date(2003, 1, 2), date(2003, 1, 5), date(2003, 1, 8)] |
2733 | - self.assertEquals(list(vevent.iterDates()), expected) |
2734 | - |
2735 | - vevent.exdates = [date(2003, 1, 6)] |
2736 | - expected = [date(2003, 1, 2), date(2003, 1, 5), date(2003, 1, 8)] |
2737 | - self.assertEquals(list(vevent.iterDates()), expected) |
2738 | - |
2739 | - vevent.exdates = [date(2003, 1, 2), date(2003, 1, 2)] |
2740 | - expected = [date(2003, 1, 5), date(2003, 1, 8)] |
2741 | - self.assertEquals(list(vevent.iterDates()), expected) |
2742 | - |
2743 | - vevent.exdates = [date(2003, 1, 5)] |
2744 | - expected = [date(2003, 1, 2), date(2003, 1, 8)] |
2745 | - self.assertEquals(list(vevent.iterDates()), expected) |
2746 | - |
2747 | - vevent.dtend = date(2003, 1, 7) |
2748 | - vevent.duration = timedelta(days=2) |
2749 | - vevent.exdates = [date(2003, 1, 5), date(2003, 1, 3)] |
2750 | - expected = [date(2003, 1, 2), date(2003, 1, 3), |
2751 | - date(2003, 1, 8), date(2003, 1, 9)] |
2752 | - self.assertEquals(list(vevent.iterDates()), expected) |
2753 | - |
2754 | - def test_validate_error_cases(self): |
2755 | - from schoolbell.icalendar import VEvent, ICalParseError |
2756 | - |
2757 | - vevent = VEvent() |
2758 | - self.assertRaises(ICalParseError, vevent.validate) |
2759 | - |
2760 | - vevent = VEvent() |
2761 | - vevent.add('dtstart', 'xyzzy', {'VALUE': 'TEXT'}) |
2762 | - self.assertRaises(ICalParseError, vevent.validate) |
2763 | - |
2764 | - vevent = VEvent() |
2765 | - vevent.add('dtstart', '20010203', {'VALUE': 'DATE'}) |
2766 | - vevent.add('dtend', '20010203T0000', {'VALUE': 'DATE-TIME'}) |
2767 | - self.assertRaises(ICalParseError, vevent.validate) |
2768 | - |
2769 | - vevent = VEvent() |
2770 | - vevent.add('dtstart', '20010203', {'VALUE': 'DATE'}) |
2771 | - vevent.add('dtend', '20010203', {'VALUE': 'DATE'}) |
2772 | - vevent.add('duration', 'P1D', {}) |
2773 | - self.assertRaises(ICalParseError, vevent.validate) |
2774 | - |
2775 | - vevent = VEvent() |
2776 | - vevent.add('dtstart', '20010203', {'VALUE': 'DATE'}) |
2777 | - vevent.add('duration', 'two years', {'VALUE': 'TEXT'}) |
2778 | - self.assertRaises(ICalParseError, vevent.validate) |
2779 | - |
2780 | - def test_validate_all_day_events(self): |
2781 | - from schoolbell.icalendar import VEvent, ICalParseError |
2782 | - |
2783 | - vevent = VEvent() |
2784 | - vevent.add('summary', 'An event', {}) |
2785 | - vevent.add('uid', 'unique', {}) |
2786 | - vevent.add('dtstart', '20010203', {'VALUE': 'DATE'}) |
2787 | - vevent.validate() |
2788 | - self.assert_(vevent.all_day_event) |
2789 | - self.assertEquals(vevent.summary, 'An event') |
2790 | - self.assertEquals(vevent.uid, 'unique') |
2791 | - self.assertEquals(vevent.dtend, date(2001, 2, 4)) |
2792 | - self.assertEquals(vevent.duration, timedelta(days=1)) |
2793 | - |
2794 | - vevent = VEvent() |
2795 | - vevent.add('summary', 'An\\nevent\\; with backslashes', {}) |
2796 | - vevent.add('uid', 'unique2', {}) |
2797 | - vevent.add('dtstart', '20010203', {'VALUE': 'DATE'}) |
2798 | - vevent.add('dtend', '20010205', {'VALUE': 'DATE'}) |
2799 | - vevent.validate() |
2800 | - self.assertEquals(vevent.summary, 'An\nevent; with backslashes') |
2801 | - self.assert_(vevent.all_day_event) |
2802 | - self.assertEquals(vevent.dtstart, date(2001, 2, 3)) |
2803 | - self.assertEquals(vevent.uid, 'unique2') |
2804 | - self.assertEquals(vevent.dtend, date(2001, 2, 5)) |
2805 | - self.assertEquals(vevent.duration, timedelta(days=2)) |
2806 | - |
2807 | - vevent = VEvent() |
2808 | - vevent.add('dtstart', '20010203', {'VALUE': 'DATE'}) |
2809 | - vevent.add('uid', 'unique3', {}) |
2810 | - vevent.add('duration', 'P2D') |
2811 | - vevent.validate() |
2812 | - self.assertEquals(vevent.summary, None) |
2813 | - self.assert_(vevent.all_day_event) |
2814 | - self.assertEquals(vevent.dtstart, date(2001, 2, 3)) |
2815 | - self.assertEquals(vevent.uid, 'unique3') |
2816 | - self.assertEquals(vevent.dtend, date(2001, 2, 5)) |
2817 | - self.assertEquals(vevent.duration, timedelta(days=2)) |
2818 | - |
2819 | - vevent = VEvent() |
2820 | - vevent.add('dtstart', '20010203', {'VALUE': 'DATE'}) |
2821 | - vevent.add('uid', 'unique4', {}) |
2822 | - vevent.add('dtend', '20010201', {'VALUE': 'DATE'}) |
2823 | - self.assertRaises(ICalParseError, vevent.validate) |
2824 | - |
2825 | - vevent = VEvent() |
2826 | - vevent.add('dtstart', '20010203', {'VALUE': 'DATE'}) |
2827 | - vevent.add('uid', 'unique5', {}) |
2828 | - vevent.add('dtend', '20010203', {'VALUE': 'DATE'}) |
2829 | - self.assertRaises(ICalParseError, vevent.validate) |
2830 | - |
2831 | - def test_validate_not_all_day_events(self): |
2832 | - from schoolbell.icalendar import VEvent, ICalParseError |
2833 | - |
2834 | - vevent = VEvent() |
2835 | - vevent.add('dtstart', '20010203T040506') |
2836 | - vevent.add('uid', 'unique', {}) |
2837 | - vevent.validate() |
2838 | - self.assert_(not vevent.all_day_event) |
2839 | - self.assertEquals(vevent.dtstart, datetime(2001, 2, 3, 4, 5, 6)) |
2840 | - self.assertEquals(vevent.dtend, datetime(2001, 2, 3, 4, 5, 6)) |
2841 | - self.assertEquals(vevent.duration, timedelta(days=0)) |
2842 | - self.assertEquals(vevent.rdates, []) |
2843 | - |
2844 | - vevent = VEvent() |
2845 | - vevent.add('dtstart', '20010203T040000') |
2846 | - vevent.add('uid', 'unique', {}) |
2847 | - vevent.add('dtend', '20010204T050102') |
2848 | - vevent.validate() |
2849 | - self.assert_(not vevent.all_day_event) |
2850 | - self.assertEquals(vevent.dtstart, datetime(2001, 2, 3, 4, 0, 0)) |
2851 | - self.assertEquals(vevent.dtend, datetime(2001, 2, 4, 5, 1, 2)) |
2852 | - self.assertEquals(vevent.duration, timedelta(days=1, hours=1, |
2853 | - minutes=1, seconds=2)) |
2854 | - |
2855 | - vevent = VEvent() |
2856 | - vevent.add('dtstart', '20010203T040000') |
2857 | - vevent.add('uid', 'unique', {}) |
2858 | - vevent.add('duration', 'P1DT1H1M2S') |
2859 | - vevent.validate() |
2860 | - self.assert_(not vevent.all_day_event) |
2861 | - self.assertEquals(vevent.dtstart, datetime(2001, 2, 3, 4, 0, 0)) |
2862 | - self.assertEquals(vevent.dtend, datetime(2001, 2, 4, 5, 1, 2)) |
2863 | - self.assertEquals(vevent.duration, timedelta(days=1, hours=1, |
2864 | - minutes=1, seconds=2)) |
2865 | - |
2866 | - vevent = VEvent() |
2867 | - vevent.add('dtstart', '20010203T010203') |
2868 | - vevent.add('uid', 'unique', {}) |
2869 | - vevent.add('rdate', '20010205T040506') |
2870 | - vevent.add('exdate', '20010206T040506') |
2871 | - vevent.validate() |
2872 | - self.assertEquals(vevent.rdates, [datetime(2001, 2, 5, 4, 5, 6)]) |
2873 | - self.assertEquals(vevent.exdates, [datetime(2001, 2, 6, 4, 5, 6)]) |
2874 | - |
2875 | - vevent = VEvent() |
2876 | - vevent.add('dtstart', '20010203T010203') |
2877 | - vevent.add('uid', 'unique', {}) |
2878 | - vevent.add('exdate', '20010206,20020307', {'VALUE': 'DATE'}) |
2879 | - vevent.add('rrule', 'FREQ=DAILY') |
2880 | - vevent.validate() |
2881 | - self.assertEquals(vevent.exdates, [date(2001, 2, 6), date(2002, 3, 7)]) |
2882 | - |
2883 | - vevent = VEvent() |
2884 | - vevent.add('dtstart', '20010203T010203') |
2885 | - vevent.add('uid', 'unique', {}) |
2886 | - vevent.add('dtend', '20010203T010202') |
2887 | - self.assertRaises(ICalParseError, vevent.validate) |
2888 | - |
2889 | - def test_validate_location(self): |
2890 | - from schoolbell.icalendar import VEvent |
2891 | - vevent = VEvent() |
2892 | - vevent.add('dtstart', '20010203T040506') |
2893 | - vevent.add('uid', 'unique5', {}) |
2894 | - vevent.add('location', 'Somewhere') |
2895 | - vevent.validate() |
2896 | - self.assertEquals(vevent.location, 'Somewhere') |
2897 | - |
2898 | -# TODO: recurring events |
2899 | -## def test_validate_rrule(self): |
2900 | -## from schoolbell.icalendar import VEvent |
2901 | -## vevent = VEvent() |
2902 | -## vevent.add('dtstart', '20010203T040506') |
2903 | -## vevent.add('uid', 'unique5', {}) |
2904 | -## vevent.add('location', 'Somewhere') |
2905 | -## vevent.add('rrule', 'FREQ=DAILY;COUNT=3') |
2906 | -## vevent.validate() |
2907 | -## |
2908 | -## self.assertEquals(vevent.rrule.interval, 1) |
2909 | -## self.assertEquals(vevent.rrule.count, 3) |
2910 | -## self.assertEquals(vevent.rrule.until, None) |
2911 | -## self.assertEquals(vevent.rrule.exceptions, ()) |
2912 | -## |
2913 | -## def test_validate_rrule_exceptions(self): |
2914 | -## from schoolbell.icalendar import VEvent |
2915 | -## vevent = VEvent() |
2916 | -## vevent.add('dtstart', '20010203T040506') |
2917 | -## vevent.add('uid', 'unique5', {}) |
2918 | -## vevent.add('location', 'Somewhere') |
2919 | -## vevent.add('rrule', 'FREQ=MONTHLY;BYDAY=3MO') |
2920 | -## vevent.add('exdate', '19960402T010000,19960404T010000',) |
2921 | -## vevent.validate() |
2922 | -## |
2923 | -## self.assertEquals(vevent.rrule.interval, 1) |
2924 | -## self.assertEquals(vevent.rrule.count, None) |
2925 | -## self.assertEquals(vevent.rrule.until, None) |
2926 | -## self.assertEquals(vevent.rrule.monthly, 'weekday') |
2927 | -## self.assertEquals(vevent.rrule.exceptions, |
2928 | -## (date(1996, 4, 2), date(1996, 4, 4))) |
2929 | -## self.assert_(not isinstance(vevent.rrule.exceptions[0], datetime)) |
2930 | - |
2931 | - def test_extractListOfDates(self): |
2932 | - from schoolbell.icalendar import VEvent, Period, ICalParseError |
2933 | - |
2934 | - vevent = VEvent() |
2935 | - vevent.add('rdate', '20010205T040506') |
2936 | - vevent.add('rdate', '20010206T040506,20010207T000000') |
2937 | - vevent.add('rdate', '20010208', {'VALUE': 'DATE'}) |
2938 | - vevent.add('rdate', '20010209T000000/20010210T000000', |
2939 | - {'VALUE': 'PERIOD'}) |
2940 | - rdates = vevent._extractListOfDates('RDATE', vevent.rdate_types, False) |
2941 | - expected = [datetime(2001, 2, 5, 4, 5, 6), |
2942 | - datetime(2001, 2, 6, 4, 5, 6), |
2943 | - datetime(2001, 2, 7, 0, 0, 0), |
2944 | - date(2001, 2, 8), |
2945 | - Period(datetime(2001, 2, 9), datetime(2001, 2, 10)), |
2946 | - ] |
2947 | - self.assertEqual(expected, rdates, |
2948 | - diff(pformat(expected), pformat(rdates))) |
2949 | - |
2950 | - vevent = VEvent() |
2951 | - vevent.add('rdate', '20010205T040506', {'VALUE': 'TEXT'}) |
2952 | - self.assertRaises(ICalParseError, vevent._extractListOfDates, 'RDATE', |
2953 | - vevent.rdate_types, False) |
2954 | - |
2955 | - vevent = VEvent() |
2956 | - vevent.add('exdate', '20010205T040506/P1D', {'VALUE': 'PERIOD'}) |
2957 | - self.assertRaises(ICalParseError, vevent._extractListOfDates, 'EXDATE', |
2958 | - vevent.exdate_types, False) |
2959 | - |
2960 | - vevent = VEvent() |
2961 | - vevent.add('rdate', '20010208', {'VALUE': 'DATE'}) |
2962 | - rdates = vevent._extractListOfDates('RDATE', vevent.rdate_types, True) |
2963 | - expected = [date(2001, 2, 8)] |
2964 | - self.assertEqual(expected, rdates, |
2965 | - diff(pformat(expected), pformat(rdates))) |
2966 | - |
2967 | - vevent = VEvent() |
2968 | - vevent.add('rdate', '20010205T040506', {'VALUE': 'DATE-TIME'}) |
2969 | - self.assertRaises(ICalParseError, vevent._extractListOfDates, 'RDATE', |
2970 | - vevent.rdate_types, True) |
2971 | - |
2972 | - vevent = VEvent() |
2973 | - vevent.add('rdate', '20010209T000000/20010210T000000', |
2974 | - {'VALUE': 'PERIOD'}) |
2975 | - self.assertRaises(ICalParseError, vevent._extractListOfDates, 'RDATE', |
2976 | - vevent.rdate_types, True) |
2977 | - |
2978 | - |
2979 | -class TestICalReader(unittest.TestCase): |
2980 | - |
2981 | - example_ical = dedent("""\ |
2982 | - BEGIN:VCALENDAR |
2983 | - VERSION |
2984 | - :2.0 |
2985 | - PRODID |
2986 | - :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN |
2987 | - METHOD |
2988 | - :PUBLISH |
2989 | - BEGIN:VEVENT |
2990 | - UID |
2991 | - :956630271 |
2992 | - SUMMARY |
2993 | - :Christmas Day |
2994 | - CLASS |
2995 | - :PUBLIC |
2996 | - X-MOZILLA-ALARM-DEFAULT-UNITS |
2997 | - :minutes |
2998 | - X-MOZILLA-ALARM-DEFAULT-LENGTH |
2999 | - :15 |
3000 | - X-MOZILLA-RECUR-DEFAULT-UNITS |
3001 | - :weeks |
3002 | - X-MOZILLA-RECUR-DEFAULT-INTERVAL |
3003 | - :1 |
3004 | - DTSTART |
3005 | - ;VALUE=DATE |
3006 | - :20031225 |
3007 | - DTEND |
3008 | - ;VALUE=DATE |
3009 | - :20031226 |
3010 | - DTSTAMP |
3011 | - :20020430T114937Z |
3012 | - END:VEVENT |
3013 | - END:VCALENDAR |
3014 | - BEGIN:VCALENDAR |
3015 | - VERSION |
3016 | - :2.0 |
3017 | - PRODID |
3018 | - :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN |
3019 | - METHOD |
3020 | - :PUBLISH |
3021 | - BEGIN:VEVENT |
3022 | - UID |
3023 | - :911737808 |
3024 | - SUMMARY |
3025 | - :Boxing Day |
3026 | - CLASS |
3027 | - :PUBLIC |
3028 | - X-MOZILLA-ALARM-DEFAULT-UNITS |
3029 | - :minutes |
3030 | - X-MOZILLA-ALARM-DEFAULT-LENGTH |
3031 | - :15 |
3032 | - X-MOZILLA-RECUR-DEFAULT-UNITS |
3033 | - :weeks |
3034 | - X-MOZILLA-RECUR-DEFAULT-INTERVAL |
3035 | - :1 |
3036 | - DTSTART |
3037 | - ;VALUE=DATE |
3038 | - :20030501 |
3039 | - DTSTAMP |
3040 | - :20020430T114937Z |
3041 | - END:VEVENT |
3042 | - BEGIN:VEVENT |
3043 | - UID |
3044 | - :wh4t3v3r |
3045 | - DTSTART;VALUE=DATE:20031225 |
3046 | - SUMMARY:Christmas again! |
3047 | - END:VEVENT |
3048 | - END:VCALENDAR |
3049 | - """) |
3050 | - |
3051 | - def test_iterEvents(self): |
3052 | - from schoolbell.icalendar import ICalReader, ICalParseError |
3053 | - file = StringIO(self.example_ical) |
3054 | - reader = ICalReader(file) |
3055 | - result = list(reader.iterEvents()) |
3056 | - self.assertEqual(len(result), 3) |
3057 | - vevent = result[0] |
3058 | - self.assertEqual(vevent.getOne('x-mozilla-recur-default-units'), |
3059 | - 'weeks') |
3060 | - self.assertEqual(vevent.getOne('dtstart'), date(2003, 12, 25)) |
3061 | - self.assertEqual(vevent.dtstart, date(2003, 12, 25)) |
3062 | - self.assertEqual(vevent.getOne('dtend'), date(2003, 12, 26)) |
3063 | - self.assertEqual(vevent.dtend, date(2003, 12, 26)) |
3064 | - vevent = result[1] |
3065 | - self.assertEqual(vevent.getOne('dtstart'), date(2003, 05, 01)) |
3066 | - self.assertEqual(vevent.dtstart, date(2003, 05, 01)) |
3067 | - vevent = result[2] |
3068 | - self.assertEqual(vevent.getOne('dtstart'), date(2003, 12, 25)) |
3069 | - self.assertEqual(vevent.dtstart, date(2003, 12, 25)) |
3070 | - |
3071 | - reader = ICalReader(StringIO(dedent("""\ |
3072 | - BEGIN:VCALENDAR |
3073 | - BEGIN:VEVENT |
3074 | - UID:hello |
3075 | - DTSTART;VALUE=DATE:20010203 |
3076 | - BEGIN:VALARM |
3077 | - X-PROP:foo |
3078 | - END:VALARM |
3079 | - END:VEVENT |
3080 | - END:VCALENDAR |
3081 | - """))) |
3082 | - result = list(reader.iterEvents()) |
3083 | - self.assertEquals(len(result), 1) |
3084 | - vevent = result[0] |
3085 | - self.assert_(vevent.hasProp('uid')) |
3086 | - self.assert_(vevent.hasProp('dtstart')) |
3087 | - self.assert_(not vevent.hasProp('x-prop')) |
3088 | - |
3089 | - reader = ICalReader(StringIO(dedent("""\ |
3090 | - BEGIN:VCALENDAR |
3091 | - BEGIN:VEVENT |
3092 | - DTSTART;VALUE=DATE:20010203 |
3093 | - END:VEVENT |
3094 | - END:VCALENDAR |
3095 | - """))) |
3096 | - # missing UID |
3097 | - self.assertRaises(ICalParseError, list, reader.iterEvents()) |
3098 | - |
3099 | - reader = ICalReader(StringIO(dedent("""\ |
3100 | - BEGIN:VCALENDAR |
3101 | - BEGIN:VEVENT |
3102 | - DTSTART;VALUE=DATE:20010203 |
3103 | - """))) |
3104 | - self.assertRaises(ICalParseError, list, reader.iterEvents()) |
3105 | - |
3106 | - reader = ICalReader(StringIO(dedent("""\ |
3107 | - BEGIN:VCALENDAR |
3108 | - BEGIN:VEVENT |
3109 | - DTSTART;VALUE=DATE:20010203 |
3110 | - END:VCALENDAR |
3111 | - END:VEVENT |
3112 | - """))) |
3113 | - self.assertRaises(ICalParseError, list, reader.iterEvents()) |
3114 | - |
3115 | - reader = ICalReader(StringIO(dedent("""\ |
3116 | - BEGIN:VCALENDAR |
3117 | - END:VCALENDAR |
3118 | - X-PROP:foo |
3119 | - """))) |
3120 | - self.assertRaises(ICalParseError, list, reader.iterEvents()) |
3121 | - |
3122 | - reader = ICalReader(StringIO(dedent("""\ |
3123 | - BEGIN:VCALENDAR |
3124 | - END:VCALENDAR |
3125 | - BEGIN:VEVENT |
3126 | - END:VEVENT |
3127 | - """))) |
3128 | - self.assertRaises(ICalParseError, list, reader.iterEvents()) |
3129 | - |
3130 | - reader = ICalReader(StringIO(dedent("""\ |
3131 | - BEGIN:VCALENDAR |
3132 | - BEGIN:VEVENT |
3133 | - DTSTART;VALUE=DATE:20010203 |
3134 | - END:VEVENT |
3135 | - END:VCALENDAR |
3136 | - END:UNIVERSE |
3137 | - """))) |
3138 | - self.assertRaises(ICalParseError, list, reader.iterEvents()) |
3139 | - |
3140 | - reader = ICalReader(StringIO(dedent("""\ |
3141 | - DTSTART;VALUE=DATE:20010203 |
3142 | - """))) |
3143 | - self.assertRaises(ICalParseError, list, reader.iterEvents()) |
3144 | - |
3145 | - reader = ICalReader(StringIO(dedent("""\ |
3146 | - This is just plain text |
3147 | - """))) |
3148 | - self.assertRaises(ICalParseError, list, reader.iterEvents()) |
3149 | - |
3150 | - reader = ICalReader(StringIO("")) |
3151 | - self.assertEquals(list(reader.iterEvents()), []) |
3152 | - |
3153 | - def test_iterRow(self): |
3154 | - from schoolbell.icalendar import ICalReader |
3155 | - file = StringIO("key1\n" |
3156 | - " :value1\n" |
3157 | - "key2\n" |
3158 | - " ;VALUE=foo\n" |
3159 | - " :value2\n" |
3160 | - "key3;VALUE=bar:value3\n") |
3161 | - reader = ICalReader(file) |
3162 | - self.assertEqual(list(reader._iterRow()), |
3163 | - [('KEY1', 'value1', {}), |
3164 | - ('KEY2', 'value2', {'VALUE': 'FOO'}), |
3165 | - ('KEY3', 'value3', {'VALUE': 'BAR'})]) |
3166 | - |
3167 | - file = StringIO("key1:value1\n" |
3168 | - "key2;VALUE=foo:value2\n" |
3169 | - "key3;VALUE=bar:value3\n") |
3170 | - reader = ICalReader(file) |
3171 | - self.assertEqual(list(reader._iterRow()), |
3172 | - [('KEY1', 'value1', {}), |
3173 | - ('KEY2', 'value2', {'VALUE': 'FOO'}), |
3174 | - ('KEY3', 'value3', {'VALUE': 'BAR'})]) |
3175 | - |
3176 | - file = StringIO("key1:value:with:colons:in:it\n") |
3177 | - reader = ICalReader(file) |
3178 | - self.assertEqual(list(reader._iterRow()), |
3179 | - [('KEY1', 'value:with:colons:in:it', {})]) |
3180 | - |
3181 | - reader = ICalReader(StringIO("ke\r\n y1\n\t:value\r\n 1 \r\n .")) |
3182 | - self.assertEqual(list(reader._iterRow()), |
3183 | - [('KEY1', 'value 1 .', {})]) |
3184 | - |
3185 | - reader = ICalReader(StringIO("key;param=\xe2\x98\xbb:\r\n" |
3186 | - " value \xe2\x98\xbb\r\n")) |
3187 | - self.assertEqual(list(reader._iterRow()), |
3188 | - [("KEY", u"value \u263B", {'PARAM': u'\u263B'})]) |
3189 | - |
3190 | - def test_parseRow(self): |
3191 | - from schoolbell.icalendar import ICalReader, ICalParseError |
3192 | - parseRow = ICalReader._parseRow |
3193 | - self.assertEqual(parseRow("key:"), ("KEY", "", {})) |
3194 | - self.assertEqual(parseRow("key:value"), ("KEY", "value", {})) |
3195 | - self.assertEqual(parseRow("key:va:lu:e"), ("KEY", "va:lu:e", {})) |
3196 | - self.assertRaises(ICalParseError, parseRow, "key but no value") |
3197 | - self.assertRaises(ICalParseError, parseRow, ":value but no key") |
3198 | - self.assertRaises(ICalParseError, parseRow, "bad name:") |
3199 | - |
3200 | - self.assertEqual(parseRow("key;param=:value"), |
3201 | - ("KEY", "value", {'PARAM': ''})) |
3202 | - self.assertEqual(parseRow("key;param=pvalue:value"), |
3203 | - ("KEY", "value", {'PARAM': 'PVALUE'})) |
3204 | - self.assertEqual(parseRow('key;param=pvalue;param2=value2:value'), |
3205 | - ("KEY", "value", {'PARAM': 'PVALUE', |
3206 | - 'PARAM2': 'VALUE2'})) |
3207 | - self.assertEqual(parseRow('key;param="pvalue":value'), |
3208 | - ("KEY", "value", {'PARAM': 'pvalue'})) |
3209 | - self.assertEqual(parseRow('key;param=pvalue;param2="value2":value'), |
3210 | - ("KEY", "value", {'PARAM': 'PVALUE', |
3211 | - 'PARAM2': 'value2'})) |
3212 | - self.assertRaises(ICalParseError, parseRow, "k;:no param") |
3213 | - self.assertRaises(ICalParseError, parseRow, "k;a?=b:bad param") |
3214 | - self.assertRaises(ICalParseError, parseRow, "k;a=\":bad param") |
3215 | - self.assertRaises(ICalParseError, parseRow, "k;a=\"\177:bad param") |
3216 | - self.assertRaises(ICalParseError, parseRow, "k;a=\001:bad char") |
3217 | - self.assertEqual(parseRow("key;param=a,b,c:value"), |
3218 | - ("KEY", "value", {'PARAM': ['A', 'B', 'C']})) |
3219 | - self.assertEqual(parseRow('key;param=a,"b,c",d:value'), |
3220 | - ("KEY", "value", {'PARAM': ['A', 'b,c', 'D']})) |
3221 | -def diff(old, new, oldlabel="expected output", newlabel="actual output"): |
3222 | - """Display a unified diff between old text and new text.""" |
3223 | - old = old.splitlines() |
3224 | - new = new.splitlines() |
3225 | - |
3226 | - diff = ['--- %s' % oldlabel, '+++ %s' % newlabel] |
3227 | - |
3228 | - def dump(tag, x, lo, hi): |
3229 | - for i in xrange(lo, hi): |
3230 | - diff.append(tag + x[i]) |
3231 | - |
3232 | - differ = difflib.SequenceMatcher(a=old, b=new) |
3233 | - for tag, alo, ahi, blo, bhi in differ.get_opcodes(): |
3234 | - if tag == 'replace': |
3235 | - dump('-', old, alo, ahi) |
3236 | - dump('+', new, blo, bhi) |
3237 | - elif tag == 'delete': |
3238 | - dump('-', old, alo, ahi) |
3239 | - elif tag == 'insert': |
3240 | - dump('+', new, blo, bhi) |
3241 | - elif tag == 'equal': |
3242 | - dump(' ', old, alo, ahi) |
3243 | - else: |
3244 | - raise AssertionError('unknown tag %r' % tag) |
3245 | - return "\n".join(diff) |
3246 | - |
3247 | - |
3248 | - |
3249 | -def test_suite(): |
3250 | - suite = unittest.TestSuite() |
3251 | - suite.addTest(doctest.DocTestSuite('schoolbell.icalendar', |
3252 | - optionflags=doctest.ELLIPSIS | doctest.REPORT_UDIFF)) |
3253 | - suite.addTest(unittest.makeSuite(TestParseDateTime)) |
3254 | - suite.addTest(unittest.makeSuite(TestPeriod)) |
3255 | - suite.addTest(unittest.makeSuite(TestVEvent)) |
3256 | - suite.addTest(unittest.makeSuite(TestICalReader)) |
3257 | - return suite |
3258 | - |
3259 | -if __name__ == '__main__': |
3260 | - unittest.main(defaultTest='test_suite') |
3261 | |
3262 | === removed file 'lib/schoolbell/tests/test_schoolbell.py' |
3263 | --- lib/schoolbell/tests/test_schoolbell.py 2005-10-31 18:29:12 +0000 |
3264 | +++ lib/schoolbell/tests/test_schoolbell.py 1970-01-01 00:00:00 +0000 |
3265 | @@ -1,74 +0,0 @@ |
3266 | -""" |
3267 | -Unit tests for schoolbell |
3268 | - |
3269 | -When this module grows too big, it will be split into a number of modules in |
3270 | -a package called tests. Each of those new modules will be named test_foo.py |
3271 | -and will test schoolbell.foo. |
3272 | -""" |
3273 | - |
3274 | -import unittest |
3275 | -from zope.testing import doctest |
3276 | - |
3277 | - |
3278 | -def doctest_interfaces(): |
3279 | - """Look for syntax errors in interfaces.py |
3280 | - |
3281 | - >>> import schoolbell.interfaces |
3282 | - |
3283 | - """ |
3284 | - |
3285 | - |
3286 | -def doctest_simple_CalendarEventMixin_replace(): |
3287 | - """Make sure CalendarEventMixin.replace does not forget any attributes. |
3288 | - |
3289 | - >>> from schoolbell.interfaces import ICalendarEvent |
3290 | - >>> from zope.schema import getFieldNames |
3291 | - >>> all_attrs = getFieldNames(ICalendarEvent) |
3292 | - |
3293 | - We will use SimpleCalendarEvent which is a trivial subclass of |
3294 | - CalendarEventMixin |
3295 | - |
3296 | - >>> from datetime import datetime, timedelta |
3297 | - >>> from schoolbell.simple import SimpleCalendarEvent |
3298 | - >>> e1 = SimpleCalendarEvent(datetime(2004, 12, 15, 18, 57), |
3299 | - ... timedelta(minutes=15), |
3300 | - ... 'Work on schoolbell.simple') |
3301 | - |
3302 | - >>> for attr in all_attrs: |
3303 | - ... e2 = e1.replace(**{attr: 'new value'}) |
3304 | - ... assert getattr(e2, attr) == 'new value', attr |
3305 | - ... assert e2 != e1, attr |
3306 | - ... assert e2.replace(**{attr: getattr(e1, attr)}) == e1, attr |
3307 | - |
3308 | - """ |
3309 | - |
3310 | - |
3311 | -def doctest_weeknum_bounds(): |
3312 | - """Unit test for schoolbell.utils.weeknum_bounds. |
3313 | - |
3314 | - Check that weeknum_bounds is the reverse of datetime.isocalendar(). |
3315 | - |
3316 | - >>> from datetime import date |
3317 | - >>> from schoolbell.utils import weeknum_bounds |
3318 | - >>> d = date(2000, 1, 1) |
3319 | - >>> while d < date(2010, 1, 1): |
3320 | - ... year, weeknum, weekday = d.isocalendar() |
3321 | - ... l, h = weeknum_bounds(year, weeknum) |
3322 | - ... assert l <= d <= h |
3323 | - ... d += d.resolution |
3324 | - |
3325 | - """ |
3326 | - |
3327 | - |
3328 | -def test_suite(): |
3329 | - suite = unittest.TestSuite() |
3330 | - suite.addTest(doctest.DocTestSuite()) |
3331 | - suite.addTest(doctest.DocTestSuite('schoolbell.mixins')) |
3332 | - suite.addTest(doctest.DocTestSuite('schoolbell.simple')) |
3333 | - suite.addTest(doctest.DocTestSuite('schoolbell.utils')) |
3334 | - suite.addTest(doctest.DocTestSuite('schoolbell.browser', |
3335 | - optionflags=doctest.ELLIPSIS | doctest.REPORT_UDIFF)) |
3336 | - return suite |
3337 | - |
3338 | -if __name__ == '__main__': |
3339 | - unittest.main(defaultTest='test_suite') |
3340 | |
3341 | === removed file 'lib/schoolbell/utils.py' |
3342 | --- lib/schoolbell/utils.py 2005-10-31 18:29:12 +0000 |
3343 | +++ lib/schoolbell/utils.py 1970-01-01 00:00:00 +0000 |
3344 | @@ -1,164 +0,0 @@ |
3345 | -""" |
3346 | -Utility functions for SchoolBell. |
3347 | - |
3348 | -These include various date manipulation routines. |
3349 | -""" |
3350 | - |
3351 | -import calendar |
3352 | -from datetime import datetime, date, timedelta, tzinfo |
3353 | - |
3354 | - |
3355 | -def prev_month(date): |
3356 | - """Calculate the first day of the previous month for a given date. |
3357 | - |
3358 | - >>> prev_month(date(2004, 8, 1)) |
3359 | - datetime.date(2004, 7, 1) |
3360 | - >>> prev_month(date(2004, 8, 31)) |
3361 | - datetime.date(2004, 7, 1) |
3362 | - >>> prev_month(date(2004, 12, 15)) |
3363 | - datetime.date(2004, 11, 1) |
3364 | - >>> prev_month(date(2005, 1, 28)) |
3365 | - datetime.date(2004, 12, 1) |
3366 | - |
3367 | - """ |
3368 | - return (date.replace(day=1) - timedelta(1)).replace(day=1) |
3369 | - |
3370 | - |
3371 | -def next_month(date): |
3372 | - """Calculate the first day of the next month for a given date. |
3373 | - |
3374 | - >>> next_month(date(2004, 8, 1)) |
3375 | - datetime.date(2004, 9, 1) |
3376 | - >>> next_month(date(2004, 8, 31)) |
3377 | - datetime.date(2004, 9, 1) |
3378 | - >>> next_month(date(2004, 12, 15)) |
3379 | - datetime.date(2005, 1, 1) |
3380 | - >>> next_month(date(2004, 2, 28)) |
3381 | - datetime.date(2004, 3, 1) |
3382 | - >>> next_month(date(2004, 2, 29)) |
3383 | - datetime.date(2004, 3, 1) |
3384 | - >>> next_month(date(2005, 2, 28)) |
3385 | - datetime.date(2005, 3, 1) |
3386 | - |
3387 | - """ |
3388 | - return (date.replace(day=28) + timedelta(7)).replace(day=1) |
3389 | - |
3390 | - |
3391 | -def week_start(date, first_day_of_week=0): |
3392 | - """Calculate the first day of the week for a given date. |
3393 | - |
3394 | - Assuming that week starts on Mondays: |
3395 | - |
3396 | - >>> week_start(date(2004, 8, 19)) |
3397 | - datetime.date(2004, 8, 16) |
3398 | - >>> week_start(date(2004, 8, 15)) |
3399 | - datetime.date(2004, 8, 9) |
3400 | - >>> week_start(date(2004, 8, 14)) |
3401 | - datetime.date(2004, 8, 9) |
3402 | - >>> week_start(date(2004, 8, 21)) |
3403 | - datetime.date(2004, 8, 16) |
3404 | - >>> week_start(date(2004, 8, 22)) |
3405 | - datetime.date(2004, 8, 16) |
3406 | - >>> week_start(date(2004, 8, 23)) |
3407 | - datetime.date(2004, 8, 23) |
3408 | - |
3409 | - Assuming that week starts on Sundays: |
3410 | - |
3411 | - >>> import calendar |
3412 | - >>> week_start(date(2004, 8, 19), calendar.SUNDAY) |
3413 | - datetime.date(2004, 8, 15) |
3414 | - >>> week_start(date(2004, 8, 15), calendar.SUNDAY) |
3415 | - datetime.date(2004, 8, 15) |
3416 | - >>> week_start(date(2004, 8, 14), calendar.SUNDAY) |
3417 | - datetime.date(2004, 8, 8) |
3418 | - >>> week_start(date(2004, 8, 21), calendar.SUNDAY) |
3419 | - datetime.date(2004, 8, 15) |
3420 | - >>> week_start(date(2004, 8, 22), calendar.SUNDAY) |
3421 | - datetime.date(2004, 8, 22) |
3422 | - >>> week_start(date(2004, 8, 23), calendar.SUNDAY) |
3423 | - datetime.date(2004, 8, 22) |
3424 | - |
3425 | - """ |
3426 | - assert 0 <= first_day_of_week < 7 |
3427 | - delta = date.weekday() - first_day_of_week |
3428 | - if delta < 0: |
3429 | - delta += 7 |
3430 | - return date - timedelta(delta) |
3431 | - |
3432 | - |
3433 | -def weeknum_bounds(year, weeknum): |
3434 | - """Calculate the inclusive date bounds for a (year, weeknum) tuple. |
3435 | - |
3436 | - Week numbers are as defined in ISO 8601 and returned by |
3437 | - datetime.date.isocalendar(). |
3438 | - |
3439 | - >>> weeknum_bounds(2003, 52) |
3440 | - (datetime.date(2003, 12, 22), datetime.date(2003, 12, 28)) |
3441 | - >>> weeknum_bounds(2004, 1) |
3442 | - (datetime.date(2003, 12, 29), datetime.date(2004, 1, 4)) |
3443 | - >>> weeknum_bounds(2004, 2) |
3444 | - (datetime.date(2004, 1, 5), datetime.date(2004, 1, 11)) |
3445 | - |
3446 | - """ |
3447 | - # The first week of a year is at least 4 days long, so January 4th |
3448 | - # is in the first week. |
3449 | - firstweek = week_start(date(year, 1, 4), calendar.MONDAY) |
3450 | - # move forward to the right week number |
3451 | - weekstart = firstweek + timedelta(weeks=weeknum-1) |
3452 | - weekend = weekstart + timedelta(days=6) |
3453 | - return (weekstart, weekend) |
3454 | - |
3455 | - |
3456 | -def check_weeknum(year, weeknum): |
3457 | - """Check to see whether a (year, weeknum) tuple refers to a real |
3458 | - ISO week number. |
3459 | - |
3460 | - >>> check_weeknum(2004, 1) |
3461 | - True |
3462 | - >>> check_weeknum(2004, 53) |
3463 | - True |
3464 | - >>> check_weeknum(2004, 0) |
3465 | - False |
3466 | - >>> check_weeknum(2004, 54) |
3467 | - False |
3468 | - >>> check_weeknum(2003, 52) |
3469 | - True |
3470 | - >>> check_weeknum(2003, 53) |
3471 | - False |
3472 | - |
3473 | - """ |
3474 | - weekstart, weekend = weeknum_bounds(year, weeknum) |
3475 | - isoyear, isoweek, isoday = weekstart.isocalendar() |
3476 | - return (year, weeknum) == (isoyear, isoweek) |
3477 | - |
3478 | -class Slots(dict): |
3479 | - """A dict with automatic key selection. |
3480 | - |
3481 | - The add method automatically selects the lowest unused numeric key |
3482 | - (starting from 0). |
3483 | - |
3484 | - Example: |
3485 | - |
3486 | - >>> s = Slots() |
3487 | - >>> s.add("first") |
3488 | - >>> s |
3489 | - {0: 'first'} |
3490 | - |
3491 | - >>> s.add("second") |
3492 | - >>> s |
3493 | - {0: 'first', 1: 'second'} |
3494 | - |
3495 | - The keys can be reused: |
3496 | - |
3497 | - >>> del s[0] |
3498 | - >>> s.add("third") |
3499 | - >>> s |
3500 | - {0: 'third', 1: 'second'} |
3501 | - |
3502 | - """ |
3503 | - |
3504 | - def add(self, obj): |
3505 | - i = 0 |
3506 | - while i in self: |
3507 | - i += 1 |
3508 | - self[i] = obj |
LAND IT!