Merge lp:~salgado/launchpad/remove-crap into lp:launchpad

Proposed by Guilherme Salgado
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
Reviewer Review Type Date Requested Status
Jonathan Lange (community) Approve
Review via email: mp+21549@code.launchpad.net

Description of the change

Just remove some crap that was no longer used:

  lib/schoolbell/
  lib/canonical/launchpad/components/cal.py
  lib/canonical/launchpad/components/crowd.py

= 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/launchpad/interfaces/launchpad.py
  lib/canonical/launchpad/zcml/configure.zcml

== Pyflakes notices ==

lib/canonical/launchpad/interfaces/launchpad.py
    25: 'UnexpectedFormData' imported but unused
    25: 'UnsafeFormGetSubmissionError' imported but unused
    25: 'NotFoundError' imported but unused
    25: 'IBasicLaunchpadRequest' imported but unused
    25: 'IOpenLaunchBag' imported but unused
    25: 'ILaunchpadRoot' imported but unused
    25: 'ILaunchBag' imported but unused

== Pylint notices ==

lib/canonical/launchpad/interfaces/launchpad.py
    17: [F0401] Unable to import 'lazr.restful.interfaces' (No module named restful)

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

LAND IT!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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