[CalendarServer-changes] [7697] CalendarServer/branches/users/cdaboo/timezones

source_changes at macosforge.org source_changes at macosforge.org
Thu Jun 30 13:44:14 PDT 2011


Revision: 7697
          http://trac.macosforge.org/projects/calendarserver/changeset/7697
Author:   cdaboo at apple.com
Date:     2011-06-30 13:44:13 -0700 (Thu, 30 Jun 2011)
Log Message:
-----------
Merge from trunk.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/timezones/benchmark
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/provision/root.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/provision/test/test_root.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tap/util.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/changeip_calendar.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/export.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/notifications.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/principals.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/purge.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/resources.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_changeip.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_gateway.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_purge_old_events.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_resources.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/util.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/warmup.py
    CalendarServer/branches/users/cdaboo/timezones/conf/caldavd-apple.plist
    CalendarServer/branches/users/cdaboo/timezones/conf/caldavd-test.plist
    CalendarServer/branches/users/cdaboo/timezones/contrib/certupdate/calendarcertupdate.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/migration/calendarmigrator.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/migration/test/test_migrator.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/benchmark.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/config.plist
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/ical.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/logger.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/population.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/profiles.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/request-data/sl_post_availability.request
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/sim.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_ical.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_profiles.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_sim.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/run.sh
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/setbackend.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/stats.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/test_stats.py
    CalendarServer/branches/users/cdaboo/timezones/doc/calendarserver_monitor_notifications.8
    CalendarServer/branches/users/cdaboo/timezones/pyflakes
    CalendarServer/branches/users/cdaboo/timezones/run
    CalendarServer/branches/users/cdaboo/timezones/setup.py
    CalendarServer/branches/users/cdaboo/timezones/support/build.sh
    CalendarServer/branches/users/cdaboo/timezones/support/py.sh
    CalendarServer/branches/users/cdaboo/timezones/test
    CalendarServer/branches/users/cdaboo/timezones/twext/enterprise/dal/syntax.py
    CalendarServer/branches/users/cdaboo/timezones/twext/enterprise/dal/test/test_sqlsyntax.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/caldavxml.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/datafilters/test/test_peruserdata.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/dateops.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/augment.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/directory.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/ldapdirectory.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/principal.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/augments-test-default.xml
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/augments-test.xml
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_aggregate.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_augment.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_livedirectory.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_opendirectory.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_principal.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_proxyprincipaldb.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_proxyprincipalmembers.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_xmlfile.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/wiki.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/xmlaugmentsparser.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/xmlfile.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directorybackedaddressbook.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/ical.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/instance.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/localization.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/mail.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/get.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/put_addressbook_common.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/report_calendar_query.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/report_multiget_common.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/notify.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/addressbookquery.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/addressbookqueryfilter.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/calendarquery.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/calendarqueryfilter.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/expression.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/sqlgenerator.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/resource.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/schedule.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/implicit.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/scheduler.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/sharing.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/storebridge.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3184A66-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3184D26-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185326-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31854DA-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31856AC-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318585A-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185A14-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185BBD-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185D63-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185F20-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31860C8-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318627C-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186426-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31865E4-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186792-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186938-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186ADE-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186C96-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186E3A-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186FE7-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318719A-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3187343-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188906-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188B3A-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188CFF-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188EAA-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189058-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189203-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31893C2-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189572-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189716-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31898D4-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189A88-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189C32-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189DEC-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189F94-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A148-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A2F3-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A4BA-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A6E1-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A898-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318AA54-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318ABFE-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318ADAA-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318AF53-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318B108-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318B2D2-1ED0-11D9-A5E0-000A958A3252.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_calendarquery.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_icalendar.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_mail.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_multiget.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_resource.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_schedule.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_sharing.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_timezonestdservice.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_validation.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_wrapping.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_xml.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/util.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/upgrade.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/vcard.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/base/datastore/subpostgres.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/base/propertystore/test/test_sql.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/file.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/index_file.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/scheduling.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/common.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_file.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_index_file.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_scheduling.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/util.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/icalendarstore.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/file.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/index_file.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/test/common.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/test/test_sql.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/util.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/iaddressbookstore.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/file.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/sql.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/sql_legacy.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/test/test_util.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/test/util.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_calverify
    CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_dbinspect
    CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_shell
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/calverify.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/cmdline.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/dbinspect.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/shell.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/tables.py
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_export.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/accounts.csv
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_trafficlogger.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/trafficlogger.py
    CalendarServer/branches/users/cdaboo/timezones/contrib/tools/backup
    CalendarServer/branches/users/cdaboo/timezones/contrib/tools/fakecalendardata.py
    CalendarServer/branches/users/cdaboo/timezones/lib-patches/cx_Oracle/
    CalendarServer/branches/users/cdaboo/timezones/lib-patches/cx_Oracle/bytes-per-nclob-character.patch
    CalendarServer/branches/users/cdaboo/timezones/sim
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/test/test_expression.py
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/AnotherEvent.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/OneEvent.ics
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/ThirdEvent.ics

Removed Paths:
-------------
    CalendarServer/branches/users/cdaboo/timezones/bin/carddavd
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/accounts.txt
    CalendarServer/branches/users/cdaboo/timezones/contrib/performance/todo
    CalendarServer/branches/users/cdaboo/timezones/lib-patches/cx_Oracle/bytes-per-nclob-character.patch

Property Changed:
----------------
    CalendarServer/branches/users/cdaboo/timezones/
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/platform/
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/platform/darwin/
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/platform/darwin/od/
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/provision/
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tap/
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/webadmin/
    CalendarServer/branches/users/cdaboo/timezones/calendarserver/webcal/
    CalendarServer/branches/users/cdaboo/timezones/support/
    CalendarServer/branches/users/cdaboo/timezones/support/build.sh
    CalendarServer/branches/users/cdaboo/timezones/twext/
    CalendarServer/branches/users/cdaboo/timezones/twext/enterprise/
    CalendarServer/branches/users/cdaboo/timezones/twext/enterprise/dal/
    CalendarServer/branches/users/cdaboo/timezones/twext/internet/
    CalendarServer/branches/users/cdaboo/timezones/twext/protocols/
    CalendarServer/branches/users/cdaboo/timezones/twext/python/
    CalendarServer/branches/users/cdaboo/timezones/twext/web2/
    CalendarServer/branches/users/cdaboo/timezones/twext/web2/auth/
    CalendarServer/branches/users/cdaboo/timezones/twext/web2/channel/
    CalendarServer/branches/users/cdaboo/timezones/twext/web2/client/
    CalendarServer/branches/users/cdaboo/timezones/twext/web2/dav/
    CalendarServer/branches/users/cdaboo/timezones/twext/web2/dav/element/
    CalendarServer/branches/users/cdaboo/timezones/twext/web2/dav/method/
    CalendarServer/branches/users/cdaboo/timezones/twext/web2/filter/
    CalendarServer/branches/users/cdaboo/timezones/twisted/plugins/
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/client/
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/datafilters/
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/
    CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/
    CalendarServer/branches/users/cdaboo/timezones/txdav/
    CalendarServer/branches/users/cdaboo/timezones/txdav/base/
    CalendarServer/branches/users/cdaboo/timezones/txdav/base/datastore/
    CalendarServer/branches/users/cdaboo/timezones/txdav/base/propertystore/
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/index_file.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_index_file.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/
    CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/
    CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/index_file.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/test/test_index_file.py
    CalendarServer/branches/users/cdaboo/timezones/txdav/common/
    CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/


Property changes on: CalendarServer/branches/users/cdaboo/timezones
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
/CalendarServer/trunk:7443-7695

Modified: CalendarServer/branches/users/cdaboo/timezones/benchmark
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/benchmark	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/benchmark	2011-06-30 20:44:13 UTC (rev 7697)
@@ -21,6 +21,6 @@
 
 wd="$(cd "$(dirname "$0")" && pwd -L)";
 
-export PYTHONPATH="$("${wd}/run" -p):${wd}/../CalDAVClientLibrary/src";
+export PYTHONPATH="$("${wd}/run" -p)";
 
 exec "${wd}/contrib/performance/benchmark" "$@";

Copied: CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_calverify (from rev 7695, CalendarServer/trunk/bin/calendarserver_calverify)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_calverify	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_calverify	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        from os.path import dirname, abspath, join
+        from subprocess import Popen, PIPE
+
+        home = dirname(dirname(abspath(__file__)))
+        run = join(home, "run")
+
+        child = Popen((run, "-p"), stdout=PIPE)
+        path, stderr = child.communicate()
+
+        path = path.rstrip("\n")
+
+        if child.wait() == 0:
+            sys.path[0:0] = path.split(":")
+
+        sys.argv[1:1] = ["-f", join(home, "conf", "caldavd-dev.plist")]
+
+    from calendarserver.tools.calverify import main
+    main()

Copied: CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_dbinspect (from rev 7695, CalendarServer/trunk/bin/calendarserver_dbinspect)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_dbinspect	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_dbinspect	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        from os.path import dirname, abspath, join
+        from subprocess import Popen, PIPE
+
+        home = dirname(dirname(abspath(__file__)))
+        run = join(home, "run")
+
+        child = Popen((run, "-p"), stdout=PIPE)
+        path, stderr = child.communicate()
+
+        path = path.rstrip("\n")
+
+        if child.wait() == 0:
+            sys.path[0:0] = path.split(":")
+
+        sys.argv[1:1] = ["-f", join(home, "conf", "caldavd-dev.plist")]
+
+    from calendarserver.tools.dbinspect import main
+    main()

Copied: CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_shell (from rev 7695, CalendarServer/trunk/bin/calendarserver_shell)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_shell	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/bin/calendarserver_shell	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        from os.path import dirname, abspath, join
+        from subprocess import Popen, PIPE
+
+        home = dirname(dirname(abspath(__file__)))
+        run = join(home, "run")
+
+        child = Popen((run, "-p"), stdout=PIPE)
+        path, stderr = child.communicate()
+
+        path = path.rstrip("\n")
+
+        if child.wait() == 0:
+            sys.path[0:0] = path.split(":")
+
+        sys.argv[1:1] = ["-f", join(home, "conf", "caldavd-dev.plist")]
+
+    from calendarserver.tools.shell import main
+    main()

Deleted: CalendarServer/branches/users/cdaboo/timezones/bin/carddavd
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/bin/carddavd	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/bin/carddavd	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,125 +0,0 @@
-#!/bin/bash
-# -*- sh-basic-offset: 2 -*-
-
-##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-#PATH
-#PYTHONPATH
-
-daemonize="";
-errorlogenabled="";
-username="";
-groupname="";
-configfile="";
-twistdpath="$(type -p twistd)";
-plugin_name="carddav";
-service_type="";
-profile="";
-twistd_reactor="";
-child_reactor=""
-
-py_version ()
-{
-  local python="$1"; shift
-  echo "$("${python}" -c "from distutils.sysconfig import get_python_version; print get_python_version()")";
-}
-
-try_python ()
-{
-  local python="$1"; shift
-
-  if [ -z "${python}" ]; then return 1; fi;
-
-  if ! type "${python}" > /dev/null 2>&1; then return 1; fi;
-  local py_version="$(py_version "${python}")";
-  if [ "${py_version/./}" -lt "24" ]; then return 1; fi;
-
-  return 0;
-}
-
-for v in "" "2.6" "2.5"; do
-  for p in                                                              \
-    "${PYTHON:=}"                                                       \
-    "python${v}"                                                        \
-    "/usr/local/bin/python${v}"                                         \
-    "/usr/local/python/bin/python${v}"                                  \
-    "/usr/local/python${v}/bin/python${v}"                              \
-    "/opt/bin/python${v}"                                               \
-    "/opt/python/bin/python${v}"                                        \
-    "/opt/python${v}/bin/python${v}"                                    \
-    "/Library/Frameworks/Python.framework/Versions/${v}/bin/python"     \
-    "/opt/local/bin/python${v}"                                         \
-    "/sw/bin/python${v}"                                                \
-    ;
-  do
-    if try_python "${p}"; then python="${p}"; break; fi;
-  done;
-  if [ -n "${python:-}" ]; then break; fi;
-done;
-
-if [ -z "${python:-}" ]; then
-  echo "No suitable python found.";
-  exit 1;
-fi;
-
-usage ()
-{
-    program="$(basename "$0")";
-
-    if [ "${1--}" != "-" ]; then echo "$@"; echo; fi;
-
-    echo "Usage: ${program} [-hX] [-u username] [-g groupname] [-T twistd] [-t type] [-f carddavd.plist] [-p statsfile]";
-    echo "Options:";
-    echo "        -h Print this help and exit";
-    echo "        -X Do not daemonize";
-    echo "        -u User name to run as";
-    echo "        -g Group name to run as";
-    echo "        -f Configuration file to read";
-    echo "        -T Path to twistd binary";
-    echo "        -t Process type (Master, Slave or Combined)";
-    echo "        -p Path to the desired pstats file.";
-    echo "        -R The Twisted Reactor to run [${reactor}]";
-
-    if [ "${1-}" == "-" ]; then return 0; fi;
-    exit 64;
-}
-
-while getopts 'hXLu:g:f:T:P:t:p:R:' option; do
-    case "${option}" in
-        '?') usage; ;;
-        'h') usage -; exit 0; ;;
-        'X') daemonize="-n"; ;;
-        'L') errorlogenabled="-o ErrorLogEnabled=False"; ;;
-        'f') configfile="-f ${OPTARG}"; ;;
-        'T') twistdpath="${OPTARG}"; ;;
-        'u') username="-u ${OPTARG}"; ;;
-        'g') groupname="-g ${OPTARG}"; ;;
-        'P') plugin_name="${OPTARG}"; ;;
-        't') service_type="-o ProcessType=${OPTARG}"; ;;
-        'p') profile="-o Profiling/Enabled=True -o Profiling/BaseDirectory=${OPTARG}"; ;;
-        'R') twistd_reactor="--reactor=${OPTARG}"; child_reactor="-o Twisted/reactor=${OPTARG}"; ;;
-    esac;
-done;
-
-shift $((${OPTIND} - 1));
-
-if [ $# != 0 ]; then usage "Unrecognized arguments:" "$@"; fi;
-
-export PYTHONPATH
-
-
-exec "${python}" "${twistdpath}" ${twistd_reactor} ${daemonize} ${username} ${groupname} "${plugin_name}" ${configfile} ${service_type} ${errorlogenabled} ${profile} ${child_reactor};


Property changes on: CalendarServer/branches/users/cdaboo/timezones/calendarserver
___________________________________________________________________
Modified: svn:ignore
   - version.py

   + version.py
*.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/calendarserver/platform
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/calendarserver/platform/darwin
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/calendarserver/platform/darwin/od
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/calendarserver/provision
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/provision/root.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/provision/root.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/provision/root.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -323,10 +323,13 @@
         if segments[0] in ("inbox", "timezones"):
             request.checkedSACL = True
 
-        elif (len(segments) > 2 and (segments[1] == "wikis" or
-            (segments[1] == "__uids__" and segments[2].startswith("wiki-")))):
-
-            # This is a wiki-related resource. SACLs are not checked.
+        elif (len(segments) > 2 and segments[0] == "calendars" and
+            (
+                segments[1] == "wikis" or
+                (segments[1] == "__uids__" and segments[2].startswith("wiki-"))
+            )
+        ):
+            # This is a wiki-related calendar resource. SACLs are not checked.
             request.checkedSACL = True
 
             # The authzuser value is set to that of the wiki principal if
@@ -368,7 +371,7 @@
                 request.extendedLogItems = {}
             request.extendedLogItems["xff"] = remote_ip[0]
 
-        if request.method == "PROPFIND" and not getattr(request, "notInCache", False) and len(segments) > 1:
+        if config.EnableResponseCache and request.method == "PROPFIND" and not getattr(request, "notInCache", False) and len(segments) > 1:
             try:
                 authnUser, authzUser = (yield self.authenticate(request))
                 request.authnUser = authnUser

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/provision/test/test_root.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/provision/test/test_root.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/provision/test/test_root.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -61,10 +61,13 @@
 
         RootResource.CheckSACL = FakeCheckSACL(sacls={"calendar": ["dreid"]})
 
-        augment.AugmentService = augment.AugmentXMLDB(
-            xmlFiles=(augmentsFile.path,)
+        directory = XMLDirectoryService(
+            {
+                "xmlFile" : xmlFile,
+                "augmentService" :
+                    augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
+            }
         )
-        directory = XMLDirectoryService({"xmlFile" : xmlFile})
 
         principals = DirectoryPrincipalProvisioningResource(
             "/principals/",


Property changes on: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tap
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tap/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tap/util.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tap/util.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -45,7 +45,7 @@
 from twisted.python.reflect import namedClass
 
 from twistedcaldav.bind import doBind
-from twistedcaldav.directory import augment, calendaruserproxy
+from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
@@ -191,11 +191,15 @@
     if txnFactory is not None:
         return CommonSQLDataStore(
             txnFactory, notifierFactory, FilePath(config.AttachmentsRoot),
-            config.EnableCalDAV, config.EnableCardDAV
+            config.EnableCalDAV, config.EnableCardDAV,
+            quota=config.UserQuota
         )
     else:
-        return CommonFileDataStore(FilePath(config.DocumentRoot),
-            notifierFactory, config.EnableCalDAV, config.EnableCardDAV) 
+        return CommonFileDataStore(
+            FilePath(config.DocumentRoot),
+            notifierFactory, config.EnableCalDAV, config.EnableCardDAV,
+            quota=config.UserQuota
+        ) 
 
 
 
@@ -212,7 +216,7 @@
     log.info("Configuring augment service of type: %s" % (augmentClass,))
 
     try:
-        augment.AugmentService = augmentClass(**config.AugmentService.params)
+        augmentService = augmentClass(**config.AugmentService.params)
     except IOError:
         log.error("Could not start augment service")
         raise
@@ -228,6 +232,7 @@
     log.info("Configuring directory service of type: %s"
         % (config.DirectoryService.type,))
 
+    config.DirectoryService.params.augmentService = augmentService
     baseDirectory = directoryClass(config.DirectoryService.params)
 
     # Wait for the directory to become available
@@ -244,6 +249,7 @@
 
         log.info("Configuring resource service of type: %s" % (resourceClass,))
 
+        config.ResourceService.params.augmentService = augmentService
         resourceDirectory = resourceClass(config.ResourceService.params)
         resourceDirectory.realmName = baseDirectory.realmName
         directories.append(resourceDirectory)


Property changes on: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Copied: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/calverify.py (from rev 7695, CalendarServer/trunk/calendarserver/tools/calverify.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/calverify.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/calverify.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,641 @@
+#!/usr/bin/env python
+# -*- test-case-name: calendarserver.tools.test.test_calverify -*-
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+This tool scans the calendar store to analyze organizer/attendee event
+states to verify that the organizer's view of attendee state matches up
+with the attendees' views. It can optionally apply a fix to bring the two
+views back into line.
+
+In theory the implicit scheduling model should eliminate the possibility
+of mismatches, however, because we store separate resources for organizer
+and attendee events, there is a possibility of mismatch. This is greatly
+lessened via the new transaction model of database changes, but it is
+possible there are edge cases or actual implicit processing errors we have
+missed. This tool will allow us to track mismatches to help determine these
+errors and get them fixed.
+
+Even in the long term if we move to a "single instance" store where the
+organizer event resource is the only one we store (with attendee views
+derived from that), in a situation where we have server-to-server scheduling
+it is possible for mismatches to creep in. In that case having a way to analyze
+multiple DBs for inconsistency would be good too. 
+
+"""
+
+from calendarserver.tap.util import directoryFromConfig
+from calendarserver.tools import tables
+from calendarserver.tools.cmdline import utilityMain
+from pycalendar import definitions
+from pycalendar.calendar import PyCalendar
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.period import PyCalendarPeriod
+from twext.enterprise.dal.syntax import Select, Parameter, Count
+from twisted.application.service import Service
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twisted.python import log
+from twisted.python.text import wordWrap
+from twisted.python.usage import Options
+from twistedcaldav.dateops import pyCalendarTodatetime
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+from txdav.common.datastore.sql_tables import schema, _BIND_MODE_OWN
+import collections
+import os
+import sys
+import time
+
+def usage(e=None):
+    if e:
+        print e
+        print ""
+    try:
+        CalVerifyOptions().opt_help()
+    except SystemExit:
+        pass
+    if e:
+        sys.exit(64)
+    else:
+        sys.exit(0)
+
+
+description = '\n'.join(
+    wordWrap(
+        """
+        Usage: calendarserver_calverify [options] [input specifiers]\n
+        """,
+        int(os.environ.get('COLUMNS', '80'))
+    )
+)
+
+class CalVerifyOptions(Options):
+    """
+    Command-line options for 'calendarserver_calverify'
+    """
+
+    synopsis = description
+
+    optFlags = [
+        ['fix', 'x', "Fix mismatches."],
+        ['missing', 'v', "Show 'orphaned' homes."],
+        ['verbose', 'v', "Verbose logging."],
+    ]
+
+    optParameters = [
+        ['config', 'f', DEFAULT_CONFIG_FILE, "Specify caldavd.plist configuration path."],
+        ['data', 'd', "./calverify-data", "Path where ancillary data is stored."],
+    ]
+
+    def __init__(self):
+        super(CalVerifyOptions, self).__init__()
+        self.outputName = '-'
+
+    def opt_output(self, filename):
+        """
+        Specify output file path (default: '-', meaning stdout).
+        """
+        self.outputName = filename
+
+    opt_o = opt_output
+
+    def openOutput(self):
+        """
+        Open the appropriate output file based on the '--output' option.
+        """
+        if self.outputName == '-':
+            return sys.stdout
+        else:
+            return open(self.outputName, 'wb')
+
+
+
+class CalVerifyService(Service, object):
+    """
+    Service which runs, exports the appropriate records, then stops the reactor.
+    """
+
+    def __init__(self, store, options, output, reactor, config):
+        super(CalVerifyService, self).__init__()
+        self.store   = store
+        self.options = options
+        self.output  = output
+        self.reactor = reactor
+        self.config = config
+        self._directory = None
+
+
+    def startService(self):
+        """
+        Start the service.
+        """
+        super(CalVerifyService, self).startService()
+        self.doCalVerify()
+
+
+    @inlineCallbacks
+    def doCalVerify(self):
+        """
+        Do the export, stopping the reactor when done.
+        """
+        self.txn = self.store.newTransaction()
+        try:
+            if self.options["missing"]:
+                yield self.doOrphans()
+            yield self.doScan(self.options["fix"])
+            yield self.txn.commit()
+            self.txn = None
+
+            self.output.close()
+        except:
+            log.err()
+
+        self.reactor.stop()
+
+
+    @inlineCallbacks
+    def doOrphans(self):
+        """
+        Report on home collections for which there are no directory records. 
+        """
+        print "\n---- Finding calendar homes with no directory record ----"
+
+        if self.options["verbose"]:
+            t = time.time()
+        uids = yield self.getAllHomeUIDs()
+        if self.options["verbose"]:
+            print "getAllHomeUIDs time: %.1fs" % (time.time() - t,)
+        missing = []
+        uids_len = len(uids)
+        uids_div = 1 if uids_len < 100 else uids_len / 100
+
+        for ctr, uid in enumerate(uids):
+            if self.options["verbose"] and divmod(ctr, uids_div)[1] == 0:
+                print "%d of %d (%d%%)" % (
+                    ctr+1,
+                    uids_len,
+                    ((ctr+1) * 100 / uids_len),
+                )
+
+            if self.directoryService().recordWithGUID(uid[0]) is None:
+                contents = yield self.countHomeContents(uid)
+                missing.append((uid[0], contents,))
+        
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("Owner UID", "Calendar Objects"))
+        for uid, count in missing:
+            table.addRow((
+                uid,
+                count,
+            ))
+        
+        self.output.write("\n")
+        self.output.write("Homes without a matching directory record (total=%d):\n" % (len(missing),))
+        table.printTable(os=self.output)
+        
+
+    @inlineCallbacks
+    def getAllHomeUIDs(self):
+        ch = schema.CALENDAR_HOME
+        rows = (yield Select(
+            [ch.OWNER_UID,],
+            From=ch,
+        ).on(self.txn))
+        returnValue(tuple(rows))
+
+    @inlineCallbacks
+    def countHomeContents(self, uid):
+        ch = schema.CALENDAR_HOME
+        cb = schema.CALENDAR_BIND
+        co = schema.CALENDAR_OBJECT
+        kwds = { "UID" : uid }
+        rows = (yield Select(
+            [Count(co.RESOURCE_ID),],
+            From=ch.join(
+                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
+                    cb.BIND_MODE == _BIND_MODE_OWN)).join(
+                co, type="inner", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
+            Where=(ch.OWNER_UID == Parameter("UID"))
+        ).on(self.txn, **kwds))
+        returnValue(int(rows[0][0]) if rows else 0)
+
+    @inlineCallbacks
+    def doScan(self, fix):
+        
+        print "\n---- Scanning calendar data ----"
+
+        self.start = PyCalendarDateTime.getToday()
+        self.start.setDateOnly(False)
+        self.end = self.start.duplicate()
+        self.end.offsetYear(1)
+        self.fix = fix
+
+        if self.options["verbose"]:
+            t = time.time()
+        rows = yield self.getAllResourceInfo(self.start)
+        if self.options["verbose"]:
+            print "getAllResourceInfo time: %.1fs" % (time.time() - t,)
+        print "Number of events to process: %s" % (len(rows,))
+        
+        # Split into organizer events and attendee events
+        self.organized = []
+        self.organized_byuid = {}
+        self.attended = []
+        self.attended_byuid = collections.defaultdict(list)
+        self.matched_attendee_to_organizer = collections.defaultdict(set)
+        for owner, resid, uid, md5, organizer in rows:
+            if organizer.startswith("urn:uuid:") and owner == organizer[9:]:
+                self.organized.append((owner, resid, uid, md5, organizer,))
+                self.organized_byuid[uid] = (owner, resid, uid, md5, organizer,)
+            else:
+                self.attended.append((owner, resid, uid, md5, organizer,))
+                self.attended_byuid[uid].append((owner, resid, uid, md5, organizer,))
+                
+        print "Number of organizer events to process: %s" % (len(self.organized),)
+        print "Number of attendee events to process: %s" % (len(self.attended,))
+
+        yield self.verifyAllAttendeesForOrganizer()
+        yield self.verifyAllOrganizersForAttendee()
+        
+        yield succeed(None)
+
+    @inlineCallbacks
+    def getAllResourceInfo(self, start):
+        co = schema.CALENDAR_OBJECT
+        cb = schema.CALENDAR_BIND
+        ch = schema.CALENDAR_HOME
+        tr = schema.TIME_RANGE
+        kwds = { "Start" : pyCalendarTodatetime(start) }
+        rows = (yield Select(
+            [ch.OWNER_UID, co.RESOURCE_ID, co.ICALENDAR_UID, co.MD5, co.ORGANIZER,],
+            From=ch.join(
+                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID)).join(
+                co, type="inner", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID).And(
+                    cb.BIND_MODE == _BIND_MODE_OWN).And(
+                    cb.CALENDAR_RESOURCE_NAME != "inbox").And(
+                    co.ORGANIZER != "")).join(
+                tr, type="left", on=(co.RESOURCE_ID == tr.CALENDAR_OBJECT_RESOURCE_ID)),
+            Where=(tr.START_DATE >= Parameter("Start")).Or(co.RECURRANCE_MAX == "1900-01-01"),
+            GroupBy=(ch.OWNER_UID, co.RESOURCE_ID, co.ICALENDAR_UID, co.MD5, co.ORGANIZER,),
+        ).on(self.txn, **kwds))
+        returnValue(tuple(rows))
+
+    @inlineCallbacks
+    def verifyAllAttendeesForOrganizer(self):
+        """
+        Make sure that for each organizer, each referenced attendee has a consistent view of the organizer's event.
+        We will look for events that an organizer has and are missing for the attendee, and events that an organizer's
+        view of attendee status does not match the attendee's view of their own status.
+        """
+        
+        print "\n---- Verifying Organizer events against Attendee copies ----"
+
+        results_missing = []
+        results_mismatch = []
+        organized_len = len(self.organized)
+        organizer_div = 1 if organized_len < 100 else organized_len / 100
+
+        # Test organized events
+        for ctr, organizerEvent in enumerate(self.organized):
+            
+            if self.options["verbose"] and divmod(ctr, organizer_div)[1] == 0:
+                print "%d of %d (%d%%) Missing: %d  Mismatched: %s" % (
+                    ctr+1,
+                    organized_len,
+                    ((ctr+1) * 100 / organized_len),
+                    len(results_missing),
+                    len(results_mismatch),
+                )
+
+            # Get the organizer's view of attendee states            
+            organizer, resid, uid, _ignore_md5, _ignore_organizer = organizerEvent
+            calendar = yield self.getCalendar(resid)
+            organizerViewOfAttendees = self.buildAttendeeStates(calendar, self.start, self.end)
+            try:
+                del organizerViewOfAttendees[organizer]
+            except KeyError:
+                # Odd - the organizer is not an attendee - this usually does not happen
+                pass
+            if len(organizerViewOfAttendees) == 0:
+                continue
+            
+            # Get attendee states for matching UID
+            eachAttendeesOwnStatus = {}
+            for attendeeEvent in self.attended_byuid.get(uid, ()):
+                owner, resid, uid, _ignore_md5, _ignore_organizer = attendeeEvent
+                calendar = yield self.getCalendar(resid)
+                eachAttendeesOwnStatus[owner] = self.buildAttendeeStates(calendar, self.start, self.end, attendee_only=owner)
+            
+            # Look at each attendee in the organizer's meeting
+            for organizerAttendee, organizerViewOfStatus in organizerViewOfAttendees.iteritems():
+                broken = False
+
+                self.matched_attendee_to_organizer[uid].add(organizerAttendee)
+
+                # If an entry for the attendee exists, then check whether attendee status matches
+                if organizerAttendee in eachAttendeesOwnStatus:
+                    attendeeOwnStatus = eachAttendeesOwnStatus[organizerAttendee].get(organizerAttendee, set())
+
+                    if organizerViewOfStatus != attendeeOwnStatus:
+                        # Check that the difference is only cancelled or declined on the organizers side
+                        for _ignore_organizerInstance, partstat in organizerViewOfStatus.difference(attendeeOwnStatus):
+                            if partstat not in ("DECLINED", "CANCELLED"):
+                                results_mismatch.append((uid, organizer, organizerAttendee))
+                                broken = True
+                                break
+                        # Check that the difference is only cancelled on the attendees side
+                        for _ignore_attendeeInstance, partstat in attendeeOwnStatus.difference(organizerViewOfStatus):
+                            if partstat not in ("CANCELLED",):
+                                if not broken:
+                                    results_mismatch.append((uid, organizer, organizerAttendee))
+                                broken = True
+                                break
+
+                # Check that the status for this attendee is always declined which means a missing copy of the event is OK
+                else:
+                    for _ignore_instance_id, partstat in organizerViewOfStatus:
+                        if partstat not in ("DECLINED", "CANCELLED"):
+                            results_missing.append((uid, organizer, organizerAttendee,))
+                            broken = True
+                            break
+                
+                # If there was a problem we can fix it
+                if broken and self.fix:
+                    # TODO: This is where we attempt a fix
+                    #self.fixEvent(organizer, organizerAttendee, eventpath, attendeePaths.get(organizerAttendee, None))
+                    pass
+                
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("Organizer", "Attendee", "Event UID",))
+        for item in results_missing:
+            uid, organizer, attendee = item
+            organizer_record = self.directoryService().recordWithGUID(organizer)
+            attendee_record = self.directoryService().recordWithGUID(attendee)
+            table.addRow((
+                "%s/%s (%s)" % (organizer_record.recordType if organizer_record else "-", organizer_record.shortNames[0] if organizer_record else "-", organizer,),
+                "%s/%s (%s)" % (attendee_record.recordType if attendee_record else "-", attendee_record.shortNames[0] if attendee_record else "-", attendee,),
+                uid,
+            ))
+        
+        self.output.write("\n")
+        self.output.write("Events missing from Attendee's calendars (total=%d):\n" % (len(results_missing),))
+        table.printTable(os=self.output)
+            
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("Organizer", "Attendee", "Event UID",))
+        for item in results_mismatch:
+            uid, organizer, attendee = item
+            organizer_record = self.directoryService().recordWithGUID(organizer)
+            attendee_record = self.directoryService().recordWithGUID(attendee)
+            table.addRow((
+                "%s/%s (%s)" % (organizer_record.recordType if organizer_record else "-", organizer_record.shortNames[0] if organizer_record else "-", organizer,),
+                "%s/%s (%s)" % (attendee_record.recordType if attendee_record else "-", attendee_record.shortNames[0] if attendee_record else "-", attendee,),
+                uid,
+            ))
+        
+        self.output.write("\n")
+        self.output.write("Events mismatched between Organizer's and Attendee's calendars (total=%d):\n" % (len(results_mismatch),))
+        table.printTable(os=self.output)
+
+    @inlineCallbacks
+    def verifyAllOrganizersForAttendee(self):
+        """
+        Make sure that for each attendee, there is a matching event for the organizer.
+        """
+
+        print "\n---- Verifying Attendee events against Organizer copies ----"
+
+        # Now try to match up each attendee event
+        missing = []
+        mismatched = []
+        attended_len = len(self.attended)
+        attended_div = 1 if attended_len < 100 else attended_len / 100
+
+        for ctr, attendeeEvent in enumerate(self.attended):
+            
+            if self.options["verbose"] and divmod(ctr, attended_div)[1] == 0:
+                print "%d of %d (%d%%) Missing: %d  Mismatched: %s" % (
+                    ctr+1,
+                    attended_len,
+                    ((ctr+1) * 100 / attended_len),
+                    len(missing),
+                    len(mismatched),
+                )
+
+            attendee, resid, uid, _ignore_md5, organizer = attendeeEvent
+            calendar = yield self.getCalendar(resid)
+            eachAttendeesOwnStatus = self.buildAttendeeStates(calendar, self.start, self.end, attendee_only=attendee)
+            if attendee not in eachAttendeesOwnStatus:
+                continue
+
+            # Only care about data for hosted organizers                
+            if not organizer.startswith("urn:uuid:"):
+                continue
+            organizer = organizer[9:]
+
+            if uid not in self.organized_byuid:
+
+                # Check whether attendee has all instances cancelled
+                if self.allCancelled(eachAttendeesOwnStatus):
+                    continue
+                
+                missing.append((uid, attendee, organizer,))
+                
+                # If there is a miss we fix by removing the attendee data
+                if self.fix:
+                    # TODO: This is where we attempt a fix
+                    pass
+
+            elif attendee not in self.matched_attendee_to_organizer[uid]:
+                # Check whether attendee has all instances cancelled
+                if self.allCancelled(eachAttendeesOwnStatus):
+                    continue
+
+                mismatched.append((uid, attendee, organizer,))
+                
+                # If there is a mismatch we fix by re-inviting the attendee
+                if self.fix:
+                    # TODO: This is where we attempt a fix
+                    pass
+
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("UID", "Owner", "Organizer",))
+        missing.sort()
+        unique_set = set()
+        for item in missing:
+            uid, attendee, organizer = item
+            unique_set.add(uid)
+            if organizer:
+                organizerRecord = self.directoryService().recordWithGUID(organizer)
+                organizer = "%s/%s (%s)" % (organizerRecord.recordType if organizerRecord else "-", organizerRecord.shortNames[0] if organizerRecord else "-", organizer,)
+            attendeeRecord = self.directoryService().recordWithGUID(attendee)
+            table.addRow((
+                uid,
+                "%s/%s (%s)" % (attendeeRecord.recordType if attendeeRecord else "-", attendeeRecord.shortNames[0] if attendeeRecord else "-", attendee,),
+                organizer,
+            ))
+        
+        self.output.write("\n")
+        self.output.write("Attendee events missing in Organizer's calendar (total=%d, unique=%d):\n" % (len(missing), len(unique_set),))
+        table.printTable(os=self.output)
+
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("UID", "Owner", "Organizer",))
+        mismatched.sort()
+        for item in mismatched:
+            uid, attendee, organizer = item
+            if organizer:
+                organizerRecord = self.directoryService().recordWithGUID(organizer)
+                organizer = "%s/%s (%s)" % (organizerRecord.recordType if organizerRecord else "-", organizerRecord.shortNames[0] if organizerRecord else "-", organizer,)
+            attendeeRecord = self.directoryService().recordWithGUID(attendee)
+            table.addRow((
+                uid,
+                "%s/%s (%s)" % (attendeeRecord.recordType if attendeeRecord else "-", attendeeRecord.shortNames[0] if attendeeRecord else "-", attendee,),
+                organizer,
+            ))
+        
+        self.output.write("\n")
+        self.output.write("Attendee events mismatched in Organizer's calendar (total=%d):\n" % (len(mismatched),))
+        table.printTable(os=self.output)
+
+
+    @inlineCallbacks
+    def getCalendar(self, resid):
+        co = schema.CALENDAR_OBJECT
+        kwds = { "ResourceID" : resid }
+        rows = (yield Select(
+            [co.ICALENDAR_TEXT],
+            From=co,
+            Where=(
+                co.RESOURCE_ID == Parameter("ResourceID")
+            ),
+        ).on(self.txn, **kwds))
+        returnValue(PyCalendar.parseText(rows[0][0]) if rows else None)
+
+    def buildAttendeeStates(self, calendar, start, end, attendee_only=None):
+        # Expand events into instances in the start/end range
+        results = []
+        calendar.getVEvents(
+            PyCalendarPeriod(
+                start=start,
+                end=end,
+            ),
+            results
+        )
+    
+        # Need to do iCal fake master fixup
+        overrides = len(calendar.getComponents(definitions.cICalComponent_VEVENT)) > 1
+    
+        # Create map of each attendee's instances with the instance id (start time) and attendee part-stat
+        attendees = {}
+        for item in results:
+            
+            # Fake master fixup
+            if overrides:
+                if not item.getOwner().isRecurrenceInstance():
+                    if item.getOwner().getRecurrenceSet() is None or not item.getOwner().getRecurrenceSet().hasRecurrence():
+                        continue
+    
+            # Get Status - ignore cancelled events
+            status = item.getOwner().loadValueString(definitions.cICalProperty_STATUS)
+            cancelled = status == definitions.cICalProperty_STATUS_CANCELLED
+    
+            # Get instance start
+            item.getInstanceStart().adjustToUTC()
+            instance_id = item.getInstanceStart().getText()
+            
+            props = item.getOwner().getProperties().get(definitions.cICalProperty_ATTENDEE, [])
+            for prop in props:
+                caladdr = prop.getCalAddressValue().getValue()
+                if caladdr.startswith("urn:uuid:"):
+                    caladdr = caladdr[9:]
+                else:
+                    continue
+                if attendee_only is not None and attendee_only != caladdr:
+                    continue
+                if cancelled:
+                    partstat = "CANCELLED"
+                else:
+                    if not prop.hasAttribute(definitions.cICalAttribute_PARTSTAT):
+                        partstat = definitions.cICalAttribute_PARTSTAT_NEEDSACTION
+                    else:
+                        partstat = prop.getAttributeValue(definitions.cICalAttribute_PARTSTAT)
+    
+                attendees.setdefault(caladdr, set()).add((instance_id, partstat))
+                    
+        return attendees
+    
+    
+    def allCancelled(self, attendeesStatus):
+        # Check whether attendees have all instances cancelled
+        all_cancelled = True
+        for _ignore_guid, states in attendeesStatus.iteritems():
+            for _ignore_instance_id, partstat in states:
+                if partstat not in ("CANCELLED",):
+                    all_cancelled = False
+                    break
+            if not all_cancelled:
+                break
+        return all_cancelled
+       
+
+    def directoryService(self):
+        """
+        Get an appropriate directory service for this L{CalVerifyService}'s
+        configuration, creating one first if necessary.
+        """
+        if self._directory is None:
+            self._directory = directoryFromConfig(self.config)
+        return self._directory
+
+
+    def stopService(self):
+        """
+        Stop the service.  Nothing to do; everything should be finished by this
+        time.
+        """
+        # TODO: stopping this service mid-export should really stop the export
+        # loop, but this is not implemented because nothing will actually do it
+        # except hitting ^C (which also calls reactor.stop(), so that will exit
+        # anyway).
+
+
+
+def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
+    """
+    Do the export.
+    """
+    if reactor is None:
+        from twisted.internet import reactor
+    options = CalVerifyOptions()
+    options.parseOptions(argv[1:])
+    try:
+        output = options.openOutput()
+    except IOError, e:
+        stderr.write("Unable to open output file for writing: %s\n" % (e))
+        sys.exit(1)
+    def makeService(store):
+        from twistedcaldav.config import config
+        return CalVerifyService(store, options, output, reactor, config)
+    utilityMain(options['config'], makeService, reactor)
+
+if __name__ == '__main__':
+    main()

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/changeip_calendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/changeip_calendar.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/changeip_calendar.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -103,6 +103,7 @@
         ("Notifications", "Services", "XMPPNotifier", "ServiceAddress"),
         ("Scheduling", "iMIP", "Receiving", "Server"),
         ("Scheduling", "iMIP", "Sending", "Server"),
+        ("Scheduling", "iMIP", "Sending", "Address"),
         ("ServerHostName",),
     )
 

Copied: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/cmdline.py (from rev 7695, CalendarServer/trunk/calendarserver/tools/cmdline.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/cmdline.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/cmdline.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,77 @@
+##
+# Copyright (c) 2005-2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Shared main-point between utilities.
+"""
+
+import sys
+
+from calendarserver.tap.caldav import CalDAVServiceMaker, CalDAVOptions
+from calendarserver.tools.util import loadConfig, autoDisableMemcached
+from twistedcaldav.config import ConfigurationError
+
+# TODO: direct unit tests for this function.
+
+def utilityMain(configFileName, serviceClass, reactor=None):
+    """
+    Shared main-point for utilities.
+
+    This function will:
+
+        - Load the configuration file named by C{configFileName},
+        - launch a L{CalDAVServiceMaker}'s with the C{ProcessType} of
+          C{"Utility"}
+        - run the reactor, with start/stop events hooked up to the service's
+          C{startService}/C{stopService} methods.
+
+    It is C{serviceClass}'s responsibility to stop the reactor when it's
+    complete.
+
+    @param configFileName: the name of the configuration file to load.
+    @type configuration: C{str}
+
+    @param serviceClass: a 1-argument callable which takes an object that
+        provides L{ICalendarStore} and/or L{IAddressbookStore} and returns an
+        L{IService}.
+
+    @param reactor: if specified, the L{IReactorTime} / L{IReactorThreads} /
+        L{IReactorTCP} (etc) provider to use.  If C{None}, the default reactor
+        will be imported and used.
+    """
+    if reactor is None:
+        from twisted.internet import reactor
+    try:
+        config = loadConfig(configFileName)
+
+        config.ProcessType = "Utility"
+        config.UtilityServiceClass = serviceClass
+
+        autoDisableMemcached(config)
+
+        maker = CalDAVServiceMaker()
+        options = CalDAVOptions
+        service = maker.makeService(options)
+
+        reactor.addSystemEventTrigger("during", "startup", service.startService)
+        reactor.addSystemEventTrigger("before", "shutdown", service.stopService)
+
+    except ConfigurationError, e:
+        sys.stderr.write("Error: %s\n" % (e,))
+        return
+
+    reactor.run()
+

Copied: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/dbinspect.py (from rev 7695, CalendarServer/trunk/calendarserver/tools/dbinspect.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/dbinspect.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/dbinspect.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,568 @@
+#!/usr/bin/env python
+# -*- test-case-name: calendarserver.tools.test.test_calverify -*-
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+This tool allows data in the database to be directly inspected using a set
+of simple commands.
+"""
+
+from calendarserver.tap.util import directoryFromConfig
+from calendarserver.tools import tables
+from calendarserver.tools.cmdline import utilityMain
+from twext.enterprise.dal.syntax import Select, Parameter, Count, Delete
+from twisted.application.service import Service
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.filepath import FilePath
+from twisted.python.reflect import namedClass
+from twisted.python.text import wordWrap
+from twisted.python.usage import Options
+from twistedcaldav.config import config
+from twistedcaldav.directory import calendaruserproxy
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+from txdav.common.datastore.sql_tables import schema, _BIND_MODE_OWN
+import os
+import sys
+import traceback
+
+def usage(e=None):
+    if e:
+        print e
+        print ""
+    try:
+        DBInspectOptions().opt_help()
+    except SystemExit:
+        pass
+    if e:
+        sys.exit(64)
+    else:
+        sys.exit(0)
+
+
+description = '\n'.join(
+    wordWrap(
+        """
+        Usage: calendarserver_calverify [options] [input specifiers]\n
+        """,
+        int(os.environ.get('COLUMNS', '80'))
+    )
+)
+
+class DBInspectOptions(Options):
+    """
+    Command-line options for 'calendarserver_dbinspect'
+    """
+
+    synopsis = description
+
+    optFlags = [
+        ['verbose', 'v', "Verbose logging."],
+    ]
+
+    optParameters = [
+        ['config', 'f', DEFAULT_CONFIG_FILE, "Specify caldavd.plist configuration path."],
+    ]
+
+    def __init__(self):
+        super(DBInspectOptions, self).__init__()
+        self.outputName = '-'
+
+def UserNameFromUID(txn, uid):
+    record = txn._directory.recordWithGUID(uid)
+    return record.shortNames[0] if record else "(%s)" % (uid,)
+    
+class Cmd(object):
+    
+    _name = None
+    
+    @classmethod
+    def name(cls):
+        return cls._name
+
+    def doIt(self, txn):
+        raise NotImplementedError
+
+class CalendarHomes(Cmd):
+    
+    _name = "List Calendar Homes"
+    
+    @inlineCallbacks
+    def doIt(self, txn):
+        
+        uids = yield self.getAllHomeUIDs(txn)
+        
+        # Print table of results
+        missing = 0
+        table = tables.Table()
+        table.addHeader(("Owner UID", "Short Name"))
+        for uid in sorted(uids):
+            shortname = UserNameFromUID(txn, uid)
+            if shortname.startswith("("):
+                missing += 1
+            table.addRow((
+                uid,
+                shortname,
+            ))
+        
+        print "\n"
+        print "Calendar Homes (total=%d, missing=%d):\n" % (len(uids), missing,)
+        table.printTable()
+
+    @inlineCallbacks
+    def getAllHomeUIDs(self, txn):
+        ch = schema.CALENDAR_HOME
+        rows = (yield Select(
+            [ch.OWNER_UID,],
+            From=ch,
+        ).on(txn))
+        returnValue(tuple([row[0] for row in rows]))
+
+
+class Calendars(Cmd):
+    
+    _name = "List Calendars"
+    
+    @inlineCallbacks
+    def doIt(self, txn):
+        
+        uids = yield self.getCalendars(txn)
+        
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("Owner UID", "Short Name", "Calendar", "Resources"))
+        for uid, calname, count in sorted(uids, key=lambda x:(x[0], x[1])):
+            shortname = UserNameFromUID(txn, uid)
+            table.addRow((
+                uid,
+                shortname,
+                calname,
+                count
+            ))
+        
+        print "\n"
+        print "Calendars with resource count (total=%d):\n" % (len(uids),)
+        table.printTable()
+
+    @inlineCallbacks
+    def getCalendars(self, txn):
+        ch = schema.CALENDAR_HOME
+        cb = schema.CALENDAR_BIND
+        co = schema.CALENDAR_OBJECT
+        rows = (yield Select(
+            [
+                ch.OWNER_UID,
+                cb.CALENDAR_RESOURCE_NAME,
+                Count(co.RESOURCE_ID),
+            ],
+            From=ch.join(
+                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
+                    cb.BIND_MODE == _BIND_MODE_OWN)).join(
+                co, type="left", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
+            GroupBy=(ch.OWNER_UID, cb.CALENDAR_RESOURCE_NAME)
+        ).on(txn))
+        returnValue(tuple(rows))
+
+
+class Events(Cmd):
+    
+    _name = "List Events"
+    
+    @inlineCallbacks
+    def doIt(self, txn):
+        
+        uids = yield self.getEvents(txn)
+        
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("Owner UID", "Short Name", "Calendar", "ID", "Type", "UID"))
+        for uid, calname, id, caltype, caluid in sorted(uids, key=lambda x:(x[0], x[1])):
+            shortname = UserNameFromUID(txn, uid)
+            table.addRow((
+                uid,
+                shortname,
+                calname,
+                id,
+                caltype,
+                caluid
+            ))
+        
+        print "\n"
+        print "Calendar events (total=%d):\n" % (len(uids),)
+        table.printTable()
+
+    @inlineCallbacks
+    def getEvents(self, txn):
+        ch = schema.CALENDAR_HOME
+        cb = schema.CALENDAR_BIND
+        co = schema.CALENDAR_OBJECT
+        rows = (yield Select(
+            [
+                ch.OWNER_UID,
+                cb.CALENDAR_RESOURCE_NAME,
+                co.RESOURCE_ID,
+                co.ICALENDAR_TYPE,
+                co.ICALENDAR_UID,
+            ],
+            From=ch.join(
+                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
+                    cb.BIND_MODE == _BIND_MODE_OWN)).join(
+                co, type="inner", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
+        ).on(txn))
+        returnValue(tuple(rows))
+
+class Event(Cmd):
+    
+    _name = "Get Event Data by Resource-ID"
+    
+    @inlineCallbacks
+    def doIt(self, txn):
+        
+        
+        rid = raw_input("Resource-ID: ")
+        try:
+            int(rid)
+        except ValueError:
+            print 'Resource ID must be an integer'
+            returnValue(None)
+        data = yield self.getData(txn, rid)
+        if data:
+            print "\n"
+            print data
+        else:
+            print "Could not find resource"
+
+    @inlineCallbacks
+    def getData(self, txn, rid):
+        co = schema.CALENDAR_OBJECT
+        rows = (yield Select(
+            [
+                co.ICALENDAR_TEXT,
+            ],
+            From=co,
+            Where=(co.RESOURCE_ID == Parameter("ResourceID")),
+        ).on(txn, **{"ResourceID": rid}))
+        returnValue(rows[0][0] if rows else None)
+
+class EventsByUID(Cmd):
+    
+    _name = "Get Event Data by iCalendar UID"
+    
+    @inlineCallbacks
+    def doIt(self, txn):
+        
+        
+        uid = raw_input("UID: ")
+        rows = yield self.getData(txn, uid)
+        if rows:
+            for owner, calendar, resource_id, resource, created, modified, data in rows:
+                shortname = UserNameFromUID(txn, owner)
+                table = tables.Table()
+                table.addRow(("User Name:", shortname,))
+                table.addRow(("Calendar:", calendar,))
+                table.addRow(("Resource Name:", resource))
+                table.addRow(("Resource ID:", resource_id))
+                table.addRow(("Created", created))
+                table.addRow(("Modified", modified))
+                print "\n"
+                table.printTable()
+                print data
+        else:
+            print "Could not find icalendar data"
+
+    @inlineCallbacks
+    def getData(self, txn, uid):
+        ch = schema.CALENDAR_HOME
+        cb = schema.CALENDAR_BIND
+        co = schema.CALENDAR_OBJECT
+        rows = (yield Select(
+            [
+                ch.OWNER_UID,
+                cb.CALENDAR_RESOURCE_NAME,
+                co.RESOURCE_ID,
+                co.RESOURCE_NAME,
+                co.CREATED,
+                co.MODIFIED,
+                co.ICALENDAR_TEXT,
+            ],
+            From=ch.join(
+                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
+                    cb.BIND_MODE == _BIND_MODE_OWN)).join(
+                co, type="inner", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
+            Where=(co.ICALENDAR_UID == Parameter("UID")),
+        ).on(txn, **{"UID": uid}))
+        returnValue(tuple(rows))
+
+
+class EventsByContent(Cmd):
+    
+    _name = "Get Event Data by Searching its Text Data"
+    
+    @inlineCallbacks
+    def doIt(self, txn):
+        
+        
+        uid = raw_input("Search for: ")
+        rows = yield self.getData(txn, uid)
+        if rows:
+            for owner, calendar, resource_id, resource, created, modified, data in rows:
+                shortname = UserNameFromUID(txn, owner)
+                table = tables.Table()
+                table.addRow(("User Name:", shortname,))
+                table.addRow(("Calendar:", calendar,))
+                table.addRow(("Resource Name:", resource))
+                table.addRow(("Resource ID:", resource_id))
+                table.addRow(("Created", created))
+                table.addRow(("Modified", modified))
+                print "\n"
+                table.printTable()
+                print data
+        else:
+            print "Could not find icalendar data"
+
+    @inlineCallbacks
+    def getData(self, txn, text):
+        ch = schema.CALENDAR_HOME
+        cb = schema.CALENDAR_BIND
+        co = schema.CALENDAR_OBJECT
+        rows = (yield Select(
+            [
+                ch.OWNER_UID,
+                cb.CALENDAR_RESOURCE_NAME,
+                co.RESOURCE_ID,
+                co.RESOURCE_NAME,
+                co.CREATED,
+                co.MODIFIED,
+                co.ICALENDAR_TEXT,
+            ],
+            From=ch.join(
+                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
+                    cb.BIND_MODE == _BIND_MODE_OWN)).join(
+                co, type="inner", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
+            Where=(co.ICALENDAR_TEXT.Contains(Parameter("Text"))),
+        ).on(txn, **{"Text": text}))
+        returnValue(tuple(rows))
+
+
+class Purge(Cmd):
+    
+    _name = "Purge all data from tables"
+    
+    @inlineCallbacks
+    def doIt(self, txn):
+        
+        
+        if raw_input("Do you really want to remove all data [y/n]: ")[0].lower() != 'y':
+            print "No data removed"
+            returnValue(None)
+
+        wipeout = (
+            # These are ordered in such a way as to ensure key constraints are not 
+            # violated as data is removed
+
+            schema.RESOURCE_PROPERTY,
+
+            schema.CALENDAR_OBJECT_REVISIONS,
+
+            schema.CALENDAR,
+            #schema.CALENDAR_BIND, - cascades
+            #schema.CALENDAR_OBJECT, - cascades
+            #schema.TIME_RANGE, - cascades
+            #schema.TRANSPARENCY, - cascades
+            
+
+            schema.CALENDAR_HOME,
+            #schema.CALENDAR_HOME_METADATA - cascades
+            schema.INVITE,
+            schema.ATTACHMENT,
+            
+            schema.ADDRESSBOOK_OBJECT_REVISIONS,
+
+            schema.ADDRESSBOOK,
+            #schema.ADDRESSBOOK_BIND, - cascades
+            #schema.ADDRESSBOOK_OBJECT, - cascades
+
+            schema.ADDRESSBOOK_HOME,
+            #schema.ADDRESSBOOK_HOME_METADATA, - cascades
+
+            schema.NOTIFICATION_HOME,
+            schema.NOTIFICATION,
+            #schema.NOTIFICATION_OBJECT_REVISIONS - cascades,
+        )
+
+        for tableschema in wipeout:
+            yield self.removeTableData(txn, tableschema)
+            print "Removed rows in table %s" % (tableschema,)
+            
+        if calendaruserproxy.ProxyDBService is not None:
+            calendaruserproxy.ProxyDBService.clean() #@UndefinedVariable
+            print "Removed all proxies"
+        else:
+            print "No proxy database to clean."
+        
+        fp = FilePath(config.AttachmentsRoot)
+        if fp.exists():
+            for child in fp.children():
+                child.remove()
+            print "Removed attachments."
+        else:
+            print "No attachments path to delete."
+
+    @inlineCallbacks
+    def removeTableData(self, txn, tableschema):
+        yield Delete(
+            From=tableschema,
+            Where=None  # Deletes all rows
+        ).on(txn)
+
+
+class DBInspectService(Service, object):
+    """
+    Service which runs, exports the appropriate records, then stops the reactor.
+    """
+
+    def __init__(self, store, options, reactor, config):
+        super(DBInspectService, self).__init__()
+        self.store   = store
+        self.options = options
+        self.reactor = reactor
+        self.config = config
+        self._directory = None
+        self.commands = []
+        self.commandMap = {}
+
+
+    def startService(self):
+        """
+        Start the service.
+        """
+        super(DBInspectService, self).startService()
+        
+        # Register commands
+        self.registerCommand(CalendarHomes)
+        self.registerCommand(Calendars)
+        self.registerCommand(Events)
+        self.registerCommand(Event)
+        self.registerCommand(EventsByUID)
+        self.registerCommand(EventsByContent)
+        self.doDBInspect()
+
+
+    def registerCommand(self, cmd):
+        self.commands.append(cmd.name())
+        self.commandMap[cmd.name()] = cmd
+
+    @inlineCallbacks
+    def runCommandByPosition(self, position):
+        try:
+            yield self.runCommandByName(self.commands[position])
+        except IndexError:
+            print "Position %d not available" % (position,)
+            returnValue(None)
+
+    @inlineCallbacks
+    def runCommandByName(self, name):
+        try:
+            yield self.runCommand(self.commandMap[name])
+        except IndexError:
+            print "Unknown command: '%s'" % (name,)
+
+    @inlineCallbacks
+    def runCommand(self, cmd):
+        txn = self.store.newTransaction()
+        txn._directory = self.directoryService()
+        try:
+            yield cmd().doIt(txn)
+            yield txn.commit()
+        except Exception, e:
+            yield txn.abort()
+            print "Command '%s' failed because of: %s" % (cmd.name(), e,)
+            traceback.print_exc()
+
+    def printCommands(self):
+        
+        print "\n<---- Commands ---->"
+        for ctr, name in enumerate(self.commands):
+            print "%d. %s" % (ctr+1, name,)
+        print "P. Purge\n"
+        print "Q. Quit\n"
+
+    @inlineCallbacks
+    def doDBInspect(self):
+        """
+        Poll for commands, stopping the reactor when done.
+        """
+        
+        while True:
+            self.printCommands()
+            cmd = raw_input("Command: ")
+            if cmd.lower() == 'q':
+                break
+            if cmd.lower() == 'p':
+                yield self.runCommand(Purge)
+            else:
+                try:
+                    position = int(cmd)
+                except ValueError:
+                    print "Invalid command. Try again.\n"
+                    continue
+            
+                yield self.runCommandByPosition(position-1)
+
+        self.reactor.stop()
+
+
+    def directoryService(self):
+        """
+        Get an appropriate directory service for this L{DBInspectService}'s
+        configuration, creating one first if necessary.
+        """
+        if self._directory is None:
+            self._directory = directoryFromConfig(self.config)
+            proxydbClass = namedClass(config.ProxyDBService.type)
+            try:
+                calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
+            except IOError:
+                print "Could not start proxydb service"
+        return self._directory
+
+
+    def stopService(self):
+        """
+        Stop the service.  Nothing to do; everything should be finished by this
+        time.
+        """
+        # TODO: stopping this service mid-export should really stop the export
+        # loop, but this is not implemented because nothing will actually do it
+        # except hitting ^C (which also calls reactor.stop(), so that will exit
+        # anyway).
+
+
+
+def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
+    """
+    Do the export.
+    """
+    if reactor is None:
+        from twisted.internet import reactor
+    options = DBInspectOptions()
+    options.parseOptions(argv[1:])
+    def makeService(store):
+        return DBInspectService(store, options, reactor, config)
+    utilityMain(options['config'], makeService, reactor)
+
+if __name__ == '__main__':
+    main()

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/export.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/export.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/export.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
-
+# -*- test-case-name: calendarserver.tools.test.test_export -*-
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -17,208 +17,357 @@
 ##
 
 """
-This tool reads calendar data from a series of inputs and generates a
-single iCalendar file which can be opened in many calendar
-applications.
+This tool reads calendar data from a series of inputs and generates a single
+iCalendar file which can be opened in many calendar applications.
 
-This can be used to quickly create an iCalendar file from a user's
-calendars.
+This can be used to quickly create an iCalendar file from a user's calendars.
 
-This tool requires access to the calendar server's configuration and
-data storage; it does not operate by talking to the server via the
-network.  It therefore does not apply any of the access restrictions
-that the server would.  As such, one should be midful that data
-exported via this tool may be sensitive.
+This tool requires access to the calendar server's configuration and data
+storage; it does not operate by talking to the server via the network.  It
+therefore does not apply any of the access restrictions that the server would.
+As such, one should be mindful that data exported via this tool may be sensitive.
+
+Please also note that this is not an appropriate tool for backups, as there is
+data associated with users and calendars beyond the iCalendar as visible to the
+owner of that calendar, including DAV properties, information about sharing, and
+per-user data such as alarms.
+
 """
 
 import os
 import sys
-from getopt import getopt, GetoptError
-from os.path import dirname, abspath
+import itertools
 
-from twistedcaldav.config import ConfigurationError
-from twistedcaldav.ical import Component as iComponent, Property as iProperty
-from twistedcaldav.ical import iCalendarProductID
-from twistedcaldav.resource import isCalendarCollectionResource,\
-    CalendarHomeResource
-from twistedcaldav.static import CalDAVFile
-from twistedcaldav.directory.directory import DirectoryService
+from twisted.python.text import wordWrap
+from twisted.python.usage import Options, UsageError
+from twisted.python import log
+from twisted.internet.defer import inlineCallbacks, returnValue
 
-from calendarserver.tools.util import UsageError
-from calendarserver.tools.util import loadConfig, getDirectory, dummyDirectoryRecord, autoDisableMemcached
+from twistedcaldav.ical import Component
 
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+from calendarserver.tools.cmdline import utilityMain
+from twisted.application.service import Service
+from calendarserver.tap.util import directoryFromConfig
+
+
 def usage(e=None):
     if e:
         print e
         print ""
-
-    name = os.path.basename(sys.argv[0])
-    print "usage: %s [options] [input_specifiers]" % (name,)
-    print ""
-    print "Generate an iCalendar file containing the merged content of each calendar"
-    print "collection read."
-    print __doc__
-    print "options:"
-    print "  -h --help: print this help and exit"
-    print "  -f --config: Specify caldavd.plist configuration path"
-    print "  -o --output: Specify output file path (default: '-', meaning stdout)"
-    print ""
-    print "input specifiers:"
-    print "  -c --collection: add a calendar collection"
-    print "  -H --home: add a calendar home (and all calendars within it)"
-    print "  -r --record: add a directory record's calendar home (format: 'recordType:shortName')"
-    print "  -u --user: add a user's calendar home (shorthand for '-r users:shortName')"
-
+    try:
+        ExportOptions().opt_help()
+    except SystemExit:
+        pass
     if e:
         sys.exit(64)
     else:
         sys.exit(0)
 
-def main():
-    try:
-        (optargs, args) = getopt(
-            sys.argv[1:], "hf:o:c:H:r:u:", [
-                "help",
-                "config=",
-                "output=",
-                "collection=", "home=", "record=", "user=",
-            ],
-        )
-    except GetoptError, e:
-        usage(e)
 
-    configFileName = None
-    outputFileName = None
+description = '\n'.join(
+    wordWrap(
+        """
+        Usage: calendarserver_export [options] [input specifiers]\n
+        """ + __doc__,
+        int(os.environ.get('COLUMNS', '80'))
+    )
+)
 
-    collections = set()
-    calendarHomes = set()
-    records = set()
+class ExportOptions(Options):
+    """
+    Command-line options for 'calendarserver_export'
 
-    def checkExists(resource):
-        if not resource.exists():
-            sys.stderr.write("No such file: %s\n" % (resource.fp.path,))
-            sys.exit(1)
+    @ivar exporters: a list of L{DirectoryExporter} objects which can identify the
+        calendars to export, given a directory service.  This list is built by
+        parsing --record and --collection options.
+    """
 
-    for opt, arg in optargs:
-        if opt in ("-h", "--help"):
-            usage()
+    synopsis = description
 
-        elif opt in ("-f", "--config"):
-            configFileName = arg
+    optParameters = [['config', 'f', DEFAULT_CONFIG_FILE,
+                      "Specify caldavd.plist configuration path."]]
 
-        elif opt in ("-o", "--output"):
-            if arg == "-":
-                outputFileName = None
-            else:
-                outputFileName = arg
+    def __init__(self):
+        super(ExportOptions, self).__init__()
+        self.exporters = []
+        self.outputName = '-'
 
-        elif opt in ("-c", "--collection"):
-            path = abspath(arg)
-            collection = CalDAVFile(path)
-            checkExists(collection)
-            if not isCalendarCollectionResource(collection):
-                sys.stderr.write("Not a calendar collection: %s\n" % (path,))
-                sys.exit(1)
-            collections.add(collection)
 
-        elif opt in ("-H", "--home"):
-            path = abspath(arg)
-            parent = CalDAVFile(dirname(abspath(path)))
-            calendarHome = CalendarHomeResource(arg, parent, dummyDirectoryRecord)
-            checkExists(calendarHome)
-            calendarHomes.add(calendarHome)
+    def opt_uid(self, uid):
+        """
+        Add a calendar home directly by its UID (which is usually a directory
+        service's GUID).
+        """
+        self.exporters.append(UIDExporter(uid))
 
-        elif opt in ("-r", "--record"):
-            try:
-                recordType, shortName = arg.split(":", 1)
-                if not recordType or not shortName:
-                    raise ValueError()
-            except ValueError:
-                sys.stderr.write("Invalid record identifier: %r\n" % (arg,))
-                sys.exit(1)
 
-            records.add((recordType, shortName))
+    def opt_record(self, recordName):
+        """
+        Add a directory record's calendar home (format: 'recordType:shortName').
+        """
+        recordType, shortName = recordName.split(":", 1)
+        self.exporters.append(DirectoryExporter(recordType, shortName))
 
-        elif opt in ("-u", "--user"):
-            records.add((DirectoryService.recordType_users, arg))
+    opt_r = opt_record
 
-    if args:
-        usage("Too many arguments: %s" % (" ".join(args),))
 
-    if records:
-        try:
-            config = loadConfig(configFileName)
-            config.directory = getDirectory()
-            autoDisableMemcached(config)
-        except ConfigurationError, e:
-            sys.stdout.write("%s\n" % (e,))
-            sys.exit(1)
+    def opt_collection(self, collectionName):
+        """
+        Add a calendar collection.  This option must be passed after --record
+        (or a synonym, like --user).  for example, to export user1's calendars
+        called 'meetings' and 'team', invoke 'calendarserver_export --user=user1
+        --collection=meetings --collection=team'.
+        """
+        self.exporters[-1].collections.append(collectionName)
 
-    for record in records:
-        recordType, shortName = record
-        calendarHome = config.directory.calendarHomeForShortName(recordType, shortName)
-        if not calendarHome:
-            sys.stderr.write("No calendar home found for record: (%s)%s\n" % (recordType, shortName))
-            sys.exit(1)
-        calendarHomes.add(calendarHome)
+    opt_c = opt_collection
 
-    for calendarHome in calendarHomes:
-        for childName in calendarHome.listChildren():
-            child = calendarHome.getChild(childName)
-            if isCalendarCollectionResource(child):
-                collections.add(child)
 
-    try:
-        calendar = iComponent("VCALENDAR")
-        calendar.addProperty(iProperty("VERSION", "2.0"))
-        calendar.addProperty(iProperty("PRODID", iCalendarProductID))
+    def opt_output(self, filename):
+        """
+        Specify output file path (default: '-', meaning stdout).
+        """
+        self.outputName = filename
 
-        uids  = set()
-        tzids = set()
+    opt_o = opt_output
 
-        for collection in collections:
-            for name, uid, type in collection.index().indexedSearch(None):
-                child = collection.getChild(name)
-                childData = child.iCalendarText()
 
-                try:
-                    childCalendar = iComponent.fromString(childData)
-                except ValueError:
-                    continue
-                assert childCalendar.name() == "VCALENDAR"
+    def opt_user(self, user):
+        """
+        Add a user's calendar home (shorthand for '-r users:shortName').
+        """
+        self.opt_record("users:" + user)
 
-                if uid in uids:
-                    sys.stderr.write("Skipping duplicate event UID %r from %s\n" % (uid, collection.fp.path))
-                    continue
-                else:
-                    uids.add(uid)
+    opt_u = opt_user
 
-                for component in childCalendar.subcomponents():
-                    # Only insert VTIMEZONEs once
-                    if component.name() == "VTIMEZONE":
-                        tzid = component.propertyValue("TZID")
-                        if tzid in tzids:
-                            continue
-                        else:
-                            tzids.add(tzid)
 
-                    calendar.addComponent(component)
+    def openOutput(self):
+        """
+        Open the appropriate output file based on the '--output' option.
+        """
+        if self.outputName == '-':
+            return sys.stdout
+        else:
+            return open(self.outputName, 'wb')
 
-        calendarData = str(calendar)
 
-        if outputFileName:
-            try:
-                output = open(outputFileName, "w")
-            except IOError, e:
-                sys.stderr.write("Unable to open output file for writing %s: %s\n" % (outputFileName, e))
-                sys.exit(1)
+
+class _ExporterBase(object):
+    """
+    Base exporter implementation that works from a home UID.
+
+    @ivar collections: A list of the names of collections that this exporter
+        should enumerate.
+
+    @type collections: C{list} of C{str}
+
+    """
+
+    def __init__(self):
+        self.collections = []
+
+
+    def getHomeUID(self, exportService):
+        """
+        Subclasses must implement this.
+        """
+        raise NotImplementedError()
+
+
+    @inlineCallbacks
+    def listCalendars(self, txn, exportService):
+        """
+        Enumerate all calendars based on the directory record and/or calendars
+        for this calendar home.
+        """
+        uid = self.getHomeUID(exportService)
+        home = yield txn.calendarHomeWithUID(uid, True)
+        result = []
+        if self.collections:
+            for collection in self.collections:
+                result.append((yield home.calendarWithName(collection)))
         else:
-            output = sys.stdout
+            for collection in (yield home.calendars()):
+                if collection.name() != 'inbox':
+                    result.append(collection)
+        returnValue(result)
 
-        output.write(calendarData)
 
+
+class UIDExporter(_ExporterBase):
+    """
+    An exporter that constructs a list of calendars based on the UID of the
+    home, and an optional list of calendar names.
+
+    @ivar uid: The UID of a calendar home to export.
+
+    @type uid: C{str}
+    """
+
+    def __init__(self, uid):
+        super(UIDExporter, self).__init__()
+        self.uid = uid
+
+
+    def getHomeUID(self, exportService):
+        return self.uid
+
+
+
+class DirectoryExporter(_ExporterBase):
+    """
+    An exporter that constructs a list of calendars based on the directory
+    services record ID of the home, and an optional list of calendar names.
+
+    @ivar recordType: The directory record type to export.  For example:
+        'users'.
+
+    @type recordType: C{str}
+
+    @ivar shortName: The shortName of the directory record to export, according
+        to C{recordType}.
+
+    @type shortName: C{str}
+    """
+
+    def __init__(self, recordType, shortName):
+        super(DirectoryExporter, self).__init__()
+        self.recordType = recordType
+        self.shortName = shortName
+
+
+    def getHomeUID(self, exportService):
+        """
+        Retrieve the home UID.
+        """
+        directory = exportService.directoryService()
+        record = directory.recordWithShortName(self.recordType, self.shortName)
+        return record.guid
+
+
+
+ at inlineCallbacks
+def exportToFile(calendars, fileobj):
+    """
+    Export some calendars to a file as their owner would see them.
+
+    @param calendars: an iterable of L{ICalendar} providers (or L{Deferred}s of
+        same).
+
+    @param fileobj: an object with a C{write} method that will accept some
+        iCalendar data.
+
+    @return: a L{Deferred} which fires when the export is complete.  (Note that
+        the file will not be closed.)
+    @rtype: L{Deferred} that fires with C{None}
+    """
+    comp = Component.newCalendar()
+    for calendar in calendars:
+        calendar = yield calendar
+        for obj in (yield calendar.calendarObjects()):
+            evt = yield obj.filteredComponent(
+                calendar.ownerCalendarHome().uid(), True)
+            for sub in evt.subcomponents():
+                if sub.name() != 'VTIMEZONE':
+                    # Omit all VTIMEZONE components here - we will include them later
+                    # when we serialize the whole calendar.
+                    comp.addComponent(sub)
+
+    fileobj.write(comp.getTextWithTimezones(True))
+
+
+
+class ExporterService(Service, object):
+    """
+    Service which runs, exports the appropriate records, then stops the reactor.
+    """
+
+    def __init__(self, store, options, output, reactor, config):
+        super(ExporterService, self).__init__()
+        self.store   = store
+        self.options = options
+        self.output  = output
+        self.reactor = reactor
+        self.config = config
+        self._directory = None
+
+
+    def startService(self):
+        """
+        Start the service.
+        """
+        super(ExporterService, self).startService()
+        self.doExport()
+
+
+    @inlineCallbacks
+    def doExport(self):
+        """
+        Do the export, stopping the reactor when done.
+        """
+        txn = self.store.newTransaction()
+        try:
+            allCalendars = itertools.chain(
+                *[(yield exporter.listCalendars(txn, self)) for exporter in
+                  self.options.exporters]
+            )
+            yield exportToFile(allCalendars, self.output)
+            yield txn.commit()
+            # TODO: should be read-only, so commit/abort shouldn't make a
+            # difference.  commit() for now, in case any transparent cache /
+            # update stuff needed to happen, don't want to undo it.
+            self.output.close()
+        except:
+            log.err()
+
+        self.reactor.stop()
+
+
+    def directoryService(self):
+        """
+        Get an appropriate directory service for this L{ExporterService}'s
+        configuration, creating one first if necessary.
+        """
+        if self._directory is None:
+            self._directory = directoryFromConfig(self.config)
+        return self._directory
+
+
+    def stopService(self):
+        """
+        Stop the service.  Nothing to do; everything should be finished by this
+        time.
+        """
+        # TODO: stopping this service mid-export should really stop the export
+        # loop, but this is not implemented because nothing will actually do it
+        # except hitting ^C (which also calls reactor.stop(), so that will exit
+        # anyway).
+
+
+
+def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
+    """
+    Do the export.
+    """
+    if reactor is None:
+        from twisted.internet import reactor
+
+    options = ExportOptions()
+    try:
+        options.parseOptions(argv[1:])
     except UsageError, e:
         usage(e)
 
-if __name__ == "__main__":
-    main()
+    try:
+        output = options.openOutput()
+    except IOError, e:
+        stderr.write("Unable to open output file for writing: %s\n" %
+                     (e))
+        sys.exit(1)
+
+    def makeService(store):
+        from twistedcaldav.config import config
+        return ExporterService(store, options, output, reactor, config)
+
+    utilityMain(options["config"], makeService, reactor)

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/notifications.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/notifications.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/notifications.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -47,10 +47,17 @@
     print "  -f --config <path>: Specify caldavd.plist configuration path"
     print "  -h --help: print this help and exit"
     print "  -H --host <hostname>: calendar server host name"
+    print "  -n --node <pubsub node>: pubsub node to subscribe to *"
     print "  -p --port <port number>: calendar server port number"
     print "  -s --ssl: use https (default is http)"
     print "  -v --verbose: print additional information including XMPP traffic"
     print ""
+    print " * The --node option is only required for calendar servers that"
+    print "   don't advertise the push-transports DAV property (such as a Snow"
+    print "   Leopard server).  In this case, --host should specify the name"
+    print "   of the XMPP server and --port should specify the port XMPP is"
+    print "   is listening on."
+    print ""
 
     if e:
         sys.stderr.write("%s\n" % (e,))
@@ -62,11 +69,12 @@
 def main():
     try:
         (optargs, args) = getopt(
-            sys.argv[1:], "a:f:hH:p:sv", [
+            sys.argv[1:], "a:f:hH:n:p:sv", [
                 "admin=",
                 "config=",
                 "help",
                 "host=",
+                "node=",
                 "port=",
                 "ssl",
                 "verbose",
@@ -78,6 +86,7 @@
     admin = None
     configFileName = None
     host = None
+    nodes = None
     port = None
     useSSL = False
     verbose = False
@@ -91,6 +100,8 @@
             admin = arg
         elif opt in ("-H", "--host"):
             host = arg
+        elif opt in ("-n", "--node"):
+            nodes = [arg]
         elif opt in ("-p", "--port"):
             port = int(arg)
         elif opt in ("-s", "--ssl"):
@@ -126,8 +137,8 @@
         password = getpass("Password for %s: " % (username,))
         admin = username
 
-    monitorService = PushMonitorService(useSSL, host, port, admin, username,
-        password, verbose)
+    monitorService = PushMonitorService(useSSL, host, port, nodes, admin,
+        username, password, verbose)
     reactor.addSystemEventTrigger("during", "startup",
         monitorService.startService)
     reactor.addSystemEventTrigger("before", "shutdown",
@@ -305,10 +316,12 @@
     using XMPP and monitored for updates.
     """
 
-    def __init__(self, useSSL, host, port, authname, username, password, verbose):
+    def __init__(self, useSSL, host, port, nodes, authname, username, password,
+        verbose):
         self.useSSL = useSSL
         self.host = host
         self.port = port
+        self.nodes = nodes
         self.authname = authname
         self.username = username
         self.password = password
@@ -317,24 +330,31 @@
     @inlineCallbacks
     def startService(self):
         try:
-            paths = set()
-            principal = "/principals/users/%s/" % (self.username,)
-            name, homes = (yield self.getPrincipalDetails(principal))
-            if self.verbose:
-                print name, homes
-            for home in homes:
-                paths.add(home)
-            for principal in (yield self.getProxyFor()):
-                name, homes = (yield self.getPrincipalDetails(principal,
-                    includeCardDAV=False))
+            subscribeNodes = { }
+            if self.nodes is None:
+                paths = set()
+                principal = "/principals/users/%s/" % (self.username,)
+                name, homes = (yield self.getPrincipalDetails(principal))
                 if self.verbose:
                     print name, homes
                 for home in homes:
                     paths.add(home)
-            subscribeNodes = { }
-            for path in paths:
-                host, port, nodes = (yield self.getPushInfo(path))
-                subscribeNodes.update(nodes)
+                for principal in (yield self.getProxyFor()):
+                    name, homes = (yield self.getPrincipalDetails(principal,
+                        includeCardDAV=False))
+                    if self.verbose:
+                        print name, homes
+                    for home in homes:
+                        paths.add(home)
+                for path in paths:
+                    host, port, nodes = (yield self.getPushInfo(path))
+                    subscribeNodes.update(nodes)
+            else:
+                for node in self.nodes:
+                    subscribeNodes[node] = ("Unknown", "Unknown", "Unknown")
+                host = self.host
+                port = self.port
+
             if subscribeNodes:
                 self.startMonitoring(host, port, subscribeNodes)
             else:

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/principals.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/principals.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -35,7 +35,6 @@
 
 from twistedcaldav.config import config, ConfigurationError
 from twistedcaldav.directory.directory import UnknownRecordTypeError, DirectoryError
-from twistedcaldav.directory import augment
 
 from calendarserver.tools.util import loadConfig, getDirectory, setupMemcached,  booleanArgument, checkDirectory
 
@@ -806,9 +805,10 @@
     else:
         record = directory.updateRecord(recordType, **kwargs)
 
-    augmentRecord = (yield augment.AugmentService.getAugmentRecord(kwargs['guid'], recordType))
+    augmentService = directory.serviceForRecordType(recordType).augmentService
+    augmentRecord = (yield augmentService.getAugmentRecord(kwargs['guid'], recordType))
     augmentRecord.autoSchedule = autoSchedule
-    (yield augment.AugmentService.addAugmentRecords([augmentRecord]))
+    (yield augmentService.addAugmentRecords([augmentRecord]))
     directory.updateRecord(recordType, **kwargs)
 
     returnValue(record)

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/purge.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/purge.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-
+# -*- test-case-name: calendarserver.tools.test.test_purge -*-
 ##
 # Copyright (c) 2006-2010 Apple Inc. All rights reserved.
 #
@@ -19,12 +19,18 @@
 import os
 import sys
 from errno import ENOENT, EACCES
-
 from getopt import getopt, GetoptError
 
+from pycalendar.datetime import PyCalendarDateTime
+
 from twisted.application.service import Service
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue
+
+from twext.python.log import Logger
+from twext.web2.dav import davxml
+from twext.web2.responsecode import NO_CONTENT
+
 from twistedcaldav import caldavxml
 from twistedcaldav.caldavxml import TimeRange
 from twistedcaldav.config import config, ConfigurationError
@@ -33,16 +39,11 @@
 from twistedcaldav.method.put_common import StoreCalendarObjectResource
 from twistedcaldav.query import calendarqueryfilter
 
-from twext.python.log import Logger
-from twext.web2.dav import davxml
-from twext.web2.responsecode import NO_CONTENT
-
-from calendarserver.tap.caldav import CalDAVServiceMaker, CalDAVOptions
 from calendarserver.tap.util import FakeRequest
 from calendarserver.tap.util import getRootResource
+
+from calendarserver.tools.cmdline import utilityMain
 from calendarserver.tools.principals import removeProxy
-from calendarserver.tools.util import loadConfig
-from pycalendar.datetime import PyCalendarDateTime
 
 log = Logger()
 
@@ -205,27 +206,7 @@
 
 
 
-def shared_main(configFileName, serviceClass):
 
-    try:
-        loadConfig(configFileName)
-
-        config.ProcessType = "Utility"
-        config.UtilityServiceClass = serviceClass
-
-        maker = CalDAVServiceMaker()
-        options = CalDAVOptions
-        service = maker.makeService(options)
-
-        reactor.addSystemEventTrigger("during", "startup", service.startService)
-        reactor.addSystemEventTrigger("before", "shutdown", service.stopService)
-
-    except ConfigurationError, e:
-        sys.stderr.write("Error: %s\n" % (e,))
-        return
-
-    reactor.run()
-
 def main_purge_events():
 
     try:
@@ -295,7 +276,7 @@
     PurgeOldEventsService.dryrun = dryrun
     PurgeOldEventsService.verbose = verbose
 
-    shared_main(
+    utilityMain(
         configFileName,
         PurgeOldEventsService,
     )
@@ -357,7 +338,7 @@
     PurgeOrphanedAttachmentsService.dryrun = dryrun
     PurgeOrphanedAttachmentsService.verbose = verbose
 
-    shared_main(
+    utilityMain(
         configFileName,
         PurgeOrphanedAttachmentsService,
     )
@@ -406,7 +387,7 @@
     PurgePrincipalService.verbose = verbose
 
 
-    shared_main(
+    utilityMain(
         configFileName,
         PurgePrincipalService
     )

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/resources.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/resources.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/resources.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -26,7 +26,6 @@
 from twisted.internet.defer import inlineCallbacks
 from twisted.python.util import switchUID
 from twistedcaldav.config import config, ConfigurationError
-from twistedcaldav.directory import augment
 from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
 from twistedcaldav.directory.directory import DirectoryService, DirectoryError
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
@@ -217,7 +216,7 @@
                         autoSchedule = autoSchedules.get(guid, 1)
                     else:
                         autoSchedule = True
-                    augmentRecord = (yield augment.AugmentService.getAugmentRecord(guid, recordType))
+                    augmentRecord = (yield destService.augmentService.getAugmentRecord(guid, recordType))
                     augmentRecord.autoSchedule = autoSchedule
                     augmentRecords.append(augmentRecord)
 
@@ -233,7 +232,7 @@
 
     destService.createRecords(directoryRecords)
 
-    (yield augment.AugmentService.addAugmentRecords(augmentRecords))
+    (yield destService.augmentService.addAugmentRecords(augmentRecords))
 
 
 

Copied: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/shell.py (from rev 7695, CalendarServer/trunk/calendarserver/tools/shell.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/shell.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/shell.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,413 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Interactive shell for navigating the data store.
+"""
+
+import os
+import sys
+import traceback
+from shlex import shlex
+
+#from twisted.python import log
+from twisted.python.text import wordWrap
+from twisted.python.usage import Options, UsageError
+from twisted.internet.defer import succeed, maybeDeferred
+from twisted.conch.stdio import runWithProtocol as shellWithProtocol
+from twisted.conch.recvline import HistoricRecvLine
+from twisted.application.service import Service
+
+from txdav.common.icommondatastore import NotFoundError
+
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+
+from calendarserver.tools.cmdline import utilityMain
+
+
+def usage(e=None):
+    if e:
+        print e
+        print ""
+    try:
+        ShellOptions().opt_help()
+    except SystemExit:
+        pass
+    if e:
+        sys.exit(64)
+    else:
+        sys.exit(0)
+
+
+class ShellOptions(Options):
+    """
+    Command line options for "calendarserver_shell".
+    """
+    synopsis = "\n".join(
+        wordWrap(
+            """
+            Usage: calendarserver_shell [options]\n
+            """ + __doc__,
+            int(os.environ.get("COLUMNS", "80"))
+        )
+    )
+
+    optParameters = [
+        ["config", "f", DEFAULT_CONFIG_FILE, "Specify caldavd.plist configuration path."],
+    ]
+
+    def __init__(self):
+        super(ShellOptions, self).__init__()
+
+
+class ShellService(Service, object):
+    def __init__(self, store, options, reactor, config):
+        super(ShellService, self).__init__()
+        self.store   = store
+        self.options = options
+        self.reactor = reactor
+        self.config = config
+
+    def startService(self):
+        """
+        Start the service.
+        """
+        super(ShellService, self).startService()
+        shellWithProtocol(lambda: ShellProtocol(self.store))
+        self.reactor.stop()
+
+    def stopService(self):
+        """
+        Stop the service.
+        """
+
+
+class ShellProtocol(HistoricRecvLine):
+    """
+    Data store shell protocol.
+    """
+
+    # FIXME:
+    # * Received lines are being echoed; find out why and stop it.
+    # * Backspace transposes characters in the terminal.
+
+    ps = ("ds% ", "... ")
+
+    def __init__(self, store):
+        HistoricRecvLine.__init__(self)
+        self.wd = RootDirectory(store)
+
+    def connectionMade(self):
+        HistoricRecvLine.connectionMade(self)
+
+        CTRL_C         = "\x03"
+        CTRL_D         = "\x04"
+        CTRL_L         = "\x0c"
+        CTRL_BACKSLASH = "\x1c"
+
+        self.keyHandlers[CTRL_C        ] = self.handle_INT
+        self.keyHandlers[CTRL_D        ] = self.handle_EOF
+        self.keyHandlers[CTRL_L        ] = self.handle_FF
+        self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
+
+    def handle_INT(self):
+        """
+        Handle ^C as an interrupt keystroke by resetting the current input
+        variables to their initial state.
+        """
+        self.pn = 0
+        self.lineBuffer = []
+        self.lineBufferIndex = 0
+        self.interpreter.resetBuffer()
+
+        self.terminal.nextLine()
+        self.terminal.write("KeyboardInterrupt")
+        self.terminal.nextLine()
+        self.terminal.write(self.ps[self.pn])
+
+    def handle_EOF(self):
+        if self.lineBuffer:
+            self.terminal.write("\a")
+        else:
+            self.handle_QUIT()
+
+    def handle_FF(self):
+        """
+        Handle a "form feed" byte - generally used to request a screen
+        refresh/redraw.
+        """
+        self.terminal.eraseDisplay()
+        self.terminal.cursorHome()
+        self.drawInputLine()
+
+    def handle_QUIT(self):
+        self.terminal.loseConnection()
+
+    def prompt(self):
+        pass
+
+    def lineReceived(self, line):
+        try:
+            lexer = shlex(line)
+            lexer.whitespace_split = True
+
+            tokens = []
+            while True:
+                token = lexer.get_token()
+                if not token:
+                    break
+                tokens.append(token)
+
+            if tokens:
+                cmd = tokens.pop(0)
+                #print "Arguments: %r" % (tokens,)
+
+                m = getattr(self, "cmd_%s" % (cmd,), None)
+                if m:
+                    def onError(f):
+                        print "Error: %s" % (f.getErrorMessage(),)
+                        print "-"*80
+                        f.printTraceback()
+                        print "-"*80
+
+                    d = maybeDeferred(m, tokens)
+                    d.addCallback(lambda _: self.prompt)
+                    d.addErrback(onError)
+                    return d
+                else:
+                    print "Unknown command: %s" % (cmd,)
+
+        except Exception, e:
+            print "Error: %s" % (e,)
+            print "-"*80
+            traceback.print_exc()
+            print "-"*80
+
+    def cmd_pwd(self, tokens):
+        """
+        Print working directory.
+        """
+        if tokens:
+            print "Unknown arguments: %s" % (tokens,)
+            return
+        print self.wd
+
+    def cmd_cd(self, tokens):
+        """
+        Change working directory.
+        """
+        if tokens:
+            dirname = tokens.pop(0)
+        else:
+            return
+
+        if tokens:
+            print "Unknown arguments: %s" % (tokens,)
+            return
+
+        path = dirname.split("/")
+
+        def notFound(f):
+            f.trap(NotFoundError)
+            print "No such directory: %s" % (dirname,)
+
+        def setWD(wd):
+            self.wd = wd
+
+        d = self.wd.locate(path)
+        d.addCallback(setWD)
+        d.addErrback(notFound)
+        return d
+
+    def cmd_ls(self, tokens):
+        """
+        List working directory.
+        """
+        if tokens:
+            print "Unknown arguments: %s" % (tokens,)
+            return
+
+        for name in self.wd.list():
+            print name
+
+    def cmd_info(self, tokens):
+        """
+        Print information about working directory.
+        """
+        d = self.wd.describe()
+        d.addCallback(lambda x: sys.stdout.write(x))
+        return d
+
+    def cmd_exit(self, tokens):
+        """
+        Exit the shell.
+        """
+        self.terminal.loseConnection()
+        # FIXME: This is insufficient.
+
+    def cmd_python(self, tokens):
+        """
+        Switch to a python prompt.
+        """
+        # Crazy idea #19568: switch to an interactive python prompt
+        # with self exposed in globals.
+        raise NotImplementedError()
+
+
+class Directory(object):
+    """
+    Location in virtual data hierarchy.
+    """
+    def __init__(self, store, path):
+        assert type(path) is tuple
+
+        self.store = store
+        self.path = path
+
+    def __str__(self):
+        return "/" + "/".join(self.path)
+
+    def describe(self):
+        return succeed(str(self))
+
+    def locate(self, path):
+        if not path:
+            return succeed(RootDirectory(self.store))
+
+        name = path[0]
+        if not name:
+            return succeed(self.locate(path[1:]))
+
+        path = list(path)
+
+        if name.startswith("/"):
+            path[0] = path[0][1:]
+            subdir = succeed(RootDirectory(self.store))
+        else:
+            path.pop(0)
+            subdir = self.subdir(name)
+
+        if path:
+            return subdir.addCallback(lambda path: locate(path))
+        else:
+            return subdir
+
+    def subdir(self, name):
+        if not name:
+            return succeed(self)
+        if name == ".":
+            return succeed(self)
+        if name == "..":
+            return RootDirectory(self.store).locate(self.path[:-1])
+
+        raise NotFoundError("Directory %r has no subdirectory %r" % (str(self), name))
+
+    def list(self):
+        return ()
+
+
+class RootDirectory(Directory):
+    """
+    Root of virtual data hierarchy.
+    """
+    def __init__(self, store):
+        Directory.__init__(self, store, ())
+
+        self._children = {}
+
+        self._childClasses = {
+            "uids": UIDDirectory,
+        }
+
+    def subdir(self, name):
+        if name in self._children:
+            return succeed(self._children[name])
+
+        if name in self._childClasses:
+            self._children[name] = self._childClasses[name](self.store, self.path + (name,))
+            return succeed(self._children[name])
+
+        return Directory.subdir(self, name)
+
+    def list(self):
+        return ("%s/" % (n,) for n in self._childClasses)
+
+
+class UIDDirectory(Directory):
+    """
+    Directory containing all principals by UID.
+    """
+    def subdir(self, name):
+        txn = self.store.newTransaction()
+
+        def gotHome(home):
+            if home:
+                return HomeDirectory(self.store, self.path + (name,), home)
+
+            return Directory.subdir(self, name)
+
+        d = txn.calendarHomeWithUID(name)
+        d.addCallback(gotHome)
+        return d
+
+    def list(self):
+        for (txn, home) in self.store.eachCalendarHome():
+            yield home.uid()
+
+
+class HomeDirectory(Directory):
+    """
+    Home directory.
+    """
+    def __init__(self, store, path, home):
+        Directory.__init__(self, store, path)
+
+        self.home = home
+
+    def describe(self):
+        return succeed(
+            """Calendar home for UID: %(uid)s\n"""
+            """Quota: %(quotaUsed)s of %(quotaMax)s (%(quotaPercent).2s%%)\n"""
+            % {
+                "uid"          : self.home.uid(),
+                "quotaUsed"    : self.home.quotaUsed(),
+                "quotaMax"     : self.home.quotaAllowedBytes(),
+                "quotaPercent" : self.home.quotaUsed() / self.home.quotaAllowedBytes(),
+            }
+        )
+
+
+def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
+    """
+    Do the export.
+    """
+    if reactor is None:
+        from twisted.internet import reactor
+
+    options = ShellOptions()
+    try:
+        options.parseOptions(argv[1:])
+    except UsageError, e:
+        usage(e)
+
+    def makeService(store):
+        from twistedcaldav.config import config
+        return ShellService(store, options, reactor, config)
+
+    print "Initializing shell..."
+
+    utilityMain(options["config"], makeService, reactor)

Copied: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/tables.py (from rev 7695, CalendarServer/trunk/calendarserver/tools/tables.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/tables.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/tables.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,275 @@
+##
+# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from sys import stdout
+import types
+
+class Table(object):
+    """
+    Class that allows pretty printing ascii tables.
+    
+    The table supports multiline headers and footers, independent
+    column formatting by row, alternative tab-delimited output. 
+    """
+    
+    class ColumnFormat(object):
+        """
+        Defines the format string, justification and span for a column.
+        """
+        
+        LEFT_JUSTIFY = 0
+        RIGHT_JUSTIFY = 1
+        CENTER_JUSTIFY = 2
+
+        def __init__(self, strFormat="%s", justify=LEFT_JUSTIFY, span=1):
+            
+            self.format = strFormat
+            self.justify = justify
+            self.span = span
+
+    def __init__(self, table=None):
+        
+        self.headers = []
+        self.headerColumnFormats = []
+        self.rows = []
+        self.footers = []
+        self.footerColumnFormats = []
+        self.columnCount = 0
+        self.defaultColumnFormats = []
+        self.columnFormatsByRow = {}
+
+        if table:
+            self.setData(table)
+
+    def setData(self, table):
+        
+        self.hasTitles = True
+        self.headers.append(table[0])
+        self.rows = table[1:]
+        self._getMaxColumnCount()
+
+    def setDefaultColumnFormats(self, columnFormats):
+        
+        self.defaultColumnFormats = columnFormats
+
+    def addDefaultColumnFormat(self, columnFormat):
+        
+        self.defaultColumnFormats.append(columnFormat)
+
+    def setHeaders(self, rows, columnFormats=None):
+        
+        self.headers = rows
+        self.headerColumnFormats = columnFormats if columnFormats else [None,] * len(self.headers)
+        self._getMaxColumnCount()
+
+    def addHeader(self, row, columnFormats=None):
+        
+        self.headers.append(row)
+        self.headerColumnFormats.append(columnFormats)
+        self._getMaxColumnCount()
+
+    def addHeaderDivider(self, skipColumns=()):
+        
+        self.headers.append((None, skipColumns,))
+        self.headerColumnFormats.append(None)
+
+    def setFooters(self, row, columnFormats=None):
+        
+        self.footers = row
+        self.footerColumnFormats = columnFormats if columnFormats else [None,] * len(self.footers)
+        self._getMaxColumnCount()
+
+    def addFooter(self, row, columnFormats=None):
+        
+        self.footers.append(row)
+        self.footerColumnFormats.append(columnFormats)
+        self._getMaxColumnCount()
+
+    def addRow(self, row=None, columnFormats=None):
+        
+        self.rows.append(row)
+        if columnFormats:
+            self.columnFormatsByRow[len(self.rows) - 1] = columnFormats
+        self._getMaxColumnCount()
+    
+    def addDivider(self, skipColumns=()):
+        
+        self.rows.append((None, skipColumns,))
+
+    def printTable(self, os=stdout):
+        
+        maxWidths = self._getMaxWidths()
+        
+        self.printDivider(os, maxWidths, False)
+        if self.headers:
+            for header, format in zip(self.headers, self.headerColumnFormats):
+                self.printRow(os, header, self._getHeaderColumnFormat(format), maxWidths)
+            self.printDivider(os, maxWidths)
+        for ctr, row in enumerate(self.rows):
+            self.printRow(os, row, self._getColumnFormatForRow(ctr), maxWidths)
+        if self.footers:
+            self.printDivider(os, maxWidths, double=True)
+            for footer, format in zip(self.footers, self.footerColumnFormats):
+                self.printRow(os, footer, self._getFooterColumnFormat(format), maxWidths)
+        self.printDivider(os, maxWidths, False)
+    
+    def printRow(self, os, row, format, maxWidths):
+        
+        if row is None or type(row) is tuple and row[0] is None:
+            self.printDivider(os, maxWidths, skipColumns=row[1] if type(row) is tuple else ())
+        else:
+            if len(row) != len(maxWidths):
+                row = list(row)
+                row.extend([""] * (len(maxWidths) - len(row)))
+
+            t = "|"
+            ctr = 0
+            while ctr < len(row):
+                startCtr = ctr
+                maxWidth = 0
+                for _ignore_span in xrange(format[startCtr].span if format else 1):
+                    maxWidth += maxWidths[ctr]
+                    ctr += 1
+                maxWidth += 3 * ((format[startCtr].span - 1) if format else 0)
+                text = self._columnText(row, startCtr, format, width=maxWidth)
+                t += " " + text + " |"
+            t += "\n"
+            os.write(t)
+            
+
+    def printDivider(self, os, maxWidths, intermediate=True, double=False, skipColumns=()):
+        t = "|" if intermediate else "+"
+        for widthctr, width in enumerate(maxWidths):
+            if widthctr in skipColumns:
+                c = " "
+            else:
+                c = "=" if double else "-"
+            t += c * (width + 2)
+            t += "+" if widthctr < len(maxWidths) - 1 else ("|" if intermediate else "+")
+        t += "\n"
+        os.write(t)
+
+    def printTabDelimitedData(self, os=stdout, footer=True):
+        
+        if self.headers:
+            titles = [""] * len(self.headers[0])
+            for row, header in enumerate(self.headers):
+                for col, item in enumerate(header):
+                    titles[col] += (" " if row and item else "") + item
+            self.printTabDelimitedRow(os, titles, self._getHeaderColumnFormat(self.headerColumnFormats[0]))
+        for ctr, row in enumerate(self.rows):
+            self.printTabDelimitedRow(os, row, self._getColumnFormatForRow(ctr))
+        if self.footers and footer:
+            for footer in self.footers:
+                self.printTabDelimitedRow(os, footer, self._getFooterColumnFormat(self.footerColumnFormats[0]))
+
+    def printTabDelimitedRow(self, os, row, format):
+        
+        if row is None:
+            row = [""] * self.columnCount
+        
+        if len(row) != self.columnCount:
+            row = list(row)
+            row.extend([""] * (self.columnCount - len(row)))
+
+        textItems = [self._columnText(row, ctr, format) for ctr in xrange((len(row)))]
+        os.write("\t".join(textItems) + "\n")
+        
+    def _getMaxColumnCount(self):
+        
+        self.columnCount = 0
+        if self.headers:
+            for header in self.headers:
+                self.columnCount = max(self.columnCount, len(header) if header else 0)
+        for row in self.rows:
+            self.columnCount = max(self.columnCount, len(row) if row else 0)
+        if self.footers:
+            for footer in self.footers:
+                self.columnCount = max(self.columnCount, len(footer) if footer else 0)
+
+    def _getMaxWidths(self):
+
+        maxWidths = [0] * self.columnCount
+
+        if self.headers:
+            for header, format in zip(self.headers, self.headerColumnFormats):
+                self._updateMaxWidthsFromRow(header, self._getHeaderColumnFormat(format), maxWidths)
+            
+        for ctr, row in enumerate(self.rows):
+            self._updateMaxWidthsFromRow(row, self._getColumnFormatForRow(ctr), maxWidths)
+
+        if self.footers:
+            for footer, format in zip(self.footers, self.footerColumnFormats):
+                self._updateMaxWidthsFromRow(footer, self._getFooterColumnFormat(format), maxWidths)
+            
+        return maxWidths
+
+    def _updateMaxWidthsFromRow(self, row, format, maxWidths):
+        
+        if row and (type(row) is not tuple or row[0] is not None):
+            ctr = 0
+            while ctr < len(row):
+                
+                text = self._columnText(row, ctr, format)       
+                startCtr = ctr
+                for _ignore_span in xrange(format[startCtr].span if format else 1):
+                    maxWidths[ctr] = max(maxWidths[ctr], len(text) / (format[startCtr].span if format else 1))
+                    ctr += 1
+    
+    def _getHeaderColumnFormat(self, format):
+        
+        if format:
+            return format
+        else:
+            justify = Table.ColumnFormat.CENTER_JUSTIFY if len(self.headers) == 1 else Table.ColumnFormat.LEFT_JUSTIFY
+            return [Table.ColumnFormat(justify = justify)] * self.columnCount
+
+    def _getFooterColumnFormat(self, format):
+        
+        if format:
+            return format
+        else:
+            return self.defaultColumnFormats
+
+    def _getColumnFormatForRow(self, ctr):
+        
+        if ctr in self.columnFormatsByRow:
+            return self.columnFormatsByRow[ctr]
+        else:
+            return self.defaultColumnFormats
+
+    def _columnText(self, row, column, format, width=0):
+        
+        if row is None or column >= len(row):
+            return ""
+        
+        colData = row[column]
+        if colData is None:
+            colData = ""
+
+        columnFormat = format[column] if format and column < len(format) else Table.ColumnFormat()
+        if type(colData) in types.StringTypes:
+            text = colData
+        else:
+            text = columnFormat.format % colData
+        if width:
+            if columnFormat.justify == Table.ColumnFormat.LEFT_JUSTIFY:
+                text = text.ljust(width)
+            elif columnFormat.justify == Table.ColumnFormat.RIGHT_JUSTIFY:
+                text = text.rjust(width)
+            elif columnFormat.justify == Table.ColumnFormat.CENTER_JUSTIFY:
+                text = text.center(width)
+        return text

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_changeip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_changeip.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_changeip.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -43,6 +43,17 @@
                     },
                 },
             },
+            "Scheduling" : {
+                "iMIP" : {
+                    "Receiving" : {
+                        "Server" : "original_hostname",
+                    },
+                    "Sending" : {
+                        "Server" : "original_hostname",
+                        "Address" : "user at original_hostname",
+                    },
+                },
+            },
         }
 
         updatePlist(plist, "10.1.1.1", "10.1.1.2", "original_hostname",
@@ -71,5 +82,16 @@
                         },
                     },
                 },
+                "Scheduling" : {
+                    "iMIP" : {
+                        "Receiving" : {
+                            "Server" : "new_hostname",
+                        },
+                        "Sending" : {
+                            "Server" : "new_hostname",
+                            "Address" : "user at new_hostname",
+                        },
+                    },
+                },
             }
         )

Copied: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_export.py (from rev 7695, CalendarServer/trunk/calendarserver/tools/test/test_export.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_export.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_export.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,487 @@
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Unit tests for L{calendarsever.tools.export}.
+"""
+
+
+import sys
+from cStringIO import StringIO
+
+from twisted.trial.unittest import TestCase
+
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.modules import getModule
+
+from twext.enterprise.ienterprise import AlreadyFinishedError
+
+from twistedcaldav.ical import Component
+from twistedcaldav.datafilters.test.test_peruserdata import dataForTwoUsers
+from twistedcaldav.datafilters.test.test_peruserdata import resultForUser2
+
+from calendarserver.tools import export
+from calendarserver.tools.export import ExportOptions, main
+from calendarserver.tools.export import DirectoryExporter, UIDExporter
+
+from twisted.python.filepath import FilePath
+from twistedcaldav.test.util import patchConfig
+from twisted.internet.defer import Deferred
+
+from txdav.common.datastore.test.util import buildStore
+from txdav.common.datastore.test.util import populateCalendarsFrom
+
+from calendarserver.tools.export import usage, exportToFile
+
+def holiday(uid):
+    return (
+        getModule("twistedcaldav.test").filePath
+            .sibling("data").child("Holidays").child(uid + ".ics")
+            .getContent()
+    )
+
+def sample(name):
+    return (
+        getModule("twistedcaldav.test").filePath
+        .sibling("data").child(name + ".ics").getContent()
+    )
+
+valentines = holiday("C31854DA-1ED0-11D9-A5E0-000A958A3252")
+newYears = holiday("C3184A66-1ED0-11D9-A5E0-000A958A3252")
+payday = sample("PayDay")
+
+one = sample("OneEvent")
+another = sample("AnotherEvent")
+third = sample("ThirdEvent")
+
+
+class CommandLine(TestCase):
+    """
+    Simple tests for command-line parsing.
+    """
+
+    def test_usageMessage(self):
+        """
+        The 'usage' message should print something to standard output (and
+        nothing to standard error) and exit.
+        """
+        orig = sys.stdout
+        orige = sys.stderr
+        try:
+            out = sys.stdout = StringIO()
+            err = sys.stderr = StringIO()
+            self.assertRaises(SystemExit, usage)
+        finally:
+            sys.stdout = orig
+            sys.stderr = orige
+        self.assertEquals(len(out.getvalue()) > 0, True, "No output.")
+        self.assertEquals(len(err.getvalue()), 0)
+
+
+    def test_oneRecord(self):
+        """
+        One '--record' option will result in a single L{DirectoryExporter}
+        object with no calendars in its list.
+        """
+        eo = ExportOptions()
+        eo.parseOptions(["--record", "users:bob"])
+        self.assertEquals(len(eo.exporters), 1)
+        exp = eo.exporters[0]
+        self.assertIsInstance(exp, DirectoryExporter)
+        self.assertEquals(exp.recordType, "users")
+        self.assertEquals(exp.shortName, "bob")
+        self.assertEquals(exp.collections, [])
+
+
+    def test_oneUID(self):
+        """
+        One '--uid' option will result in a single L{UIDExporter} object with no
+        calendars in its list.
+        """
+        eo = ExportOptions()
+        eo.parseOptions(["--uid", "bob's your guid"])
+        self.assertEquals(len(eo.exporters), 1)
+        exp = eo.exporters[0]
+        self.assertIsInstance(exp, UIDExporter)
+        self.assertEquals(exp.uid, "bob's your guid")
+
+
+    def test_homeAndCollections(self):
+        """
+        The --collection option adds specific calendars to the last home that
+        was exported.
+        """
+        eo = ExportOptions()
+        eo.parseOptions(["--record", "users:bob",
+                         "--collection", "work stuff",
+                         "--uid", "jethroUID",
+                         "--collection=fun stuff"])
+        self.assertEquals(len(eo.exporters), 2)
+        exp = eo.exporters[0]
+        self.assertEquals(exp.recordType, "users")
+        self.assertEquals(exp.shortName, "bob")
+        self.assertEquals(exp.collections, ["work stuff"])
+        exp = eo.exporters[1]
+        self.assertEquals(exp.uid, "jethroUID")
+        self.assertEquals(exp.collections, ["fun stuff"])
+
+
+    def test_outputFileSelection(self):
+        """
+        The --output option selects the file to write to, '-' or no parameter
+        meaning stdout; the L{ExportOptions.openOutput} method returns that
+        file.
+        """
+        eo = ExportOptions()
+        eo.parseOptions([])
+        self.assertIdentical(eo.openOutput(), sys.stdout)
+        eo = ExportOptions()
+        eo.parseOptions(["--output", "-"])
+        self.assertIdentical(eo.openOutput(), sys.stdout)
+        eo = ExportOptions()
+        tmpnam = self.mktemp()
+        eo.parseOptions(["--output", tmpnam])
+        self.assertEquals(eo.openOutput().name, tmpnam)
+
+
+    def test_outputFileError(self):
+        """
+        If the output file cannot be opened for writing, an error will be
+        displayed to the user on stderr.
+        """
+        io = StringIO()
+        systemExit = self.assertRaises(
+            SystemExit, main, ['calendarserver_export',
+                               '--output', '/not/a/file'], io
+        )
+        self.assertEquals(systemExit.code, 1)
+        self.assertEquals(
+            io.getvalue(),
+            "Unable to open output file for writing: "
+            "[Errno 2] No such file or directory: '/not/a/file'\n")
+
+
+
+class IntegrationTests(TestCase):
+    """
+    Tests for exporting data from a live store.
+    """
+
+    accountsFile = 'no-accounts.xml'
+    augmentsFile = 'no-augments.xml'
+
+    @inlineCallbacks
+    def setUp(self):
+        """
+        Set up a store and fix the imported C{utilityMain} function (normally
+        from L{calendarserver.tools.cmdline.utilityMain}) to point to a
+        temporary method of this class.  Also, patch the imported C{reactor},
+        since the SUT needs to call C{reactor.stop()} in order to work with
+        L{utilityMain}.
+        """
+        self.mainCalled = False
+        self.patch(export, "utilityMain", self.fakeUtilityMain)
+
+
+        self.store = yield buildStore(self, None)
+        self.waitToStop = Deferred()
+
+
+    def stop(self):
+        """
+        Emulate reactor.stop(), which the service must call when it is done with
+        work.
+        """
+        self.waitToStop.callback(None)
+
+
+    def fakeUtilityMain(self, configFileName, serviceClass, reactor=None):
+        """
+        Verify a few basic things.
+        """
+        if self.mainCalled:
+            raise RuntimeError(
+                "Main called twice during this test; duplicate reactor run.")
+
+        patchConfig(
+            self,
+            DirectoryService=dict(
+                type="twistedcaldav.directory.xmlfile.XMLDirectoryService",
+                params=dict(
+                    xmlFile=self.accountsFile
+                )
+            ),
+            ResourceService=dict(Enabled=False),
+            AugmentService=dict(
+                type="twistedcaldav.directory.augment.AugmentXMLDB",
+                params=dict(
+                    xmlFiles=[self.augmentsFile]
+                )
+            )
+        )
+
+        self.mainCalled = True
+        self.usedConfigFile = configFileName
+        self.usedReactor = reactor
+        self.exportService = serviceClass(self.store)
+        self.exportService.startService()
+        self.addCleanup(self.exportService.stopService)
+
+
+    @inlineCallbacks
+    def test_serviceState(self):
+        """
+        export.main() invokes utilityMain with the configuration file specified
+        on the command line, and creates an L{ExporterService} pointed at the
+        appropriate store.
+        """
+        tempConfig = self.mktemp()
+        main(['calendarserver_export', '--config', tempConfig, '--output',
+              self.mktemp()], reactor=self)
+        self.assertEquals(self.mainCalled, True, "Main not called.")
+        self.assertEquals(self.usedConfigFile, tempConfig)
+        self.assertEquals(self.usedReactor, self)
+        self.assertEquals(self.exportService.store, self.store)
+        yield self.waitToStop
+
+
+    @inlineCallbacks
+    def test_emptyCalendar(self):
+        """
+        Exporting an empty calendar results in an empty calendar.
+        """
+        io = StringIO()
+        value = yield exportToFile([], io)
+        # it doesn't return anything, it writes to the file.
+        self.assertEquals(value, None)
+        # but it should write a valid component to the file.
+        self.assertEquals(Component.fromString(io.getvalue()),
+                          Component.newCalendar())
+
+
+    def txn(self):
+        """
+        Create a new transaction and automatically clean it up when the test
+        completes.
+        """
+        aTransaction = self.store.newTransaction()
+        @inlineCallbacks
+        def maybeAbort():
+            try:
+                yield aTransaction.abort()
+            except AlreadyFinishedError:
+                pass
+        self.addCleanup(maybeAbort)
+        return aTransaction
+
+
+    @inlineCallbacks
+    def test_oneEventCalendar(self):
+        """
+        Exporting an calendar with one event in it will result in just that
+        event.
+        """
+        yield populateCalendarsFrom(
+            {
+                "home1": {
+                    "calendar1": {
+                        "valentines-day.ics": (valentines, {})
+                    }
+                }
+            }, self.store
+        )
+
+        expected = Component.newCalendar()
+        [theComponent] = Component.fromString(valentines).subcomponents()
+        expected.addComponent(theComponent)
+
+        io = StringIO()
+        yield exportToFile(
+            [(yield self.txn().calendarHomeWithUID("home1"))
+              .calendarWithName("calendar1")], io
+        )
+        self.assertEquals(Component.fromString(io.getvalue()),
+                          expected)
+
+
+    @inlineCallbacks
+    def test_twoSimpleEvents(self):
+        """
+        Exporting a calendar with two events in it will result in a VCALENDAR
+        component with both VEVENTs in it.
+        """
+        yield populateCalendarsFrom(
+            {
+                "home1": {
+                    "calendar1": {
+                        "valentines-day.ics": (valentines, {}),
+                        "new-years-day.ics": (newYears, {})
+                    }
+                }
+            }, self.store
+        )
+
+        expected = Component.newCalendar()
+        a = Component.fromString(valentines)
+        b = Component.fromString(newYears)
+        for comp in a, b:
+            for sub in comp.subcomponents():
+                expected.addComponent(sub)
+
+        io = StringIO()
+        yield exportToFile(
+            [(yield self.txn().calendarHomeWithUID("home1"))
+              .calendarWithName("calendar1")], io
+        )
+        self.assertEquals(Component.fromString(io.getvalue()),
+                          expected)
+
+
+    @inlineCallbacks
+    def test_onlyOneVTIMEZONE(self):
+        """
+        C{VTIMEZONE} subcomponents with matching TZIDs in multiple event
+        calendar objects should only be rendered in the resulting output once.
+
+        (Note that the code to suppor this is actually in PyCalendar, not the
+        export tool itself.)
+        """
+        yield populateCalendarsFrom(
+            {
+                "home1": {
+                    "calendar1": {
+                        "1.ics": (one, {}), # EST
+                        "2.ics": (another, {}), # EST
+                        "3.ics": (third, {}) # PST
+                    }
+                }
+            }, self.store
+        )
+
+        io = StringIO()
+        yield exportToFile(
+            [(yield self.txn().calendarHomeWithUID("home1"))
+              .calendarWithName("calendar1")], io
+        )
+        result = Component.fromString(io.getvalue())
+
+        def filtered(name):
+            for c in result.subcomponents():
+                if c.name() == name:
+                    yield c
+
+        timezones = list(filtered("VTIMEZONE"))
+        events = list(filtered("VEVENT"))
+
+        # Sanity check to make sure we picked up all three events:
+        self.assertEquals(len(events), 3)
+
+        self.assertEquals(len(timezones), 2)
+        self.assertEquals(set([tz.propertyValue("TZID") for tz in timezones]),
+
+                          # Use an intentionally wrong TZID in order to make
+                          # sure we don't depend on caching effects elsewhere.
+                          set(["America/New_Yrok", "US/Pacific"]))
+
+
+    @inlineCallbacks
+    def test_perUserFiltering(self):
+        """
+        L{exportToFile} performs per-user component filtering based on the owner
+        of that calendar.
+        """
+        yield populateCalendarsFrom(
+            {
+                "user02": {
+                    "calendar1": {
+                        "peruser.ics": (dataForTwoUsers, {}), # EST
+                    }
+                }
+            }, self.store
+        )
+        io = StringIO()
+        yield exportToFile(
+            [(yield self.txn().calendarHomeWithUID("user02"))
+              .calendarWithName("calendar1")], io
+        )
+        self.assertEquals(
+            Component.fromString(resultForUser2),
+            Component.fromString(io.getvalue())
+        )
+
+
+    @inlineCallbacks
+    def test_full(self):
+        """
+        Running C{calendarserver_export} on the command line exports an ics
+        file. (Almost-full integration test, starting from the main point, using
+        as few test fakes as possible.)
+
+        Note: currently the only test for directory interaction.
+        """
+        yield populateCalendarsFrom(
+            {
+                "user02": {
+                    # TODO: more direct test for skipping inbox
+                    "inbox": {
+                        "inbox-item.ics": (valentines, {})
+                    },
+                    "calendar1": {
+                        "peruser.ics": (dataForTwoUsers, {}), # EST
+                    }
+                }
+            }, self.store
+        )
+
+        augmentsData = """
+            <augments>
+              <record>
+                <uid>Default</uid>
+                <enable>true</enable>
+                <enable-calendar>true</enable-calendar>
+                <enable-addressbook>true</enable-addressbook>
+              </record>
+            </augments>
+        """
+        augments = FilePath(self.mktemp())
+        augments.setContent(augmentsData)
+
+        accountsData = """
+            <accounts realm="Test Realm">
+                <user>
+                    <uid>user-under-test</uid>
+                    <guid>user02</guid>
+                    <name>Not Interesting</name>
+                    <password>very-secret</password>
+                </user>
+            </accounts>
+        """
+        accounts = FilePath(self.mktemp())
+        accounts.setContent(accountsData)
+        output = FilePath(self.mktemp())
+        self.accountsFile = accounts.path
+        self.augmentsFile = augments.path
+        main(['calendarserver_export', '--output',
+              output.path, '--user', 'user-under-test'], reactor=self)
+
+        yield self.waitToStop
+
+        self.assertEquals(
+            Component.fromString(resultForUser2),
+            Component.fromString(output.getContent())
+        )
+
+

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_gateway.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_gateway.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_gateway.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -27,9 +27,7 @@
 from twistedcaldav.test.util import TestCase, CapturingProcessProtocol
 from calendarserver.tools.util import getDirectory
 
-from twistedcaldav.directory import augment
 
-
 class GatewayTestCase(TestCase):
 
     def setUp(self):
@@ -153,7 +151,8 @@
 
         # This appears to be necessary in order for record.autoSchedule to
         # reflect the change prior to the directory record expiration
-        augment.AugmentService.refresh()
+        augmentService = directory.serviceForRecordType(directory.recordType_locations).augmentService
+        augmentService.refresh()
 
         record = directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
         self.assertEquals(record.fullName.decode("utf-8"), "Created Location 01 %s" % unichr(208))
@@ -187,7 +186,8 @@
 
         # This appears to be necessary in order for record.autoSchedule to
         # reflect the change
-        augment.AugmentService.refresh()
+        augmentService = directory.serviceForRecordType(directory.recordType_locations).augmentService
+        augmentService.refresh()
 
         record = directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
 

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_purge_old_events.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_purge_old_events.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_purge_old_events.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -26,7 +26,6 @@
 from twisted.trial import unittest
 
 from twistedcaldav.config import config
-from twistedcaldav.memcacher import Memcacher
 from twistedcaldav.vcard import Component as VCardComponent
 
 from txdav.common.datastore.test.util import buildStore, populateCalendarsFrom, CommonCommonTests
@@ -348,9 +347,6 @@
 
     @inlineCallbacks
     def setUp(self):
-        self.patch(config.Memcached.Pools.Default, "ClientEnabled", False)
-        self.patch(config.Memcached.Pools.Default, "ServerEnabled", False)
-        self.patch(Memcacher, "allowTestCache", True)
         yield super(PurgeOldEventsTests, self).setUp()
         self._sqlCalendarStore = yield buildStore(self, self.notifierFactory)
         yield self.populate()

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_resources.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_resources.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/test/test_resources.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -16,7 +16,6 @@
 
 from calendarserver.tools.resources import migrateResources
 from twisted.internet.defer import inlineCallbacks, succeed
-from twistedcaldav.directory import augment
 from twistedcaldav.directory.directory import DirectoryService
 from twistedcaldav.test.util import TestCase
 
@@ -41,8 +40,9 @@
 
 class StubDirectoryService(object):
 
-    def __init__(self):
+    def __init__(self, augmentService):
         self.records = {}
+        self.augmentService = augmentService
 
     def recordWithGUID(self, guid):
         return None
@@ -117,8 +117,7 @@
         def queryMethod(sourceService, recordType, verbose=False):
             return data[recordType]
 
-        self.patch(augment, "AugmentService", StubAugmentService)
-        directoryService = StubDirectoryService()
+        directoryService = StubDirectoryService(StubAugmentService())
         yield migrateResources(None, directoryService, queryMethod=queryMethod)
         for guid, recordType in (
             ('6C99E240-E915-4012-82FA-99E0F638D7EF', DirectoryService.recordType_resources),

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/util.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/util.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2008-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2008-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,6 +14,10 @@
 # limitations under the License.
 ##
 
+"""
+Utility functionality shared between calendarserver tools.
+"""
+
 __all__ = [
     "loadConfig",
     "getDirectory",
@@ -34,9 +38,10 @@
 
 
 from calendarserver.provision.root import RootResource
+
 from twistedcaldav import memcachepool
 from twistedcaldav.config import config, ConfigurationError
-from twistedcaldav.directory import augment, calendaruserproxy
+from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
 from twistedcaldav.notify import NotifierFactory
@@ -124,13 +129,14 @@
 
     # Load augment/proxy db classes now
     augmentClass = namedClass(config.AugmentService.type)
-    augment.AugmentService = augmentClass(**config.AugmentService.params)
+    augmentService = augmentClass(**config.AugmentService.params)
 
     proxydbClass = namedClass(config.ProxyDBService.type)
     calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
 
     # Wait for directory service to become available
     BaseDirectoryService = namedClass(config.DirectoryService.type)
+    config.DirectoryService.params.augmentService = augmentService
     directory = BaseDirectoryService(config.DirectoryService.params)
     while not directory.isAvailable():
         sleep(5)
@@ -140,6 +146,7 @@
 
     if config.ResourceService.Enabled:
         resourceClass = namedClass(config.ResourceService.type)
+        config.ResourceService.params.augmentService = augmentService
         resourceDirectory = resourceClass(config.ResourceService.params)
         resourceDirectory.realmName = directory.realmName
         directories.append(resourceDirectory)
@@ -270,3 +277,5 @@
             % (description, dirpath)
         )
 
+
+

Modified: CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/warmup.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/warmup.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/calendarserver/tools/warmup.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -168,7 +168,7 @@
                 child = calendarCollection.getChild(name)
 
                 #sys.stdout.write("+")
-                child.iCalendarText()
+                child._text()
 
                 readProperties(child)
 


Property changes on: CalendarServer/branches/users/cdaboo/timezones/calendarserver/webadmin
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/calendarserver/webcal
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/conf/caldavd-apple.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/conf/caldavd-apple.plist	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/conf/caldavd-apple.plist	2011-06-30 20:44:13 UTC (rev 7697)
@@ -522,6 +522,8 @@
     <!-- Directory searching -->
     <key>DirectoryAddressBook</key>
     <dict>
+	<key>Enabled</key>
+	<true/>
         <key>params</key>
         <dict>
             <key>queryUserRecords</key>

Modified: CalendarServer/branches/users/cdaboo/timezones/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/conf/caldavd-test.plist	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/conf/caldavd-test.plist	2011-06-30 20:44:13 UTC (rev 7697)
@@ -259,6 +259,10 @@
             <string></string>
             <key>recordName</key>
             <string>uid</string>
+            <key>loginEnabledAttr</key>
+            <string>loginEnabled</string>
+            <key>loginEnabledValue</key>
+            <string>yes</string>
           </dict>
           <key>groups</key>
           <dict>

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/certupdate/calendarcertupdate.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/certupdate/calendarcertupdate.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/certupdate/calendarcertupdate.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -18,11 +18,10 @@
 # Apple's written consent.
 
 import datetime
-import os
 import subprocess
 import sys
 
-from plistlib import readPlist, readPlistFromString, writePlist
+from plistlib import readPlist, writePlist
 
 LOG = "/var/log/caldavd/certupdate.log"
 SERVICE_NAME = "calendar"

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/migration/calendarmigrator.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/migration/calendarmigrator.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/migration/calendarmigrator.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -305,8 +305,9 @@
     """
 
     if enableCalDAV or enableCardDAV:
-        log("Starting service via serveradmin")
-        ret = subprocess.call([SERVER_ADMIN, "start", "calendar"])
+        serviceName = "calendar" if enableCalDAV else "addressbook"
+        log("Starting service via serveradmin start %s" % (serviceName,))
+        ret = subprocess.call([SERVER_ADMIN, "start", serviceName])
         log("serveradmin exited with %d" % (ret,))
 
 
@@ -444,16 +445,28 @@
         if key in caldav:
             combined[key] = caldav[key]
 
-    # "Wiki" is a new authentication in v2.x; copy all "Authentication" sub-keys    # over, and "Wiki" will be picked up from the new plist:
+    # Copy all "Authentication" sub-keys
     if "Authentication" in caldav:
+        if "Authentication" not in combined:
+            combined["Authentication"] = { }
         for key in caldav["Authentication"]:
             combined["Authentication"][key] = caldav["Authentication"][key]
 
+        # Examine the Wiki URL -- if it's using :8089 then we leave the Wiki
+        # section as is.  Otherwise, reset it so that it picks up the coded
+        # default
+        if "Wiki" in combined["Authentication"]:
+            if "URL" in combined["Authentication"]["Wiki"]:
+                url = combined["Authentication"]["Wiki"]["URL"]
+                if ":8089" not in url:
+                    combined["Authentication"]["Wiki"] = { "Enabled" : True }
+
+
     # Strip out any unknown params from the DirectoryService:
     if "DirectoryService" in caldav:
         combined["DirectoryService"] = caldav["DirectoryService"]
         for key in combined["DirectoryService"]["params"].keys():
-            if key not in ("node", "cacheTimeout", "xmlFile"):
+            if key in ("requireComputerRecord",):
                 del combined["DirectoryService"]["params"][key]
 
     # Merge ports

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/migration/test/test_migrator.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/migration/test/test_migrator.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/migration/test/test_migrator.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -20,6 +20,28 @@
 )
 import contrib.migration.calendarmigrator
 
+class FakeUser(object):
+    pw_uid = 6543
+
+
+class FakeGroup(object):
+    gr_gid = 7654
+
+
+class FakePwd(object):
+    def getpwnam(self, nam):
+        if nam != 'calendar':
+            raise RuntimeError("Only 'calendar' user supported for testing.")
+        return FakeUser()
+
+
+class FakeGrp(object):
+    def getgrnam(self, nam):
+        if nam != 'calendar':
+            raise RuntimeError("Only 'calendar' group supported for testing.")
+        return FakeGroup()
+
+
 class MigrationTests(twistedcaldav.test.util.TestCase):
     """
     Calendar Server Migration Tests
@@ -27,7 +49,10 @@
 
     def setUp(self):
         # Disable logging during tests
+
         self.patch(contrib.migration.calendarmigrator, "log", lambda _: None)
+        self.patch(contrib.migration.calendarmigrator, "pwd", FakePwd())
+        self.patch(contrib.migration.calendarmigrator, "grp", FakeGrp())
 
 
     def test_mergeSSL(self):
@@ -293,6 +318,125 @@
         self.assertEquals(newCombined, expected)
 
 
+    def test_mergeDirectoryService(self):
+
+        # Ensure caldavd DirectoryService config is carried over, except
+        # for requireComputerRecord which was is obsolete.
+
+        oldCalDAV = {
+            "DirectoryService": {
+                "type" : "twistedcaldav.directory.appleopendirectory.OpenDirectoryService",
+                "params" : {
+                    "node" : "/Search",
+                    "cacheTimeout" : 15,
+                    "restrictToGroup" : "test-group",
+                    "restrictEnabledRecords" : True,
+                    "negativeCaching" : False,
+                    "requireComputerRecord" : True,
+                },
+            },
+        }
+        oldCardDAV = { "Is this ignored?" : True }
+        expected = {
+            "DirectoryService": {
+                "type" : "twistedcaldav.directory.appleopendirectory.OpenDirectoryService",
+                "params" : {
+                    "node" : "/Search",
+                    "cacheTimeout" : 15,
+                    "restrictToGroup" : "test-group",
+                    "restrictEnabledRecords" : True,
+                    "negativeCaching" : False,
+                },
+            },
+            "BindHTTPPorts": [8008, 8800],
+            "BindSSLPorts": [8443, 8843],
+            "EnableSSL" : False,
+            "HTTPPort": 8008,
+            "RedirectHTTPToHTTPS": False,
+            "SSLAuthorityChain": "",
+            "SSLCertificate": "",
+            "SSLPort": 8443,
+            "SSLPrivateKey": "",
+        }
+        newCombined = { }
+        mergePlist(oldCalDAV, oldCardDAV, newCombined)
+        self.assertEquals(newCombined, expected)
+
+
+    def test_mergeAuthentication(self):
+
+        # Ensure caldavd Authentication config for wiki gets reset because
+        # the port number has changed for the wiki rpc url
+
+        oldCalDAV = {
+            "Authentication": {
+                "Wiki" : {
+                    "UseSSL" : False,
+                    "Enabled" : True,
+                    "Hostname" : "127.0.0.1",
+                    "URL" : "http://127.0.0.1/RPC2",
+                },
+            },
+        }
+        oldCardDAV = { "Is this ignored?" : True }
+        expected = {
+            "Authentication": {
+                "Wiki" : {
+                    "Enabled" : True,
+                },
+            },
+            "BindHTTPPorts": [8008, 8800],
+            "BindSSLPorts": [8443, 8843],
+            "EnableSSL" : False,
+            "HTTPPort": 8008,
+            "RedirectHTTPToHTTPS": False,
+            "SSLAuthorityChain": "",
+            "SSLCertificate": "",
+            "SSLPort": 8443,
+            "SSLPrivateKey": "",
+        }
+        newCombined = { }
+        mergePlist(oldCalDAV, oldCardDAV, newCombined)
+        self.assertEquals(newCombined, expected)
+
+        # If the port is :8089, leave the wiki config as is, since it's
+        # already set for Lio
+
+        oldCalDAV = {
+            "Authentication": {
+                "Wiki" : {
+                    "UseSSL" : False,
+                    "Enabled" : True,
+                    "Hostname" : "127.0.0.1",
+                    "URL" : "http://127.0.0.1:8089/RPC2",
+                },
+            },
+        }
+        oldCardDAV = { "Is this ignored?" : True }
+        expected = {
+            "Authentication": {
+                "Wiki" : {
+                    "UseSSL" : False,
+                    "Enabled" : True,
+                    "Hostname" : "127.0.0.1",
+                    "URL" : "http://127.0.0.1:8089/RPC2",
+                },
+            },
+            "BindHTTPPorts": [8008, 8800],
+            "BindSSLPorts": [8443, 8843],
+            "EnableSSL" : False,
+            "HTTPPort": 8008,
+            "RedirectHTTPToHTTPS": False,
+            "SSLAuthorityChain": "",
+            "SSLCertificate": "",
+            "SSLPort": 8443,
+            "SSLPrivateKey": "",
+        }
+        newCombined = { }
+        mergePlist(oldCalDAV, oldCardDAV, newCombined)
+        self.assertEquals(newCombined, expected)
+
+
     def test_examinePreviousSystem(self):
         """
         Set up a virtual system in various configurations, then ensure the
@@ -361,7 +505,7 @@
                 "/Library/CalendarServer/Documents", # Old Cal DocRoot value
                 "/Library/CalendarServer/Data", # Old Cal DataRoot value
                 "/Library/AddressBookServer/Documents", # Old AB DocRoot value
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             )
         ),
 
@@ -420,7 +564,7 @@
                 "/Library/CalendarServer/Documents", # Old Cal DocRoot value
                 "/Library/CalendarServer/Data", # Old Cal DataRoot value
                 "/Library/AddressBookServer/Documents", # Old AB DocRoot value
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             )
         ),
 
@@ -479,7 +623,7 @@
                 "/NonStandard/CalendarServer/Documents", # Old Cal DocRoot Value
                 "/NonStandard/CalendarServer/Data", # Old Cal DataRoot Value
                 "/NonStandard/AddressBookServer/Documents", # Old AB DocRoot Value
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             )
         ),
 
@@ -538,7 +682,7 @@
                 "/Volumes/External/CalendarServer/Documents", # Old Cal DocRoot Value
                 "/Volumes/External/CalendarServer/Data", # Old Cal DataRoot Value
                 "/Library/AddressBookServer/Documents", # Old AB DocRoot Value
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             )
         ),
 
@@ -582,7 +726,7 @@
                 None, # Old Cal DocRoot value
                 None, # Old Cal DataRoot value
                 "/Library/AddressBookServer/Documents", # Old AB DocRoot value
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             )
         ),
 
@@ -630,7 +774,7 @@
                 "Documents", # Old Cal DocRoot value
                 "Data", # Old Cal DataRoot value
                 None, # Old AB Docs
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             )
         ),
 
@@ -678,7 +822,7 @@
                 "/Volumes/External/Calendar/Documents", # Old Cal DocRoot value
                 "/Volumes/External/Calendar/Data", # Old Cal DataRoot value
                 None, # Old AB Docs
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             )
         ),
 
@@ -726,7 +870,7 @@
                 "Documents", # Old Cal DocRoot value
                 "Data", # Old Cal DocRoot value
                 None, # Old AB Docs
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             )
         ),
 
@@ -777,7 +921,7 @@
                 "/Volumes/External/CalendarDocuments/", # Old Cal DocRoot value
                 "/CalendarData", # Old Cal DocRoot value
                 None, # Old AB Docs
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             )
         ),
 
@@ -868,7 +1012,7 @@
                 "/Library/CalendarServer/Documents", # oldCalDocumentRootValue
                 "/Library/CalendarServer/Data", # oldCalDataRootValue
                 "/Library/AddressBookServer/Documents", # oldABDocumentRootValue
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             ),
             (   # expected return values
                 "/Volumes/new/Library/Server/Calendar and Contacts",
@@ -878,11 +1022,11 @@
             ),
             [   # expected DiskAccessor history
                 ('ditto', '/Volumes/old/Library/CalendarServer/Documents', '/Volumes/new/Library/Server/Calendar and Contacts/Documents'),
-                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Documents', 93, 93),
+                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Documents', FakeUser.pw_uid, FakeGroup.gr_gid),
                 ('ditto', '/Volumes/old/Library/CalendarServer/Data', '/Volumes/new/Library/Server/Calendar and Contacts/Data'),
-                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Data', 93, 93),
+                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Data', FakeUser.pw_uid, FakeGroup.gr_gid),
                 ('ditto', '/Volumes/old/Library/AddressBookServer/Documents/addressbooks', '/Volumes/new/Library/Server/Calendar and Contacts/Documents/addressbooks'),
-                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Documents/addressbooks', 93, 93),
+                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Documents/addressbooks', FakeUser.pw_uid, FakeGroup.gr_gid),
             ]
         ),
 
@@ -943,7 +1087,7 @@
                 "/NonStandard/CalendarServer/Documents", # oldCalDocumentRootValue
                 "/NonStandard/CalendarServer/Data", # oldCalDataRootValue
                 "/NonStandard/AddressBookServer/Documents", # oldABDocumentRootValue
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             ),
             (   # expected return values
                 "/Volumes/new/Library/Server/Calendar and Contacts",
@@ -953,11 +1097,11 @@
             ),
             [
                 ('ditto', '/Volumes/old/NonStandard/CalendarServer/Documents', '/Volumes/new/Library/Server/Calendar and Contacts/Documents'),
-                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Documents', 93, 93),
+                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Documents', FakeUser.pw_uid, FakeGroup.gr_gid),
                 ('ditto', '/Volumes/old/NonStandard/CalendarServer/Data', '/Volumes/new/Library/Server/Calendar and Contacts/Data'),
-                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Data', 93, 93),
+                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Data', FakeUser.pw_uid, FakeGroup.gr_gid),
                 ('ditto', '/Volumes/old/NonStandard/AddressBookServer/Documents/addressbooks', '/Volumes/new/Library/Server/Calendar and Contacts/Documents/addressbooks'),
-                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Documents/addressbooks', 93, 93),
+                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Documents/addressbooks', FakeUser.pw_uid, FakeGroup.gr_gid),
             ]
         ),
 
@@ -1018,7 +1162,7 @@
                 "/Volumes/External/CalendarServer/Documents", # oldCalDocumentRootValue
                 "/Volumes/External/CalendarServer/Data", # oldCalDataRootValue
                 "/Library/AddressBookServer/Documents", # oldABDocumentRootValue
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             ),
             (   # expected return values
                 "/Volumes/new/Library/Server/Calendar and Contacts",
@@ -1028,7 +1172,7 @@
             ),
             [
                 ('ditto', '/Volumes/old/Library/AddressBookServer/Documents/addressbooks', '/Volumes/External/CalendarServer/Documents/addressbooks'),
-                ('chown-recursive', '/Volumes/External/CalendarServer/Documents/addressbooks', 93, 93),
+                ('chown-recursive', '/Volumes/External/CalendarServer/Documents/addressbooks', FakeUser.pw_uid, FakeGroup.gr_gid),
             ]
         ),
 
@@ -1079,7 +1223,7 @@
                 "Documents", # oldCalDocumentRootValue
                 "Data", # oldCalDataRootValue
                 None, # oldABDocumentRootValue
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             ),
             (   # expected return values
                 "/Volumes/new/Library/Server/Calendar and Contacts",
@@ -1089,9 +1233,9 @@
             ),
             [
                 ('ditto', '/Volumes/old/Library/Server/Calendar and Contacts/Documents', '/Volumes/new/Library/Server/Calendar and Contacts/Documents'),
-                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Documents', 93, 93),
+                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Documents', FakeUser.pw_uid, FakeGroup.gr_gid),
                 ('ditto', '/Volumes/old/Library/Server/Calendar and Contacts/Data', '/Volumes/new/Library/Server/Calendar and Contacts/Data'),
-                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Data', 93, 93),
+                ('chown-recursive', '/Volumes/new/Library/Server/Calendar and Contacts/Data', FakeUser.pw_uid, FakeGroup.gr_gid),
             ]
         ),
 
@@ -1142,7 +1286,7 @@
                 "Documents", # oldCalDocumentRootValue
                 "Data", # oldCalDataRootValue
                 None, # oldABDocumentRootValue
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             ),
             (   # expected return values
                 "/Volumes/External/Library/Server/Calendar and Contacts",
@@ -1203,7 +1347,7 @@
                 "/Volumes/External/CalendarDocuments/", # oldCalDocumentRootValue
                 "/CalendarData", # oldCalDataRootValue
                 None, # oldABDocumentRootValue
-                93, 93, # user id, group id
+                FakeUser.pw_uid, FakeGroup.gr_gid, # user id, group id
             ),
             (   # expected return values
                 "/Volumes/External/Library/Server/Calendar and Contacts",
@@ -1213,7 +1357,7 @@
             ),
             [
                 ('ditto', '/Volumes/old/CalendarData', '/Volumes/External/Library/Server/Calendar and Contacts/Data'),
-                ('chown-recursive', '/Volumes/External/Library/Server/Calendar and Contacts/Data', 93, 93),
+                ('chown-recursive', '/Volumes/External/Library/Server/Calendar and Contacts/Data', FakeUser.pw_uid, FakeGroup.gr_gid),
             ]
         ),
 

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/benchmark.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/benchmark.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/benchmark.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -24,7 +24,6 @@
 
 from twisted.python.filepath import FilePath
 from twisted.python.usage import UsageError, Options, portCoerce
-from twisted.python.reflect import namedAny
 from twisted.internet.protocol import ProcessProtocol
 from twisted.protocols.basic import LineReceiver
 from twisted.internet.defer import (

Copied: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/accounts.csv (from rev 7695, CalendarServer/trunk/contrib/performance/loadtest/accounts.csv)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/accounts.csv	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/accounts.csv	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,99 @@
+user01,user01,User 01,user01 at example.com
+user02,user02,User 02,user02 at example.com
+user03,user03,User 03,user03 at example.com
+user04,user04,User 04,user04 at example.com
+user05,user05,User 05,user05 at example.com
+user06,user06,User 06,user06 at example.com
+user07,user07,User 07,user07 at example.com
+user08,user08,User 08,user08 at example.com
+user09,user09,User 09,user09 at example.com
+user10,user10,User 10,user10 at example.com
+user11,user11,User 11,user11 at example.com
+user12,user12,User 12,user12 at example.com
+user13,user13,User 13,user13 at example.com
+user14,user14,User 14,user14 at example.com
+user15,user15,User 15,user15 at example.com
+user16,user16,User 16,user16 at example.com
+user17,user17,User 17,user17 at example.com
+user18,user18,User 18,user18 at example.com
+user19,user19,User 19,user19 at example.com
+user20,user20,User 20,user20 at example.com
+user21,user21,User 21,user21 at example.com
+user22,user22,User 22,user22 at example.com
+user23,user23,User 23,user23 at example.com
+user24,user24,User 24,user24 at example.com
+user25,user25,User 25,user25 at example.com
+user26,user26,User 26,user26 at example.com
+user27,user27,User 27,user27 at example.com
+user28,user28,User 28,user28 at example.com
+user29,user29,User 29,user29 at example.com
+user30,user30,User 30,user30 at example.com
+user31,user31,User 31,user31 at example.com
+user32,user32,User 32,user32 at example.com
+user33,user33,User 33,user33 at example.com
+user34,user34,User 34,user34 at example.com
+user35,user35,User 35,user35 at example.com
+user36,user36,User 36,user36 at example.com
+user37,user37,User 37,user37 at example.com
+user38,user38,User 38,user38 at example.com
+user39,user39,User 39,user39 at example.com
+user40,user40,User 40,user40 at example.com
+user41,user41,User 41,user41 at example.com
+user42,user42,User 42,user42 at example.com
+user43,user43,User 43,user43 at example.com
+user44,user44,User 44,user44 at example.com
+user45,user45,User 45,user45 at example.com
+user46,user46,User 46,user46 at example.com
+user47,user47,User 47,user47 at example.com
+user48,user48,User 48,user48 at example.com
+user49,user49,User 49,user49 at example.com
+user50,user50,User 50,user50 at example.com
+user51,user51,User 51,user51 at example.com
+user52,user52,User 52,user52 at example.com
+user53,user53,User 53,user53 at example.com
+user54,user54,User 54,user54 at example.com
+user55,user55,User 55,user55 at example.com
+user56,user56,User 56,user56 at example.com
+user57,user57,User 57,user57 at example.com
+user58,user58,User 58,user58 at example.com
+user59,user59,User 59,user59 at example.com
+user60,user60,User 60,user60 at example.com
+user61,user61,User 61,user61 at example.com
+user62,user62,User 62,user62 at example.com
+user63,user63,User 63,user63 at example.com
+user64,user64,User 64,user64 at example.com
+user65,user65,User 65,user65 at example.com
+user66,user66,User 66,user66 at example.com
+user67,user67,User 67,user67 at example.com
+user68,user68,User 68,user68 at example.com
+user69,user69,User 69,user69 at example.com
+user70,user70,User 70,user70 at example.com
+user71,user71,User 71,user71 at example.com
+user72,user72,User 72,user72 at example.com
+user73,user73,User 73,user73 at example.com
+user74,user74,User 74,user74 at example.com
+user75,user75,User 75,user75 at example.com
+user76,user76,User 76,user76 at example.com
+user77,user77,User 77,user77 at example.com
+user78,user78,User 78,user78 at example.com
+user79,user79,User 79,user79 at example.com
+user80,user80,User 80,user80 at example.com
+user81,user81,User 81,user81 at example.com
+user82,user82,User 82,user82 at example.com
+user83,user83,User 83,user83 at example.com
+user84,user84,User 84,user84 at example.com
+user85,user85,User 85,user85 at example.com
+user86,user86,User 86,user86 at example.com
+user87,user87,User 87,user87 at example.com
+user88,user88,User 88,user88 at example.com
+user89,user89,User 89,user89 at example.com
+user90,user90,User 90,user90 at example.com
+user91,user91,User 91,user91 at example.com
+user92,user92,User 92,user92 at example.com
+user93,user93,User 93,user93 at example.com
+user94,user94,User 94,user94 at example.com
+user95,user95,User 95,user95 at example.com
+user96,user96,User 96,user96 at example.com
+user97,user97,User 97,user97 at example.com
+user98,user98,User 98,user98 at example.com
+user99,user99,User 99,user99 at example.com

Deleted: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/accounts.txt
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/accounts.txt	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/accounts.txt	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,99 +0,0 @@
-user01 user01
-user02 user02
-user03 user03
-user04 user04
-user05 user05
-user06 user06
-user07 user07
-user08 user08
-user09 user09
-user10 user10
-user11 user11
-user12 user12
-user13 user13
-user14 user14
-user15 user15
-user16 user16
-user17 user17
-user18 user18
-user19 user19
-user20 user20
-user21 user21
-user22 user22
-user23 user23
-user24 user24
-user25 user25
-user26 user26
-user27 user27
-user28 user28
-user29 user29
-user30 user30
-user31 user31
-user32 user32
-user33 user33
-user34 user34
-user35 user35
-user36 user36
-user37 user37
-user38 user38
-user39 user39
-user40 user40
-user41 user41
-user42 user42
-user43 user43
-user44 user44
-user45 user45
-user46 user46
-user47 user47
-user48 user48
-user49 user49
-user50 user50
-user51 user51
-user52 user52
-user53 user53
-user54 user54
-user55 user55
-user56 user56
-user57 user57
-user58 user58
-user59 user59
-user60 user60
-user61 user61
-user62 user62
-user63 user63
-user64 user64
-user65 user65
-user66 user66
-user67 user67
-user68 user68
-user69 user69
-user70 user70
-user71 user71
-user72 user72
-user73 user73
-user74 user74
-user75 user75
-user76 user76
-user77 user77
-user78 user78
-user79 user79
-user80 user80
-user81 user81
-user82 user82
-user83 user83
-user84 user84
-user85 user85
-user86 user86
-user87 user87
-user88 user88
-user89 user89
-user90 user90
-user91 user91
-user92 user92
-user93 user93
-user94 user94
-user95 user95
-user96 user96
-user97 user97
-user98 user98
-user99 user99

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/config.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/config.plist	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/config.plist	2011-06-30 20:44:13 UTC (rev 7697)
@@ -19,65 +19,243 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
   <dict>
-
+    <!-- Identify the server to be load tested. -->
     <key>server</key>
-    <dict>
-      <key>host</key>
-      <string>127.0.0.1</string>
+    <string>https://127.0.0.1:8443/</string>
 
-      <key>port</key>
-      <integer>8008</integer>
-    </dict>
-
+    <!-- Define the credentials of the clients which will be used to load test
+         the server.  These credentials must already be valid on the
+         server. -->
     <key>accounts</key>
     <dict>
+      <!-- The loader is the fully-qualified Python name of a callable which
+           returns a list of directory service records defining all of the
+           client accounts to use.
+
+	   loadtest.sim.recordsFromCSVFile reads username, password, mailto
+	   triples from a CSV file and returns them as a list of faked
+	   directory service records. -->
       <key>loader</key>
-      <string>loadtest.sim.recordsFromTextFile</string>
+      <string>loadtest.sim.recordsFromCSVFile</string>
+
+      <!-- Keyword arguments may be passed to the loader. -->
       <key>params</key>
       <dict>
 	<key>path</key>
-	<string>loadtest/accounts.txt</string>
+	<string>loadtest/accounts.csv</string>
       </dict>
     </dict>
 
+    <!-- Define how many clients will participate in the load test and how they
+         will show up. -->
     <key>arrival</key>
     <dict>
+
+      <!-- Specify a class which creates new clients and introduces them into the test.
+
+	   loadtest.population.SmoothRampUp introduces groups of new clients at
+	   fixed intervals up to a maximum.  The size of the group, interval,
+	   and maximum are configured by the parameters below.  The total
+	   number of clients is groups * groupSize, which needs to be no larger
+	   than the number of credentials created in the accounts section.  -->
       <key>factory</key>
       <string>loadtest.population.SmoothRampUp</string>
 
-      <key>groups</key>
-      <integer>60</integer>
+      <key>params</key>
+      <dict>
+	<!-- groups gives the total number of groups of clients to introduce.-->
+	<key>groups</key>
+	<integer>60</integer>
 
-      <key>groupSize</key>
-      <integer>1</integer>
+	<!-- groupSize is the number of clients in each group of clients.  It's
+	     really only a "smooth" ramp up if this is pretty small. -->
+	<key>groupSize</key>
+	<integer>1</integer>
 
-      <key>interval</key>
-      <integer>13</integer>
+	<!-- Number of seconds between the introduction of each group. -->
+	<key>interval</key>
+	<integer>13</integer>
+      </dict>
 
     </dict>
 
+    <!-- Define the kinds of software and user behavior the load simulation
+         will simulate. -->
     <key>clients</key>
+
+    <!-- Have as many different kinds of software and user behavior
+         configurations as you want.  Each is a dict  -->
     <array>
+
       <dict>
+
+	<!-- Here is a Snow Leopard iCal simulator. -->
 	<key>software</key>
 	<string>loadtest.ical.SnowLeopard</string>
 
+	<!-- The profiles define certain types of user behavior on top of the
+	     client software being simulated.  -->
 	<key>profiles</key>
 	<array>
-	  <string>loadtest.profiles.Eventer</string>
-	  <string>loadtest.profiles.Inviter</string>
-	  <string>loadtest.profiles.Accepter</string>
+
+	  <!-- First an event-creating profile, which will periodically create
+	       new events at a random time on a random calendar. -->
+	  <dict>
+	    <key>class</key>
+	    <string>loadtest.profiles.Eventer</string>
+
+	    <key>params</key>
+	    <dict>
+	      <!-- Define the interval (in seconds) at which this profile will
+	           use its client to create a new event. -->
+	      <key>interval</key>
+	      <integer>60</integer>
+
+	      <!-- Define how start times (DTSTART) for the randomly generated
+	           events will be selected.  This is an example of a
+	           "Distribution" parameter.  The value for most "Distribution"
+	           parameters are interchangeable and extensible.  -->
+	      <key>eventStartDistribution</key>
+	      <dict>
+
+		<!-- This distribution is pretty specialized.  It produces
+		     timestamps in the near future, limited to certain days of
+		     the week and certain hours of the day. -->
+		<key>type</key>
+		<string>stats.WorkDistribution</string>
+
+		<key>params</key>
+		<dict>
+		  <!-- These are the days of the week the distribution will
+		       use. -->
+		  <key>daysOfWeek</key>
+		  <array>
+		    <string>mon</string>
+		    <string>tue</string>
+		    <string>wed</string>
+		    <string>thu</string>
+		    <string>fri</string>
+		  </array>
+
+		  <!-- The earliest hour of a day at which an event might be
+		       scheduled. -->
+		  <key>beginHour</key>
+		  <integer>8</integer>
+
+		  <!-- And the latest hour of a day (at which an event will be
+                       scheduled to begin!). -->
+		  <key>endHour</key>
+		  <integer>16</integer>
+
+                  <!-- The timezone in which the event is scheduled.
+                       (XXX Does this really work right?) -->
+                  <key>tzname</key>
+                  <string>America/Los_Angeles</string>
+		</dict>
+	      </dict>
+	    </dict>
+	  </dict>
+
+	  <!-- This profile invites new attendees to existing events. -->
+	  <dict>
+	    <key>class</key>
+	    <string>loadtest.profiles.Inviter</string>
+	    
+	    <key>params</key>
+	    <dict>
+	      <!-- Define the frequency at which new invitations will be sent
+	           out. -->
+	      <key>sendInvitationDistribution</key>
+	      <dict>
+		<key>type</key>
+		<string>stats.NormalDistribution</string>
+		<key>params</key>
+		<dict>
+		  <!-- mu gives the mean of the normal distribution (in
+		       seconds). -->
+		  <key>mu</key>
+		  <integer>60</integer>
+
+		  <!-- and sigma gives its standard deviation. -->
+		  <key>sigma</key>
+		  <integer>5</integer>
+		</dict>
+	      </dict>
+
+	      <!-- Define the distribution of who will be invited to an event.
+	           Each set of credentials loaded by the load tester has an
+	           index; samples from this distribution will be added to that
+	           index to arrive at the index of some other credentials,
+	           which will be the target of the invitation. -->
+	      <key>inviteeDistanceDistribution</key>
+	      <dict>
+		<key>type</key>
+		<string>stats.UniformIntegerDistribution</string>
+		<key>params</key>
+		<dict>
+		  <!-- The minimum value (inclusive) of the uniform
+		       distribution. -->
+		  <key>min</key>
+		  <integer>-100</integer>
+		  <!-- The maximum value (exclusive) of the uniform
+		       distribution. -->
+		  <key>max</key>
+		  <integer>101</integer>
+		</dict>
+	      </dict>
+	    </dict>
+
+	  </dict>
+
+	  <!-- This profile accepts invitations to events. -->
+	  <dict>
+	    <key>class</key>
+	    <string>loadtest.profiles.Accepter</string>
+
+	    <key>params</key>
+	    <dict>
+	      <!-- Define how long to wait after seeing a new invitation before
+	           accepting it. -->
+	      <key>acceptDelayDistribution</key>
+	      <dict>
+		<key>type</key>
+		<string>stats.NormalDistribution</string>
+		<key>params</key>
+		<dict>
+		  <!-- mean -->
+		  <key>mu</key>
+		  <integer>360</integer>
+		  <!-- standard deviation --> 
+		  <key>sigma</key>
+		  <integer>60</integer>
+		</dict>
+	      </dict>
+	    </dict>
+	  
+	  </dict>
 	</array>
 
+	<!-- Determine the frequency at which this client configuration will
+	     appear in the clients which are created by the load tester. -->
 	<key>weight</key>
 	<integer>1</integer>
       </dict>
     </array>
 
+    <!-- Define some log observers to report on the load test. -->
     <key>observers</key>
     <array>
+      <!-- ReportStatistics generates an end-of-run summary of the HTTP
+           requests made, their timings, and their results. -->
       <string>loadtest.population.ReportStatistics</string>
+
+      <!-- RequestLogger generates a realtime log of all HTTP requests made
+           during the load test. -->
       <string>loadtest.ical.RequestLogger</string>
+
+      <!-- OperationLogger generates an end-of-run summary of the gross
+           operations performed (logical operations which may span more than
+           one HTTP request, such as inviting an attendee to an event). -->
       <string>loadtest.profiles.OperationLogger</string>
     </array>
 

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/ical.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/ical.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -16,9 +16,7 @@
 ##
 
 from uuid import uuid4
-from operator import getitem
-from pprint import pformat
-from datetime import datetime
+from datetime import timedelta, datetime
 from urlparse import urlparse, urlunparse
 
 from xml.etree import ElementTree
@@ -31,7 +29,7 @@
 from twisted.python.log import addObserver, err, msg
 from twisted.python.filepath import FilePath
 from twisted.python.failure import Failure
-from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.task import LoopingCall
 from twisted.web.http_headers import Headers
 from twisted.web.http import OK, MULTI_STATUS, CREATED, NO_CONTENT
@@ -143,7 +141,7 @@
     # The default interval, used if none is specified in external
     # configuration.  This is also the actual value used by Snow
     # Leopard iCal.
-    CALENDAR_HOME_POLL_INTERVAL = 60 * 15
+    CALENDAR_HOME_POLL_INTERVAL = 15 * 60
 
     _STARTUP_PRINCIPAL_PROPFIND = loadRequestBody('sl_startup_principal_propfind')
     _STARTUP_PRINCIPALS_REPORT = loadRequestBody('sl_startup_principals_report')
@@ -159,10 +157,10 @@
 
     email = None
 
-    def __init__(self, reactor, host, port, user, auth, calendarHomePollInterval=None):
+    def __init__(self, reactor, root, user, auth, calendarHomePollInterval=None):
         self.reactor = reactor
         self.agent = AuthHandlerAgent(Agent(self.reactor), auth)
-        self.root = 'http://%s:%d/' % (host, port)
+        self.root = root
         self.user = user
 
         if calendarHomePollInterval is None:
@@ -268,7 +266,7 @@
         d = self._request(
             MULTI_STATUS,
             'PROPFIND',
-            self.root + principalURL[1:],
+            self.root + principalURL[1:].encode('utf-8'),
             Headers({
                     'content-type': ['text/xml'],
                     'depth': ['0']}),
@@ -350,14 +348,15 @@
                 response = yield self._eventReport(url, responseHref)
                 body = yield readBody(response)
                 res = self._parseMultiStatus(body)[responseHref]
-                text = res.getTextProperties()
-                etag = text[davxml.getetag]
-                try:
-                    scheduleTag = text[caldavxml.schedule_tag]
-                except KeyError:
-                    scheduleTag = None
-                body = text[caldavxml.calendar_data]
-                self.eventChanged(responseHref, etag, scheduleTag, body)
+                if res.getStatus() is None or " 404 " not in res.getStatus():
+                    text = res.getTextProperties()
+                    etag = text[davxml.getetag]
+                    try:
+                        scheduleTag = text[caldavxml.schedule_tag]
+                    except KeyError:
+                        scheduleTag = None
+                    body = text[caldavxml.calendar_data]
+                    self.eventChanged(responseHref, etag, scheduleTag, body)
 
 
     def eventChanged(self, href, etag, scheduleTag, body):
@@ -392,7 +391,6 @@
         def ebCalendars(reason):
             reason.trap(IncorrectResponseCode)
         d.addErrback(ebCalendars)
-        d.addErrback(err, "Unexpected failure during calendar home poll")
         return d
 
 
@@ -466,8 +464,9 @@
         """
         pollCalendarHome = LoopingCall(
             self._checkCalendarsForEvents, calendarHome)
-        pollCalendarHome.start(self.calendarHomePollInterval, now=False)
+        return pollCalendarHome.start(self.calendarHomePollInterval, now=False)
 
+
     def _newOperation(self, label, deferred):
         before = self.reactor.seconds()
         msg(type="operation", phase="start", user=self.user, label=label)
@@ -492,11 +491,12 @@
             hrefs = principal.getHrefProperties()
             calendarHome = hrefs[caldavxml.calendar_home_set].toString()
             yield self._checkCalendarsForEvents(calendarHome)
-            self._calendarCheckLoop(calendarHome)
-        yield self._newOperation("startup", startup())
+            returnValue(calendarHome)
+        calendarHome = yield self._newOperation("startup", startup())
 
-        # XXX Oops, should probably stop sometime.
-        yield Deferred()
+        # This completes when the calendar home poll loop completes, which
+        # currently it never will except due to an unexpected error.
+        yield self._calendarCheckLoop(calendarHome)
 
 
     def _makeSelfAttendee(self):
@@ -559,25 +559,11 @@
         d.addCallback(narrowed)
         def specific(ignored):
             # Now learn about the attendee's availability
-            uid = vevent.contents[u'vevent'][0].contents[u'uid'][0].value
-            start = vevent.contents[u'vevent'][0].contents[u'dtstart'][0].value
-            end = vevent.contents[u'vevent'][0].contents[u'dtend'][0].value
-            now = datetime.now()
-            d = self._request(
-                OK, 'POST', self.root + 'calendars/__uids__/' + self.user + '/outbox/',
-                Headers({
-                        'content-type': ['text/calendar'],
-                        'originator': [self.email],
-                        'recipient': ['mailto:' + email]}),
-                StringProducer(self._POST_AVAILABILITY % {
-                        'attendee': 'mailto:' + email,
-                        'organizer': self.email,
-                        'vfreebusy-uid': str(uuid4()).upper(),
-                        'event-uid': uid.encode('utf-8'),
-                        'start': dateTimeToString(start, convertToUTC=True),
-                        'end': dateTimeToString(end, convertToUTC=True),
-                        'now': dateTimeToString(now, convertToUTC=True)}))
-            d.addCallback(readBody)
+            return self.requestAvailability(
+                vevent.contents[u'vevent'][0].contents[u'dtstart'][0].value,
+                vevent.contents[u'vevent'][0].contents[u'dtend'][0].value,
+                [self.email, u'mailto:' + email],
+                [vevent.contents[u'vevent'][0].contents[u'uid'][0].value])
             return d
         d.addCallback(specific)
         def availability(ignored):
@@ -681,7 +667,79 @@
         return d
 
 
+    def requestAvailability(self, start, end, users, mask=set()):
+        """
+        Issue a VFREEBUSY request for I{roughly} the given date range for the
+        given users.  The date range is quantized to one day.  Because of this
+        it is an error for the range to span more than 24 hours.
 
+        @param start: A C{datetime} instance giving the beginning of the
+            desired range.
+
+        @param end: A C{datetime} instance giving the end of the desired range.
+
+        @param users: An iterable of user UUIDs which will be included in the
+            request.
+
+        @param mask: An iterable of event UIDs which are to be ignored for the
+            purposes of this availability lookup.
+
+        @return: A C{Deferred} which fires with a C{dict}.  Keys in the dict
+            are user UUIDs (those requested) and values are something else.
+        """
+        outbox = self.root + 'calendars/__uids__/%s/outbox/' % (
+            self.user.encode('utf-8'),)
+
+        if mask:
+            maskStr = u'\r\n'.join(['X-CALENDARSERVER-MASK-UID:' + uid
+                                    for uid in mask]) + u'\r\n'
+        else:
+            maskStr = u''
+        maskStr = maskStr.encode('utf-8')
+
+        attendeeStr = '\r\n'.join(['ATTENDEE:' + uuid.encode('utf-8')
+                                   for uuid in users]) + '\r\n'
+
+        # iCal issues 24 hour wide vfreebusy requests, starting and ending at 4am.
+        if start.date() != end.date():
+            msg("Availability request spanning multiple days (%r to %r), "
+                "dropping the end date." % (start, end))
+
+        start = start.replace(hour=0, minute=0, second=0, microsecond=0)
+        end = start + timedelta(hours=24)
+
+        start = dateTimeToString(start, convertToUTC=True)
+        end = dateTimeToString(end, convertToUTC=True)
+        now = dateTimeToString(datetime.now(), convertToUTC=True)
+
+        # XXX Why does it not end up UTC sometimes?
+        if not start.endswith('Z'):
+            start = start + 'Z'
+        if not end.endswith('Z'):
+            end = end + 'Z'
+        if not now.endswith('Z'):
+            now = now + 'Z'
+
+        d = self._request(
+            OK, 'POST', outbox,
+            Headers({
+                    'content-type': ['text/calendar'],
+                    'originator': [self.email],
+                    'recipient': [u', '.join(users).encode('utf-8')]}),
+            StringProducer(self._POST_AVAILABILITY % {
+                    'attendees': attendeeStr,
+                    'summary': (u'Availability for %s' % (', '.join(users),)).encode('utf-8'),
+                    'organizer': self.email.encode('utf-8'),
+                    'vfreebusy-uid': str(uuid4()).upper(),
+                    'event-mask': maskStr,
+                    'start': start,
+                    'end': end,
+                    'now': now}))
+        d.addCallback(readBody)
+        return d
+
+
+
 class RequestLogger(object):
     format = u"%(user)s request %(code)s%(success)s[%(duration)5.2f s] %(method)8s %(url)s"
     success = u"\N{CHECK MARK}"
@@ -689,12 +747,19 @@
 
     def observe(self, event):
         if event.get("type") == "response":
-            event['url'] = urlunparse(('', '') + urlparse(event['url'])[2:])
+            formatArgs = dict(
+                user=event['user'],
+                method=event['method'],
+                url=urlunparse(('', '') + urlparse(event['url'])[2:]),
+                code=event['code'],
+                duration=event['duration'],
+                )
+                
             if event['success']:
-                event['success'] = self.success
+                formatArgs['success'] = self.success
             else:
-                event['success'] = self.failure
-            print (self.format % event).encode('utf-8')
+                formatArgs['success'] = self.failure
+            print (self.format % formatArgs).encode('utf-8')
 
 
     def report(self):

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/logger.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/logger.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/logger.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,4 +1,4 @@
-from stats import mean, median, stddev, mad
+from stats import mean, median
 
 class SummarizingMixin(object):
     def printHeader(self, fields):

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/population.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/population.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/population.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -20,20 +20,44 @@
 certain usage parameters.
 """
 
+from tempfile import mkdtemp
 from itertools import izip
 
+from twisted.python.filepath import FilePath
 from twisted.python.util import FancyEqMixin
 from twisted.python.log import msg, err
 
 from stats import mean, median, stddev, mad
+from loadtest.trafficlogger import loggedReactor
 from loadtest.logger import SummarizingMixin
 from loadtest.ical import SnowLeopard, RequestLogger
 from loadtest.profiles import Eventer, Inviter, Accepter
 
+
+class ProfileType(object, FancyEqMixin):
+    """
+    @ivar profileType: A L{ProfileBase} subclass, or an L{ICalendarUserProfile}
+        implementation.
+
+    @ivar params: A C{dict} which will be passed to C{profileType} as keyword
+        arguments to create a new profile instance.
+    """
+    compareAttributes = ("profileType", "params")
+
+    def __init__(self, profileType, params):
+        self.profileType = profileType
+        self.params = params
+
+
+    def __call__(self, reactor, simulator, client, number):
+        return self.profileType(reactor, simulator, client, number, **self.params)
+
+
+
 class ClientType(object, FancyEqMixin):
     """
     @ivar clientType: An L{ICalendarClient} implementation
-    @ivar profileTypes: A list of L{ICalendarUserProfile} implementations
+    @ivar profileTypes: A list of L{ProfileType} instances
     """
     compareAttributes = ("clientType", "profileTypes")
 
@@ -112,16 +136,19 @@
 
 
 class CalendarClientSimulator(object):
-    def __init__(self, records, populator, parameters, reactor, host, port):
+    def __init__(self, records, populator, parameters, reactor, server):
         self._records = records
         self.populator = populator
         self.reactor = reactor
-        self.host = host
-        self.port = port
+        self.server = server
         self._pop = self.populator.populate(parameters)
         self._user = 0
 
 
+    def getUserRecord(self, index):
+        return self._records[index]
+
+
     def _nextUserNumber(self):
         result = self._user
         self._user += 1
@@ -135,9 +162,9 @@
         auth = HTTPDigestAuthHandler()
         auth.add_password(
             realm="Test Realm",
-            uri="http://%s:%d/" % (self.host, self.port),
-            user=user,
-            passwd=record.password)
+            uri=self.server,
+            user=user.encode('utf-8'),
+            passwd=record.password.encode('utf-8'))
         return user, auth
 
 
@@ -147,25 +174,43 @@
             user, auth = self._createUser(number)
 
             clientType = self._pop.next()
+            reactor = loggedReactor(self.reactor)
             client = clientType.clientType(
-                self.reactor, self.host, self.port, user, auth)
+                reactor, self.server, user, auth)
             d = client.run()
-            d.addCallbacks(self._clientSuccess, self._clientFailure)
+            d.addErrback(self._clientFailure, reactor)
 
             for profileType in clientType.profileTypes:
-                profileType(self.reactor, client, number).run()
+                d = profileType(reactor, self, client, number).run()
+                d.addErrback(self._profileFailure, profileType, reactor)
         msg(type="status", clientCount=self._user - 1)
 
 
-    def _clientSuccess(self, result):
-        pass
+    def _dumpLogs(self, loggingReactor, reason):
+        path = FilePath(mkdtemp())
+        logstate = loggingReactor.getLogFiles()
+        i = 0
+        for i, log in enumerate(logstate.finished):
+            path.child('%03d.log' % (i,)).setContent(log.getvalue())
+        for i, log in enumerate(logstate.active, i):
+            path.child('%03d.log' % (i,)).setContent(log.getvalue())
+        path.child('reason.log').setContent(reason.getTraceback())
+        return path
 
 
-    def _clientFailure(self, reason):
-        err(reason, "Client stopped with error")
+    def _clientFailure(self, reason, reactor):
+        where = self._dumpLogs(reactor, reason)
+        err(reason, "Client stopped with error; recent traffic in %r" % (
+                where.path,))
 
 
+    def _profileFailure(self, reason, profileType, reactor):
+        where = self._dumpLogs(reactor, reason)
+        err(reason, "Profile stopped with error; recent traffic in %r" % (
+                where.path,))
 
+
+
 class SmoothRampUp(object):
     def __init__(self, reactor, groups, groupSize, interval):
         self.reactor = reactor
@@ -242,7 +287,6 @@
     import random
 
     from twisted.internet import reactor
-    from twisted.internet.task import LoopingCall
     from twisted.python.log import addObserver
 
     from twisted.python.failure import startDebugMode

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/profiles.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/profiles.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/profiles.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -33,11 +33,11 @@
 from twisted.python import context
 from twisted.python.log import msg
 from twisted.python.failure import Failure
-from twisted.internet.defer import succeed, fail
+from twisted.internet.defer import Deferred, succeed, fail
 from twisted.internet.task import LoopingCall
 from twisted.web.http import PRECONDITION_FAILED
 
-from stats import mean, median
+from stats import NearFutureDistribution, NormalDistribution, UniformDiscreteDistribution, mean
 from loadtest.logger import SummarizingMixin
 from loadtest.ical import IncorrectResponseCode
 
@@ -49,12 +49,18 @@
     """
     random = random
 
-    def __init__(self, reactor, client, userNumber):
+    def __init__(self, reactor, simulator, client, userNumber, **params):
         self._reactor = reactor
+        self._sim = simulator
         self._client = client
         self._number = userNumber
+        self.setParameters(**params)
 
 
+    def setParameters(self):
+        pass
+
+
     def _calendarsOfType(self, calendarType):
         return [
             cal 
@@ -104,16 +110,35 @@
     """
 
 
+def loopWithDistribution(reactor, distribution, function):
+    result = Deferred()
 
+    def repeat(ignored):
+        reactor.callLater(distribution.sample(), iterate)
+
+    def iterate():
+        d = function()
+        d.addCallbacks(repeat, result.errback)
+
+    repeat(None)
+    return result
+
+
+
 class Inviter(ProfileBase):
     """
     A Calendar user who invites and de-invites other users to events.
     """
+    def setParameters(self,
+                      sendInvitationDistribution=NormalDistribution(600, 60),
+                      inviteeDistanceDistribution=UniformDiscreteDistribution(range(-10, 11))):
+        self._sendInvitationDistribution = sendInvitationDistribution
+        self._inviteeDistanceDistribution = inviteeDistanceDistribution
+
+
     def run(self):
-        self._call = LoopingCall(self._invite)
-        self._call.clock = self._reactor
-        # XXX Base this on something real
-        self._call.start(20)
+        return loopWithDistribution(
+            self._reactor, self._sendInvitationDistribution, self._invite)
 
 
     def _addAttendee(self, event, attendees):
@@ -121,26 +146,29 @@
         Create a new attendee to add to the list of attendees for the
         given event.
         """
-        invitees = set([u'urn:uuid:user%02d' % (self._number,)])
+        selfRecord = self._sim.getUserRecord(self._number)
+        invitees = set([u'urn:uuid:%s' % (selfRecord.uid,)])
         for att in attendees:
             invitees.add(att.value)
 
         for i in range(10):
-            invitee = max(1, int(self.random.gauss(self._number, 3)))
-            uuid = u'urn:uuid:user%02d' % (invitee,)
+            invitee = max(
+                1, self._number + self._inviteeDistanceDistribution.sample())
+            try:
+                record = self._sim.getUserRecord(invitee)
+            except IndexError:
+                continue
+            uuid = u'urn:uuid:%s' % (record.uid,)
             if uuid not in invitees:
                 break
         else:
             return fail(CannotAddAttendee("Can't find uninvited user to invite."))
 
-        user = u'User %02d' % (invitee,)
-        email = u'user%02d at example.com' % (invitee,)
-
         attendee = ContentLine(
             name=u'ATTENDEE', params=[
-                [u'CN', user],
+                [u'CN', record.commonName],
                 [u'CUTYPE', u'INDIVIDUAL'],
-                [u'EMAIL', email],
+                [u'EMAIL', record.email],
                 [u'PARTSTAT', u'NEEDS-ACTION'],
                 [u'ROLE', u'REQ-PARTICIPANT'],
                 [u'RSVP', u'TRUE'],
@@ -201,20 +229,24 @@
                     lambda reason: reason.trap(CannotAddAttendee))
                 return self._newOperation("invite", d)
 
+        # Oops, either no events or no calendars to play with.
+        return succeed(None)
 
 
+
 class Accepter(ProfileBase):
     """
     A Calendar user who accepts invitations to events.
     """
-
-    def __init__(self, reactor, client, userNumber):
-        ProfileBase.__init__(self, reactor, client, userNumber)
+    def setParameters(self, acceptDelayDistribution=NormalDistribution(1200, 60)):
         self._accepting = set()
+        self._acceptDelayDistribution = acceptDelayDistribution
 
 
     def run(self):
         self._subscription = self._client.catalog["eventChanged"].subscribe(self.eventChanged)
+        # TODO: Propagate errors from eventChanged and _acceptInvitation to this Deferred
+        return Deferred()
 
 
     def eventChanged(self, href):
@@ -236,8 +268,7 @@
         for attendee in attendees:
             if self._isSelfAttendee(attendee):
                 if attendee.params[u'PARTSTAT'][0] == 'NEEDS-ACTION':
-                    # XXX Base this on something real
-                    delay = self.random.gauss(10, 2)
+                    delay = self._acceptDelayDistribution.sample()
                     self._accepting.add(href)
                     self._reactor.callLater(
                         delay, self._acceptInvitation, href, attendee)
@@ -330,12 +361,22 @@
 END:VEVENT
 END:VCALENDAR
 """))[0]
+    def setParameters(
+        self, interval=25,
+        eventStartDistribution=NearFutureDistribution(),
+        eventDurationDistribution=UniformDiscreteDistribution([
+                15 * 60, 30 * 60,
+                45 * 60, 60 * 60,
+                120 * 60])):
+        self._interval = interval
+        self._eventStartDistribution = eventStartDistribution
+        self._eventDurationDistribution = eventDurationDistribution
 
+
     def run(self):
         self._call = LoopingCall(self._addEvent)
         self._call.clock = self._reactor
-        # XXX Base this on something real
-        self._call.start(25)
+        return self._call.start(self._interval)
 
 
     def _addEvent(self):
@@ -351,10 +392,12 @@
             vevent = vcalendar.contents[u'vevent'][0]
             tz = vevent.contents[u'created'][0].value.tzinfo
             dtstamp = datetime.now(tz)
+            dtstart = datetime.fromtimestamp(self._eventStartDistribution.sample(), tz)
+            dtend = dtstart + timedelta(seconds=self._eventDurationDistribution.sample())
             vevent.contents[u'created'][0].value = dtstamp
             vevent.contents[u'dtstamp'][0].value = dtstamp
-            vevent.contents[u'dtstart'][0].value = dtstamp
-            vevent.contents[u'dtend'][0].value = dtstamp + timedelta(hours=1)
+            vevent.contents[u'dtstart'][0].value = dtstart
+            vevent.contents[u'dtend'][0].value = dtend
             vevent.contents[u'uid'][0].value = unicode(uuid4())
 
             href = '%s%s.ics' % (

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/request-data/sl_post_availability.request
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/request-data/sl_post_availability.request	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/request-data/sl_post_availability.request	2011-06-30 20:44:13 UTC (rev 7697)
@@ -6,11 +6,9 @@
 BEGIN:VFREEBUSY
 UID:%(vfreebusy-uid)s
 DTEND:%(end)s
-ATTENDEE:%(attendee)s
-DTSTART:%(start)s
-X-CALENDARSERVER-MASK-UID:%(event-uid)s
-DTSTAMP:%(now)s
+%(attendees)sDTSTART:%(start)s
+%(event-mask)sDTSTAMP:%(now)s
 ORGANIZER:%(organizer)s
-SUMMARY:Availability for %(attendee)s
+SUMMARY:%(summary)s
 END:VFREEBUSY
 END:VCALENDAR

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/sim.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/sim.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/sim.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -15,33 +15,36 @@
 #
 ##
 
-from sys import argv
+from xml.parsers.expat import ExpatError
+from sys import argv, stdout
 from random import Random
 from plistlib import readPlist
 from collections import namedtuple
 
 from twisted.python import context
 from twisted.python.filepath import FilePath
-from twisted.python.log import addObserver, removeObserver
+from twisted.python.log import startLogging, addObserver, removeObserver
 from twisted.python.usage import UsageError, Options
 from twisted.python.reflect import namedAny
 
 from loadtest.ical import SnowLeopard
 from loadtest.profiles import Eventer, Inviter, Accepter
 from loadtest.population import (
-    Populator, ClientType, PopulationParameters, SmoothRampUp,
+    Populator, ProfileType, ClientType, PopulationParameters, SmoothRampUp,
     CalendarClientSimulator)
 
 
 class _DirectoryRecord(object):
-    def __init__(self, uid, password):
+    def __init__(self, uid, password, commonName, email):
         self.uid = uid
         self.password = password
+        self.commonName = commonName
+        self.email = email
 
 
-def recordsFromTextFile(path):
+def recordsFromCSVFile(path):
     return [
-        _DirectoryRecord(*line.split())
+        _DirectoryRecord(*line.decode('utf-8').split(u','))
         for line
         in FilePath(path).getContent().splitlines()]
 
@@ -87,11 +90,21 @@
             raise UsageError("--config %s: %s" % (path, e.strerror))
         try:
             self.config = readPlist(configFile)
-        except Exception, e:
-            raise UsageError(
-                "--config %s: %s" % (path, str(e)))
+        except ExpatError, e:
+            raise UsageError("--config %s: %s" % (path, e)) 
 
 
+    def opt_logfile(self, filename):
+        """
+        Enable normal logging to some file.  - for stdout.
+        """
+        if filename == "-":
+            fObj = stdout
+        else:
+            fObj = file(filename, "a")
+        startLogging(fObj, setStdout=False)
+
+
     def opt_debug(self):
         """
         Enable Deferred and Failure debugging.
@@ -121,7 +134,6 @@
             raise UsageError("Specify a configuration file using --config <path>")
 
 
-Server = namedtuple('Server', 'host port')
 Arrival = namedtuple('Arrival', 'factory parameters')
 
 
@@ -130,7 +142,7 @@
     A L{LoadSimulator} simulates some configuration of calendar
     clients.
 
-    @type server: L{Server}
+    @type server: C{str}
     @type arrival: L{Arrival}
     @type parameters: L{PopulationParameters}
 
@@ -160,17 +172,14 @@
         except UsageError, e:
             raise SystemExit(str(e))
 
+        server = 'http://127.0.0.1:8008/'
         if 'server' in options.config:
-            server = Server( 
-                options.config['server']['host'],
-                options.config['server']['port'])
-        else:
-            server = Server('127.0.0.1', 8008)
+            server = options.config['server']
 
         if 'arrival' in options.config:
-            params = options.config['arrival']
-            factory = namedAny(params.pop('factory'))
-            arrival = Arrival(factory, params)
+            arrival = Arrival(
+                namedAny(options.config['arrival']['factory']), 
+                options.config['arrival']['params'])
         else:
             arrival = Arrival(
                 SmoothRampUp, dict(groups=10, groupSize=1, interval=3))
@@ -182,7 +191,9 @@
                     clientConfig["weight"],
                     ClientType(
                         namedAny(clientConfig["software"]),
-                        [namedAny(profile)
+                        [ProfileType(
+                                namedAny(profile["class"]),
+                                cls._convertParams(profile["params"]))
                          for profile in clientConfig["profiles"]]))
         if not parameters.clients:
             parameters.addClient(
@@ -202,20 +213,41 @@
         return cls(server, arrival, parameters,
                    observers=observers, records=records)
 
+    @classmethod
+    def _convertParams(cls, params):
+        """
+        Find parameter values which should be more structured than plistlib is
+        capable of constructing and replace them with the more structured form.
 
+        Specifically, find keys that end with C{"Distribution"} and convert
+        them into some kind of distribution object using the associated
+        dictionary of keyword arguments.
+        """
+        for k, v in params.iteritems():
+            if k.endswith('Distribution'):
+                params[k] = cls._convertDistribution(v)
+        return params
+
+
     @classmethod
+    def _convertDistribution(cls, value):
+        """
+        Construct and return a new distribution object using the type and
+        params specified by C{value}.
+        """
+        return namedAny(value['type'])(**value['params'])
+
+
+    @classmethod
     def main(cls, args=None):
         simulator = cls.fromCommandLine(args)
         raise SystemExit(simulator.run())
 
 
     def createSimulator(self):
-        host = self.server.host
-        port = self.server.port
         populator = Populator(Random())
         return CalendarClientSimulator(
-            self.records, populator, self.parameters, self.reactor,
-            host, port)
+            self.records, populator, self.parameters, self.reactor, self.server)
 
 
     def createArrivalPolicy(self):

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_ical.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_ical.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -15,11 +15,19 @@
 #
 ##
 
+from datetime import datetime
+
 from vobject import readComponents
 from vobject.base import Component, ContentLine
+from vobject.icalendar import dateTimeToString
 
+from twisted.python.failure import Failure
 from twisted.internet.defer import Deferred
 from twisted.trial.unittest import TestCase
+from twisted.web.http import OK, NO_CONTENT, CREATED, MULTI_STATUS
+from twisted.web.http_headers import Headers
+from twisted.web.client import ResponseDone
+from twisted.internet.protocol import ProtocolToConsumerAdapter
 
 from protocol.url import URL
 from protocol.webdav.definitions import davxml
@@ -27,7 +35,7 @@
 from protocol.caldav.definitions import csxml
 
 from loadtest.ical import Event, Calendar, SnowLeopard
-from httpclient import MemoryConsumer
+from httpclient import MemoryConsumer, StringProducer
 
 EVENT_UID = 'D94F247D-7433-43AF-B84B-ADD684D023B0'
 
@@ -790,14 +798,48 @@
 </multistatus>
 """
 
-class SnowLeopardTests(TestCase):
+
+class MemoryResponse(object):
+    def __init__(self, version, code, phrase, headers, bodyProducer):
+        self.version = version
+        self.code = code
+        self.phrase = phrase
+        self.headers = headers
+        self.length = bodyProducer.length
+        self._bodyProducer = bodyProducer
+
+
+    def deliverBody(self, protocol):
+        protocol.makeConnection(self._bodyProducer)
+        d = self._bodyProducer.startProducing(ProtocolToConsumerAdapter(protocol))
+        d.addCallback(lambda ignored: protocol.connectionLost(Failure(ResponseDone())))
+
+
+
+class SnowLeopardMixin:
     """
-    Tests for L{SnowLeopard}.
+    Mixin for L{TestCase}s for L{SnowLeopard}.
     """
     def setUp(self):
-        self.client = SnowLeopard(None, "127.0.0.1", 80, None, None)
+        self.user = "user91"
+        self.client = SnowLeopard(None, "http://127.0.0.1/", self.user, None)
 
 
+    def interceptRequests(self):
+        requests = []
+        def request(*args):
+            result = Deferred()
+            requests.append((result, args))
+            return result
+        self.client._request = request
+        return requests
+
+
+
+class SnowLeopardTests(SnowLeopardMixin, TestCase):
+    """
+    Tests for L{SnowLeopard}.
+    """
     def test_parsePrincipalPROPFINDResponse(self):
         """
         L{Principal._parsePROPFINDResponse} accepts an XML document
@@ -871,16 +913,6 @@
         self.assertEquals(outbox.ctag, None)
 
 
-    def interceptRequests(self):
-        requests = []
-        def request(*args):
-            result = Deferred()
-            requests.append((result, args))
-            return result
-        self.client._request = request
-        return requests
-
-
     def test_changeEventAttendee(self):
         """
         SnowLeopard.changeEventAttendee removes one attendee from an
@@ -902,7 +934,7 @@
         # iCal PUTs the new VCALENDAR object.
         expectedResponseCode, method, url, headers, body = req
         self.assertEquals(method, 'PUT')
-        self.assertEquals(url, 'http://127.0.0.1:80' + event.url)
+        self.assertEquals(url, 'http://127.0.0.1' + event.url)
         self.assertIsInstance(url, str)
         self.assertEquals(headers.getRawHeaders('content-type'), ['text/calendar'])
 
@@ -925,6 +957,9 @@
         """
         requests = self.interceptRequests()
 
+        calendar = Calendar(caldavxml.calendar, u'calendar', u'/mumble/', None)
+        self.client._calendars[calendar.url] = calendar
+
         vcalendar = list(readComponents(EVENT))[0]
         d = self.client.addEvent(u'/mumble/frotz.ics', vcalendar)
 
@@ -932,10 +967,11 @@
 
         # iCal PUTs the new VCALENDAR object.
         expectedResponseCode, method, url, headers, body = req
-        self.assertEquals(method, 'PUT')
-        self.assertEquals(url, 'http://127.0.0.1:80/mumble/frotz.ics')
+        self.assertEqual(expectedResponseCode, CREATED)
+        self.assertEqual(method, 'PUT')
+        self.assertEqual(url, 'http://127.0.0.1/mumble/frotz.ics')
         self.assertIsInstance(url, str)
-        self.assertEquals(headers.getRawHeaders('content-type'), ['text/calendar'])
+        self.assertEqual(headers.getRawHeaders('content-type'), ['text/calendar'])
 
         consumer = MemoryConsumer()
         finished = body.startProducing(consumer)
@@ -944,9 +980,17 @@
                 list(readComponents(consumer.value()))[0],
                 vcalendar)
         finished.addCallback(cbFinished)
-        return finished
 
+        def requested(ignored):
+            response = MemoryResponse(
+                ('HTTP', '1', '1'), CREATED, "Created", Headers({}),
+                StringProducer(""))
+            result.callback(response)
+        finished.addCallback(requested)
 
+        return d
+
+
     def test_deleteEvent(self):
         """
         L{SnowLeopard.deleteEvent} DELETEs the event at the relative
@@ -965,14 +1009,22 @@
         result, req = requests.pop()
 
         expectedResponseCode, method, url = req
-        self.assertEquals(method, 'DELETE')
-        self.assertEquals(url, 'http://127.0.0.1:80' + event.url)
+
+        self.assertEqual(expectedResponseCode, NO_CONTENT)
+        self.assertEqual(method, 'DELETE')
+        self.assertEqual(url, 'http://127.0.0.1' + event.url)
         self.assertIsInstance(url, str)
 
         self.assertNotIn(event.url, self.client._events)
         self.assertNotIn(u'bar.ics', calendar.events)
 
+        response = MemoryResponse(
+            ('HTTP', '1', '1'), NO_CONTENT, "No Content", None,
+            StringProducer(""))
+        result.callback(response)
+        return d
 
+
     def assertComponentsEqual(self, first, second):
         self.assertEquals(first.name, second.name, "Component names not equal")
         self.assertEquals(first.behavior, second.behavior, "Component behaviors not equal")
@@ -1003,3 +1055,167 @@
         self.assertEquals(
             first.singletonparams, second.singletonparams,
             "ContentLine singletonparams not equal")
+
+
+
+class UpdateCalendarTests(SnowLeopardMixin, TestCase):
+    """
+    Tests for L{SnowLeopard._updateCalendar}.
+    """
+
+    _CALENDAR_PROPFIND_RESPONSE_BODY = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<multistatus xmlns='DAV:'>
+  <response>
+    <href>/something/anotherthing.ics</href>
+    <propstat>
+      <prop>
+        <resourcetype>
+          <collection/>
+        </resourcetype>
+        <getetag>"None"</getetag>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+    <propstat>
+      <prop>
+      </prop>
+      <status>HTTP/1.1 404 Not Found</status>
+    </propstat>
+  </response>
+  <response>
+    <href>/something/else.ics</href>
+    <propstat>
+      <prop>
+        <resourcetype>
+          <collection/>
+        </resourcetype>
+        <getetag>"None"</getetag>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+   </response>
+</multistatus>
+"""
+    _CALENDAR_REPORT_RESPONSE_BODY = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<multistatus xmlns='DAV:'>
+  <response>
+    <href>/something/anotherthing.ics</href>
+    <status>HTTP/1.1 404 Not Found</status>
+  </response>
+</multistatus>
+"""
+    def test_eventMissing(self):
+        """
+        If an event included in the calendar PROPFIND response no longer exists
+        by the time a REPORT is issued for that event, the 404 is handled and
+        the rest of the normal update logic for that event is skipped.
+        """
+        requests = self.interceptRequests()
+
+        calendar = Calendar(None, 'calendar', '/something/', None)
+        self.client._calendars[calendar.url] = calendar
+        self.client._updateCalendar(calendar)
+        result, req = requests.pop(0)
+        expectedResponseCode, method, url, headers, body = req
+        self.assertEqual('PROPFIND', method)
+        self.assertEqual('http://127.0.0.1/something/', url)
+        self.assertEqual(MULTI_STATUS, expectedResponseCode)
+
+        result.callback(
+            MemoryResponse(
+                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
+                StringProducer(self._CALENDAR_PROPFIND_RESPONSE_BODY)))
+        
+        result, req = requests.pop(0)
+        expectedResponseCode, method, url, headers, body = req
+        self.assertEqual('REPORT', method)
+        self.assertEqual('http://127.0.0.1/something/', url)
+        self.assertEqual(MULTI_STATUS, expectedResponseCode)
+
+        # Someone else comes along and gets rid of the event
+        del self.client._events["/something/anotherthing.ics"]
+
+        result.callback(
+            MemoryResponse(
+                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
+                StringProducer(self._CALENDAR_REPORT_RESPONSE_BODY)))
+
+        # Verify that processing proceeded to the response after the one with a
+        # 404 status.
+        self.assertIn('/something/else.ics', self.client._events)
+
+
+
+class VFreeBusyTests(SnowLeopardMixin, TestCase):
+    """
+    Tests for L{SnowLeopard.requestAvailability}.
+    """
+    def test_requestAvailability(self):
+        """
+        L{SnowLeopard.requestAvailability} accepts a date range and a set of
+        account uuids and issues a VFREEBUSY request.  It returns a Deferred
+        which fires with a dict mapping account uuids to availability range
+        information.
+        """
+        self.client.uuid = u'urn:uuid:user01'
+        self.client.email = u'mailto:user01 at example.com'
+        requests = self.interceptRequests()
+
+        start = datetime(2011, 6, 10, 10, 45, 0)
+        end = datetime(2011, 6, 10, 11, 15, 0)
+        d = self.client.requestAvailability(
+            start, end, [u"urn:uuid:user05", u"urn:uuid:user10"])
+
+        result, req = requests.pop(0)
+        expectedResponseCode, method, url, headers, body = req
+
+        self.assertEqual(OK, expectedResponseCode)
+        self.assertEqual('POST', method)
+        self.assertEqual(
+            'http://127.0.0.1/calendars/__uids__/%s/outbox/' % (self.user,),
+            url)
+
+        self.assertEqual(headers.getRawHeaders('originator'), ['mailto:user01 at example.com'])
+        self.assertEqual(headers.getRawHeaders('recipient'), ['urn:uuid:user05, urn:uuid:user10'])
+        self.assertEqual(headers.getRawHeaders('content-type'), ['text/calendar'])
+
+        consumer = MemoryConsumer()
+        finished = body.startProducing(consumer)
+        def cbFinished(ignored):
+            vevent = list(readComponents(consumer.value()))[0]
+            uid = vevent.contents[u'vfreebusy'][0].contents[u'uid'][0].value.encode('utf-8')
+            dtstamp = vevent.contents[u'vfreebusy'][0].contents[u'dtstamp'][0].value
+            dtstamp = dateTimeToString(dtstamp, False)
+            self.assertEqual(
+"""\
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VFREEBUSY
+UID:%(uid)s
+DTEND:20110611T000000Z
+ATTENDEE:urn:uuid:user05
+ATTENDEE:urn:uuid:user10
+DTSTART:20110610T000000Z
+DTSTAMP:%(dtstamp)s
+ORGANIZER:mailto:user01 at example.com
+SUMMARY:Availability for urn:uuid:user05, urn:uuid:user10
+END:VFREEBUSY
+END:VCALENDAR
+""".replace('\n', '\r\n') % {'uid': uid, 'dtstamp': dtstamp},consumer.value())
+
+        finished.addCallback(cbFinished)
+
+        def requested(ignored):
+            response = MemoryResponse(
+                ('HTTP', '1', '1'), OK, "Ok", Headers({}),
+                StringProducer(""))
+            result.callback(response)
+        finished.addCallback(requested)
+
+        return d
+

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_profiles.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_profiles.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_profiles.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -30,6 +30,7 @@
 from twisted.web.client import Response
 
 from loadtest.profiles import Eventer, Inviter, Accepter
+from loadtest.population import Populator, CalendarClientSimulator
 from loadtest.ical import IncorrectResponseCode, Calendar, Event, BaseClient
 
 SIMPLE_EVENT = """\
@@ -150,8 +151,24 @@
 """
 
 
+class AnyUser(object):
+    def __getitem__(self, index):
+        return _AnyRecord(index)
 
+
+class _AnyRecord(object):
+    def __init__(self, index):
+        self.uid = u"user%02d" % (index,)
+        self.password = u"user%02d" % (index,)
+        self.commonName = u"User %02d" % (index,)
+        self.email = u"user%02d at example.com" % (index,)
+
+
 class Deterministic(object):
+    def __init__(self, value=None):
+        self.value = value
+
+
     def gauss(self, mean, stddev):
         """
         Pretend to return a value from a gaussian distribution with mu
@@ -165,7 +182,11 @@
         return sequence[0]
 
 
+    def sample(self):
+        return self.value
 
+
+
 class StubClient(BaseClient):
     """
     Stand in for an iCalendar client.
@@ -187,7 +208,7 @@
 
 
     def deleteEvent(self, href):
-        event = self._events.pop(href)
+        del self._events[href]
         calendar, uid = href.rsplit('/', 1)
         del self._calendars[calendar + '/'].events[uid]
 
@@ -219,10 +240,25 @@
 
 
 
+class SequentialDistribution(object):
+    def __init__(self, values):
+        self.values = values
+
+
+    def sample(self):
+        return self.values.pop(0)
+
+
+
 class InviterTests(TestCase):
     """
     Tests for loadtest.profiles.Inviter.
     """
+    def setUp(self):
+        self.sim = CalendarClientSimulator(
+            AnyUser(), Populator(None), None, None, None)
+
+
     def _simpleAccount(self, userNumber, eventText):
         vevent = list(readComponents(eventText))[0]
         calendar = Calendar(
@@ -245,7 +281,7 @@
         vevent, event, calendar, client = self._simpleAccount(
             userNumber, SIMPLE_EVENT)
         calendar.resourceType = caldavxml.schedule_inbox
-        inviter = Inviter(None, client, userNumber)
+        inviter = Inviter(None, self.sim, client, userNumber)
         inviter._invite()
         self.assertNotIn(u'attendee', vevent.contents[u'vevent'][0].contents)
 
@@ -257,7 +293,7 @@
         """
         userNumber = 13
         client = StubClient(userNumber)
-        inviter = Inviter(None, client, userNumber)
+        inviter = Inviter(None, self.sim, client, userNumber)
         inviter._invite()
         self.assertEquals(client._events, {})
         self.assertEquals(client._calendars, {})
@@ -273,7 +309,7 @@
         vevent, event, calendar, client = self._simpleAccount(
             userNumber, SIMPLE_EVENT)
         event.vevent = event.etag = event.scheduleTag = None
-        inviter = Inviter(None, client, userNumber)
+        inviter = Inviter(None, self.sim, client, userNumber)
         inviter._invite()
         self.assertEquals(client._events, {event.url: event})
         self.assertEquals(client._calendars, {calendar.url: calendar})
@@ -287,8 +323,8 @@
         userNumber = 16
         vevent, event, calendar, client = self._simpleAccount(
             userNumber, SIMPLE_EVENT)
-        inviter = Inviter(Clock(), client, userNumber)
-        inviter.random = Deterministic()
+        inviter = Inviter(Clock(), self.sim, client, userNumber)
+        inviter.setParameters(inviteeDistanceDistribution=Deterministic(1))
         inviter._invite()
         attendees = vevent.contents[u'vevent'][0].contents[u'attendee']
         self.assertEquals(len(attendees), 1)
@@ -312,11 +348,10 @@
             selfNumber, SIMPLE_EVENT)
 
         otherNumber = 20
-        values = [selfNumber, otherNumber]
+        values = [selfNumber - selfNumber, otherNumber - selfNumber]
 
-        inviter = Inviter(Clock(), client, selfNumber)
-        inviter.random = Deterministic()
-        inviter.random.gauss = lambda mu, sigma: values.pop(0)
+        inviter = Inviter(Clock(), self.sim, client, selfNumber)
+        inviter.setParameters(inviteeDistanceDistribution=SequentialDistribution(values))
         inviter._invite()
         attendees = vevent.contents[u'vevent'][0].contents[u'attendee']
         self.assertEquals(len(attendees), 1)
@@ -342,11 +377,10 @@
         invitee = vevent.contents[u'vevent'][0].contents[u'attendee'][0]
         inviteeNumber = int(invitee.params[u'CN'][0].split()[1])
         anotherNumber = inviteeNumber + 5
-        values = [inviteeNumber, anotherNumber]
+        values = [inviteeNumber - selfNumber, anotherNumber - selfNumber]
 
-        inviter = Inviter(Clock(), client, selfNumber)
-        inviter.random = Deterministic()
-        inviter.random.gauss = lambda mu, sigma: values.pop(0)
+        inviter = Inviter(Clock(), self.sim, client, selfNumber)
+        inviter.setParameters(inviteeDistanceDistribution=SequentialDistribution(values))
         inviter._invite()
         attendees = vevent.contents[u'vevent'][0].contents[u'attendee']
         self.assertEquals(len(attendees), 3)
@@ -368,10 +402,9 @@
         selfNumber = 1
         vevent, event, calendar, client = self._simpleAccount(
             selfNumber, INVITED_EVENT)
-        inviter = Inviter(Clock(), client, selfNumber)
-        inviter.random = Deterministic()
+        inviter = Inviter(Clock(), self.sim, client, selfNumber)
         # Always return a user number which has already been invited.
-        inviter.random.gauss = lambda mu, sigma: 2
+        inviter.setParameters(inviteeDistanceDistribution=Deterministic(2 - selfNumber))
         inviter._invite()
         attendees = vevent.contents[u'vevent'][0].contents[u'attendee']
         self.assertEquals(len(attendees), 2)
@@ -386,7 +419,7 @@
         selfNumber = 2
         vevent, event, calendar, client = self._simpleAccount(
             selfNumber, INVITED_EVENT)
-        inviter = Inviter(None, client, selfNumber)
+        inviter = Inviter(None, self.sim, client, selfNumber)
         # Try to send an invitation, but with only one event on the
         # calendar, of which we are not the organizer.  It should be
         # unchanged afterwards.
@@ -402,13 +435,18 @@
     """
     Tests for loadtest.profiles.Accepter.
     """
+    def setUp(self):
+        self.sim = CalendarClientSimulator(
+            AnyUser(), Populator(None), None, None, None)
+
+
     def test_ignoreEventOnUnknownCalendar(self):
         """
         If an event on an unknown calendar changes, it is ignored.
         """
         userNumber = 13
         client = StubClient(userNumber)
-        accepter = Accepter(None, client, userNumber)
+        accepter = Accepter(None, self.sim, client, userNumber)
         accepter.eventChanged('/some/calendar/1234.ics')
 
 
@@ -423,7 +461,7 @@
             caldavxml.schedule_inbox, u'inbox', calendarURL, None)
         client = StubClient(userNumber)
         client._calendars[calendarURL] = calendar
-        accepter = Accepter(None, client, userNumber)
+        accepter = Accepter(None, self.sim, client, userNumber)
         accepter.eventChanged(calendarURL + '1234.ics')
 
 
@@ -442,7 +480,7 @@
         client._calendars[calendarURL] = calendar
         event = Event(calendarURL + u'1234.ics', None, vevent)
         client._events[event.url] = event
-        accepter = Accepter(None, client, userNumber)
+        accepter = Accepter(None, self.sim, client, userNumber)
         accepter.eventChanged(event.url)
 
 
@@ -465,7 +503,7 @@
         client._calendars[calendarURL] = calendar
         event = Event(calendarURL + u'1234.ics', None, vevent)
         client._events[event.url] = event
-        accepter = Accepter(clock, client, userNumber)
+        accepter = Accepter(clock, self.sim, client, userNumber)
         accepter.random = Deterministic()
         accepter.random.gauss = lambda mu, sigma: randomDelay
         accepter.eventChanged(event.url)
@@ -503,9 +541,8 @@
         inboxEvent = Event(inboxURL + u'4321.ics', None, vevent)
         client._setEvent(inboxEvent.url, inboxEvent)
 
-        accepter = Accepter(clock, client, userNumber)
-        accepter.random = Deterministic()
-        accepter.random.gauss = lambda mu, sigma: randomDelay
+        accepter = Accepter(clock, self.sim, client, userNumber)
+        accepter.setParameters(Deterministic(randomDelay))
         accepter.eventChanged(event.url)
         clock.advance(randomDelay)
 
@@ -540,9 +577,8 @@
         client._calendars[calendarURL] = calendar
         event = Event(calendarURL + u'1234.ics', None, vevent)
         client._events[event.url] = event
-        accepter = Accepter(clock, client, userNumber)
-        accepter.random = Deterministic()
-        accepter.random.gauss = lambda mu, sigma: randomDelay
+        accepter = Accepter(clock, self.sim, client, userNumber)
+        accepter.setParameters(Deterministic(randomDelay))
         accepter.eventChanged(event.url)
         clock.advance(randomDelay)
 
@@ -584,9 +620,8 @@
         event = Event(calendarURL + u'1234.ics', None, vevent)
         client._setEvent(event.url, event)
 
-        accepter = Accepter(clock, client, userNumber)
-        accepter.random = Deterministic()
-        accepter.random.gauss = lambda mu, sigma: randomDelay
+        accepter = Accepter(clock, self.sim, client, userNumber)
+        accepter.setParameters(Deterministic(randomDelay))
 
         client.rescheduled.add(event.url)
 
@@ -602,6 +637,11 @@
     Tests for loadtest.profiles.Eventer, a profile which adds new
     events on calendars.
     """
+    def setUp(self):
+        self.sim = CalendarClientSimulator(
+            AnyUser(), Populator(None), None, None, None)
+
+
     def test_doNotAddEventOnInbox(self):
         """
         When the only calendar is a schedule inbox, no attempt is made
@@ -612,7 +652,7 @@
         client = StubClient(21)
         client._calendars.update({calendar.url: calendar})
 
-        eventer = Eventer(None, client, None)
+        eventer = Eventer(None, self.sim, client, None)
         eventer._addEvent()
 
         self.assertEquals(client._events, {})
@@ -628,7 +668,7 @@
         client = StubClient(31)
         client._calendars.update({calendar.url: calendar})
 
-        eventer = Eventer(Clock(), client, None)
+        eventer = Eventer(Clock(), self.sim, client, None)
         eventer._addEvent()
 
         self.assertEquals(len(client._events), 1)

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_sim.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_sim.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_sim.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -15,39 +15,33 @@
 #
 ##
 
-from operator import setitem
 from plistlib import writePlistToString
 
-from zope.interface.verify import verifyClass
-
-from twisted.python.log import LogPublisher, theLogPublisher, msg
+from twisted.python.log import msg
 from twisted.python.usage import UsageError
 from twisted.python.filepath import FilePath
 from twisted.trial.unittest import TestCase
-from twisted.internet.defer import succeed
-from twisted.internet.task import Clock
 
-from twistedcaldav.directory.idirectory import IDirectoryService
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+from twistedcaldav.directory.directory import DirectoryRecord
 
+from stats import NormalDistribution
 from loadtest.ical import SnowLeopard
 from loadtest.profiles import Eventer, Inviter, Accepter
 from loadtest.population import (
     SmoothRampUp, ClientType, PopulationParameters, Populator, CalendarClientSimulator,
-    SimpleStatistics)
+    ProfileType, SimpleStatistics)
 from loadtest.sim import (
-    Server, Arrival, SimOptions, LoadSimulator, LagTrackingReactor, main)
+    Arrival, SimOptions, LoadSimulator, LagTrackingReactor)
 
 VALID_CONFIG = {
-    'server': {
-        'host': '127.0.0.1',
-        'port': 8008,
-        },
+    'server': 'tcp:127.0.0.1:8008',
     'arrival': {
         'factory': 'loadtest.population.SmoothRampUp',
-        'groups': 10,
-        'groupSize': 1,
-        'interval': 3,
+        'params': {
+            'groups': 10,
+            'groupSize': 1,
+            'interval': 3,
+            },
         },
     }
 
@@ -120,7 +114,7 @@
         """
         calsim = CalendarClientSimulator(
             [self._user('alice'), self._user('bob'), self._user('carol')],
-            Populator(None), None, None, 'example.org', 1234)
+            Populator(None), None, None, 'http://example.org:1234/')
         users = sorted([
                 calsim._createUser(0)[0],
                 calsim._createUser(1)[0],
@@ -136,7 +130,7 @@
         """
         calsim = CalendarClientSimulator(
             [self._user('alice')],
-            Populator(None), None, None, 'example.org', 1234)
+            Populator(None), None, None, 'http://example.org:1234/')
         user, auth = calsim._createUser(0)
         self.assertEqual(
             auth.passwd.find_user_password('Test Realm', 'http://example.org:1234/')[1],
@@ -202,16 +196,14 @@
         with its own reactor and host and port information from the
         configuration file.
         """
-        host = '127.0.0.7'
-        port = 1243
+        server = 'http://127.0.0.7:1243/'
         reactor = object()
-        sim = LoadSimulator(Server(host, port), None, None, reactor=reactor)
+        sim = LoadSimulator(server, None, None, reactor=reactor)
         calsim = sim.createSimulator()
         self.assertIsInstance(calsim, CalendarClientSimulator)
         self.assertIsInstance(calsim.reactor, LagTrackingReactor)
         self.assertIdentical(calsim.reactor._reactor, reactor)
-        self.assertEquals(calsim.host, host)
-        self.assertEquals(calsim.port, port)
+        self.assertEquals(calsim.server, server)
 
 
     def test_loadAccountsFromFile(self):
@@ -219,10 +211,10 @@
         L{LoadSimulator.
         """
         accounts = FilePath(self.mktemp())
-        accounts.setContent("foo bar\nbaz quux\n")
+        accounts.setContent("foo,bar,baz,quux\nfoo2,bar2,baz2,quux2\n")
         config = VALID_CONFIG.copy()
         config["accounts"] = {
-            "loader": "loadtest.sim.recordsFromTextFile",
+            "loader": "loadtest.sim.recordsFromCSVFile",
             "params": {
                 "path": accounts.path},
             }
@@ -232,8 +224,12 @@
         self.assertEqual(2, len(sim.records))
         self.assertEqual(sim.records[0].uid, 'foo')
         self.assertEqual(sim.records[0].password, 'bar')
-        self.assertEqual(sim.records[1].uid, 'baz')
-        self.assertEqual(sim.records[1].password, 'quux')
+        self.assertEqual(sim.records[0].commonName, 'baz')
+        self.assertEqual(sim.records[0].email, 'quux')
+        self.assertEqual(sim.records[1].uid, 'foo2')
+        self.assertEqual(sim.records[1].password, 'bar2')
+        self.assertEqual(sim.records[1].commonName, 'baz2')
+        self.assertEqual(sim.records[1].email, 'quux2')
 
 
     def test_loadServerConfig(self):
@@ -243,13 +239,9 @@
         """
         config = FilePath(self.mktemp())
         config.setContent(writePlistToString({
-                    "server": {
-                        "host": "127.0.0.1",
-                        "port": 1234,
-                        },
-                    }))
+                    "server": "https://127.0.0.3:8432/"}))
         sim = LoadSimulator.fromCommandLine(['--config', config.path])
-        self.assertEquals(sim.server, Server("127.0.0.1", 1234))
+        self.assertEquals(sim.server, "https://127.0.0.3:8432/")
 
 
     def test_loadArrivalConfig(self):
@@ -261,9 +253,11 @@
         config.setContent(writePlistToString({
                     "arrival": {
                         "factory": "loadtest.population.SmoothRampUp",
-                        "groups": 10,
-                        "groupSize": 1,
-                        "interval": 3,
+                        "params": {
+                            "groups": 10,
+                            "groupSize": 1,
+                            "interval": 3,
+                            },
                         },
                     }))
         sim = LoadSimulator.fromCommandLine(['--config', config.path])
@@ -302,12 +296,25 @@
         config.setContent(writePlistToString({
                     "clients": [{
                             "software": "loadtest.ical.SnowLeopard",
-                            "profiles": ["loadtest.profiles.Eventer"],
+                            "profiles": [{
+                                    "params": {
+                                        "interval": 25,
+                                        "eventStartDistribution": {
+                                            "type": "stats.NormalDistribution",
+                                            "params": {
+                                                "mu": 123,
+                                                "sigma": 456,
+                                                }}},
+                                    "class": "loadtest.profiles.Eventer"}],
                             "weight": 3,
                             }]}))
+                            
         sim = LoadSimulator.fromCommandLine(['--config', config.path])
         expectedParameters = PopulationParameters()
-        expectedParameters.addClient(3, ClientType(SnowLeopard, [Eventer]))
+        expectedParameters.addClient(
+            3, ClientType(SnowLeopard, [ProfileType(Eventer, {
+                            "interval": 25,
+                            "eventStartDistribution": NormalDistribution(123, 456)})]))
         self.assertEquals(sim.parameters, expectedParameters)
 
         
@@ -338,6 +345,7 @@
         self.assertEquals(len(sim.observers), 1)
         self.assertIsInstance(sim.observers[0], SimpleStatistics)
 
+
     def test_observeRunReport(self):
         """
         Each log observer is added to the log publisher before the
@@ -346,7 +354,7 @@
         """
         observers = [Observer()]
         sim = LoadSimulator(
-            Server('example.com', 123), 
+            "http://example.com:123/",
             Arrival(lambda reactor: NullArrival(), {}),
             None, observers, reactor=Reactor())
         sim.run()

Copied: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_trafficlogger.py (from rev 7695, CalendarServer/trunk/contrib/performance/loadtest/test_trafficlogger.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_trafficlogger.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/test_trafficlogger.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,207 @@
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+
+from zope.interface import Interface, implements
+
+from twisted.internet.protocol import ClientFactory, Protocol
+from twisted.trial.unittest import TestCase
+from twisted.test.proto_helpers import StringTransport, MemoryReactor
+from twisted.protocols.wire import Discard
+
+from loadtest.trafficlogger import _TrafficLoggingFactory, loggedReactor
+
+
+class IProbe(Interface):
+    """
+    An interface which can be used to verify some interface-related behavior of
+    L{loggedReactor}.
+    """
+    def probe():
+        pass
+
+
+class Probe(object):
+    implements(IProbe)
+
+    _probed = False
+
+    def __init__(self, result=None):
+        self._result = result
+
+    def probe(self):
+        self._probed = True
+        return self._result
+
+
+class TrafficLoggingReactorTests(TestCase):
+    """
+    Tests for L{loggedReactor}.
+    """
+    def test_nothing(self):
+        """
+        L{loggedReactor} returns the object passed to it, if the object passed
+        to it doesn't provide any interfaces.  This is mostly for testing
+        convenience rather than a particularly useful feature.
+        """
+        probe = object()
+        self.assertIdentical(probe, loggedReactor(probe))
+
+
+    def test_interfaces(self):
+        """
+        The object returned by L{loggedReactor} provides all of the interfaces
+        provided by the object passed to it.
+        """
+        probe = Probe()
+        reactor = loggedReactor(probe)
+        self.assertTrue(IProbe.providedBy(reactor))
+
+
+    def test_passthrough(self):
+        """
+        Methods on interfaces on the object passed to L{loggedReactor} can be
+        called by calling them on the object returned by L{loggedReactor}.
+        """
+        expected = object()
+        probe = Probe(expected)
+        reactor = loggedReactor(probe)
+        result = reactor.probe()
+        self.assertTrue(probe._probed)
+        self.assertIdentical(expected, result)
+
+
+    def test_connectTCP(self):
+        """
+        Called on the object returned by L{loggedReactor}, C{connectTCP} calls
+        the wrapped reactor's C{connectTCP} method with the original factory
+        wrapped in a L{_TrafficLoggingFactory}.
+        """
+        class RecordDataProtocol(Protocol):
+            def dataReceived(self, data):
+                self.data = data
+        proto = RecordDataProtocol()
+        factory = ClientFactory()
+        factory.protocol = lambda: proto
+        reactor = MemoryReactor()
+        logged = loggedReactor(reactor)
+        logged.connectTCP('192.168.1.2', 1234, factory, 21, '127.0.0.2')
+        [(host, port, factory, timeout, bindAddress)] = reactor.tcpClients
+        self.assertEqual('192.168.1.2', host)
+        self.assertEqual(1234, port)
+        self.assertIsInstance(factory, _TrafficLoggingFactory)
+        self.assertEqual(21, timeout)
+        self.assertEqual('127.0.0.2', bindAddress)
+
+        # Verify that the factory and protocol specified are really being used
+        protocol = factory.buildProtocol(None)
+        protocol.makeConnection(None)
+        protocol.dataReceived("foo")
+        self.assertEqual(proto.data, "foo")
+
+
+    def test_getLogFiles(self):
+        """
+        The reactor returned by L{loggedReactor} has a C{getLogFiles} method
+        which returns a L{logstate} instance containing the active and
+        completed log files tracked by the logging wrapper.
+        """
+        wrapped = ClientFactory()
+        wrapped.protocol = Discard
+        reactor = MemoryReactor()
+        logged = loggedReactor(reactor)
+        logged.connectTCP('127.0.0.1', 1234, wrapped)
+        factory = reactor.tcpClients[0][2]
+
+        finished = factory.buildProtocol(None)
+        finished.makeConnection(StringTransport())
+        finished.dataReceived('finished')
+        finished.connectionLost(None)
+
+        active = factory.buildProtocol(None)
+        active.makeConnection(StringTransport())
+        active.dataReceived('active')
+
+        logs = logged.getLogFiles()
+        self.assertEqual(1, len(logs.finished))
+        self.assertIn('finished', logs.finished[0].getvalue())
+        self.assertEqual(1, len(logs.active))
+        self.assertIn('active', logs.active[0].getvalue())
+
+
+
+class TrafficLoggingFactoryTests(TestCase):
+    """
+    Tests for L{_TrafficLoggingFactory}.
+    """
+    def setUp(self):
+        self.wrapped = ClientFactory()
+        self.wrapped.protocol = Discard
+        self.factory = _TrafficLoggingFactory(self.wrapped)
+
+        
+    def test_receivedBytesLogged(self):
+        """
+        When bytes are delivered through a protocol created by
+        L{_TrafficLoggingFactory}, they are added to a log kept on that
+        factory.
+        """
+        protocol = self.factory.buildProtocol(None)
+
+        # The factory should now have a new StringIO log file
+        self.assertEqual(1, len(self.factory.logs))
+
+        transport = StringTransport()
+        protocol.makeConnection(transport)
+
+        protocol.dataReceived("hello, world")
+        self.assertEqual(
+            "*\nC 0: 'hello, world'\n", self.factory.logs[0].getvalue())
+
+
+    def test_finishedLogs(self):
+        """
+        When connections are lost, the corresponding log files are moved into
+        C{_TrafficLoggingFactory.finishedLogs}.
+        """
+        protocol = self.factory.buildProtocol(None)
+        transport = StringTransport()
+        protocol.makeConnection(transport)
+        logfile = self.factory.logs[0]
+        protocol.connectionLost(None)
+        self.assertEqual(0, len(self.factory.logs))
+        self.assertEqual([logfile], self.factory.finishedLogs)
+
+
+    def test_finishedLogsLimit(self):
+        """
+        Only the most recent C{_TrafficLoggingFactory.LOGFILE_LIMIT} logfiles
+        are kept in C{_TrafficLoggingFactory.finishedLogs}.
+        """
+        self.factory.LOGFILE_LIMIT = 2
+        first = self.factory.buildProtocol(None)
+        first.makeConnection(StringTransport())
+        second = self.factory.buildProtocol(None)
+        second.makeConnection(StringTransport())
+        third = self.factory.buildProtocol(None)
+        third.makeConnection(StringTransport())
+
+        second.connectionLost(None)
+        first.connectionLost(None)
+        third.connectionLost(None)
+
+        self.assertEqual(
+            [first.logfile, third.logfile], self.factory.finishedLogs)

Copied: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/trafficlogger.py (from rev 7695, CalendarServer/trunk/contrib/performance/loadtest/trafficlogger.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/trafficlogger.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/loadtest/trafficlogger.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,114 @@
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+
+"""
+This module implements a reactor wrapper which will cause all traffic on
+connections set up using that reactor to be logged.
+"""
+
+__all__ = ['loggedReactor']
+
+from weakref import ref
+from StringIO import StringIO
+from collections import namedtuple
+
+from zope.interface import providedBy
+
+from twisted.python.components import proxyForInterface
+from twisted.internet.interfaces import IReactorTCP
+from twisted.protocols.policies import WrappingFactory, TrafficLoggingProtocol
+
+
+logstate = namedtuple('logstate', 'active finished')
+
+
+def loggedReactor(reactor):
+    """
+    Construct and return a wrapper around the given C{reactor} which provides
+    all of the same interfaces, but which will log all traffic over outgoing
+    TCP connections it establishes.
+    """
+    bases = []
+    for iface in providedBy(reactor):
+        if iface is IReactorTCP:
+            bases.append(_TCPTrafficLoggingReactor)
+        else:
+            bases.append(proxyForInterface(iface, '_reactor'))
+    if bases:
+        return type('(Logged Reactor)', tuple(bases), {})(reactor)
+    return reactor
+
+
+class _TCPTrafficLoggingReactor(proxyForInterface(IReactorTCP, '_reactor')):
+    """
+    A mixin for a reactor wrapper which defines C{connectTCP} so as to cause
+    traffic to be logged.
+    """
+    _factories = None
+
+    @property
+    def factories(self):
+        if self._factories is None:
+            self._factories = []
+        return self._factories
+
+
+    def getLogFiles(self):
+        active = []
+        finished = []
+        for factoryref in self.factories:
+            factory = factoryref()
+            active.extend(factory.logs)
+            finished.extend(factory.finishedLogs)
+        return logstate(active, finished)
+
+
+    def connectTCP(self, host, port, factory, *args, **kwargs):
+        wrapper = _TrafficLoggingFactory(factory)
+        self.factories.append(ref(wrapper, self.factories.remove))
+        return self._reactor.connectTCP(
+            host, port, wrapper, *args, **kwargs)
+
+
+class _TrafficLoggingFactory(WrappingFactory):
+    """
+    A wrapping factory which applies L{TrafficLoggingProtocolWrapper}.
+    """
+    LOGFILE_LIMIT = 20
+
+    protocol = TrafficLoggingProtocol
+
+    noisy = False
+
+    def __init__(self, wrappedFactory):
+        WrappingFactory.__init__(self, wrappedFactory)
+        self.logs = []
+        self.finishedLogs = []
+
+
+    def unregisterProtocol(self, protocol):
+        WrappingFactory.unregisterProtocol(self, protocol)
+        self.logs.remove(protocol.logfile)
+        self.finishedLogs.append(protocol.logfile)
+        del self.finishedLogs[:-self.LOGFILE_LIMIT]
+
+
+    def buildProtocol(self, addr):
+        logfile = StringIO()
+        self.logs.append(logfile)
+        return self.protocol(
+            self, self.wrappedFactory.buildProtocol(addr), logfile, None, 0)

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/run.sh
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/run.sh	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/run.sh	2011-06-30 20:44:13 UTC (rev 7697)
@@ -20,6 +20,6 @@
 source support/shell.sh
 popd
 
-export PYTHONPATH=$PYTHONPATH:~/Projects/CalDAVClientLibrary/trunk/src
+export PYTHONPATH=$PYTHONPATH:~/Projects/CalendarServer/CalDAVClientLibrary/src
 
 exec "$@"

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/setbackend.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/setbackend.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/setbackend.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -22,7 +22,6 @@
 
 import sys
 from xml.etree import ElementTree
-from xml.etree import ElementPath
 
 def main():
     conf = ElementTree.parse(file(sys.argv[1]))

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/stats.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/stats.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/stats.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -14,6 +14,14 @@
 # limitations under the License.
 ##
 
+import random, time, datetime
+
+import pytz
+
+from zope.interface import Interface, implements
+
+from twisted.python.util import FancyEqMixin
+
 import sqlparse
 
 NANO = 1000000000.0
@@ -200,3 +208,157 @@
 
     def summarize(self, samples):
         return _Statistic.summarize(self, self.squash(samples))
+
+
+def quantize(data):
+    """
+    Given some continuous data, quantize it into appropriately sized
+    discrete buckets (eg, as would be suitable for constructing a
+    histogram of the values).
+    """
+    #buckets = {}
+    return []
+
+
+class IPopulation(Interface):
+    def sample():
+        pass
+
+
+class UniformDiscreteDistribution(object, FancyEqMixin):
+    """
+    
+    """
+    implements(IPopulation)
+
+    compareAttributes = ['_values']
+
+    def __init__(self, values):
+        self._values = values
+        self._refill()
+
+
+    def _refill(self):
+        self._remaining = self._values[:]
+        random.shuffle(self._remaining)
+
+
+    def sample(self):
+        if not self._remaining:
+            self._refill()
+        return self._remaining.pop()
+
+
+
+class LogNormalDistribution(object, FancyEqMixin):
+    """
+    """
+    implements(IPopulation)
+
+    compareAttributes = ['_mu', '_sigma']
+
+    def __init__(self, mu, sigma):
+        self._mu = mu
+        self._sigma = sigma
+
+
+    def sample(self):
+        return random.lognormvariate(self._mu, self._sigma)
+
+
+
+class NearFutureDistribution(object, FancyEqMixin):
+    compareAttributes = ['_offset']
+
+    def __init__(self):
+        self._offset = LogNormalDistribution(7, 0.8)
+
+
+    def sample(self):
+        return time.time() + self._offset.sample()
+
+
+
+class NormalDistribution(object, FancyEqMixin):
+    compareAttributes = ['_mu', '_sigma']
+
+    def __init__(self, mu, sigma):
+        self._mu = mu
+        self._sigma = sigma
+
+
+    def sample(self):
+        return random.normalvariate(self._mu, self._sigma)
+
+
+
+class UniformIntegerDistribution(object, FancyEqMixin):
+    compareAttributes = ['_min', '_max']
+
+    def __init__(self, min, max):
+        self._min = min
+        self._max = max
+
+
+    def sample(self):
+        return int(random.uniform(self._min, self._max))
+
+
+NUM_WEEKDAYS = 7
+
+class WorkDistribution(object, FancyEqMixin):
+    compareAttributes = ["_daysOfWeek", "_beginHour", "_endHour"]
+
+    _weekdayNames = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
+
+    now = staticmethod(datetime.datetime.now)
+
+    def __init__(self, daysOfWeek=["mon", "tue", "wed", "thu", "fri"], beginHour=8, endHour=17, tzname="UTC"):
+        self._daysOfWeek = [self._weekdayNames.index(day) for day in daysOfWeek]
+        self._beginHour = beginHour
+        self._endHour = endHour
+        self._tzinfo = pytz.timezone(tzname)
+        self._helperDistribution = NormalDistribution(
+            # Mean 6 workdays in the future
+            60 * 60 * 8 * 6,
+            # Standard deviation of 4 workdays
+            60 * 60 * 8 * 4)
+
+
+    def astimestamp(self, dt):
+        return time.mktime(dt.timetuple())
+
+
+    def _findWorkAfter(self, when):
+        """
+        Return a two-tuple of the start and end of work hours following
+        C{when}.  If C{when} falls within work hours, then the start time will
+        be equal to when.
+        """
+        # Find a workday that follows the timestamp
+        weekday = when.weekday()
+        for i in range(NUM_WEEKDAYS):
+            day = when + datetime.timedelta(days=i)
+            if (weekday + i) % NUM_WEEKDAYS in self._daysOfWeek:
+                # Joy, a day on which work might occur.  Find the first hour on
+                # this day when work may start.
+                begin = day.replace(
+                    hour=self._beginHour, minute=0, second=0, microsecond=0)
+                end = begin.replace(hour=self._endHour)
+                if end > when:
+                    return begin, end
+
+
+    def sample(self):
+        offset = datetime.timedelta(seconds=self._helperDistribution.sample())
+        beginning = self.now(self._tzinfo)
+        while offset:
+            start, end = self._findWorkAfter(beginning)
+            if end - start > offset:
+                result = start + offset
+                return self.astimestamp(
+                    result.replace(
+                        minute=result.minute // 15 * 15,
+                        second=0, microsecond=0))
+            offset -= (end - start)
+            beginning = end

Modified: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/test_stats.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/test_stats.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/test_stats.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -14,10 +14,15 @@
 # limitations under the License.
 ##
 
-from unittest import TestCase
+import pytz
+from datetime import datetime
 
-from stats import SQLDuration
+from twisted.trial.unittest import TestCase
 
+from stats import (
+    SQLDuration, LogNormalDistribution, UniformDiscreteDistribution,
+    UniformIntegerDistribution, WorkDistribution, quantize)
+
 class SQLDurationTests(TestCase):
     def setUp(self):
         self.stat = SQLDuration('foo')
@@ -40,3 +45,87 @@
             self.stat.normalize('SELECT foo FROM bar WHERE True'),
             'SELECT foo FROM bar WHERE ?')
 
+
+
+class DistributionTests(TestCase):
+    def test_lognormal(self):
+        dist = LogNormalDistribution(1, 1)
+        for i in range(100):
+            value = dist.sample()
+            self.assertIsInstance(value, float)
+            self.assertTrue(value >= 0.0, "negative value %r" % (value,))
+            self.assertTrue(value <= 1000, "implausibly high value %r" % (value,))
+
+
+    def test_uniformdiscrete(self):
+        population = [1, 5, 6, 9]
+        counts = dict.fromkeys(population, 0)
+        dist = UniformDiscreteDistribution(population)
+        for i in range(len(population) * 10):
+            counts[dist.sample()] += 1
+        self.assertEqual(dict.fromkeys(population, 10), counts)
+
+
+    def test_workdistribution(self):
+        tzname = "US/Eastern"
+        tzinfo = pytz.timezone(tzname)
+        dist = WorkDistribution(["mon", "wed", "thu", "sat"], 10, 20, tzname)
+        dist._helperDistribution = UniformDiscreteDistribution([35 * 60 * 60 + 30 * 60])
+        dist.now = lambda tz=None: datetime(2011, 5, 29, 18, 5, 36, tzinfo=tz)
+        value = dist.sample()
+        self.assertEqual(
+            # Move past three workdays - monday, wednesday, thursday - using 30
+            # of the hours, and then five and a half hours into the fourth
+            # workday, saturday.  Workday starts at 10am, so the sample value
+            # is 3:30pm, ie 1530 hours.
+            datetime(2011, 6, 4, 15, 30, 0, tzinfo=tzinfo),
+            datetime.fromtimestamp(value, tzinfo))
+
+
+    def test_uniform(self):
+        dist = UniformIntegerDistribution(-5, 10)
+        for i in range(100):
+            value = dist.sample()
+            self.assertTrue(-5 <= value < 10)
+            self.assertIsInstance(value, int)
+
+
+
+class QuantizationTests(TestCase):
+    """
+    Tests for L{quantize} which constructs discrete datasets of
+    dynamic quantization from continuous datasets.
+    """
+    skip = "nothing implemented yet, maybe not necessary"
+
+    def test_one(self):
+        """
+        A single data point is put into a bucket equal to its value and returned.
+        """
+        dataset = [5.0]
+        expected = [(5.0, [5.0])]
+        self.assertEqual(quantize(dataset), expected)
+
+
+    def test_two(self):
+        """
+        Each of two values are put into buckets the size of the
+        standard deviation of the sample.
+        """
+        dataset = [2.0, 5.0]
+        expected = [(1.5, [2.0]), (4.5, [5.0])]
+        self.assertEqual(quantize(dataset), expected)
+
+
+    def xtest_three(self):
+        """
+        If two out of three values fall within one bucket defined by
+        the standard deviation of the sample, that bucket is split in
+        half so each bucket has one value.
+        """
+
+
+    def xtest_alpha(self):
+        """
+        This exercises one simple case of L{quantize} with a small amount of data.
+        """

Deleted: CalendarServer/branches/users/cdaboo/timezones/contrib/performance/todo
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/performance/todo	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/performance/todo	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,4 +0,0 @@
-description of each benchmark on the website
-investigate the performance regression near r6270 to see if it is real
-benchmarks for larger calendars (more events)
-extend report to know about linear scaling, quadratic scaling, etc perhaps

Copied: CalendarServer/branches/users/cdaboo/timezones/contrib/tools/backup (from rev 7695, CalendarServer/trunk/contrib/tools/backup)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/tools/backup	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/tools/backup	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,230 @@
+#!/usr/bin/python
+
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from getopt import getopt, GetoptError
+import os
+import subprocess
+import sys
+import tarfile
+from tempfile import mkstemp
+from shutil import rmtree
+
+from twistedcaldav.config import config
+from calendarserver.tools.util import loadConfig
+
+USERNAME      = "caldav"
+DATABASENAME  = "caldav"
+DUMPFILENAME  = "db_backup"
+PLISTPATH     = "/etc/caldavd/caldavd.plist"
+PLISTNAME     = "caldavd.plist"
+
+PSQL          = "/usr/bin/psql"
+PGDUMP        = "/usr/bin/pg_dump"
+
+def usage(e=None):
+    name = os.path.basename(sys.argv[0])
+    print "usage: %s [options] command backup-file" % (name,)
+    print ""
+    print " Backup or restore calendar and addressbook data"
+    print ""
+    print "options:"
+    print "  -f --config <path>: Specify caldavd.plist configuration path"
+    print "  -h --help: print this help and exit"
+    print "  -v --verbose: print additional information"
+    print ""
+    print "commands:"
+    print "  backup: create backup-file"
+    print "  restore: restore from backup-file"
+    print ""
+
+    if e:
+        sys.stderr.write("%s\n" % (e,))
+        sys.exit(64)
+    else:
+        sys.exit(0)
+
+
+def dumpData(dumpFile, verbose=False):
+    """
+    Use pg_dump to dump data to dumpFile
+    """
+
+    cmdArgs = [
+        PGDUMP,
+        "--username=%s" % (USERNAME,),
+        "--clean",
+        "--no-privileges",
+        "--file=%s" % (dumpFile,),
+        DATABASENAME
+    ]
+    try:
+        if verbose:
+            print "\nDumping data to %s" % (dumpFile,)
+            print "Executing: %s" % (" ".join(cmdArgs))
+        out = subprocess.check_output(cmdArgs, stderr=subprocess.STDOUT)
+        if verbose:
+            print out
+    except subprocess.CalledProcessError, e:
+        if verbose:
+            print e.output
+        raise BackupError(
+            "%s failed:\n%s (exit code = %d)" %
+            (PGDUMP, e.output, e.returncode)
+        )
+
+
+def loadData(dumpFile, verbose=False):
+    """
+    Use psql to load data from dumpFile
+    """
+
+    cmdArgs = [
+        PSQL,
+        "--username=%s" % (USERNAME,),
+        "--file=%s" % (dumpFile,)
+    ]
+    try:
+        if verbose:
+            print "\nLoading data from %s" % (dumpFile,)
+            print "Executing: %s" % (" ".join(cmdArgs))
+        out = subprocess.check_output(cmdArgs, stderr=subprocess.STDOUT)
+        if verbose:
+            print out
+    except subprocess.CalledProcessError, e:
+        if verbose:
+            print e.output
+        raise BackupError(
+            "%s failed:\n%s (exit code = %d)" %
+            (PGDUMP, e.output, e.returncode)
+        )
+
+
+class BackupError(Exception):
+    pass
+
+
+def error(s):
+    sys.stderr.write("%s\n" % (s,))
+    sys.exit(1)
+
+
+def main():
+    try:
+        (optargs, args) = getopt(
+            sys.argv[1:], "f:hv", [
+                "config=",
+                "help",
+                "verbose",
+            ],
+        )
+    except GetoptError, e:
+        usage(e)
+
+    verbose = False
+    configFileName = None
+
+    for opt, arg in optargs:
+        if opt in ("-h", "--help"):
+            usage()
+        elif opt in ("-f", "--config"):
+            configFileName = arg
+        elif opt in ("-v", "--verbose"):
+            verbose = True
+        else:
+            raise NotImplementedError(opt)
+
+    if len(args) != 2:
+        usage("Must specify a command and a backup-file name.")
+
+    command = args[0]
+    filename = args[1]
+
+    loadConfig(configFileName)
+
+    serverRoot = config.ServerRoot
+    dataRoot = config.DataRoot
+    docRoot = config.DocumentRoot
+
+    if command == "backup":
+
+        fd, tmpPath = mkstemp(suffix=".dbdump")
+
+        try:
+            dumpData(tmpPath, verbose=verbose)
+
+            if verbose:
+                print "Creating %s" % (filename,)
+            tar = tarfile.open(filename, "w:gz")
+            if verbose:
+                print "Adding %s" % (dataRoot,)
+            tar.add(dataRoot, "Data")
+            if verbose:
+                print "Adding %s" % (docRoot,)
+            tar.add(docRoot, "Documents")
+            if verbose:
+                print "Adding %s" % (tmpPath,)
+            tar.add(tmpPath, DUMPFILENAME)
+            if verbose:
+                print "Adding %s" % (PLISTPATH,)
+            tar.add(PLISTPATH, PLISTNAME)
+            tar.close()
+
+            if verbose:
+                print "Removing %s" % (tmpPath,)
+            os.remove(tmpPath)
+
+            if verbose:
+                print "Done"
+        except BackupError, e:
+            error("Failed to dump database; error: %s" % (e,))
+
+    elif command == "restore":
+
+        try:
+            tar = tarfile.open(filename, "r:gz")
+            os.chdir(serverRoot)
+
+            if os.path.exists(dataRoot):
+                if verbose:
+                    print "Removing old DataRoot: %s" % (dataRoot,)
+                rmtree(dataRoot)
+
+            if os.path.exists(docRoot):
+                if verbose:
+                    print "Removing old DocumentRoot: %s" % (docRoot,)
+                rmtree(docRoot)
+
+            if verbose:
+                print "Extracting from backup file: %s" % (filename,)
+            tar.extractall()
+
+            loadData(DUMPFILENAME, verbose=verbose)
+
+            if verbose:
+                print "Cleaning up database dump file: %s" % (DUMPFILENAME,)
+            os.remove(DUMPFILENAME)
+
+        except BackupError, e:
+            error("Failed to dump database; error: %s" % (e,))
+            raise
+
+    else:
+        error("Unknown command '%s'" % (command,))
+
+if __name__ == "__main__":
+    main()

Copied: CalendarServer/branches/users/cdaboo/timezones/contrib/tools/fakecalendardata.py (from rev 7695, CalendarServer/trunk/contrib/tools/fakecalendardata.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/contrib/tools/fakecalendardata.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/contrib/tools/fakecalendardata.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,212 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import datetime
+import getopt
+import os
+import random
+import sys
+import uuid
+
+outputFile = None
+fileCount = 0
+lastWeek = None
+
+calendar_template = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VTIMEZONE
+TZID:America/Los_Angeles
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+TZNAME:PST
+TZOFFSETFROM:-0700
+TZOFFSETTO:-0800
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+TZNAME:PDT
+TZOFFSETFROM:-0800
+TZOFFSETTO:-0700
+END:DAYLIGHT
+END:VTIMEZONE
+%(VEVENTS)s\
+END:VCALENDAR
+"""
+
+vevent_template = """\
+BEGIN:VEVENT
+UID:%(UID)s
+DTSTART;TZID=America/Los_Angeles:%(START)s
+DURATION:P1H
+%(RRULE)s\
+CREATED:20100729T193912Z
+DTSTAMP:20100729T195557Z
+%(ORGANIZER)s\
+%(ATTENDEES)s\
+SEQUENCE:0
+SUMMARY:%(SUMMARY)s
+TRANSP:OPAQUE
+END:VEVENT
+"""
+
+attendee_template = """\
+ATTENDEE;CN=User %(SEQUENCE)02d;CUTYPE=INDIVIDUAL;EMAIL=user%(SEQUENCE)02d at example.com;PARTSTAT=NE
+ EDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:urn:uuid:user%(SEQUENCE)02d
+"""
+
+organizer_template = """\
+ORGANIZER;CN=User %(SEQUENCE)02d;EMAIL=user%(SEQUENCE)02d at example.com:urn:uuid:user%(SEQUENCE)02d
+ATTENDEE;CN=User %(SEQUENCE)02d;EMAIL=user%(SEQUENCE)02d at example.com;PARTSTAT=ACCEPTE
+ D:urn:uuid:user%(SEQUENCE)02d
+"""
+
+summary_template = "Event %d"
+rrules_template = (
+    "RRULE:FREQ=DAILY;COUNT=5\n",
+    "RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR\n",
+    "RRULE:FREQ=YEARLY\n",
+)
+
+def makeVEVENT(recurring, atendees, date, hour, count):
+    
+    subs = {
+        "UID": str(uuid.uuid4()),
+        "START" : "",
+        "RRULE" : "",
+        "ORGANIZER" : "",
+        "ATTENDEES" : "",
+        "SUMMARY"   : summary_template % (count,)
+    }
+
+    if recurring:
+        subs["RRULE"] = random.choice(rrules_template)
+    
+    if attendees:
+        subs["ORGANIZER"] = organizer_template % {"SEQUENCE": 1}
+        for ctr in range(2, random.randint(2, 10)):
+            subs["ATTENDEES"] += attendee_template % {"SEQUENCE": ctr}
+    
+    subs["START"] = "%04d%02d%02dT%02d0000" % (date.year, date.month, date.day, hour)
+
+    return vevent_template % subs 
+
+def argPath(path):
+    fpath = os.path.expanduser(path)
+    if not fpath.startswith("/"):
+        fpath = os.path.join(pwd, fpath)
+    return fpath
+
+def usage(error_msg=None):
+    if error_msg:
+        print error_msg
+
+    print """Usage: fakecalendardata [options]
+Options:
+    -h          Print this help and exit
+    -a          Percentage of events that should include attendees
+    -c          Total number of events to generate
+    -d          Directory to store separate .ics files into
+    -r          Percentage of recurring events to create
+    -p          Numbers of years in the past to start at
+    -f          Number of years into the future to end at
+
+Arguments: None
+
+Description:
+This utility will generate fake iCalendar data either into a single .ics
+file or into multiple .ics files.
+"""
+
+    if error_msg:
+        raise ValueError(error_msg)
+    else:
+        sys.exit(0)
+
+if __name__ == "__main__":
+
+    outputDir = None
+    totalCount = 10
+    percentRecurring = 20
+    percentWithAttendees = 10
+    yearsPast = 2
+    yearsFuture = 1
+
+    options, args = getopt.getopt(sys.argv[1:], "a:c:f:hd:p:r:", [])
+
+    for option, value in options:
+        if option == "-h":
+            usage()
+        elif option == "-a":
+            percentWithAttendees = int(value)
+        elif option == "-c":
+            totalCount = int(value)
+        elif option == "-d":
+            outputDir = argPath(value)
+        elif option == "-f":
+            yearsFuture = int(value)
+        elif option == "-p":
+            yearsPast = int(value)
+        elif option == "-r":
+            percentRecurring = int(value)
+        else:
+            usage("Unrecognized option: %s" % (option,))
+
+    if outputDir and not os.path.isdir(outputDir):
+        usage("Must specify a valid output directory.")
+
+    # Process arguments
+    if len(args) != 0:
+        usage("No arguments allowed")
+
+    pwd = os.getcwd()
+
+    totalRecurring = (totalCount * percentRecurring) / 100
+    totalRecurringWithAttendees = (totalRecurring * percentWithAttendees) / 100
+    totalRecurringWithoutAttendees = totalRecurring - totalRecurringWithAttendees
+    
+    totalNonRecurring = totalCount - totalRecurring
+    totalNonRecurringWithAttendees = (totalNonRecurring * percentWithAttendees) / 100
+    totalNonRecurringWithoutAttendees = totalNonRecurring - totalNonRecurringWithAttendees
+
+    eventTypes = []
+    eventTypes.extend([(True, True) for _ignore in range(totalRecurringWithAttendees)])
+    eventTypes.extend([(True, False) for _ignore in range(totalRecurringWithoutAttendees)])
+    eventTypes.extend([(False, True) for _ignore in range(totalNonRecurringWithAttendees)])
+    eventTypes.extend([(False, False) for _ignore in range(totalNonRecurringWithoutAttendees)])
+    random.shuffle(eventTypes)
+
+    totalYears = yearsPast + yearsFuture
+    totalDays = totalYears * 365
+    
+    startDate = datetime.date.today() - datetime.timedelta(days=yearsPast*365)
+
+    for i in range(len(eventTypes)):
+        eventTypes[i] += (
+            startDate + datetime.timedelta(days=random.randint(0, totalDays)),
+            random.randint(8, 18),
+        )
+
+    vevents = []
+    for count, (recurring, attendees, date, hour) in enumerate(eventTypes):
+        #print recurring, attendees, date, hour
+        vevents.append(makeVEVENT(recurring, attendees, date, hour, count+1))
+
+    print calendar_template % {"VEVENTS" : "".join(vevents)}

Modified: CalendarServer/branches/users/cdaboo/timezones/doc/calendarserver_monitor_notifications.8
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/doc/calendarserver_monitor_notifications.8	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/doc/calendarserver_monitor_notifications.8	2011-06-30 20:44:13 UTC (rev 7697)
@@ -25,6 +25,7 @@
 .Op Fl -admin Ar username
 .Op Fl -config Ar file
 .Op Fl -host Ar hostname
+.Op Fl -node Ar pubsub-node-name
 .Op Fl -port Ar port-number
 .Op Fl -ssl
 .Op Fl -verbose
@@ -44,6 +45,8 @@
 Use the Calendar Server configuration specified in the given file.  Defaults to /etc/caldavd/caldavd.plist.
 .It Fl H, -host Ar HOSTNAME
 Connect to the specified calendar server.  If not passed on the command line, the host name is looked up in the configuration file.
+.It Fl n, -node Ar NODENAME
+Bypass the auto-discovery of pubsub nodes and specify one explicitly.  Useful on calendar servers which don't support the push-transports DAV property.  When using this option, the host and port options instead refer to the XMPP server host and port numbers.
 .It Fl p, -port Ar NUMBER
 Connect to the specified port number.  If not passed on the command line, the port number is looked up in the configuration file.
 .It Fl s, -ssl

Deleted: CalendarServer/branches/users/cdaboo/timezones/lib-patches/cx_Oracle/bytes-per-nclob-character.patch
===================================================================
--- CalendarServer/trunk/lib-patches/cx_Oracle/bytes-per-nclob-character.patch	2011-06-30 16:37:39 UTC (rev 7695)
+++ CalendarServer/branches/users/cdaboo/timezones/lib-patches/cx_Oracle/bytes-per-nclob-character.patch	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,17 +0,0 @@
-Index: ExternalLobVar.c
-===================================================================
---- ExternalLobVar.c	(revision 355)
-+++ ExternalLobVar.c	(working copy)
-@@ -259,10 +259,9 @@
-             amount = 1;
-     }
-     length = amount;
--    if (var->lobVar->type == &vt_CLOB)
-+    if ((var->lobVar->type == &vt_CLOB) ||
-+        (var->lobVar->type == &vt_NCLOB))
-         bufferSize = amount * var->lobVar->environment->maxBytesPerCharacter;
--    else if (var->lobVar->type == &vt_NCLOB)
--        bufferSize = amount * 2;
-     else bufferSize = amount;
- 
-     // create a string for retrieving the value

Copied: CalendarServer/branches/users/cdaboo/timezones/lib-patches/cx_Oracle/bytes-per-nclob-character.patch (from rev 7695, CalendarServer/trunk/lib-patches/cx_Oracle/bytes-per-nclob-character.patch)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/lib-patches/cx_Oracle/bytes-per-nclob-character.patch	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/lib-patches/cx_Oracle/bytes-per-nclob-character.patch	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,17 @@
+Index: ExternalLobVar.c
+===================================================================
+--- ExternalLobVar.c	(revision 355)
++++ ExternalLobVar.c	(working copy)
+@@ -259,10 +259,9 @@
+             amount = 1;
+     }
+     length = amount;
+-    if (var->lobVar->type == &vt_CLOB)
++    if ((var->lobVar->type == &vt_CLOB) ||
++        (var->lobVar->type == &vt_NCLOB))
+         bufferSize = amount * var->lobVar->environment->maxBytesPerCharacter;
+-    else if (var->lobVar->type == &vt_NCLOB)
+-        bufferSize = amount * 2;
+     else bufferSize = amount;
+ 
+     // create a string for retrieving the value

Modified: CalendarServer/branches/users/cdaboo/timezones/pyflakes
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/pyflakes	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/pyflakes	2011-06-30 20:44:13 UTC (rev 7697)
@@ -9,7 +9,7 @@
 export PYTHONPATH="${flakes}:${PYTHONPATH:-}";
 
 if [ $# -eq 0 ]; then
-  set - calendarserver twext twisted twistedcaldav txdav;
+  set - calendarserver twext twisted twistedcaldav txdav contrib;
 fi;
 
 tmp="$(mktemp -t pyflakes)";

Modified: CalendarServer/branches/users/cdaboo/timezones/run
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/run	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/run	2011-06-30 20:44:13 UTC (rev 7697)
@@ -44,7 +44,7 @@
     echo;
   fi;
 
-  echo "Usage: ${program} [-hvgsfnpdkrR] [-K key] [-iI dst] [-t type] [-S statsdirectory] [-P plugin]";
+  echo "Usage: ${program} [-hvgsfnpdkrR] [-K key] [-iIb dst] [-t type] [-S statsdirectory] [-P plugin]";
   echo "Options:";
   echo "	-h  Print this help and exit";
   echo "	-v  Be verbose";
@@ -59,6 +59,7 @@
   echo "	-K  Print value of configuration key and exit";
   echo "	-i  Perform a system install into dst; implies -s";
   echo "	-I  Perform a home install into dst; implies -s";
+  echo "	-b  Perform a bundled install (include all dependencies) into dst; implies -s";
   echo "	-t  Select the server process type (Master, Slave or Combined) [${service_type}]";
   echo "	-S  Write a pstats object for each process to the given directory when the server is stopped.";
   echo "	-P  Select the twistd plugin name [${plugin_name}]";
@@ -74,7 +75,7 @@
 # functions in build.sh.
 parse_options () {
   OPTIND=1;
-  while getopts "ahvgsfnpdkrK:i:I:t:S:P:R:" option; do
+  while getopts "ahvgsfnpdkrK:i:I:b:t:S:P:R:" option; do
     case "${option}" in
       '?') usage; ;;
       'h') usage -; exit 0; ;;
@@ -90,9 +91,32 @@
       'S')       profile="-p ${OPTARG}"; ;;
       'g') do_get="true" ; do_setup="false"; do_run="false"; ;;
       's') do_get="true" ; do_setup="true" ; do_run="false"; ;;
-      'p') do_get="false"; do_setup="false"; do_run="false"; print_path="true"; ;;
-      'i') do_get="true" ; do_setup="true" ; do_run="false"; install="${OPTARG}"; install_flag="--root="; ;;
-      'I') do_get="true" ; do_setup="true" ; do_run="false"; install="${wd}/build/dst"; install_flag="--root="; install_home="${OPTARG}"; ;;
+      'p')
+        do_get="false"; do_setup="false"; do_run="false"; print_path="true";
+        ;;
+      'i')
+        do_get="true";
+        do_setup="true";
+        do_run="false";
+        install="${OPTARG}";
+        install_flag="--root=";
+        ;;
+      'I')
+        do_get="true";
+        do_setup="true";
+        do_run="false";
+        install="${wd}/build/dst";
+        install_flag="--root=";
+        install_home="${OPTARG}";
+        ;;
+      'b')
+        do_bundle="true";
+        do_get="true";
+        do_setup="true";
+        do_run="false";
+        install="${OPTARG}";
+        install_flag="--root=";
+        ;;
       'n') do_get="false" ; do_setup="false"; ;;
     esac;
   done;
@@ -210,6 +234,10 @@
     do_home_install;
   fi;
 
+  if "${do_bundle}"; then
+    write_environment;
+  fi;
+
   # Finally, run the server.
   run;
 }

Modified: CalendarServer/branches/users/cdaboo/timezones/setup.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/setup.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/setup.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -29,11 +29,16 @@
     ]
 
     for root, dirs, files in os.walk("."):
-        for exclude in (
+        excludes = [
             ".svn",
             "_trial_temp",
             "build",
-        ):
+        ]
+
+        if root == ".":
+            excludes.append("data")
+
+        for exclude in excludes:
             if exclude in dirs:
                 dirs.remove(exclude)
 
@@ -86,7 +91,7 @@
 # Run setup
 #
 
-if __name__ == "__main__":
+def doSetup():
     from distutils.core import setup
 
     dist = setup(
@@ -134,6 +139,8 @@
 
     if "install" in dist.commands:
         install_obj = dist.command_obj["install"]
+        if install_obj.root is None:
+            return
         install_scripts = os.path.normpath(install_obj.install_scripts)
         install_lib = os.path.normpath(install_obj.install_lib)
         root = os.path.normpath(install_obj.root)
@@ -182,3 +189,10 @@
             newScript = open(scriptPath, "w")
             newScript.write("\n".join(script))
             newScript.close()
+
+
+
+if __name__ == "__main__":
+    doSetup()
+
+

Copied: CalendarServer/branches/users/cdaboo/timezones/sim (from rev 7695, CalendarServer/trunk/sim)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/sim	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/sim	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+##
+# Copyright (c) 2005-2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+set -e;
+set -u;
+
+wd="$(cd "$(dirname "$0")" && pwd -L)";
+
+export PYTHONPATH="$("${wd}/run" -p)";
+
+exec "${wd}/contrib/performance/sim" "$@";


Property changes on: CalendarServer/branches/users/cdaboo/timezones/support
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/support/build.sh
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/support/build.sh	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/support/build.sh	2011-06-30 20:44:13 UTC (rev 7697)
@@ -41,6 +41,7 @@
          do_get="true";
        do_setup="true";
          do_run="true";
+      do_bundle="false";
     force_setup="false";
   disable_setup="false";
      print_path="false";
@@ -340,8 +341,10 @@
       svn checkout -r "${revision}" "${uri}@${revision}" "${path}";
     }
 
-    if [ "${revision}" != "HEAD" ] && [ -n "${cache_deps}" ] && [ -n "${hash}" ]; then
-      local cache_file="${cache_deps}/${name}-$(echo "${uri}" | hash)@r${revision}.tgz";
+    if [ "${revision}" != "HEAD" ] && [ -n "${cache_deps}" ] \
+        && [ -n "${hash}" ]; then
+      local cacheid="${name}-$(echo "${uri}" | hash)";
+      local cache_file="${cache_deps}/${cacheid}@r${revision}.tgz";
 
       mkdir -p "${cache_deps}";
 
@@ -372,10 +375,12 @@
   if "${do_setup}"; then
     echo "Building ${name}...";
     cd "${path}";
-    if ! "${python}" ./setup.py -q build --build-lib "build/${py_platform_libdir}" "$@"; then
+    if ! "${python}" ./setup.py -q build \
+        --build-lib "build/${py_platform_libdir}" "$@"; then
       if "${optional}"; then
         echo "WARNING: ${name} failed to build.";
-        echo "WARNING: ${name} is not required to run the server; continuing without it.";
+        echo "WARNING: ${name} is not required to run the server;"\
+             "continuing without it.";
       else
         return $?;
       fi;
@@ -394,7 +399,13 @@
     echo "";
     echo "Installing ${name}...";
     cd "${path}";
-    "${python}" ./setup.py install "${install_flag}${install}";
+    if "${do_bundle}"; then
+      # Since we've built our own Python, an option-free installation is the
+      # best bet.
+      "${python}" ./setup.py install;
+    else
+      "${python}" ./setup.py install "${install_flag}${install}";
+    fi;
     cd /;
   fi;
 }
@@ -403,16 +414,15 @@
 # Declare a dependency on a Python project.
 py_dependency () {
   local optional="false"; # Is this dependency optional?
-  local override="false"; # Do I need to get this dependency even if
-                          # the system already has it?
-  local  inplace="";      # Do development in-place; don't run
-                          # setup.py to build, and instead add the
-                          # source directory plus the given relative
-                          # path directly to sys.path.  twisted and
-                          # vobject are developed often enough that
-                          # this is convenient.
-  local skip_egg="false"; # Skip even the 'egg_info' step, because
-                          # nothing needs to be built.
+  local override="false"; # Do I need to get this dependency even if the system
+                          # already has it?
+  local  inplace="";      # Do development in-place; don't run setup.py to
+                          # build, and instead add the source directory plus the
+                          # given relative path directly to sys.path.  twisted
+                          # and vobject are developed often enough that this is
+                          # convenient.
+  local skip_egg="false"; # Skip even the 'egg_info' step, because nothing needs
+                          # to be built.
   local revision="0";     # Revision (if svn)
   local get_type="www";   # Protocol to use
   local  version="";      # Minimum version required
@@ -441,7 +451,8 @@
   # args
   local         name="$1"; shift; # the name of the package (for display)
   local       module="$1"; shift; # the name of the python module.
-  local distribution="$1"; shift; # the name of the directory to put the distribution into.
+  local distribution="$1"; shift; # the name of the directory to put the
+                                  # distribution into.
   local      get_uri="$1"; shift; # what URL should be fetched?
 
   local srcdir="${top}/${distribution}"
@@ -455,7 +466,7 @@
       if "${do_setup}" && "${override}" && ! "${skip_egg}"; then
         echo;
         if py_have_module setuptools; then
-          echo "Building ${name}... [overrides system, building egg-info metadata only]";
+          echo "Building ${name}... [overrides system, building egg-info only]";
           cd "${srcdir}";
           "${python}" ./setup.py -q egg_info 2>&1 | (
             grep -i -v 'Unrecognized .svn/entries' || true);
@@ -489,6 +500,8 @@
   fi;
 }
 
+# Run 'make' with the given command line, prepending a -j option appropriate to
+# the number of CPUs on the current machine, if that can be determined.
 jmake () {
   case "$(uname -s)" in
     Darwin|Linux)
@@ -525,25 +538,48 @@
 
   # Extra arguments are processed below, as arguments to './configure'.
 
-  srcdir="${top}/${path}";
+  if "${do_bundle}"; then
+    local dstroot="${install}";
+    srcdir="${install}/src/${path}";
+  else
+    srcdir="${top}/${path}";
+    local dstroot="${srcdir}/_root";
+  fi;
 
   www_get ${f_hash} "${name}" "${srcdir}" "${uri}";
 
+
+  export              PATH="${dstroot}/bin:${PATH}";
+  export    C_INCLUDE_PATH="${dstroot}/include:${C_INCLUDE_PATH:-}";
+  export   LD_LIBRARY_PATH="${dstroot}/lib:${LD_LIBRARY_PATH:-}";
+  export          CPPFLAGS="-I${dstroot}/include ${CPPFLAGS:-} ";
+  export           LDFLAGS="-L${dstroot}/lib ${LDFLAGS:-} ";
+  export DYLD_LIBRARY_PATH="${dstroot}/lib:${DYLD_LIBRARY_PATH:-}";
+
   if "${do_setup}" && (
-      "${force_setup}" || [ ! -d "${srcdir}/_root" ]); then
+      "${force_setup}" || "${do_bundle}" || [ ! -d "${dstroot}" ]); then
     echo "Building ${name}...";
     cd "${srcdir}";
-    ./configure --prefix="${srcdir}/_root" "$@";
+    ./configure --prefix="${dstroot}" "$@";
     jmake;
     jmake install;
   fi;
+}
 
-  export              PATH="${PATH}:${srcdir}/_root/bin";
-  export    C_INCLUDE_PATH="${C_INCLUDE_PATH:-}:${srcdir}/_root/include";
-  export   LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:${srcdir}/_root/lib";
-  export          CPPFLAGS="${CPPFLAGS:-} -I${srcdir}/_root/include";
-  export           LDFLAGS="${LDFLAGS:-} -L${srcdir}/_root/lib";
-  export DYLD_LIBRARY_PATH="${DYLD_LIBRARY_PATH:-}:${srcdir}/_root/lib";
+# Used only when bundling: write out, into the bundle, an 'environment.sh' file
+# that contains all the environment variables necessary to invoke commands in
+# the deployed bundle.
+
+write_environment () {
+  local dstroot="${install}";
+  cat > "${dstroot}/environment.sh" << __EOF__
+export              PATH="${dstroot}/bin:\${PATH}";
+export    C_INCLUDE_PATH="${dstroot}/include:\${C_INCLUDE_PATH:-}";
+export   LD_LIBRARY_PATH="${dstroot}/lib:\${LD_LIBRARY_PATH:-}";
+export          CPPFLAGS="-I${dstroot}/include \${CPPFLAGS:-} ";
+export           LDFLAGS="-L${dstroot}/lib \${LDFLAGS:-} ";
+export DYLD_LIBRARY_PATH="${dstroot}/lib:\${DYLD_LIBRARY_PATH:-}";
+__EOF__
 }
 
 
@@ -560,6 +596,25 @@
   # Dependencies compiled from C source code
   #
 
+
+  if "${do_bundle}"; then
+    # First a bit of bootstrapping: fill out the standard directory structure.
+    for topdir in bin lib include share src; do
+      mkdir -p "${install}/${topdir}";
+    done;
+
+    # Normally we depend on the system Python, but a bundle install should be as
+    # self-contained as possible.
+    local pyfn="Python-2.7.1";
+    c_dependency -m "aa27bc25725137ba155910bd8e5ddc4f" \
+        "Python" "${pyfn}" \
+        "http://www.python.org/ftp/python/2.7.1/${pyfn}.tar.bz2" \
+        --enable-shared;
+    # Be sure to use the Python we just built.
+    export PYTHON="$(type -p python)";
+    init_py;
+  fi;
+
   if ! type memcached > /dev/null 2>&1; then
     local le="libevent-1.4.13-stable";
     local mc="memcached-1.4.5";
@@ -568,8 +623,7 @@
       "http://monkey.org/~provos/${le}.tar.gz";
     c_dependency -m "583441a25f937360624024f2881e5ea8" \
       "memcached" "${mc}" \
-      "http://memcached.googlecode.com/files/${mc}.tar.gz" \
-      --enable-threads --with-libevent="${top}/${le}/_root";
+      "http://memcached.googlecode.com/files/${mc}.tar.gz";
   fi;
 
   if ! type postgres > /dev/null 2>&1; then
@@ -599,7 +653,13 @@
 
   # Sourceforge mirror hostname.
   local sf="superb-sea2.dl.sourceforge.net";
+  local st="setuptools-0.6c11";
+  local pypi="http://pypi.python.org/packages/source";
 
+  py_dependency -m "7df2a529a074f613b509fb44feefe74e" \
+    "setuptools" "setuptools" "${st}" \
+    "$pypi/s/setuptools/setuptools-0.6c11.tar.gz";
+
   local zi="zope.interface-3.3.0";
   py_dependency -m "93668855e37b4691c5c956665c33392c" \
     "Zope Interface" "zope.interface" "${zi}" \
@@ -614,7 +674,7 @@
   local po="pyOpenSSL-0.10";
   py_dependency -v 0.9 -m "34db8056ec53ce80c7f5fc58bee9f093" \
     "PyOpenSSL" "OpenSSL" "${po}" \
-    "http://pypi.python.org/packages/source/p/pyOpenSSL/${po}.tar.gz";
+    "${pypi}/p/pyOpenSSL/${po}.tar.gz";
 
   if type krb5-config > /dev/null 2>&1; then
     py_dependency -r 4241 \
@@ -639,21 +699,22 @@
           "http://${sf}/project/cx-oracle/5.1/${cx}.tar.gz";
   fi;
 
-  if [ "${py_version}" != "${py_version##2.5}" ] && ! py_have_module select26; then
+  if [ "${py_version}" != "${py_version##2.5}" ] && \
+      ! py_have_module select26; then
     local s26="select26-0.1a3";
     py_dependency -m "01b8929e7cfc4a8deb777b92e3115c15" \
       "select26" "select26" "${s26}" \
-      "http://pypi.python.org/packages/source/s/select26/${s26}.tar.gz";
+      "${pypi}/s/select26/${s26}.tar.gz";
   fi;
 
   local pg="PyGreSQL-4.0";
   py_dependency -v 4.0 -m "1aca50e59ff4cc56abe9452a9a49c5ff" -o \
     "PyGreSQL" "pgdb" "${pg}" \
-    "http://pypi.python.org/packages/source/P/PyGreSQL/${pg}.tar.gz";
+    "${pypi}/P/PyGreSQL/${pg}.tar.gz";
 
-  py_dependency -v 10.1 -r 30159 \
+  py_dependency -v 11 -r 31512 \
     "Twisted" "twisted" "Twisted" \
-    "svn://svn.twistedmatrix.com/svn/Twisted/tags/releases/twisted-10.1.0";
+    "svn://svn.twistedmatrix.com/svn/Twisted/tags/releases/twisted-11.0.0";
 
   local du="python-dateutil-1.5";
   py_dependency -m "35f3732db3f2cc4afdc68a8533b60a52" \
@@ -663,7 +724,7 @@
   local ld="python-ldap-2.3.13";
   py_dependency -v "2.3.13" -m "895223d32fa10bbc29aa349bfad59175" \
     "python-ldap" "python-ldap" "${ld}" \
-    "http://pypi.python.org/packages/source/p/python-ldap/${ld}.tar.gz";
+    "${pypi}/p/python-ldap/${ld}.tar.gz";
 
   # XXX actually vObject should be imported in-place.
   py_dependency -fe -i "" -r 219 \
@@ -671,29 +732,37 @@
     "http://svn.osafoundation.org/vobject/trunk";
 
   # XXX actually PyCalendar should be imported in-place.
-  py_dependency -fe -i "src" -r 161 \
+  py_dependency -fe -i "src" -r 169 \
     "pycalendar" "pycalendar" "pycalendar" \
-    "http://svn.mulberrymail.com/repos/PyCalendar/branches/server-stable";
+    "http://svn.mulberrymail.com/repos/PyCalendar/branches/server";
 
   #
   # Tool dependencies.  The code itself doesn't depend on these, but
   # they are useful to developers.
   #
 
-  py_dependency -v 0.1.2 -m "aa9852ad81822723adcd9f96838de14e" \
+  py_dependency -o -v 0.1.2 -m "aa9852ad81822723adcd9f96838de14e" \
     "SQLParse" "sqlparse" "sqlparse-0.1.2" \
     "http://python-sqlparse.googlecode.com/files/sqlparse-0.1.2.tar.gz";
 
-  py_dependency -v 0.4.0 -m "630a72510aae8758f48cf60e4fa17176" \
+  py_dependency -o -v 0.4.0 -m "630a72510aae8758f48cf60e4fa17176" \
     "Pyflakes" "pyflakes" "pyflakes-0.4.0" \
-    "http://pypi.python.org/packages/source/p/pyflakes/pyflakes-0.4.0.tar.gz";
+    "${pypi}/p/pyflakes/pyflakes-0.4.0.tar.gz";
+ 
+  py_dependency -o -r HEAD \
+    "CalDAVClientLibrary" "CalDAVClientLibrary" "CalDAVClientLibrary" \
+    "${svn_uri_base}/CalDAVClientLibrary/trunk";
 
-  svn_get "CalDAVTester" "${top}/CalDAVTester" "${svn_uri_base}/CalDAVTester/trunk" HEAD;
+  # Can't add "-v 2011g" to args because the version check expects numbers.
+  py_dependency -o -m "9ffda6e87b5f067a7ca37c54629c9e58" \
+    "pytz" "pytz" "pytz-2011g" \
+    "http://pypi.python.org/packages/source/p/pytz/pytz-2011g.tar.gz";
 
-  svn_get "CalDAVClientLibrary" "${top}/CalDAVClientLibrary" "${svn_uri_base}/CalDAVClientLibrary/trunk" HEAD;
+  svn_get "CalDAVTester" "${top}/CalDAVTester" \
+      "${svn_uri_base}/CalDAVTester/trunk" HEAD;
 
   local pd="pydoctor-0.3";
-  py_dependency -m "b000aa1fb458fe25952dadf26049ae68" \
+  py_dependency -o -m "b000aa1fb458fe25952dadf26049ae68" \
     "pydoctor" "pydoctor" "${pd}" \
     "http://launchpadlibrarian.net/42323121/${pd}.tar.gz";
 


Property changes on: CalendarServer/branches/users/cdaboo/timezones/support/build.sh
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation/support/build.sh:4379-4443
/CalendarServer/branches/egg-info-351/support/build.sh:4589-4615
/CalendarServer/branches/generic-sqlstore/support/build.sh:6167-6191
/CalendarServer/branches/new-store/support/build.sh:5594-5934
/CalendarServer/branches/new-store-no-caldavfile/support/build.sh:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2/support/build.sh:5936-5981
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/support/build.sh:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/support/build.sh:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/support/build.sh:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/support/build.sh:4465-4957
/CalendarServer/branches/users/cdaboo/pods/support/build.sh:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar/support/build.sh:7085-7206
/CalendarServer/branches/users/cdaboo/pycard/support/build.sh:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/support/build.sh:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/support/build.sh:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/support/build.sh:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/support/build.sh:4971-5080
/CalendarServer/branches/users/glyph/dalify/support/build.sh:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect/support/build.sh:6824-6876
/CalendarServer/branches/users/glyph/dont-start-postgres/support/build.sh:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/support/build.sh:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes/support/build.sh:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6/support/build.sh:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7/support/build.sh:6369-6445
/CalendarServer/branches/users/glyph/oracle-nulls/support/build.sh:7340-7351
/CalendarServer/branches/users/glyph/sendfdport/support/build.sh:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/support/build.sh:6490-6550
/CalendarServer/branches/users/glyph/sql-store/support/build.sh:5929-6073
/CalendarServer/branches/users/glyph/subtransactions/support/build.sh:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted/support/build.sh:5084-5149
/CalendarServer/branches/users/sagen/inboxitems/support/build.sh:7380-7381
/CalendarServer/branches/users/sagen/locations-resources/support/build.sh:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2/support/build.sh:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events/support/build.sh:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/support/build.sh:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/support/build.sh:4068-4075
/CalendarServer/branches/users/sagen/resources-2/support/build.sh:5084-5093
/CalendarServer/branches/users/wsanchez/transations/support/build.sh:5515-5593
   + /CalendarServer/branches/config-separation/support/build.sh:4379-4443
/CalendarServer/branches/egg-info-351/support/build.sh:4589-4615
/CalendarServer/branches/generic-sqlstore/support/build.sh:6167-6191
/CalendarServer/branches/new-store-no-caldavfile-2/support/build.sh:5936-5981
/CalendarServer/branches/new-store-no-caldavfile/support/build.sh:5911-5935
/CalendarServer/branches/new-store/support/build.sh:5594-5934
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/support/build.sh:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/support/build.sh:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/support/build.sh:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/support/build.sh:4465-4957
/CalendarServer/branches/users/cdaboo/pods/support/build.sh:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar/support/build.sh:7085-7206
/CalendarServer/branches/users/cdaboo/pycard/support/build.sh:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/support/build.sh:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/support/build.sh:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/support/build.sh:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/support/build.sh:4971-5080
/CalendarServer/branches/users/glyph/dalify/support/build.sh:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect/support/build.sh:6824-6876
/CalendarServer/branches/users/glyph/deploybuild/support/build.sh:7563-7572
/CalendarServer/branches/users/glyph/dont-start-postgres/support/build.sh:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/support/build.sh:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes/support/build.sh:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6/support/build.sh:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7/support/build.sh:6369-6445
/CalendarServer/branches/users/glyph/new-export/support/build.sh:7444-7485
/CalendarServer/branches/users/glyph/oracle-nulls/support/build.sh:7340-7351
/CalendarServer/branches/users/glyph/sendfdport/support/build.sh:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/support/build.sh:6490-6550
/CalendarServer/branches/users/glyph/sql-store/support/build.sh:5929-6073
/CalendarServer/branches/users/glyph/subtransactions/support/build.sh:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted/support/build.sh:5084-5149
/CalendarServer/branches/users/sagen/inboxitems/support/build.sh:7380-7381
/CalendarServer/branches/users/sagen/locations-resources-2/support/build.sh:5052-5061
/CalendarServer/branches/users/sagen/locations-resources/support/build.sh:5032-5051
/CalendarServer/branches/users/sagen/purge_old_events/support/build.sh:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/support/build.sh:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/support/build.sh:4068-4075
/CalendarServer/branches/users/sagen/resources-2/support/build.sh:5084-5093
/CalendarServer/branches/users/wsanchez/transations/support/build.sh:5515-5593
/CalendarServer/trunk/support/build.sh:7443-7695

Modified: CalendarServer/branches/users/cdaboo/timezones/support/py.sh
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/support/py.sh	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/support/py.sh	2011-06-30 20:44:13 UTC (rev 7697)
@@ -32,15 +32,16 @@
 
   if [ -z "${python}" ]; then
     return 1;
-  fi
+  fi;
+
   if ! type "${python}" > /dev/null 2>&1; then
     return 1;
-  fi
+  fi;
 
   local py_version="$(py_version "${python}")";
   if [ "${py_version/./}" -lt "25" ]; then
     return 1;
-  fi
+  fi;
   return 0;
 }
 
@@ -67,9 +68,9 @@
       if try_python "${p}"; then
         echo "${p}";
         return 0;
-      fi
-    done
-  done
+      fi;
+    done;
+  done;
   return 1;
 }
 
@@ -192,3 +193,4 @@
 }
 
 init_py;
+

Modified: CalendarServer/branches/users/cdaboo/timezones/test
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/test	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/test	2011-06-30 20:44:13 UTC (rev 7697)
@@ -71,12 +71,23 @@
 trial="$(type -p trial)";
 
 if [ $# -gt 0 ]; then
-    test_modules="$@";
+  test_modules="$@";
 else
-    test_modules="calendarserver twistedcaldav twext txdav ${m_twisted}";
+  test_modules="calendarserver twistedcaldav twext txdav contrib ${m_twisted}";
 fi;
 
 find "${wd}" -name \*.pyc -print0 | xargs -0 rm;
 
 mkdir -p "${wd}/data";
 cd "${wd}" && "${python}" "${trial}" --temp-directory="${wd}/data/trial" --rterrors ${random} ${until_fail} ${no_colour} ${coverage} ${test_modules};
+
+tmp="$(mktemp "/tmp/calendarserver_test.XXXXX")";
+
+echo "";
+echo "Running pyflakes...";
+cd "${wd}" && ./pyflakes "${test_modules}" | tee "${tmp}" 2>&1;
+if [ -s "${tmp}" ]; then
+  echo "**** Pyflakes says you have some code to clean up. ****";
+  exit 1;
+fi;
+rm -f "${tmp}";


Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/enterprise
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/enterprise/dal
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/twext/enterprise/dal/syntax.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twext/enterprise/dal/syntax.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twext/enterprise/dal/syntax.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -298,7 +298,18 @@
         return CompoundComparison(self, 'in', subselect)
 
 
+    def StartsWith(self, other):
+        return CompoundComparison(self, "like", CompoundComparison(Constant(other), '||', Constant('%')))
 
+
+    def EndsWith(self, other):
+        return CompoundComparison(self, "like", CompoundComparison(Constant('%'), '||', Constant(other)))
+
+
+    def Contains(self, other):
+        return CompoundComparison(self, "like", CompoundComparison(Constant('%'), '||', CompoundComparison(Constant(other), '||', Constant('%'))))
+
+
 class FunctionInvocation(ExpressionSyntax):
     def __init__(self, function, *args):
         self.function = function
@@ -377,6 +388,7 @@
 
 
 
+Count = Function("count")
 Max = Function("max")
 Len = Function("character_length", "length")
 
@@ -1093,6 +1105,9 @@
     """
 
     def __init__(self, From, Where, Return=None):
+        """
+        If Where is None then all rows will be deleted.
+        """
         self.From = From
         self.Where = Where
         self.Return = Return
@@ -1103,8 +1118,9 @@
         allTables = self.From.tables()
         result.text += 'delete from '
         result.append(self.From.subSQL(metadata, allTables))
-        result.text += ' where '
-        result.append(self.Where.subSQL(metadata, allTables))
+        if self.Where is not None:
+            result.text += ' where '
+            result.append(self.Where.subSQL(metadata, allTables))
         return self._returningClause(metadata, result, allTables)
 
 

Modified: CalendarServer/branches/users/cdaboo/timezones/twext/enterprise/dal/test/test_sqlsyntax.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twext/enterprise/dal/test/test_sqlsyntax.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twext/enterprise/dal/test/test_sqlsyntax.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -461,6 +461,63 @@
                 "select character_length(MYTEXT) from TEXTUAL"))
 
 
+    def test_startswith(self):
+        """
+        Test for the string starts with comparison.
+        (Note that this should be updated to use different techniques
+        as necessary in different databases.)
+        """
+        self.assertEquals(
+            Select([
+                self.schema.TEXTUAL.MYTEXT],
+                From=self.schema.TEXTUAL,
+                Where=self.schema.TEXTUAL.MYTEXT.StartsWith("test"),
+            ).toSQL(),
+            SQLFragment(
+                "select MYTEXT from TEXTUAL where MYTEXT like (? || ?)",
+                ["test", "%"]
+            )
+        )
+
+
+    def test_endswith(self):
+        """
+        Test for the string starts with comparison.
+        (Note that this should be updated to use different techniques
+        as necessary in different databases.)
+        """
+        self.assertEquals(
+            Select([
+                self.schema.TEXTUAL.MYTEXT],
+                From=self.schema.TEXTUAL,
+                Where=self.schema.TEXTUAL.MYTEXT.EndsWith("test"),
+            ).toSQL(),
+            SQLFragment(
+                "select MYTEXT from TEXTUAL where MYTEXT like (? || ?)",
+                ["%", "test"]
+            )
+        )
+
+
+    def test_contains(self):
+        """
+        Test for the string starts with comparison.
+        (Note that this should be updated to use different techniques
+        as necessary in different databases.)
+        """
+        self.assertEquals(
+            Select([
+                self.schema.TEXTUAL.MYTEXT],
+                From=self.schema.TEXTUAL,
+                Where=self.schema.TEXTUAL.MYTEXT.Contains("test"),
+            ).toSQL(),
+            SQLFragment(
+                "select MYTEXT from TEXTUAL where MYTEXT like (? || (? || ?))",
+                ["%", "test", "%"]
+            )
+        )
+
+
     def test_insert(self):
         """
         L{Insert.toSQL} generates an 'insert' statement with all the relevant
@@ -560,7 +617,7 @@
         enough, as the code needs to actually retrieve the values from the out
         parameters.
         """
-        conn, pool, factory = self.simulateOracleConnection()
+        conn, _ignore_pool, factory = self.simulateOracleConnection()
         i = Insert({self.schema.FOO.BAR: 40,
                     self.schema.FOO.BAZ: 50},
                    Return=(self.schema.FOO.BAR, self.schema.FOO.BAZ))
@@ -706,7 +763,13 @@
                 "delete from FOO where BAR = ?", [12])
         )
 
+        self.assertEquals(
+            Delete(self.schema.FOO,
+                   Where=None).toSQL(),
+            SQLFragment("delete from FOO")
+        )
 
+
     def test_lock(self):
         """
         L{Lock.exclusive} generates a ('lock table') statement, locking the
@@ -895,7 +958,7 @@
         other statement types as well, specifically those with 'returning'
         clauses.
         """
-        conn, pool, factory = self.simulateOracleConnection()
+        conn, _ignore_pool, factory = self.simulateOracleConnection()
         # Add 2 cursor variable values so that these will be used by
         # FakeVariable.getvalue.
         factory.varvals.extend([None, None])


Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/internet
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/protocols
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/python
___________________________________________________________________
Added: svn:ignore
   + *.so
*.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/web2
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/web2/auth
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/web2/channel
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/web2/client
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/web2/dav
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/web2/dav/element
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/web2/dav/method
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twext/web2/filter
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twisted/plugins
___________________________________________________________________
Modified: svn:ignore
   - dropin.cache

   + dropin.cache
*.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/caldavxml.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/caldavxml.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -57,6 +57,10 @@
     "inbox-availability",
 )
 
+caldav_query_extended_compliance = (
+    "calendar-query-extended",
+)
+
 class CalDAVElement (davxml.WebDAVElement):
     """
     CalDAV XML element.
@@ -387,16 +391,6 @@
                 # optimize them originals away
                 self.children = (data,)
 
-                # Verify that we have valid calendar data, but don't call
-                # validateForCalDAV() on the result, since some responses may
-                # require a calendar-data element with iCalendar data not meant
-                # for use as a CalDAV resource.
-                #try:
-                #    self.calendar()
-                #except ValueError, e:
-                #    log.err("Invalid iCalendar data (%s): %r" % (e, data))
-                #    raise
-
         if "content-type" in attributes:
             self.content_type = attributes["content-type"]
         else:
@@ -637,7 +631,10 @@
         (caldav_namespace, "comp-filter"    ): (0, None),
         (caldav_namespace, "prop-filter"    ): (0, None),
     }
-    allowed_attributes = { "name": True }
+    allowed_attributes = {
+        "name": True,
+        "test": False,
+    }
 
 class PropertyFilter (CalDAVElement):
     """
@@ -652,7 +649,10 @@
         (caldav_namespace, "text-match"     ): (0, 1),
         (caldav_namespace, "param-filter"   ): (0, None),
     }
-    allowed_attributes = { "name": True }
+    allowed_attributes = {
+        "name": True,
+        "test": False,
+    }
 
 class ParameterFilter (CalDAVElement):
     """
@@ -698,7 +698,8 @@
 
     allowed_attributes = {
         "caseless": False,
-        "negate-condition": False
+        "negate-condition": False,
+        "match-type": False,
     }
 
 class TimeZone (CalDAVTimeZoneElement):


Property changes on: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/client
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/datafilters
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/datafilters/test/test_peruserdata.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/datafilters/test/test_peruserdata.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/datafilters/test/test_peruserdata.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -18,11 +18,7 @@
 from twistedcaldav.ical import Component
 from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 
-class PerUserDataFilterTestNotRecurring (twistedcaldav.test.util.TestCase):
-
-    def test_public_noperuser(self):
-        
-        data = """BEGIN:VCALENDAR
+dataForTwoUsers = """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
@@ -31,19 +27,38 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test01
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test02
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
 END:VCALENDAR
 """.replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), data)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), data)
 
-    def test_public_oneuser(self):
-        
-        data = """BEGIN:VCALENDAR
+
+resultForUser1 = """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
@@ -52,23 +67,20 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
 BEGIN:VALARM
 ACTION:DISPLAY
-DESCRIPTION:Test
+DESCRIPTION:Test01
 TRIGGER;RELATED=START:-PT10M
 END:VALARM
-TRANSP:OPAQUE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
+END:VEVENT
 END:VCALENDAR
 """.replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
+
+
+resultForUser2 = """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
@@ -77,17 +89,20 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
+TRANSP:TRANSPARENT
 BEGIN:VALARM
 ACTION:DISPLAY
-DESCRIPTION:Test
+DESCRIPTION:Test02
 TRIGGER;RELATED=START:-PT10M
 END:VALARM
 END:VEVENT
 END:VCALENDAR
 """.replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
+
+
+resultForOtherUser = """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
@@ -96,19 +111,39 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
 """.replace("\n", "\r\n")
+
+
+
+class PerUserDataFilterTestNotRecurring (twistedcaldav.test.util.TestCase):
+
+    def test_public_noperuser(self):
         
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
         for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), data)
         for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), data)
 
-    def test_public_twousers(self):
+    def test_public_oneuser(self):
         
         data = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -119,6 +154,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -127,24 +163,12 @@
 BEGIN:X-CALENDARSERVER-PERINSTANCE
 BEGIN:VALARM
 ACTION:DISPLAY
-DESCRIPTION:Test01
+DESCRIPTION:Test
 TRIGGER;RELATED=START:-PT10M
 END:VALARM
 TRANSP:OPAQUE
 END:X-CALENDARSERVER-PERINSTANCE
 END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test02
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:TRANSPARENT
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
 END:VCALENDAR
 """.replace("\n", "\r\n")
         result01 = """BEGIN:VCALENDAR
@@ -156,11 +180,12 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
 ACTION:DISPLAY
-DESCRIPTION:Test01
+DESCRIPTION:Test
 TRIGGER;RELATED=START:-PT10M
 END:VALARM
 END:VEVENT
@@ -175,39 +200,41 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test02
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
 END:VEVENT
 END:VCALENDAR
 """.replace("\n", "\r\n")
-        result03 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-
+        
         for item in (data, Component.fromString(data),):
             self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
         for item in (data, Component.fromString(data),):
             self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
         for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user03").filter(item)), result03)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), result03)
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
 
+
+    def test_public_twousers(self):
+        """
+        A component with data for 2 users can return results for either of the
+        two users, or for a third user who has no per-user data embedded in it.
+        """
+
+        for item in (dataForTwoUsers, Component.fromString(dataForTwoUsers),):
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)),
+                             resultForUser1)
+        for item in (dataForTwoUsers, Component.fromString(dataForTwoUsers),):
+            self.assertEqual(str(PerUserDataFilter("user02").filter(item)),
+                             resultForUser2)
+        for item in (dataForTwoUsers, Component.fromString(dataForTwoUsers),):
+            self.assertEqual(str(PerUserDataFilter("user03").filter(item)),
+                             resultForOtherUser)
+        for item in (dataForTwoUsers, Component.fromString(dataForTwoUsers),):
+            self.assertEqual(str(PerUserDataFilter("").filter(item)),
+                             resultForOtherUser)
+
+
+
 class PerUserDataFilterTestRecurring (twistedcaldav.test.util.TestCase):
 
     def test_public_noperuser(self):
@@ -221,6 +248,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -231,6 +259,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -252,6 +281,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -262,6 +292,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -287,6 +318,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -303,6 +335,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -322,6 +355,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -332,6 +366,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -355,6 +390,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -365,6 +401,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -399,6 +436,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -415,6 +453,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:TRANSPARENT
 BEGIN:VALARM
@@ -434,6 +473,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -444,6 +484,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -467,6 +508,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -477,6 +519,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -503,6 +546,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -513,6 +557,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:TRANSPARENT
 BEGIN:VALARM
@@ -532,6 +577,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -542,6 +588,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -565,6 +612,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -600,6 +648,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -616,6 +665,7 @@
 DTEND:20080602T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:TRANSPARENT
 BEGIN:VALARM
@@ -635,6 +685,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -659,6 +710,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -715,6 +767,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -731,6 +784,7 @@
 DTEND:20080602T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:TRANSPARENT
 BEGIN:VALARM
@@ -750,6 +804,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -766,6 +821,7 @@
 DTEND:20080603T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:TRANSPARENT
 BEGIN:VALARM
@@ -785,6 +841,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -810,6 +867,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -837,6 +895,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:TRANSPARENT
 BEGIN:VALARM
@@ -857,6 +916,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -882,6 +942,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -895,6 +956,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -922,6 +984,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -941,6 +1004,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -966,6 +1030,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -987,6 +1052,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1018,6 +1084,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1028,6 +1095,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1041,6 +1109,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1051,6 +1120,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1078,6 +1148,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -1094,6 +1165,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -1113,6 +1185,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1123,6 +1196,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1148,6 +1222,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1158,6 +1233,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1179,6 +1255,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -1195,6 +1272,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:TRANSPARENT
 BEGIN:VALARM
@@ -1214,6 +1292,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1224,6 +1303,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1258,6 +1338,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1268,6 +1349,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1289,6 +1371,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1299,6 +1382,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:TRANSPARENT
 BEGIN:VALARM
@@ -1318,6 +1402,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1328,6 +1413,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1356,6 +1442,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1366,6 +1453,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1387,6 +1475,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -1403,6 +1492,7 @@
 DTEND:20080602T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:TRANSPARENT
 BEGIN:VALARM
@@ -1422,6 +1512,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1457,6 +1548,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1467,6 +1559,7 @@
 DTEND:20080602T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1488,6 +1581,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -1504,6 +1598,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:TRANSPARENT
 BEGIN:VALARM
@@ -1523,6 +1618,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1533,6 +1629,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1567,6 +1664,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1577,6 +1675,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1600,6 +1699,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1613,6 +1713,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1632,6 +1733,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1652,6 +1754,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -1671,6 +1774,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1696,6 +1800,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1728,6 +1833,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -1747,6 +1853,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1784,6 +1891,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1828,6 +1936,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1841,6 +1950,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1878,6 +1988,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -1918,6 +2029,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1932,6 +2044,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1952,6 +2065,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1972,6 +2086,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -1982,6 +2097,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1995,6 +2111,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -2005,6 +2122,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2024,6 +2142,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -2045,6 +2164,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -2059,6 +2179,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2079,6 +2200,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -2099,6 +2221,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -2119,6 +2242,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2144,6 +2268,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -2177,6 +2302,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -2193,6 +2319,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -2212,6 +2339,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2237,6 +2365,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -2247,6 +2376,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2289,6 +2419,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -2309,6 +2440,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2335,6 +2467,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -2369,6 +2502,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -2389,6 +2523,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2426,6 +2561,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -2471,6 +2607,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -2487,6 +2624,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -2506,6 +2644,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2543,6 +2682,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -2553,6 +2693,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2607,6 +2748,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -2626,6 +2768,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2664,6 +2807,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2697,6 +2841,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -2711,6 +2856,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2748,6 +2894,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -2787,6 +2934,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -2797,6 +2945,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -2810,6 +2959,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2847,6 +2997,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -2857,6 +3008,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2897,6 +3049,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -2910,6 +3063,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2948,6 +3102,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -2977,6 +3132,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -2991,6 +3147,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3011,6 +3168,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3032,6 +3190,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3046,6 +3205,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3066,6 +3226,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3076,6 +3237,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -3096,6 +3258,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -3109,6 +3272,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3129,6 +3293,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -3149,6 +3314,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -3168,6 +3334,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3194,6 +3361,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3226,6 +3394,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -3245,6 +3414,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3255,6 +3425,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3280,6 +3451,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3312,6 +3484,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -3332,6 +3505,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3358,6 +3532,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3390,6 +3565,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -3409,6 +3585,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3447,6 +3624,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3491,6 +3669,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -3510,6 +3689,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3520,6 +3700,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3575,6 +3756,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3619,6 +3801,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -3639,6 +3822,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3678,6 +3862,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3710,6 +3895,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3724,6 +3910,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3761,6 +3948,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3800,6 +3988,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3810,6 +3999,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -3823,6 +4013,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3860,6 +4051,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -3870,6 +4062,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3909,6 +4102,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -3923,6 +4117,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3962,6 +4157,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -3990,6 +4186,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4004,6 +4201,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4024,6 +4222,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4045,6 +4244,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -4065,6 +4265,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4091,6 +4292,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4124,6 +4326,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -4144,6 +4347,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4182,6 +4386,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4227,6 +4432,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4241,6 +4447,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4279,6 +4486,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4318,6 +4526,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4332,6 +4541,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4379,6 +4589,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4420,6 +4631,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4430,6 +4642,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -4443,6 +4656,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4453,6 +4667,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -4472,6 +4687,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4482,6 +4698,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -4502,6 +4719,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -4518,6 +4736,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -4537,6 +4756,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4547,6 +4767,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -4581,6 +4802,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4591,6 +4813,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -4632,6 +4855,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 TRANSP:OPAQUE
@@ -4648,6 +4872,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -4667,6 +4892,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4677,6 +4903,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -4732,6 +4959,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4742,6 +4970,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -4795,6 +5024,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4805,6 +5035,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -4818,6 +5049,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4828,6 +5060,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -4883,6 +5116,7 @@
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -4893,6 +5127,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -4934,6 +5169,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -4948,6 +5184,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -4969,6 +5206,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -4990,6 +5228,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -5010,6 +5249,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -5037,6 +5277,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -5071,6 +5312,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 TRANSP:OPAQUE
 BEGIN:VALARM
@@ -5091,6 +5333,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -5131,6 +5374,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -5165,6 +5409,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -5179,6 +5424,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -5219,6 +5465,7 @@
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
@@ -5248,6 +5495,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY;COUNT=5
 TRANSP:OPAQUE
@@ -5268,6 +5516,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY;COUNT=10
 END:VEVENT
@@ -5324,6 +5573,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY;COUNT=5
 END:VEVENT
@@ -5378,6 +5628,7 @@
 DTEND:20080602T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY;COUNT=10
 TRANSP:OPAQUE
@@ -5398,6 +5649,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY;COUNT=10
 END:VEVENT
@@ -5445,6 +5697,7 @@
 DTEND:20080602T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY;COUNT=10
 END:VEVENT
@@ -5499,6 +5752,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY;COUNT=10
 TRANSP:OPAQUE
@@ -5519,6 +5773,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RDATE:20080602T150000Z
 RRULE:FREQ=DAILY;COUNT=10
@@ -5567,6 +5822,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY;COUNT=10
 END:VEVENT
@@ -5613,6 +5869,7 @@
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
 EXDATE:20080602T110000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY;COUNT=10
 TRANSP:OPAQUE
@@ -5633,6 +5890,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY;COUNT=10
 END:VEVENT
@@ -5680,6 +5938,7 @@
 DTEND:20080601T120000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 EXDATE:20080602T110000Z
 ORGANIZER;CN=User 01:mailto:user1 at example.com
 RRULE:FREQ=DAILY;COUNT=10

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/dateops.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/dateops.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/dateops.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -29,7 +29,6 @@
     "compareDateTime",
     "differenceDateTime",
     "timeRangesOverlap",
-    "periodEnd",
     "normalizePeriodList",
     "clipPeriod"
 ]


Property changes on: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/appleopendirectory.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/appleopendirectory.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -33,7 +33,6 @@
 from twext.web2.auth.digest import DigestedCredentials
 
 from twistedcaldav.config import config
-from twistedcaldav.directory import augment
 from twistedcaldav.directory.cachingdirectory import CachingDirectoryService,\
     CachingDirectoryRecord
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
@@ -77,6 +76,7 @@
                 self.recordType_users,
                 self.recordType_groups,
             ),
+            'augmentService' : None,
         }
         ignored = ('requireComputerRecord',)
         params = self.getParams(params, defaults, ignored)
@@ -94,6 +94,7 @@
             self.log_error("OpenDirectory (node=%s) Initialization error: %s" % (params['node'], e))
             raise
 
+        self.augmentService = params['augmentService']
         self.realmName = params['node']
         self.directory = directory
         self.node = params['node']
@@ -239,6 +240,7 @@
 
         guids = set()
 
+        self.log_info("Looking up which groups %s is a member of" % (guid,))
         try:
             self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
                 self.directory,
@@ -299,6 +301,8 @@
             if recordGUID:
                 guids.add(recordGUID)
 
+        self.log_info("%s is a member of %d groups" % (guid, len(guids)))
+
         return guids
 
     def proxiesForGUID(self, recordType, guid):
@@ -470,7 +474,7 @@
                     # TODO: this needs to be deferred but for now we hard code
                     # the deferred result because we know it is completing
                     # immediately.
-                    d = augment.AugmentService.getAugmentRecord(record.guid,
+                    d = self.augmentService.getAugmentRecord(record.guid,
                         recordType)
                     d.addCallback(lambda x:record.addAugmentInformation(x))
 
@@ -757,7 +761,7 @@
             # Look up augment information
             # TODO: this needs to be deferred but for now we hard code the deferred result because
             # we know it is completing immediately.
-            d = augment.AugmentService.getAugmentRecord(record.guid,
+            d = self.augmentService.getAugmentRecord(record.guid,
                 recordType)
             d.addCallback(lambda x:record.addAugmentInformation(x))
 

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/augment.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/augment.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/augment.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -49,6 +49,7 @@
         enabledForCalendaring=False,
         autoSchedule=False,
         enabledForAddressBooks=False,
+        enabledForLogin=True,
     ):
         self.uid = uid
         self.enabled = enabled
@@ -56,6 +57,7 @@
         self.partitionID = partitionID
         self.enabledForCalendaring = enabledForCalendaring
         self.enabledForAddressBooks = enabledForAddressBooks
+        self.enabledForLogin = enabledForLogin
         self.autoSchedule = autoSchedule
         self.clonedFromDefault = False
 
@@ -114,6 +116,7 @@
             enabled=True,
             enabledForCalendaring=True,
             enabledForAddressBooks=True,
+            enabledForLogin=True,
         )
         self.cachedRecords["Default"] = result
         result = copy.deepcopy(result)
@@ -201,8 +204,6 @@
         """
 
         raise NotImplementedError("Child class must define this.")
-        
-AugmentService = AugmentDB()   # Global augment service
 
 
 class AugmentXMLDB(AugmentDB):
@@ -414,6 +415,7 @@
             addSubElement(recordNode, xmlaugmentsparser.ELEMENT_PARTITIONID, record.partitionID)
         addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLECALENDAR, "true" if record.enabledForCalendaring else "false")
         addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLEADDRESSBOOK, "true" if record.enabledForAddressBooks else "false")
+        addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLELOGIN, "true" if record.enabledForLogin else "false")
         addSubElement(recordNode, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE, "true" if record.autoSchedule else "false")
 
     def refresh(self):
@@ -473,6 +475,7 @@
                 enabled=True,
                 enabledForCalendaring=True,
                 enabledForAddressBooks=True,
+                enabledForLogin=True,
             )
 
         # Compare previously seen modification time and size of each
@@ -525,11 +528,11 @@
         """
         
         # Query for the record information
-        results = (yield self.query("select UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE from AUGMENTS where UID = :1", (uid,)))
+        results = (yield self.query("select UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, LOGINENABLED from AUGMENTS where UID = :1", (uid,)))
         if not results:
             returnValue(None)
         else:
-            uid, enabled, serverid, partitionid, enabledForCalendaring, enabledForAddressBooks, autoSchedule = results[0]
+            uid, enabled, serverid, partitionid, enabledForCalendaring, enabledForAddressBooks, autoSchedule, enabledForLogin = results[0]
             
             record = AugmentRecord(
                 uid = uid,
@@ -538,6 +541,7 @@
                 partitionID = partitionid,
                 enabledForCalendaring = enabledForCalendaring == "T",
                 enabledForAddressBooks = enabledForAddressBooks == "T",
+                enabledForLogin = enabledForLogin == "T",
                 autoSchedule = autoSchedule == "T",
             )
             
@@ -600,6 +604,7 @@
                 ("CALENDARING",  "text(1)"),
                 ("ADDRESSBOOKS", "text(1)"),
                 ("AUTOSCHEDULE", "text(1)"),
+                ("LOGINENABLED", "text(1)"),
             ),
             ifnotexists=True,
         )
@@ -622,8 +627,8 @@
     def _addRecord(self, record):
         yield self.execute(
             """insert or replace into AUGMENTS
-            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE)
-            values (:1, :2, :3, :4, :5, :6, :7)""",
+            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, LOGINENABLED)
+            values (:1, :2, :3, :4, :5, :6, :7, :8)""",
             (
                 record.uid,
                 "T" if record.enabled else "F",
@@ -632,6 +637,7 @@
                 "T" if record.enabledForCalendaring else "F",
                 "T" if record.enabledForAddressBooks else "F",
                 "T" if record.autoSchedule else "F",
+                "T" if record.enabledForLogin else "F",
             )
         )
 
@@ -652,8 +658,8 @@
     def _addRecord(self, record):
         yield self.execute(
             """insert into AUGMENTS
-            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE)
-            values (:1, :2, :3, :4, :5, :6, :7)""",
+            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, LOGINENABLED)
+            values (:1, :2, :3, :4, :5, :6, :7, :8)""",
             (
                 record.uid,
                 "T" if record.enabled else "F",
@@ -662,6 +668,7 @@
                 "T" if record.enabledForCalendaring else "F",
                 "T" if record.enabledForAddressBooks else "F",
                 "T" if record.autoSchedule else "F",
+                "T" if record.enabledForLogin else "F",
             )
         )
 
@@ -669,8 +676,8 @@
     def _modifyRecord(self, record):
         yield self.execute(
             """update AUGMENTS set
-            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE) =
-            (:1, :2, :3, :4, :5, :6, :7) where UID = :8""",
+            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, LOGINENABLED) =
+            (:1, :2, :3, :4, :5, :6, :7 :8) where UID = :9""",
             (
                 record.uid,
                 "T" if record.enabled else "F",
@@ -679,6 +686,7 @@
                 "T" if record.enabledForCalendaring else "F",
                 "T" if record.enabledForAddressBooks else "F",
                 "T" if record.autoSchedule else "F",
+                "T" if record.enabledForLogin else "F",
                 record.uid,
             )
         )

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/calendaruserproxy.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/calendaruserproxy.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -596,12 +596,12 @@
 
             # Cache miss; compute members and update cache
             def gotMembersFromDB(dbmembers):
-                members = set([row[0] for row in dbmembers])
+                members = set([row[0].encode("utf-8") for row in dbmembers])
                 d = self._memcacher.setMembers(principalUID, members)
                 d.addCallback(lambda _: members)
                 return d
 
-            d =  self.query("select MEMBER from GROUPS where GROUPNAME = :1", (principalUID,))
+            d =  self.query("select MEMBER from GROUPS where GROUPNAME = :1", (principalUID.decode("utf-8"),))
             d.addCallback(gotMembersFromDB)
             return d
 
@@ -621,12 +621,12 @@
 
             # Cache miss; compute memberships and update cache
             def gotMembershipsFromDB(dbmemberships):
-                memberships = set([row[0] for row in dbmemberships])
+                memberships = set([row[0].encode("utf-8") for row in dbmemberships])
                 d = self._memcacher.setMemberships(principalUID, memberships)
                 d.addCallback(lambda _: memberships)
                 return d
 
-            d =  self.query("select GROUPNAME from GROUPS where MEMBER = :1", (principalUID,))
+            d =  self.query("select GROUPNAME from GROUPS where MEMBER = :1", (principalUID.decode("utf-8"),))
             d.addCallback(gotMembershipsFromDB)
             return d
 
@@ -647,7 +647,7 @@
                 """
                 insert into GROUPS (GROUPNAME, MEMBER)
                 values (:1, :2)
-                """, (principalUID, member,)
+                """, (principalUID.decode("utf-8"), member,)
             )
 
     def _add_to_db_one(self, principalUID, memberUID):
@@ -661,7 +661,7 @@
             """
             insert into GROUPS (GROUPNAME, MEMBER)
             values (:1, :2)
-            """, (principalUID, memberUID,)
+            """, (principalUID.decode("utf-8"), memberUID.decode("utf-8"),)
         )
 
     def _delete_from_db(self, principalUID):
@@ -670,7 +670,7 @@
 
         @param principalUID: the UID of the group principal to remove.
         """
-        return self.execute("delete from GROUPS where GROUPNAME = :1", (principalUID,))
+        return self.execute("delete from GROUPS where GROUPNAME = :1", (principalUID.decode("utf-8"),))
 
     def _delete_from_db_one(self, principalUID, memberUID):
         """
@@ -679,7 +679,7 @@
         @param principalUID: the UID of the group principal to remove.
         @param memberUID: the UID of the principal that is being removed as a member of this group.
         """
-        return self.execute("delete from GROUPS where GROUPNAME = :1 and MEMBER = :2", (principalUID, memberUID,))
+        return self.execute("delete from GROUPS where GROUPNAME = :1 and MEMBER = :2", (principalUID.decode("utf-8"), memberUID.decode("utf-8"),))
 
     def _delete_from_db_member(self, principalUID):
         """
@@ -687,7 +687,7 @@
 
         @param principalUID: the UID of the member principal to remove.
         """
-        return self.execute("delete from GROUPS where MEMBER = :1", (principalUID,))
+        return self.execute("delete from GROUPS where MEMBER = :1", (principalUID.decode("utf-8"),))
 
     def _db_version(self):
         """

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/directory.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/directory.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -115,6 +115,11 @@
         if credentials.authnPrincipal is None:
             raise UnauthorizedLogin("No such user: %s" % (credentials.credentials.username,))
 
+        # See if record is enabledForLogin
+        if not credentials.authnPrincipal.record.isLoginEnabled():
+            raise UnauthorizedLogin("User not allowed to log in: %s" %
+                (credentials.credentials.username,))
+
         # Handle Kerberos as a separate behavior
         try:
             from twistedcaldav.authkerb import NegotiateCredentials
@@ -358,6 +363,7 @@
         calendarUserAddresses=set(), autoSchedule=False, enabledForCalendaring=None,
         enabledForAddressBooks=None,
         uid=None,
+        enabledForLogin=True,
         **kwargs
     ):
         assert service.realmName is not None
@@ -389,6 +395,7 @@
         self.enabledForCalendaring  = enabledForCalendaring
         self.autoSchedule           = autoSchedule
         self.enabledForAddressBooks = enabledForAddressBooks
+        self.enabledForLogin        = enabledForLogin
         self.extras                 = kwargs
 
 
@@ -437,6 +444,7 @@
             self.enabledForCalendaring = augment.enabledForCalendaring
             self.enabledForAddressBooks = augment.enabledForAddressBooks
             self.autoSchedule = augment.autoSchedule
+            self.enabledForLogin = augment.enabledForLogin
 
             if (self.enabledForCalendaring or self.enabledForAddressBooks) and self.recordType == self.service.recordType_groups:
                 self.enabledForCalendaring = False
@@ -454,6 +462,7 @@
             self.partitionID = ""
             self.enabledForCalendaring = False
             self.enabledForAddressBooks = False
+            self.enabledForLogin = False
 
 
     def applySACLs(self):
@@ -472,6 +481,13 @@
                                % (username,))
                 self.enabledForAddressBooks = False
 
+    def isLoginEnabled(self):
+        """
+        Returns True if the user should be allowed to log in, based on the
+        enabledForLogin attribute, which is currently controlled by the
+        DirectoryService implementation.
+        """
+        return self.enabledForLogin
 
     def members(self):
         return ()

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/ldapdirectory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/ldapdirectory.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/ldapdirectory.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -52,8 +52,8 @@
 from twisted.cred.credentials import UsernamePassword
 from twistedcaldav.directory.cachingdirectory import (CachingDirectoryService,
     CachingDirectoryRecord)
-from twistedcaldav.directory import augment
 from twistedcaldav.directory.directory import DirectoryConfigurationError
+from twistedcaldav.directory.augment import AugmentRecord
 from twisted.internet.defer import succeed
 
 class LdapDirectoryService(CachingDirectoryService):
@@ -74,6 +74,7 @@
         """
 
         defaults = {
+            "augmentService" : None,
             "cacheTimeout": 1,
             "negativeCaching": False,
             "restrictEnabledRecords": False,
@@ -98,6 +99,8 @@
                     "emailSuffix": None, # used only to synthesize email address
                     "filter": None, # additional filter for this type
                     "recordName": "uid", # uniquely identifies user records
+                    "loginEnabledAttr" : "loginEnabled", # attribute controlling login
+                    "loginEnabledValue" : "yes", # value of above attribute
                 },
                 "groups": {
                     "rdn": "ou=Group",
@@ -141,6 +144,7 @@
         super(LdapDirectoryService, self).__init__(params["cacheTimeout"],
                                                    params["negativeCaching"])
 
+        self.augmentService = params["augmentService"]
         self.realmName = params["uri"]
         self.uri = params["uri"]
         self.tls = params["tls"]
@@ -172,6 +176,8 @@
             attrSet.add(self.groupSchema["nestedGroupsAttr"])
         if self.groupSchema["memberIdAttr"]:
             attrSet.add(self.groupSchema["memberIdAttr"])
+        if self.rdnSchema["users"]["loginEnabledAttr"]:
+            attrSet.add(self.rdnSchema["users"]["loginEnabledAttr"])
         self.attrList = list(attrSet)
 
         self.typeRDNs = {}
@@ -246,6 +252,7 @@
             self.authLDAP = self.createLDAPConnection()
         self.log_debug("Authenticating %s" % (dn,))
         self.authLDAP.bind_s(dn, password)
+        self.log_debug("Authentication succeeded for %s" % (dn,))
 
 
     @property
@@ -388,9 +395,10 @@
         firstName = None
         lastName = None
         emailAddresses = set()
-        calendarUserAddresses = set()
         enabledForCalendaring = None
+        enabledForAddressBooks = None
         uid = None
+        enabledForLogin = True
 
         # First check for and add guid
         guidAttr = self.rdnSchema["guidAttr"]
@@ -401,6 +409,7 @@
         emailAddresses = self._getMultipleLdapAttributes(attrs, "mail")
         emailSuffix = self.rdnSchema[recordType]["emailSuffix"]
 
+
         if len(emailAddresses) == 0 and emailSuffix is not None:
             emailPrefix = self._getUniqueLdapAttribute(attrs,
                 self.rdnSchema[recordType]["attr"])
@@ -413,16 +422,28 @@
                 "displayName", "gecos")
             firstName = self._getUniqueLdapAttribute(attrs, "givenName")
             lastName = self._getUniqueLdapAttribute(attrs, "sn", "surname")
-            calendarUserAddresses = emailAddresses
             enabledForCalendaring = True
+            enabledForAddressBooks = True
+
+            # Check login control attribute
+            loginEnabledAttr = self.rdnSchema[recordType]["loginEnabledAttr"]
+            if loginEnabledAttr:
+                loginEnabledValue = self.rdnSchema[recordType]["loginEnabledValue"]
+                enabledForLogin = self._getUniqueLdapAttribute(attrs,
+                    loginEnabledAttr) == loginEnabledValue
+
         elif recordType == self.recordType_groups:
             fullName = self._getUniqueLdapAttribute(attrs, "cn")
             enabledForCalendaring = False
+            enabledForAddressBooks = False
+            enabledForLogin = False
+
         elif recordType in (self.recordType_resources,
             self.recordType_locations):
             fullName = self._getUniqueLdapAttribute(attrs, "cn")
-            calendarUserAddresses = emailAddresses
             enabledForCalendaring = True
+            enabledForAddressBooks = False
+            enabledForLogin = False
 
         record = LdapDirectoryRecord(
             service                 = self,
@@ -434,19 +455,23 @@
             firstName               = firstName,
             lastName                = lastName,
             emailAddresses          = emailAddresses,
-            calendarUserAddresses   = calendarUserAddresses,
-            enabledForCalendaring   = enabledForCalendaring,
             uid                     = uid,
             dn                      = dn,
             attrs                   = attrs,
         )
 
-        # Look up augment information
-        # TODO: this needs to be deferred but for now we hard code the
-        # deferred result because we know it is completing immediately.
-        d = augment.AugmentService.getAugmentRecord(record.guid,
-            recordType)
-        d.addCallback(lambda x:record.addAugmentInformation(x))
+        # Generate an augment record based on information retrieved from LDAP
+        augmentRecord = AugmentRecord(
+            guid,
+            enabled=True,
+            serverID="", # TODO: add to LDAP?
+            partitionID="", # TODO: add to LDAP?
+            enabledForCalendaring=enabledForCalendaring,
+            autoSchedule=False, # TODO: add to LDAP?
+            enabledForAddressBooks=enabledForAddressBooks, # TODO: add to LDAP?
+            enabledForLogin=enabledForLogin,
+        )
+        record.addAugmentInformation(augmentRecord)
 
         return record
 
@@ -544,9 +569,9 @@
         Carries out the work of a principal-property-search against LDAP
         Returns a deferred list of directory records.
         """
-
         records = []
 
+        self.log_debug("Peforming principal property search for %s" % (fields,))
         recordTypes = [recordType] if recordType else self.recordTypes()
         for recordType in recordTypes:
             filter = buildFilter(self.attributeMapping, fields, operand=operand)
@@ -560,11 +585,13 @@
                     (ldap.dn.dn2str(base), filter))
                 results = self.ldap.search_s(ldap.dn.dn2str(base),
                     ldap.SCOPE_SUBTREE, filter, self.attrList)
+                self.log_debug("LDAP search returned %d results" % (len(results),))
 
                 for dn, attrs in results:
                     # Skip if group restriction is in place and guid is not
                     # a member
-                    if self.restrictedGUIDs is not None:
+                    if (recordType != self.recordType_groups and
+                        self.restrictedGUIDs is not None):
                         guidAttr = self.rdnSchema["guidAttr"]
                         if guidAttr:
                             guid = self._getUniqueLdapAttribute(attrs, guidAttr)
@@ -574,6 +601,7 @@
                     record = self._ldapResultToRecord(dn, attrs, recordType)
                     records.append(record)
 
+        self.log_debug("Principal property search matched %d records" % (len(records),))
         return succeed(records)
 
 
@@ -618,8 +646,7 @@
         self, service, recordType,
         guid, shortNames, authIDs, fullName,
         firstName, lastName, emailAddresses,
-        calendarUserAddresses, enabledForCalendaring, uid,
-        dn, attrs
+        uid, dn, attrs
     ):
         super(LdapDirectoryRecord, self).__init__(
             service               = service,
@@ -631,8 +658,6 @@
             firstName             = firstName,
             lastName              = lastName,
             emailAddresses        = emailAddresses,
-            calendarUserAddresses = calendarUserAddresses,
-            enabledForCalendaring = enabledForCalendaring,
             uid                   = uid,
         )
 
@@ -824,3 +849,4 @@
                 raise DirectoryConfigurationError(msg)
 
         return super(LdapDirectoryRecord, self).verifyCredentials(credentials)
+

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/principal.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/principal.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -52,7 +52,6 @@
 from twistedcaldav.config import config
 from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
 from twistedcaldav.directory import calendaruserproxy
-from twistedcaldav.directory import augment
 from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyPrincipalResource
 from twistedcaldav.directory.common import uidsResourceName
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
@@ -62,7 +61,6 @@
 from twistedcaldav.directory.idirectory import IDirectoryService
 from twistedcaldav import caldavxml, customxml
 from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.directory.wiki import getWikiACL
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
 
 log = Logger()
@@ -71,18 +69,9 @@
     def defaultAccessControlList(self):
         return authReadACL
 
-    @inlineCallbacks
     def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
 
-        wikiACL = (yield getWikiACL(self, request))
-        if wikiACL is not None:
-            # ACL depends on wiki server...
-            log.debug("Wiki ACL: %s" % (wikiACL.toxml(),))
-            returnValue(wikiACL)
-        else:
-            # ...otherwise permissions are fixed, and are not subject to
-            # inheritance rules, etc.
-            returnValue(self.defaultAccessControlList())
+        return succeed(self.defaultAccessControlList())
 
 
 
@@ -790,9 +779,9 @@
     @inlineCallbacks
     def setAutoSchedule(self, autoSchedule):
         self.record.autoSchedule = autoSchedule
-        augmentRecord = (yield augment.AugmentService.getAugmentRecord(self.record.guid, self.record.recordType))
+        augmentRecord = (yield self.record.service.augmentService.getAugmentRecord(self.record.guid, self.record.recordType))
         augmentRecord.autoSchedule = autoSchedule
-        (yield augment.AugmentService.addAugmentRecords([augmentRecord]))
+        (yield self.record.service.augmentService.addAugmentRecords([augmentRecord]))
 
     def getAutoSchedule(self):
         return self.record.autoSchedule
@@ -879,6 +868,29 @@
 
         return addresses
 
+    def canonicalCalendarUserAddress(self):
+        """
+        Return a CUA for this principal, preferring in this order:
+            urn:uuid: form
+            mailto: form
+            first in calendarUserAddresses( ) list
+        """
+
+        cua = ""
+        for candidate in self.calendarUserAddresses():
+            # Pick the first one, but urn:uuid: and mailto: can override
+            if not cua:
+                cua = candidate
+            # But always immediately choose the urn:uuid: form
+            if candidate.startswith("urn:uuid:"):
+                cua = candidate
+                break
+            # Prefer mailto: if no urn:uuid:
+            elif candidate.startswith("mailto:"):
+                cua = candidate
+        return cua
+
+
     def enabledAsOrganizer(self):
         if self.record.recordType == DirectoryService.recordType_users:
             return True

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/augments-test-default.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/augments-test-default.xml	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/augments-test-default.xml	2011-06-30 20:44:13 UTC (rev 7697)
@@ -116,4 +116,15 @@
     <enable-addressbook>true</enable-addressbook>
     <auto-schedule>true</auto-schedule>
   </record>
+  <record>
+    <uid>FC674703-8008-4A77-B80E-0DB55A9CE620</uid>
+    <enable-login>false</enable-login>
+  </record>
+  <record>
+    <uid>B473DC32-1B0D-45EE-9BAC-DA878AE9CE74</uid>
+    <enable-login>true</enable-login>
+  </record>
+  <record>
+    <uid>9F2B176D-B3F5-483A-AA63-0A1FC6E6D54B</uid>
+  </record>
 </augments>

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/augments-test.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/augments-test.xml	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/augments-test.xml	2011-06-30 20:44:13 UTC (rev 7697)
@@ -58,4 +58,15 @@
     <enable-addressbook>true</enable-addressbook>
     <auto-schedule>true</auto-schedule>
   </record>
+  <record>
+    <uid>FC674703-8008-4A77-B80E-0DB55A9CE620</uid>
+    <enable-login>false</enable-login>
+  </record>
+  <record>
+    <uid>B473DC32-1B0D-45EE-9BAC-DA878AE9CE74</uid>
+    <enable-login>true</enable-login>
+  </record>
+  <record>
+    <uid>9F2B176D-B3F5-483A-AA63-0A1FC6E6D54B</uid>
+  </record>
 </augments>

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_aggregate.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_aggregate.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_aggregate.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -62,8 +62,13 @@
         """
         Returns an IDirectoryService.
         """
-        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
-        xmlService = XMLDirectoryService({'xmlFile' : xmlFile})
+        xmlService = XMLDirectoryService(
+            {
+                'xmlFile' : xmlFile,
+                'augmentService' :
+                    augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
+            }
+        )
         xmlService.recordTypePrefix = xml_prefix
 
 

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_augment.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_augment.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_augment.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -35,6 +35,9 @@
     {"uid":"5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1", "enabled":True,  "partitionID":"00001", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
     {"uid":"543D28BA-F74F-4D5F-9243-B3E3A61171E5", "enabled":True,  "partitionID":"00002", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
     {"uid":"6A73326A-F781-47E7-A9F8-AF47364D4152", "enabled":True,  "partitionID":"00002", "enabledForCalendaring":True, "enabledForAddressBooks":True, "autoSchedule":True},
+    {"uid":"FC674703-8008-4A77-B80E-0DB55A9CE620", "enabledForLogin":False,}, # Explicitly false
+    {"uid":"B473DC32-1B0D-45EE-9BAC-DA878AE9CE74", "enabledForLogin":True,}, # Explicitly True
+    {"uid":"9F2B176D-B3F5-483A-AA63-0A1FC6E6D54B", "enabledForLogin":True,}, # Default is True
 )
 
 testRecordWildcardDefault = (

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_livedirectory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_livedirectory.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_livedirectory.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -67,9 +67,6 @@
     from twistedcaldav.directory.test.test_xmlfile import augmentsFile
     from twisted.internet.defer import inlineCallbacks
 
-    augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
-
-
     class LiveDirectoryTests(object):
 
         def test_ldapRecordWithShortName(self):
@@ -153,6 +150,8 @@
 
             def setUp(self):
                 params = {
+                    "augmentService":
+                        augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
                     "uri": "ldap://%s" % (testServer,),
                     "rdnSchema": {
                         "base": base,

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_opendirectory.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_opendirectory.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -55,9 +55,13 @@
 
         def setUp(self):
             super(OpenDirectory, self).setUp()
-            augment.AugmentService = augment.AugmentXMLDB(xmlFiles=())
             try:
-                self._service = OpenDirectoryService({"node" : "/Search"})
+                self._service = OpenDirectoryService(
+                    {
+                        "node" : "/Search",
+                        "augmentService": augment.AugmentXMLDB(xmlFiles=()),
+                    }
+                )
             except ImportError, e:
                 raise SkipTest("OpenDirectory module is not available: %s" % (e,))
 
@@ -451,10 +455,10 @@
 
         def setUp(self):
             super(OpenDirectorySubset, self).setUp()
-            augment.AugmentService = augment.AugmentXMLDB(xmlFiles=())
             self._service = OpenDirectoryService(
                 {
                     "node" : "/Search",
                     "recordTypes" : (DirectoryService.recordType_users, DirectoryService.recordType_groups),
+                    "augmentService" : augment.AugmentXMLDB(xmlFiles=()),
                 }
             )

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_principal.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_principal.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_principal.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -53,11 +53,12 @@
     def setUp(self):
         super(ProvisionedPrincipals, self).setUp()
 
-        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
         self.directoryServices = (
             XMLDirectoryService(
                 {
                     'xmlFile' : xmlFile,
+                    'augmentService' :
+                        augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
                 }
             ),
         )
@@ -430,6 +431,14 @@
                 record.enabledForCalendaring = False
                 self.failIf(recordResource.calendarUserAddresses())
 
+    def test_canonicalCalendarUserAddress(self):
+        """
+        DirectoryPrincipalResource.canonicalCalendarUserAddress()
+        """
+        for provisioningResource, recordType, recordResource, record in self._allRecords():
+            if record.enabledForCalendaring:
+                self.failUnless(recordResource.canonicalCalendarUserAddress().startswith("urn:uuid:"))
+
     def test_addressBookHomeURLs(self):
         """
         DirectoryPrincipalResource.addressBookHomeURLs(),

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_proxyprincipaldb.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_proxyprincipaldb.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_proxyprincipaldb.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -86,6 +86,21 @@
         self.assertEqual(membershipsB, set(("A",)))
 
     @inlineCallbacks
+    def test_normalDBNonAscii(self):
+    
+        # Get the DB
+        db_path = os.path.abspath(self.mktemp())
+        db = ProxySqliteDB(db_path)
+        principalID = "Test \xe4\xbd\x90\xe8\x97\xa4"
+        yield db.setGroupMembers(principalID, ("B", "C", "D",))
+        
+        membersA = yield db.getMembers(principalID)
+        membershipsB = yield db.getMemberships("B")
+        
+        self.assertEqual(membersA, set(("B", "C", "D",)))
+        self.assertEqual(membershipsB, set((principalID,)))
+
+    @inlineCallbacks
     def test_DBIndexed(self):
     
         # Get the DB

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -38,8 +38,13 @@
     def setUp(self):
         super(ProxyPrincipals, self).setUp()
 
-        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
-        self.directoryService = XMLDirectoryService({'xmlFile' : xmlFile})
+        self.directoryService = XMLDirectoryService(
+            {
+                'xmlFile' : xmlFile,
+                'augmentService' :
+                    augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
+            }
+        )
         calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
 
         # Set up a principals hierarchy for each service we're testing with
@@ -568,4 +573,4 @@
         yield self._addProxy(principal, proxyType, proxyPrincipal)
         memberships = yield proxyPrincipal._calendar_user_proxy_index().getMemberships(proxyPrincipal.principalUID())
         for uid in memberships:
-            subPrincipal = provisioningResource.principalForUID(uid)
+            provisioningResource.principalForUID(uid)

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_xmlfile.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_xmlfile.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/test/test_xmlfile.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -97,9 +97,14 @@
     Test XML file based directory implementation.
     """
     def service(self):
-        self.patch(augment, "AugmentService",
-                   augment.AugmentXMLDB(xmlFiles=(self.augmentsFile().path,)))
-        directory = XMLDirectoryService({'xmlFile' : self.xmlFile()}, alwaysStat=True)
+        directory = XMLDirectoryService(
+            {
+                'xmlFile' : self.xmlFile(),
+                'augmentService' :
+                   augment.AugmentXMLDB(xmlFiles=(self.augmentsFile().path,)),
+            },
+            alwaysStat=True
+        )
         return directory
 
     def test_changedXML(self):
@@ -162,7 +167,7 @@
 </augments>
 """
         )
-        augment.AugmentService.refresh()
+        service.augmentService.refresh()
 
         for recordType, expectedRecords in (
             ( DirectoryService.recordType_users     , ()             ),
@@ -316,12 +321,17 @@
     ))
 
     def test_recordTypesSubset(self):
-        self.patch(augment, "AugmentService",
-                   augment.AugmentXMLDB(xmlFiles=(self.augmentsFile().path,)))
         directory = XMLDirectoryService(
-            {'xmlFile' : self.xmlFile(), 
-             'recordTypes' : (DirectoryService.recordType_users, 
-                              DirectoryService.recordType_groups)}, 
+            {
+                'xmlFile' : self.xmlFile(),
+                'augmentService' :
+                    augment.AugmentXMLDB(xmlFiles=(self.augmentsFile().path,)),
+                'recordTypes' :
+                    (
+                        DirectoryService.recordType_users,
+                        DirectoryService.recordType_groups
+                    ),
+            },
             alwaysStat=True
         )
         self.assertEquals(set(("users", "groups")), set(directory.recordTypes()))

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/wiki.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/wiki.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/wiki.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -128,7 +128,38 @@
         self.enabled = True
 
 
+ at inlineCallbacks
+def getWikiAccess(userID, wikiID):
+    """
+    Ask the wiki server we're paired with what level of access the userID has
+    for the given wikiID.  Possible values are "read", "write", and "admin"
+    (which we treat as "write").
+    """
+    wikiConfig = config.Authentication.Wiki
+    proxy = Proxy(wikiConfig["URL"])
+    try:
 
+        log.debug("Looking up Wiki ACL for: user [%s], wiki [%s]" % (userID,
+            wikiID))
+        access = (yield proxy.callRemote(wikiConfig["WikiMethod"],
+            userID, wikiID))
+
+        log.debug("Wiki ACL result: user [%s], wiki [%s], access [%s]" % (userID,
+            wikiID, access))
+        returnValue(access)
+
+    except Fault, fault:
+
+        log.debug("Wiki ACL result: user [%s], wiki [%s], FAULT [%s]" % (userID,
+            wikiID, fault))
+
+        if fault.faultCode == 2: # non-existent user
+            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, fault.faultString))
+
+        else: # fault.faultCode == 12, non-existent wiki
+            raise HTTPError(StatusResponse(responsecode.NOT_FOUND, fault.faultString))
+
+
 @inlineCallbacks
 def getWikiACL(resource, request):
     """
@@ -151,7 +182,6 @@
     if hasattr(request, 'wikiACL'):
         returnValue(request.wikiACL)
 
-    wikiConfig = config.Authentication.Wiki
     userID = "unauthenticated"
     wikiID = resource.record.shortNames[0]
 
@@ -164,17 +194,9 @@
         # TODO: better error handling
         pass
 
-    proxy = Proxy(wikiConfig["URL"])
     try:
+        access = (yield getWikiAccess(userID, wikiID))
 
-        log.debug("Looking up Wiki ACL for: user [%s], wiki [%s]" % (userID,
-            wikiID))
-        access = (yield proxy.callRemote(wikiConfig["WikiMethod"],
-            userID, wikiID))
-
-        log.debug("Wiki ACL result: user [%s], wiki [%s], access [%s]" % (userID,
-            wikiID, access))
-
         # The ACL we returns has ACEs for the end-user and the wiki principal
         # in case authzUser is the wiki principal.
         if access == "read":
@@ -246,18 +268,6 @@
                 )
             )
 
-
-    except Fault, fault:
-
-        log.debug("Wiki ACL result: user [%s], wiki [%s], FAULT [%s]" % (userID,
-            wikiID, fault))
-
-        if fault.faultCode == 2: # non-existent user
-            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, fault.faultString))
-
-        else: # fault.faultCode == 12, non-existent wiki
-            raise HTTPError(StatusResponse(responsecode.NOT_FOUND, fault.faultString))
-
     except HTTPError:
         # pass through the HTTPError we might have raised above
         raise

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/xmlaugmentsparser.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/xmlaugmentsparser.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/xmlaugmentsparser.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -40,6 +40,7 @@
 ELEMENT_HOSTEDAT          = "hosted-at"   # Backwards compatibility
 ELEMENT_ENABLECALENDAR    = "enable-calendar"
 ELEMENT_ENABLEADDRESSBOOK = "enable-addressbook"
+ELEMENT_ENABLELOGIN       = "enable-login"
 ELEMENT_AUTOSCHEDULE      = "auto-schedule"
 
 ATTRIBUTE_REPEAT          = "repeat"
@@ -55,6 +56,7 @@
     ELEMENT_HOSTEDAT:          "partitionID",   # Backwards compatibility
     ELEMENT_ENABLECALENDAR:    "enabledForCalendaring",
     ELEMENT_ENABLEADDRESSBOOK: "enabledForAddressBooks",
+    ELEMENT_ENABLELOGIN:       "enabledForLogin",
     ELEMENT_AUTOSCHEDULE:      "autoSchedule",
 }
 
@@ -104,6 +106,7 @@
                     ELEMENT_ENABLE,
                     ELEMENT_ENABLECALENDAR,
                     ELEMENT_ENABLEADDRESSBOOK,
+                    ELEMENT_ENABLELOGIN,
                     ELEMENT_AUTOSCHEDULE,
                 ):
                     fields[node.tag] = node.text == VALUE_TRUE

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/xmlfile.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/xmlfile.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directory/xmlfile.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -32,7 +32,6 @@
 from twistedcaldav.config import config
 
 from twistedcaldav.config import fullServerPath
-from twistedcaldav.directory import augment
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord, DirectoryError
 from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser, XMLAccountRecord
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
@@ -70,6 +69,7 @@
             ),
             'realmName' : '/Search',
             'statSeconds' : 15,
+            'augmentService' : None,
         }
         ignored = None
         params = self.getParams(params, defaults, ignored)
@@ -77,6 +77,7 @@
         self._recordTypes = params['recordTypes']
         self.realmName = params['realmName']
         self.statSeconds = params['statSeconds']
+        self.augmentService = params['augmentService']
 
         super(XMLDirectoryService, self).__init__()
 
@@ -169,7 +170,7 @@
                             shortNames    = tuple(xmlAccountRecord.shortNames),
                             xmlPrincipal  = xmlAccountRecord,
                         )
-                        d = augment.AugmentService.getAugmentRecord(record.guid,
+                        d = self.augmentService.getAugmentRecord(record.guid,
                             record.recordType)
                         d.addCallback(lambda x:record.addAugmentInformation(x))
 

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directorybackedaddressbook.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directorybackedaddressbook.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/directorybackedaddressbook.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -87,7 +87,12 @@
             params = config.DirectoryAddressBook.params.copy()
             params["directoryBackedAddressBook"] = self
 
-            self.directory = directoryClass(params)
+            try:
+                self.directory = directoryClass(params)
+            except ImportError, e:
+                log.error("Unable to set up directory address book: %s" % (e,))
+                return succeed(None)
+
             return self.directory.createCache()
             
             #print ("DirectoryBackedAddressBookResource.provisionDirectory: provisioned")

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/ical.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/ical.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,3 +1,4 @@
+# -*- test-case-name: twistedcaldav.test.test_icalendar -*-
 ##
 # Copyright (c) 2005-2011 Apple Inc. All rights reserved.
 #
@@ -49,7 +50,7 @@
 from pycalendar.componentbase import PyCalendarComponentBase
 from pycalendar.datetime import PyCalendarDateTime
 from pycalendar.duration import PyCalendarDuration
-from pycalendar.exceptions import PyCalendarInvalidData
+from pycalendar.exceptions import PyCalendarError
 from pycalendar.period import PyCalendarPeriod
 from pycalendar.property import PyCalendarProperty
 from pycalendar.timezone import PyCalendarTimezone
@@ -136,6 +137,10 @@
 
 ignoredComponents = ("VTIMEZONE", "X-CALENDARSERVER-PERUSER",)
 
+# Used for min/max time-range query limits
+minDateTime = PyCalendarDateTime(1900, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
+maxDateTime = PyCalendarDateTime(2100, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
+
 class InvalidICalendarDataError(ValueError):
     pass
 
@@ -340,7 +345,7 @@
         cal = PyCalendar()
         try:
             result = cal.parse(stream)
-        except PyCalendarInvalidData:
+        except PyCalendarError:
             result = None
         if not result:
             stream.seek(0)
@@ -364,6 +369,22 @@
         def parse(data): return clazz.fromString(data)
         return allDataFromStream(IStream(stream), parse)
 
+
+    @classmethod
+    def newCalendar(cls):
+        """
+        Create and return an empty C{VCALENDAR} component.
+
+        @return: a new C{VCALENDAR} component with appropriate metadata
+            properties already set (version, product ID).
+        @rtype: an instance of this class
+        """
+        self = cls("VCALENDAR")
+        self.addProperty(Property("VERSION", "2.0"))
+        self.addProperty(Property("PRODID", iCalendarProductID))
+        return self
+
+
     def __init__(self, name, **kwargs):
         """
         Use this constructor to initialize an empty L{Component}.
@@ -402,6 +423,9 @@
             self._parent = None
 
     def __str__ (self):
+        """
+        NB This does not automatically include timezones in VCALENDAR objects.
+        """
         return str(self._pycalendar)
 
     def __repr__(self):
@@ -422,6 +446,8 @@
         """
         Return text representation and include timezones if the option is on
         """
+        assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
+        
         return self._pycalendar.getText(includeTimezones=includeTimezones)
 
     # FIXME: Should this not be in __eq__?
@@ -649,6 +675,26 @@
 
         return due.duplicateAsUTC() if due is not None else None
  
+    def getCompletedDateUTC(self):
+        """
+        Return the completed date or date-time for the specified component
+        converted to UTC.
+        @param component: the Component whose start should be returned.
+        @return: the datetime.date or datetime.datetime for the start.
+        """
+        completed = self.propertyValue("COMPLETED")
+        return completed.duplicateAsUTC() if completed is not None else None
+ 
+    def getCreatedDateUTC(self):
+        """
+        Return the created date or date-time for the specified component
+        converted to UTC.
+        @param component: the Component whose start should be returned.
+        @return: the datetime.date or datetime.datetime for the start.
+        """
+        created = self.propertyValue("CREATED")
+        return created.duplicateAsUTC() if created is not None else None
+ 
     def getRecurrenceIDUTC(self):
         """
         Return the recurrence-id for the specified component.
@@ -1227,9 +1273,11 @@
 
         return changed
 
-    def validCalendarForCalDAV(self):
+    def validCalendarData(self, doFix=True, doRaise=True):
         """
-        @raise InvalidICalendarDataError: if the given calendar data is not valid.
+        @return: tuple of fixed, unfixed issues
+        @raise InvalidICalendarDataError: if the given calendar data is not valid and
+            cannot be fixed.
         """
         if self.name() != "VCALENDAR":
             log.debug("Not a calendar: %s" % (self,))
@@ -1238,34 +1286,30 @@
             log.debug("Unknown resource type: %s" % (self,))
             raise InvalidICalendarDataError("Unknown resource type")
 
-        version = self.propertyValue("VERSION")
-        if version != "2.0":
-            msg = "Not a version 2.0 iCalendar (version=%s)" % (version,)
-            log.debug(msg)
-            raise InvalidICalendarDataError(msg)
+        # Do underlying iCalendar library validation with data fix
+        fixed, unfixed = self._pycalendar.validate(doFix=doFix)
+        if unfixed:
+            log.debug("Calendar data had unfixable problems:\n  %s" % ("\n  ".join(unfixed),))
+            if doRaise:
+                raise InvalidICalendarDataError("Calendar data had unfixable problems:\n  %s" % ("\n  ".join(unfixed),))
+        if fixed:
+            log.debug("Calendar data had fixable problems:\n  %s" % ("\n  ".join(fixed),))
+        
+        return fixed, unfixed
 
-    def validateForCalDAV(self):
+    def validCalendarForCalDAV(self, methodAllowed):
         """
+        @param methodAllowed:     True if METHOD property is allowed, False otherwise.
         @raise InvalidICalendarDataError: if the given calendar component is not valid for
             use as a X{CalDAV} resource.
         """
-        self.validCalendarForCalDAV()
 
         # Disallowed in CalDAV-Access-08, section 4.1
-        if self.hasProperty("METHOD"):
+        if not methodAllowed and self.hasProperty("METHOD"):
             msg = "METHOD property is not allowed in CalDAV iCalendar data"
             log.debug(msg)
             raise InvalidICalendarDataError(msg)
 
-        self.validateComponentsForCalDAV(False)
-
-    def validateComponentsForCalDAV(self, method, fix=False):
-        """
-        @param method:     True if METHOD property is allowed, False otherwise.
-        @param fix:        True to try and fix bogus data
-        @raise InvalidICalendarDataError: if the given calendar component is not valid for
-            use as a X{CalDAV} resource.
-        """
         #
         # Must not contain more than one type of iCalendar component, except for
         # the required timezone components, and component UIDs must match
@@ -1280,12 +1324,6 @@
         #master_recurring = False
         
         for subcomponent in self.subcomponents():
-            # Disallowed in CalDAV-Access-08, section 4.1
-            if not method and subcomponent.hasProperty("METHOD"):
-                msg = "METHOD property is not allowed in CalDAV iCalendar data"
-                log.debug(msg)
-                raise InvalidICalendarDataError(msg)
-        
             if subcomponent.name() == "VTIMEZONE":
                 timezones.add(subcomponent.propertyValue("TZID"))
             elif subcomponent.name() in ignoredComponents:
@@ -1351,32 +1389,6 @@
                 else:
                     component_rids.add(rid)
 
-                # Check for mismatch in DTSTART and UNTIL value type
-                # If they're not both date or both date-time, raise error
-                if (subcomponent.hasProperty("DTSTART") and
-                    subcomponent.hasProperty("RRULE")):
-                    dtValue = subcomponent.propertyValue("DTSTART")
-                    dtutc = dtValue.duplicateAsUTC()
-                    # Using properties("RRULE") rather than getRRuleSet() here
-                    # because the dateutil rrule's _until values are datetime
-                    # even if the UNTIL is a date (and therefore we can't
-                    # check validity without doing the following):
-                    rrules = subcomponent._pycalendar.getRecurrenceSet()
-                    for rrule in rrules.getRules():
-                        if rrule.getUseUntil():
-                            if rrule.getUntil().isDateOnly() ^ dtValue.isDateOnly():
-                                msg = "Calendar resources must have matching type for DTSTART and UNTIL"
-                                log.debug(msg)
-                                if fix:
-                                    log.debug("Fixing mismatch")
-                                    rrule.getUntil().setDateOnly(dtValue.isDateOnly())
-                                    if not dtValue.isDateOnly():
-                                        rrule.getUntil().setHHMMSS(dtutc.getHours(), dtutc.getMinutes(), dtutc.getSeconds())
-                                        rrule.getUntil().setTimezone(PyCalendarTimezone(utc=True))
-                                    rrules.changed()
-                                else:
-                                    raise InvalidICalendarDataError(msg)
-
                 timezone_refs.update(subcomponent.timezoneIDs())
         
         #
@@ -1487,7 +1499,7 @@
                 return False
             
             # First make sure components are all of the same time (excluding VTIMEZONE)
-            self.validateComponentsForCalDAV(True)
+            self.validCalendarForCalDAV(methodAllowed=True)
             
             # Next we could check the iTIP status for each type of method/component pair, however
             # we can also leave that up to the server except for the REQUEST/VFREEBUSY case which

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/instance.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/instance.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/instance.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -165,16 +165,14 @@
         if len(self.instances) > max_allowed_instances:
             raise TooManyInstancesError()
 
-    def _addMasterEventComponent(self, component, limit):
+    def _getMasterEventDetails(self, component):
         """
-        Add the specified master VEVENT Component to the instance list, expanding it
-        within the supplied time range.
-        @param component: the Component to expand
-        @param limit: the end L{PyCalendarDateTime} for expansion
+        Logic here comes from RFC4791 Section 9.9
         """
+
         start = component.getStartDateUTC()
         if start is None:
-            return
+            return None
         rulestart = component.propertyValue("DTSTART")
 
         end = component.getEndDateUTC()
@@ -189,7 +187,22 @@
             end = start + duration
         else:
             duration = differenceDateTime(start, end)
+        
+        return (rulestart, start, end, duration,)
 
+    def _addMasterEventComponent(self, component, limit):
+        """
+        Add the specified master VEVENT Component to the instance list, expanding it
+        within the supplied time range.
+        @param component: the Component to expand
+        @param limit: the end L{PyCalendarDateTime} for expansion
+        """
+        
+        details = self._getMasterEventDetails(component)
+        if details is None:
+            return
+        rulestart, start, end, duration = details
+
         self._addMasterComponent(component, limit, rulestart, start, end, duration)
 
     def _addOverrideEventComponent(self, component, limit, got_master):
@@ -202,23 +215,58 @@
         
         #TODO: This does not take into account THISANDPRIOR - only THISANDFUTURE
         
-        start = component.getStartDateUTC()
-        if start is None:
+        details = self._getMasterEventDetails(component)
+        if details is None:
             return
+        _ignore_rulestart, start, end, _ignore_duration = details
 
-        end = component.getEndDateUTC()
-        duration = None
-        if end is None:
-            if not start.isDateOnly():
-                # Timed event with zero duration
-                duration = PyCalendarDuration(days=0)
+        self._addOverrideComponent(component, limit, start, end, got_master)
+
+    def _getMasterToDoDetails(self, component):
+        """
+        Logic here comes from RFC4791 Section 9.9
+        """
+
+        dtstart = component.getStartDateUTC()
+        dtend = component.getEndDateUTC()
+        dtdue = component.getDueDateUTC()
+
+        # DTSTART and DURATION or DUE case
+        if dtstart is not None:
+            rulestart = component.propertyValue("DTSTART")
+            start = dtstart
+            if dtend is not None:
+                end = dtend
+            elif dtdue is not None:
+                end = dtdue
             else:
-                # All day event default duration is one day
-                duration = PyCalendarDuration(days=1)
-            end = start + duration
+                end = dtstart
+        
+        # DUE case
+        elif dtdue is not None:
+            rulestart = component.propertyValue("DUE")
+            start = end = dtdue
+        
+        # Fall back to COMPLETED or CREATED - cannot be recurring
+        else:
+            rulestart = None
+            from twistedcaldav.ical import maxDateTime, minDateTime
+            dtcreated = component.getCreatedDateUTC()
+            dtcompleted = component.getCompletedDateUTC()
+            if dtcompleted:
+                end = dtcompleted
+                start = dtcreated if dtcreated else dtend
+            elif dtcreated:
+                start = dtcreated
+                end = maxDateTime
+            else:
+                start = minDateTime
+                end = maxDateTime
 
-        self._addOverrideComponent(component, limit, start, end, got_master)
+        duration = differenceDateTime(start, end)
 
+        return (rulestart, start, end, duration,)
+
     def _addMasterToDoComponent(self, component, limit):
         """
         Add the specified master VTODO Component to the instance list, expanding it
@@ -226,22 +274,13 @@
         @param component: the Component to expand
         @param limit: the end L{PyCalendarDateTime} for expansion
         """
-        start = component.getStartDateUTC()
-        due = component.getDueDateUTC()
-
-        if start is None and due is None:
+        details = self._getMasterToDoDetails(component)
+        if details is None:
             return
+        rulestart, start, end, duration = details
 
-        rulestart = component.propertyValue("DTSTART")
-        if start is None:
-            start = due
-            rulestart = component.propertyValue("DUE")
-        elif due is None:
-            due = start
-        duration = differenceDateTime(start, due)
+        self._addMasterComponent(component, limit, rulestart, start, end, duration)
 
-        self._addMasterComponent(component, limit, rulestart, start, due, duration)
-
     def _addOverrideToDoComponent(self, component, limit, got_master):
         """
         Add the specified overridden VTODO Component to the instance list, replacing 
@@ -252,23 +291,17 @@
         
         #TODO: This does not take into account THISANDPRIOR - only THISANDFUTURE
         
-        start = component.getStartDateUTC()
-        due = component.getDueDateUTC()
-
-        if start is None and due is None:
+        details = self._getMasterToDoDetails(component)
+        if details is None:
             return
+        _ignore_rulestart, start, end, _ignore_duration = details
 
-        if start is None:
-            start = due
-        elif due is None:
-            due = start
+        self._addOverrideComponent(component, limit, start, end, got_master)
 
-        self._addOverrideComponent(component, limit, start, due, got_master)
-
     def _addMasterComponent(self, component, limit, rulestart, start, end, duration):
         
         rrules = component.getRecurrenceSet()
-        if rrules is not None:
+        if rrules is not None and rulestart is not None:
             # Do recurrence set expansion
             expanded = []
             limited = rrules.expand(rulestart, PyCalendarPeriod(start, limit), expanded)

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/localization.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/localization.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/localization.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -17,7 +17,6 @@
 from __future__ import with_statement
 import gettext
 import inspect
-import datetime
 import os
 import struct
 import array

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/mail.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/mail.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/mail.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -529,6 +529,19 @@
 # Service
 #
 
+class MailGatewayService(service.MultiService):
+
+    def startService(self):
+        """
+        Purge old database tokens -- doing this in startService so that
+        it happens after we've shed privileges
+        """
+        service.MultiService.startService(self)
+        mailer = getattr(self, "mailer", None)
+        if mailer is not None:
+            mailer.purge()
+
+
 class MailGatewayServiceMaker(LoggingMixIn):
     implements(IPlugin, service.IServiceMaker)
 
@@ -543,7 +556,7 @@
             config.Memcached.MaxClients,
         )
 
-        multiService = service.MultiService()
+        mailGatewayService = MailGatewayService()
 
         settings = config.Scheduling['iMIP']
         if settings['Enabled']:
@@ -560,18 +573,19 @@
                 # TODO: raise error?
                 self.log_error("Invalid iMIP type in configuration: %s" %
                     (mailType,))
-                return multiService
+                return mailGatewayService
 
-            client.setServiceParent(multiService)
+            client.setServiceParent(mailGatewayService)
 
-
             # Set up /inbox -- server POSTs to it to send out iMIP invites
-            IScheduleService(settings, mailer).setServiceParent(multiService)
+            IScheduleService(settings, mailer).setServiceParent(mailGatewayService)
 
         else:
+            mailer = None
             self.log_info("Mail Gateway Service not enabled")
 
-        return multiService
+        mailGatewayService.mailer = mailer
+        return mailGatewayService
 
 
 class IScheduleService(service.MultiService, LoggingMixIn):
@@ -609,9 +623,14 @@
         if dataRoot is None:
             dataRoot = config.DataRoot
         self.db = MailGatewayTokensDatabase(dataRoot)
-        days = config.Scheduling['iMIP']['InvitationDaysToLive']
+        self.days = config.Scheduling['iMIP']['InvitationDaysToLive']
+
+    def purge(self):
+        """
+        Purge old database tokens
+        """
         self.db.purgeOldTokens(datetime.date.today() -
-            datetime.timedelta(days=days))
+            datetime.timedelta(days=self.days))
 
     def checkDSN(self, message):
         # returns (isDSN, Action, icalendar attachment)
@@ -835,7 +854,7 @@
         for attendeeProp in calendar.getAllAttendeeProperties():
             cutype = attendeeProp.parameterValue('CUTYPE', None)
             if cutype == "INDIVIDUAL":
-                cn = attendeeProp.parameterValue("CN", None)
+                cn = attendeeProp.parameterValue("CN", None).decode("utf-8")
                 cuaddr = normalizeCUAddr(attendeeProp.value())
                 if cuaddr.startswith("mailto:"):
                     mailto = cuaddr[7:]
@@ -880,6 +899,7 @@
             addressWithToken = "%s+%s@%s" % (pre, token, post)
 
             organizerProperty = calendar.getOrganizerProperty()
+            organizerEmailAddress = organizerProperty.parameterValue("EMAIL", None)
             organizerValue = organizerProperty.value()
             organizerProperty.setValue("mailto:%s" % (addressWithToken,))
 
@@ -892,8 +912,8 @@
             # The email's From will include the originator's real name email
             # address if available.  Otherwise it will be the server's email
             # address (without # + addressing)
-            if originator.startswith("mailto:"):
-                orgEmail = fromAddr = originator[7:]
+            if organizerEmailAddress:
+                orgEmail = fromAddr = organizerEmailAddress
             else:
                 fromAddr = serverAddress
                 orgEmail = None
@@ -909,11 +929,16 @@
 
         else: # REPLY
             inviteState = "reply"
-            originator = originator.lower()
-            if not originator.startswith("mailto:"):
-                raise ValueError("Originator address '%s' must be mailto: for REPLY." % (originator,))
-            formattedFrom = fromAddr = originator = originator[7:]
 
+            # Look up the attendee property corresponding to the originator
+            # of this reply
+            originatorAttendeeProperty = calendar.getAttendeeProperty([originator])
+            formattedFrom = fromAddr = originator = ""
+            if originatorAttendeeProperty:
+                originatorAttendeeEmailAddress = originatorAttendeeProperty.parameterValue("EMAIL", None)
+                if originatorAttendeeEmailAddress:
+                    formattedFrom = fromAddr = originator = originatorAttendeeEmailAddress
+
             organizerMailto = str(calendar.getOrganizer())
             if not organizerMailto.lower().startswith("mailto:"):
                 raise ValueError("ORGANIZER address '%s' must be mailto: for REPLY." % (organizerMailto,))
@@ -1223,7 +1248,7 @@
         results['month'] = dtStart.getMonth()
         results['day'] = dtStart.getDay()
 
-        summary = component.propertyValue("SUMMARY")
+        summary = component.propertyValue("SUMMARY").decode("utf-8")
         if summary is None:
             summary = ""
         results['summary'] = summary


Property changes on: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/get.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/get.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/get.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -82,7 +82,7 @@
                 if self.accessMode:
             
                     # Non DAV:owner's have limited access to the data
-                    isowner = (yield self.isOwner(request, adminprincipals=True, readprincipals=True))
+                    isowner = (yield self.isOwner(request))
                     
                     # Now "filter" the resource calendar data
                     caldata = PrivateEventFilter(self.accessMode, isowner).filter(caldata)

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/put_addressbook_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/put_addressbook_common.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/put_addressbook_common.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -198,12 +198,22 @@
                             "Could not parse vCard",
                         ))
                         
-                # Valid vcard data for CalDAV check
+                # Valid vcard data check
                 result, message = self.validAddressDataCheck()
                 if not result:
                     log.err(message)
                     raise HTTPError(ErrorResponse(
                         responsecode.FORBIDDEN,
+                        (carddav_namespace, "valid-address-data"),
+                        description=message
+                    ))
+                    
+                # Valid vcard data for CalDAV check
+                result, message = self.validCardDAVDataCheck()
+                if not result:
+                    log.err(message)
+                    raise HTTPError(ErrorResponse(
+                        responsecode.FORBIDDEN,
                         (carddav_namespace, "valid-addressbook-object-resource"),
                         "Invalid vCard data",
                     ))
@@ -283,8 +293,8 @@
         
     def validAddressDataCheck(self):
         """
-        Check that the vcard data is valid vCard.
-        @return:         tuple: (True/False if the vcard data is valid,
+        Check that the calendar data is valid iCalendar.
+        @return:         tuple: (True/False if the calendar data is valid,
                                  log message string).
         """
         result = True
@@ -294,13 +304,29 @@
             message = "Empty resource not allowed in vcard collection"
         else:
             try:
-                self.vcard.validForCardDAV()
+                self.vcard.validVCardData()
             except ValueError, e:
                 result = False
                 message = "Invalid vcard data: %s" % (e,)
         
         return result, message
     
+    def validCardDAVDataCheck(self):
+        """
+        Check that the vcard data is valid vCard.
+        @return:         tuple: (True/False if the vcard data is valid,
+                                 log message string).
+        """
+        result = True
+        message = ""
+        try:
+            self.vcard.validForCardDAV()
+        except ValueError, e:
+            result = False
+            message = "vCard data does not conform to CardDAV requirements: %s" % (e,)
+        
+        return result, message
+    
     def validSizeCheck(self):
         """
         Make sure that the content-type of the source resource is text/vcard.

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/put_common.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/put_common.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -468,7 +468,7 @@
             message = "Empty resource not allowed in calendar collection"
         else:
             try:
-                self.calendar.validCalendarForCalDAV()
+                self.calendar.validCalendarData()
             except ValueError, e:
                 result = False
                 message = "Invalid calendar data: %s" % (e,)
@@ -484,10 +484,7 @@
         result = True
         message = ""
         try:
-            if self.isiTIP:
-                self.calendar.validateComponentsForCalDAV(True)
-            else:
-                self.calendar.validateForCalDAV()
+            self.calendar.validCalendarForCalDAV(methodAllowed=self.isiTIP)
         except ValueError, e:
             result = False
             message = "Calendar data does not conform to CalDAV requirements: %s" % (e,)

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/report_calendar_query.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/report_calendar_query.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/report_calendar_query.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -174,7 +174,7 @@
             filteredaces = (yield calresource.inheritedACEsforChildren(request))
 
             # Check private events access status
-            isowner = (yield calresource.isOwner(request, adminprincipals=True, readprincipals=True))
+            isowner = (yield calresource.isOwner(request))
 
             # Check for disabled access
             if filteredaces is not None:
@@ -229,7 +229,7 @@
                     timezone = tuple(tz.calendar().subcomponents())[0]
 
             # Check private events access status
-            isowner = (yield calresource.isOwner(request, adminprincipals=True, readprincipals=True))
+            isowner = (yield calresource.isOwner(request))
 
             calendar = (yield calresource.iCalendarForUser(request))
             yield queryCalendarObjectResource(calresource, uri, None, calendar, timezone)

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/report_multiget_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/report_multiget_common.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/method/report_multiget_common.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -139,7 +139,7 @@
             disabled = True
             
         # Check private events access status
-        isowner = (yield self.isOwner(request, adminprincipals=True, readprincipals=True))
+        isowner = (yield self.isOwner(request))
 
     elif self.isAddressBookCollection():
         requestURIis = "addressbook"
@@ -352,7 +352,7 @@
                         filteredaces = (yield parent.inheritedACEsforChildren(request))
 
                         # Check private events access status
-                        isowner = (yield parent.isOwner(request, adminprincipals=True, readprincipals=True))
+                        isowner = (yield parent.isOwner(request))
                 else:
                     name = unquote(resource_uri[resource_uri.rfind("/") + 1:])
                     if (resource_uri != request.uri) or not self.exists():
@@ -376,7 +376,7 @@
                     filteredaces = (yield parent.inheritedACEsforChildren(request))
 
                     # Check private events access status
-                    isowner = (yield parent.isOwner(request, adminprincipals=True, readprincipals=True))
+                    isowner = (yield parent.isOwner(request))
         
                 # Check privileges - must have at least DAV:read
                 try:

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/notify.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/notify.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/notify.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -689,7 +689,7 @@
             iq = IQ(self.xmlStream)
             pubsubElement = iq.addElement('pubsub', defaultUri=self.pubsubNS)
             publishElement = pubsubElement.addElement('publish')
-            publishElement['node'] = nodeName
+            publishElement['node'] = nodeName.decode("utf-8")
             if self.settings["NodeConfiguration"]["pubsub#deliver_payloads"] == '1':
                 itemElement = publishElement.addElement('item')
                 itemElement.addElement('plistfrag', defaultUri='plist-apple')
@@ -742,7 +742,7 @@
             iq = IQ(self.xmlStream)
             pubsubElement = iq.addElement('pubsub', defaultUri=self.pubsubNS)
             child = pubsubElement.addElement('create')
-            child['node'] = nodeName
+            child['node'] = nodeName.decode("utf-8")
             d = iq.send(to=self.settings['ServiceAddress'])
             d.addCallback(self.createNodeSuccess, nodeName, publish)
             d.addErrback(self.createNodeFailure, nodeName, publish)
@@ -794,7 +794,7 @@
             child = iq.addElement('pubsub',
                 defaultUri=self.pubsubNS+"#owner")
             child = child.addElement('configure')
-            child['node'] = nodeName
+            child['node'] = nodeName.decode("utf-8")
             d = iq.send(to=self.settings['ServiceAddress'])
             d.addCallback(self.requestConfigurationFormSuccess, nodeName,
                 publish)
@@ -829,7 +829,7 @@
                         filledPubSub = filledIq.addElement('pubsub',
                             defaultUri=self.pubsubNS+"#owner")
                         filledConfigure = filledPubSub.addElement('configure')
-                        filledConfigure['node'] = nodeName
+                        filledConfigure['node'] = nodeName.decode("utf-8")
                         filledForm = filledConfigure.addElement('x',
                             defaultUri='jabber:x:data')
                         filledForm['type'] = 'submit'
@@ -859,7 +859,7 @@
                             cancelPubSub = cancelIq.addElement('pubsub',
                                 defaultUri=self.pubsubNS+"#owner")
                             cancelConfig = cancelPubSub.addElement('configure')
-                            cancelConfig['node'] = nodeName
+                            cancelConfig['node'] = nodeName.decode("utf-8")
                             cancelX = cancelConfig.addElement('x',
                                 defaultUri='jabber:x:data')
                             cancelX['type'] = 'cancel'
@@ -940,7 +940,7 @@
             pubsubElement = iq.addElement('pubsub',
                 defaultUri=self.pubsubNS+"#owner")
             publishElement = pubsubElement.addElement('delete')
-            publishElement['node'] = nodeName
+            publishElement['node'] = nodeName.decode("utf-8")
             self.sendDebug("Deleting (%s)" % (nodeName,), iq)
             d = iq.send(to=self.settings['ServiceAddress'])
             d.addCallback(self.deleteNodeSuccess, nodeName)


Property changes on: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/addressbookquery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/addressbookquery.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/addressbookquery.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -26,8 +26,7 @@
     "sqladdressbookquery",
 ]
 
-from twistedcaldav.query import expression, sqlgenerator
-from twistedcaldav import carddavxml
+from twistedcaldav.query import expression, sqlgenerator, addressbookqueryfilter
 
 # SQL Index column (field) names
 
@@ -76,33 +75,32 @@
         # Test for <<field>> != "*"
         return expression.isExpression(fields["UID"], "", True)
     
-    # Handle text-match
-    tm = None
-    if propfilter.qualifier and isinstance(propfilter.qualifier, carddavxml.TextMatch):
-        if propfilter.qualifier.negate:
-            tm = expression.notcontainsExpression(fields[propfilter.filter_name], str(propfilter.qualifier), propfilter.qualifier)
-        else:
-            tm = expression.containsExpression(fields[propfilter.filter_name], str(propfilter.qualifier), propfilter.qualifier)
-    
-    # Handle embedded parameters - we do not right now as our Index does not handle them
+    # Handle embedded parameters/text-match
     params = []
-    if len(propfilter.filters) > 0:
-        raise ValueError
+    for filter in propfilter.filters:
+        if isinstance(filter, addressbookqueryfilter.TextMatch):
+            if filter.match_type == "equals":
+                tm = expression.isnotExpression if filter.negate else expression.isExpression
+            elif filter.match_type == "contains":
+                tm = expression.notcontainsExpression if filter.negate else expression.containsExpression
+            elif filter.match_type == "starts-with":
+                tm = expression.notstartswithExpression if filter.negate else expression.startswithExpression
+            elif filter.match_type == "ends-with":
+                tm = expression.notendswithExpression if filter.negate else expression.endswithExpression
+            params.append(tm(fields[propfilter.filter_name], str(filter.text), True))
+        else:
+            # No embedded parameters - not right now as our Index does not handle them
+            raise ValueError
+
+    # Now build return expression
     if len(params) > 1:
-        paramsExpression = expression.orExpression[params]
+        if propfilter.propfilter_test == "anyof":
+            return expression.orExpression(params)
+        else:
+            return expression.andExpression(params)
     elif len(params) == 1:
-        paramsExpression = params[0]
+        return params[0]
     else:
-        paramsExpression = None
-
-    # Now build return expression
-    if (tm is not None) and (paramsExpression is not None):
-        return expression.andExpression([tm, paramsExpression])
-    elif tm is not None:
-        return tm
-    elif paramsExpression is not None:
-        return paramsExpression
-    else:
         return None
 
 def sqladdressbookquery(filter, addressbookid=None, generator=sqlgenerator.sqlgenerator):
@@ -116,7 +114,7 @@
     """
     try:
         expression = addressbookquery(filter, generator.FIELDS)
-        sql = generator(expression, addressbookid)
+        sql = generator(expression, addressbookid, None)
         return sql.generate()
     except ValueError:
         return None

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/addressbookqueryfilter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/addressbookqueryfilter.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/addressbookqueryfilter.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2010 Apple Inc. All rights reserved.
+# Copyright (c) 2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -69,7 +69,7 @@
         if len(self.children) > 0:
             allof = self.filter_test == "allof"
             for propfilter in self.children:
-                if allof != propfilter.test(vcard):
+                if allof != propfilter._match(vcard):
                     return not allof
             return allof
         else:
@@ -153,7 +153,7 @@
         if len(self.filters) > 0:
             allof = self.propfilter_test == "allof"
             for filter in self.filters:
-                if allof != filter.test(item):
+                if allof != filter._match(item):
                     return not allof
             return allof
         else:
@@ -164,7 +164,7 @@
     Limits a search to specific properties.
     """
 
-    def test(self, vcard):
+    def _match(self, vcard):
         # At least one property must match (or is-not-defined is set)
         for property in vcard.properties():
             if property.name().upper() == self.filter_name.upper() and self.match(property): break
@@ -188,7 +188,7 @@
     Limits a search to specific parameters.
     """
 
-    def test(self, property):
+    def _match(self, property):
 
         # At least one parameter must match (or is-not-defined is set)
         result = not self.defined
@@ -246,7 +246,7 @@
         else:
             self.match_type = "contains"
 
-    def test(self, item):
+    def _match(self, item):
         """
         Match the text for the item.
         If the item is a property, then match the property value,

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/calendarquery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/calendarquery.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/calendarquery.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -48,15 +48,18 @@
     assert vcalfilter.filter_name == "VCALENDAR"
     
     if len(vcalfilter.filters) > 0:
+        # Determine logical expression grouping
+        logical = expression.andExpression if vcalfilter.filter_test == "allof" else expression.orExpression
+        
         # Only comp-filters are handled
         for _ignore in [x for x in vcalfilter.filters if not isinstance(x, calendarqueryfilter.ComponentFilter)]:
             raise ValueError
         
-        return compfilterListExpression(vcalfilter.filters, fields)
+        return compfilterListExpression(vcalfilter.filters, fields, logical)
     else:
         return expression.allExpression()
 
-def compfilterListExpression(compfilters, fields):
+def compfilterListExpression(compfilters, fields, logical):
     """
     Create an expression for a list of comp-filter elements.
     
@@ -67,7 +70,7 @@
     if len(compfilters) == 1:
         return compfilterExpression(compfilters[0], fields)
     else:
-        return expression.orExpression([compfilterExpression(c, fields) for c in compfilters])
+        return logical([compfilterExpression(c, fields) for c in compfilters])
 
 def compfilterExpression(compfilter, fields):
     """
@@ -81,7 +84,10 @@
     if not compfilter.defined:
         # Test for TYPE != <<component-type name>>
         return expression.isnotExpression(fields["TYPE"], compfilter.filter_name, True)
-        
+    
+    # Determine logical expression grouping
+    logical = expression.andExpression if compfilter.filter_test == "allof" else expression.orExpression
+    
     expressions = []
     if isinstance(compfilter.filter_name, str):
         expressions.append(expression.isExpression(fields["TYPE"], compfilter.filter_name, True))
@@ -98,7 +104,7 @@
     for p in [x for x in compfilter.filters if isinstance(x, calendarqueryfilter.PropertyFilter)]:
         props.append(propfilterExpression(p, fields))
     if len(props) > 1:
-        propsExpression = expression.orExpression[props]
+        propsExpression = logical(props)
     elif len(props) == 1:
         propsExpression = props[0]
     else:
@@ -109,7 +115,7 @@
     for _ignore in [x for x in compfilter.filters if isinstance(x, calendarqueryfilter.ComponentFilter)]:
         raise ValueError
     if len(comps) > 1:
-        compsExpression = expression.orExpression[comps]
+        compsExpression = logical(comps)
     elif len(comps) == 1:
         compsExpression = comps[0]
     else:
@@ -117,7 +123,7 @@
 
     # Now build compound expression
     if ((propsExpression is not None) and (compsExpression is not None)):
-        expressions.append(expression.orExpression([propsExpression, compsExpression]))
+        expressions.append(logical([propsExpression, compsExpression]))
     elif propsExpression is not None:
         expressions.append(propsExpression)
     elif compsExpression is not None:
@@ -143,6 +149,9 @@
         # Test for <<field>> != "*"
         return expression.isExpression(fields["UID"], "", True)
     
+    # Determine logical expression grouping
+    logical = expression.andExpression if propfilter.filter_test == "allof" else expression.orExpression
+    
     # Handle time-range - we cannot do this with our Index right now
     if propfilter.qualifier and isinstance(propfilter.qualifier, calendarqueryfilter.TimeRange):
         raise ValueError
@@ -150,17 +159,22 @@
     # Handle text-match
     tm = None
     if propfilter.qualifier and isinstance(propfilter.qualifier, calendarqueryfilter.TextMatch):
-        if propfilter.qualifier.negate:
-            tm = expression.notcontainsExpression(fields[propfilter.filter_name], propfilter.qualifier.text, propfilter.qualifier.caseless)
-        else:
-            tm = expression.containsExpression(fields[propfilter.filter_name], propfilter.qualifier.text, propfilter.qualifier.caseless)
+        if propfilter.qualifier.match_type == "equals":
+            tm = expression.isnotExpression if propfilter.qualifier.negate else expression.isExpression
+        elif propfilter.qualifier.match_type == "contains":
+            tm = expression.notcontainsExpression if propfilter.qualifier.negate else expression.containsExpression
+        elif propfilter.qualifier.match_type == "starts-with":
+            tm = expression.notstartswithExpression if propfilter.qualifier.negate else expression.startswithExpression
+        elif propfilter.qualifier.match_type == "ends-with":
+            tm = expression.notendswithExpression if propfilter.qualifier.negate else expression.endswithExpression
+        tm = tm(fields[propfilter.filter_name], propfilter.qualifier.text, propfilter.qualifier.caseless)
     
     # Handle embedded parameters - we do not right now as our Index does not handle them
     params = []
     for _ignore in propfilter.filters:
         raise ValueError
     if len(params) > 1:
-        paramsExpression = expression.orExpression[params]
+        paramsExpression = logical(params)
     elif len(params) == 1:
         paramsExpression = params[0]
     else:
@@ -168,7 +182,7 @@
 
     # Now build return expression
     if (tm is not None) and (paramsExpression is not None):
-        return expression.andExpression([tm, paramsExpression])
+        return logical([tm, paramsExpression])
     elif tm is not None:
         return tm
     elif paramsExpression is not None:

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/calendarqueryfilter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/calendarqueryfilter.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/calendarqueryfilter.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -184,6 +184,11 @@
             self.filter_name = self.filter_name.encode("utf-8")
         self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined)
 
+        filter_test = xml_element.attributes.get("test", "allof")
+        if filter_test not in ("anyof", "allof"):
+            raise ValueError("Test must be only one of anyof, allof")
+        self.filter_test = filter_test
+
     def match(self, item, access=None):
         """
         Returns True if the given calendar item (either a component, property or parameter value)
@@ -197,10 +202,11 @@
         if self.qualifier and not self.qualifier.match(item, access): return False
 
         if len(self.filters) > 0:
+            allof = self.filter_test == "allof"
             for filter in self.filters:
-                if filter._match(item, access):
-                    return True
-            return False
+                if allof != filter._match(item, access):
+                    return not allof
+            return allof
         else:
             return True
 
@@ -224,10 +230,11 @@
         if self.qualifier and not self.qualifier.matchinstance(item, self.instances): return False
 
         if len(self.filters) > 0:
+            allof = self.filter_test == "allof"
             for filter in self.filters:
-                if filter._match(item, access):
-                    return True
-            return False
+                if allof != filter._match(item, access):
+                    return not allof
+            return allof
         else:
             return True
 
@@ -489,6 +496,18 @@
         else:
             self.negate = False
 
+        if "match-type" in xml_element.attributes:
+            self.match_type = xml_element.attributes["match-type"]
+            if self.match_type not in (
+                "equals",
+                "contains",
+                "starts-with",
+                "ends-with",
+            ):
+                self.match_type = "contains"
+        else:
+            self.match_type = "contains"
+
     def match(self, item, access):
         """
         Match the text for the item.
@@ -508,25 +527,29 @@
 
         def _textCompare(s):
             if self.caseless:
-                if s.lower().find(test) != -1:
-                    return True, not self.negate
+                s = s.lower()
+
+            if self.match_type == "equals":
+                return s == test
+            elif self.match_type == "contains":
+                return s.find(test) != -1 
+            elif self.match_type == "starts-with":
+                return s.startswith(test)
+            elif self.match_type == "ends-with":
+                return s.endswith(test)
             else:
-                if s.find(test) != -1:
-                    return True, not self.negate
-            return False, False
+                return False
 
         for value in values:
             # NB Its possible that we have a text list value which appears as a Python list,
             # so we need to check for that and iterate over the list.
             if isinstance(value, list):
                 for subvalue in value:
-                    matched, result = _textCompare(unicode(subvalue, "utf-8"))
-                    if matched:
-                        return result
+                    if _textCompare(unicode(subvalue, "utf-8")):
+                        return not self.negate
             else:
-                matched, result = _textCompare(unicode(value, "utf-8"))
-                if matched:
-                    return result
+                if _textCompare(unicode(value, "utf-8")):
+                    return not self.negate
         
         return self.negate
 

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/expression.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/expression.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/expression.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -32,6 +32,10 @@
     "notcontainsExpression",
     "isExpression",
     "isnotExpression",
+    "startswithExpression",
+    "notstartswithExpression",
+    "endswithExpression",
+    "notendswithExpression",
     "inExpression",
     "notinExpression",
 ]
@@ -55,7 +59,22 @@
         
         return False
     
-class allExpression(object):
+    def _collapsedExpression(self):
+        return self
+
+    def andWith(self, other):
+        if isinstance(other, andExpression):
+            return andExpression((self._collapsedExpression(),) + tuple(other.expressions))
+        else:
+            return andExpression((self._collapsedExpression(), other._collapsedExpression(),))
+
+    def orWith(self, other):
+        if isinstance(other, orExpression):
+            return orExpression((self._collapsedExpression(),) + tuple(other.expressions))
+        else:
+            return orExpression((self._collapsedExpression(), other._collapsedExpression(),))
+
+class allExpression(baseExpression):
     """
     Match everything.
     """
@@ -99,6 +118,12 @@
         
         return True
 
+    def _collapsedExpression(self):
+        if self.multi() and len(self.expressions) == 1:
+            return self.expressions[0]._collapsedExpression()
+        else:
+            return self
+
 class notExpression(logicExpression):
     """
     Logical NOT operation.
@@ -135,6 +160,10 @@
     def operator(self):
         return "AND"
 
+    def andWith(self, other):
+        self.expressions = tuple(self.expressions) + (other._collapsedExpression(),)
+        return self
+
 class orExpression(logicExpression):
     """
     Logical OR operation.
@@ -146,6 +175,10 @@
     def operator(self):
         return "OR"
 
+    def orWith(self, other):
+        self.expressions = tuple(self.expressions) + (other._collapsedExpression(),)
+        return self
+
 class timerangeExpression(baseExpression):
     """
     CalDAV time-range comparison expression.
@@ -193,7 +226,7 @@
         super(notcontainsExpression, self).__init__(field, text, caseless)
 
     def operator(self):
-        return " does not contain"
+        return "does not contain"
 
 class isExpression(textcompareExpression):
     """
@@ -217,6 +250,50 @@
     def operator(self):
         return "is not"
 
+class startswithExpression(textcompareExpression):
+    """
+    Text STARTSWITH (sub-string match) expression.
+    """
+    
+    def __init__(self, field, text, caseless):
+        super(startswithExpression, self).__init__(field, text, caseless)
+
+    def operator(self):
+        return "starts with"
+
+class notstartswithExpression(textcompareExpression):
+    """
+    Text NOT STARTSWITH (sub-string match) expression.
+    """
+    
+    def __init__(self, field, text, caseless):
+        super(notstartswithExpression, self).__init__(field, text, caseless)
+
+    def operator(self):
+        return "does not start with"
+
+class endswithExpression(textcompareExpression):
+    """
+    Text STARTSWITH (sub-string match) expression.
+    """
+    
+    def __init__(self, field, text, caseless):
+        super(endswithExpression, self).__init__(field, text, caseless)
+
+    def operator(self):
+        return "ends with"
+
+class notendswithExpression(textcompareExpression):
+    """
+    Text NOT STARTSWITH (sub-string match) expression.
+    """
+    
+    def __init__(self, field, text, caseless):
+        super(notendswithExpression, self).__init__(field, text, caseless)
+
+    def operator(self):
+        return "does not end with"
+
 class inExpression(textcompareExpression):
     """
     Text IN (exact string match to one of the supplied items) expression.

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/sqlgenerator.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/sqlgenerator.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/sqlgenerator.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -30,21 +30,25 @@
 
 class sqlgenerator(object):
     
-    FROM           =" from "
-    WHERE          =" where "
-    RESOURCEDB     = "RESOURCE"
-    TIMESPANDB     = "TIMESPAN"
-    TRANSPARENCYDB = "TRANSPARENCY"
-    PERUSERDB      = "PERUSER"
-    NOTOP          = "NOT "
-    ANDOP          = " AND "
-    OROP           = " OR "
-    CONTAINSOP     = " GLOB "
-    NOTCONTAINSOP  = " NOT GLOB "
-    ISOP           = " == "
-    ISNOTOP        = " != "
-    INOP           = " IN "
-    NOTINOP        = " NOT IN "
+    FROM             =" from "
+    WHERE            =" where "
+    RESOURCEDB       = "RESOURCE"
+    TIMESPANDB       = "TIMESPAN"
+    TRANSPARENCYDB   = "TRANSPARENCY"
+    PERUSERDB        = "PERUSER"
+    NOTOP            = "NOT "
+    ANDOP            = " AND "
+    OROP             = " OR "
+    CONTAINSOP       = " GLOB "
+    NOTCONTAINSOP    = " NOT GLOB "
+    ISOP             = " == "
+    ISNOTOP          = " != "
+    STARTSWITHOP     = " GLOB "
+    NOTSTARTSWITHOP  = " NOT GLOB "
+    ENDSWITHOP       = " GLOB "
+    NOTENDSWITHOP    = " NOT GLOB "
+    INOP             = " IN "
+    NOTINOP          = " NOT IN "
 
     FIELDS         = {
         "TYPE": "RESOURCE.TYPE",
@@ -187,6 +191,30 @@
             self.sout.write(self.ISNOTOP)
             self.addArgument(expr.text)
         
+        # STARTSWITH
+        elif isinstance(expr, expression.startswithExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.STARTSWITHOP)
+            self.addArgument(self.startswithArgument(expr.text))
+        
+        # NOT STARTSWITH
+        elif isinstance(expr, expression.notstartswithExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.NOTSTARTSWITHOP)
+            self.addArgument(self.startswithArgument(expr.text))
+        
+        # ENDSWITH
+        elif isinstance(expr, expression.endswithExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.ENDSWITHOP)
+            self.addArgument(self.endswithArgument(expr.text))
+        
+        # NOT ENDSWITH
+        elif isinstance(expr, expression.notendswithExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.NOTENDSWITHOP)
+            self.addArgument(self.endswithArgument(expr.text))
+        
         # IN
         elif isinstance(expr, expression.inExpression):
             self.sout.write(expr.field)
@@ -259,6 +287,12 @@
     def containsArgument(self, arg):
         return "*%s*" % (arg,)
 
+    def startswithArgument(self, arg):
+        return "%s*" % (arg,)
+
+    def endswithArgument(self, arg):
+        return "*%s" % (arg,)
+
 if __name__ == "__main__":
     
     e1 = expression.isExpression("TYPE", "VEVENT", False)

Copied: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/test/test_expression.py (from rev 7695, CalendarServer/trunk/twistedcaldav/query/test/test_expression.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/test/test_expression.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/query/test/test_expression.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,166 @@
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.query import expression
+import twistedcaldav.test.util
+
+class Tests(twistedcaldav.test.util.TestCase):
+
+    def test_andWith(self):
+
+        tests = (
+            (
+                expression.isExpression("A", "1", True),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) AND is(B, 2, True) AND is(C, 3, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) AND (is(B, 2, True) OR is(C, 3, True)))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "(is(A, 1, True) AND is(B, 2, True) AND is(C, 3, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "((is(A, 1, True) OR is(B, 2, True)) AND is(C, 3, True))"
+            ),
+        )
+        
+        for expr1, expr2, result in tests:
+            self.assertEqual(str(expr1.andWith(expr2)), result, msg="Failed on %s" % (result,))
+
+    def test_orWith(self):
+
+        tests = (
+            (
+                expression.isExpression("A", "1", True),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) OR (is(B, 2, True) AND is(C, 3, True)))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) OR is(B, 2, True) OR is(C, 3, True))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "((is(A, 1, True) AND is(B, 2, True)) OR is(C, 3, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "(is(A, 1, True) OR is(B, 2, True) OR is(C, 3, True))"
+            ),
+        )
+        
+        for expr1, expr2, result in tests:
+            self.assertEqual(str(expr1.orWith(expr2)), result, msg="Failed on %s" % (result,))

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/resource.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/resource.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -51,8 +51,7 @@
 from twext.web2.dav.resource import AccessDeniedError, DAVPrincipalCollectionResource,\
     davPrivilegeSet
 from twext.web2.dav.resource import TwistedACLInheritable
-from twext.web2.dav.util import joinURL, parentForURL, normalizeURL,\
-    unimplemented
+from twext.web2.dav.util import joinURL, parentForURL, normalizeURL
 from twext.web2.http import HTTPError, RedirectResponse, StatusResponse, Response
 from twext.web2.http_headers import MimeType
 from twext.web2.stream import MemoryStream
@@ -878,7 +877,7 @@
         """
         Return the DAV:owner property value (MUST be a DAV:href or None).
         """
-        
+
         isVirt = self.isVirtualShare()
         if isVirt:
             parent = (yield self.locateParent(request, self._share.hosturl))
@@ -928,33 +927,21 @@
             returnValue(None)
 
 
-    def isOwner(self, request, adminprincipals=False, readprincipals=False):
+    @inlineCallbacks
+    def isOwner(self, request):
         """
-        Determine whether the DAV:owner of this resource matches the currently authorized principal
-        in the request. Optionally test for admin or read principals and allow those.
+        Determine whether the DAV:owner of this resource matches the currently
+        authorized principal in the request, or if the user is a read-only or
+        read-write administrator.
         """
+        current = self.currentPrincipal(request)
+        if current in config.AllAdminPrincipalObjects:
+            returnValue(True)
+        if davxml.Principal((yield self.owner(request))) == current:
+            returnValue(True)
+        returnValue(False)
 
-        def _gotOwner(owner):
-            current = self.currentPrincipal(request)
-            if davxml.Principal(owner) == current:
-                return True
-            
-            if adminprincipals:
-                for principal in config.AdminPrincipals:
-                    if davxml.Principal(davxml.HRef(principal)) == current:
-                        return True
 
-            if readprincipals:
-                for principal in config.AdminPrincipals:
-                    if davxml.Principal(davxml.HRef(principal)) == current:
-                        return True
-                
-            return False
-
-        d = self.owner(request)
-        d.addCallback(_gotOwner)
-        return d
-
     ##
     # DAVResource
     ##
@@ -1535,15 +1522,15 @@
 
 
     @inlineCallbacks
-    def iCalendarTextFiltered(self, isowner, accessUID=None):
+    def iCalendarFiltered(self, isowner, accessUID=None):
 
         # Now "filter" the resource calendar data
         caldata = PrivateEventFilter(self.accessMode, isowner).filter(
-            (yield self.iCalendarText())
+            (yield self.iCalendar())
         )
         if accessUID:
             caldata = PerUserDataFilter(accessUID).filter(caldata)
-        returnValue(str(caldata))
+        returnValue(caldata)
 
 
     def iCalendarText(self):
@@ -2094,7 +2081,7 @@
         @return: a C{int} containing the maximum allowed bytes if this
             collection is quota-controlled, or C{None} if not quota controlled.
         """
-        return config.UserQuota if config.UserQuota != 0 else None
+        return self._newStoreHome.quotaAllowedBytes()
 
     def currentQuotaUse(self, request):
         """
@@ -2602,7 +2589,7 @@
             if defaultAddressBookProperty and len(defaultAddressBookProperty.children) == 1:
                 defaultAddressBook = str(defaultAddressBookProperty.children[0])
                 adbk = (yield request.locateResource(str(defaultAddressBook)))
-                if adbk is not None and adbk.exists() and isAddressBookCollectionResource(adbk):
+                if adbk is not None and isAddressBookCollectionResource(adbk) and adbk.exists() and not adbk.isVirtualShare():
                     returnValue(defaultAddressBookProperty) 
             
             # Default is not valid - we have to try to pick one
@@ -2624,7 +2611,7 @@
             if len(new_adbk) == 1:
                 adbkURI = str(new_adbk[0])
                 adbk = (yield request.locateResource(str(new_adbk[0])))
-            if adbk is None or not adbk.exists() or not isAddressBookCollectionResource(adbk):
+            if adbk is None or not adbk.exists() or not isAddressBookCollectionResource(adbk) or adbk.isVirtualShare():
                 # Validate that href's point to a valid addressbook.
                 raise HTTPError(ErrorResponse(
                     responsecode.CONFLICT,
@@ -2685,7 +2672,7 @@
         defaultAddressBookURL = joinURL(self.url(), "addressbook")
         defaultAddressBook = (yield self.makeRegularChild("addressbook"))
         if defaultAddressBook is None or not defaultAddressBook.exists():
-            getter = iter((yield self._newStoreHome.addressbooks()))
+            getter = iter((yield self._newStoreHome.addressbooks()))  # These are only unshared children
             # FIXME: the back-end should re-provision a default addressbook here.
             # Really, the dead property shouldn't be necessary, and this should
             # be entirely computed by a back-end method like 'defaultAddressBook()'

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/schedule.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/schedule.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -168,7 +168,7 @@
             if defaultCalendarProperty and len(defaultCalendarProperty.children) == 1:
                 defaultCalendar = str(defaultCalendarProperty.children[0])
                 cal = (yield request.locateResource(str(defaultCalendar)))
-                if cal is not None and isCalendarCollectionResource(cal) and cal.exists():
+                if cal is not None and isCalendarCollectionResource(cal) and cal.exists() and not cal.isVirtualShare():
                     returnValue(defaultCalendarProperty) 
             
             # Default is not valid - we have to try to pick one
@@ -223,7 +223,7 @@
                 calURI = str(new_calendar[0])
                 cal = (yield request.locateResource(str(new_calendar[0])))
             # TODO: check that owner of the new calendar is the same as owner of this inbox
-            if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
+            if cal is None or not cal.exists() or not isCalendarCollectionResource(cal) or cal.isVirtualShare():
                 # Validate that href's point to a valid calendar.
                 raise HTTPError(ErrorResponse(
                     responsecode.CONFLICT,
@@ -263,16 +263,17 @@
         defaultCalendarURL = joinURL(calendarHomeURL, "calendar")
         defaultCalendar = (yield request.locateResource(defaultCalendarURL))
         if defaultCalendar is None or not defaultCalendar.exists():
-            getter = iter((yield self.parent._newStoreHome.calendars()))
             # FIXME: the back-end should re-provision a default calendar here.
             # Really, the dead property shouldn't be necessary, and this should
             # be entirely computed by a back-end method like 'defaultCalendar()'
-            try:
-                aCalendar = getter.next()
-            except StopIteration:
-                raise RuntimeError("No calendars at all.")
+            for calendarName in (yield self.parent._newStoreHome.listCalendars()):  # These are only unshared children
+                if calendarName != "inbox":
+                    aCalendar = calendarName
+                    break
+            else:
+                raise RuntimeError("No valid calendars to use as a default calendar.")
 
-            defaultCalendarURL = joinURL(calendarHomeURL, aCalendar.name())
+            defaultCalendarURL = joinURL(calendarHomeURL, aCalendar)
 
         self.writeDeadProperty(
             caldavxml.ScheduleDefaultCalendarURL(


Property changes on: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/icaldiff.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/icaldiff.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -283,7 +283,7 @@
                 # Get all EXDATEs in UTC
                 exdates = set()
                 for exdate in master.properties("EXDATE"):
-                    exdates.update([value.getValue().adjustToUTC() for value in exdate.value()])
+                    exdates.update([value.getValue().duplicate().adjustToUTC() for value in exdate.value()])
                
             return exdates, map, master
         
@@ -347,7 +347,9 @@
                     overridden = self.newCalendar.overriddenComponent(rid)
                     self.newCalendar.removeComponent(overridden)
                     if self.newMaster:
-                        self.newMaster.addProperty(Property("EXDATE", [rid,]))
+                        # Use the original R-ID value so we preserve the timezone
+                        original_rid = component.propertyValue("RECURRENCE-ID")
+                        self.newMaster.addProperty(Property("EXDATE", [original_rid,]))
         
         # Derive a new component in the new calendar for each new one in setnew
         for key in setnew - setold:
@@ -578,7 +580,7 @@
 
             newdue = component.getProperty("DUE")
             if newdue is not None:
-                newdue.value().adjustToUTC()
+                newdue = newdue.value().duplicate().adjustToUTC()
             
         # Recurrence rules - we need to normalize the order of the value parts
         newrrules = set()
@@ -595,14 +597,14 @@
         for rdate in rdates:
             for value in rdate.value():
                 if isinstance(PyCalendarDateTime()):
-                    value.adjustToUTC()
+                    value = value.duplicate().adjustToUTC()
                 newrdates.add(value)
         
         # EXDATEs
         newexdates = set()
         exdates = component.properties("EXDATE")
         for exdate in exdates:
-            newexdates.update([value.getValue().adjustToUTC() for value in exdate.value()])
+            newexdates.update([value.getValue().duplicate().adjustToUTC() for value in exdate.value()])
 
         return timeRange.getStart(), timeRange.getEnd(), newdue, newrrules, newrdates, newexdates
 

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/implicit.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/implicit.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2005-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -336,15 +336,10 @@
                     (caldav_namespace, "invalid-originator"),
                     "Originator not enabled",
                 ))
-    
-            # Pick the first mailto cu address or the first other type
-            for item in self.originatorPrincipal.calendarUserAddresses():
-                if not self.originator:
-                    self.originator = item
-                if item.startswith("mailto:"):
-                    self.originator = item
-                    break
 
+            # Pick the canonical CUA:
+            self.originator = self.originatorPrincipal.canonicalCalendarUserAddress()
+
         # Get the ORGANIZER and verify it is the same for all components
         try:
             self.organizer = self.calendar.validOrganizerForScheduling()
@@ -553,9 +548,45 @@
 
                 # Check to see whether a change to R-ID's happened
                 if rid == "":
-                    if "RRULE" in props or "DTSTART" in props and self.calendar.masterComponent().hasProperty("RRULE"):
+
+                    if "DTSTART" in props and self.calendar.masterComponent().hasProperty("RRULE"):
+                        # DTSTART change with RRULE present is always a reschedule
                         recurrence_reschedule = True
 
+                    elif "RRULE" in props:
+                        
+                        # Need to see if the RRULE change is a simple truncation or expansion - i.e. a change to
+                        # COUNT or UNTIL only. If so we don't need to treat this as a complete re-schedule.
+
+                        # Start off assuming they are different
+                        recurrence_reschedule = True
+
+                        # Get each RRULE (can be only one in the master)
+                        oldrrule = tuple(self.oldcalendar.masterComponent().properties("RRULE"))
+                        oldrrule = oldrrule[0].value() if len(oldrrule) else None
+                        newrrule = tuple(self.calendar.masterComponent().properties("RRULE"))
+                        newrrule = newrrule[0].value() if len(newrrule) else None
+                        
+                        if newrrule is not None and oldrrule is not None:
+                            
+                            # Normalize the rrules by removing COUNT/UNTIL and then compare
+                            oldrrule = oldrrule.duplicate()
+                            newrrule = newrrule.duplicate()
+                            
+                            oldrrule.setUseUntil(False)
+                            oldrrule.setUntil(None)
+                            oldrrule.setUseCount(False)
+                            oldrrule.setCount(0)
+ 
+                            newrrule.setUseUntil(False)
+                            newrrule.setUntil(None)
+                            newrrule.setUseCount(False)
+                            newrrule.setCount(0)
+                            
+                            # If they are equal we have a simple change - no overall reschedule
+                            if newrrule == oldrrule:
+                                recurrence_reschedule = False
+
             if checkOrganizerValue:
                 oldOrganizer = self.oldcalendar.getOrganizer()
                 newOrganizer = self.calendar.getOrganizer()
@@ -915,7 +946,7 @@
         local_organizer = type(self.organizerAddress) in (LocalCalendarUser, PartitionedCalendarUser, OtherServerCalendarUser,)
 
         if config.Scheduling.iMIP.Enabled and self.organizerAddress.cuaddr.lower().startswith("mailto:"):
-            return True
+            return is_server
 
         if not local_organizer and is_server:
             # Coerce ORGANIZER to SCHEDULE-AGENT=NONE

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/scheduler.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/scheduling/scheduler.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,4 +1,4 @@
-##
+
 # Copyright (c) 2005-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -173,13 +173,8 @@
             if originatorPrincipalURL:
                 originatorPrincipal = (yield self.request.locateResource(originatorPrincipalURL))
                 if originatorPrincipal:
-                    # Pick the first mailto cu address or the first other type
-                    for item in originatorPrincipal.calendarUserAddresses():
-                        if not originator:
-                            originator = item
-                        if item.startswith("mailto:"):
-                            originator = item
-                            break
+                    # Pick the canonical CUA:
+                    originator = originatorPrincipal.canonicalCalendarUserAddress()
 
         if not originator:
             log.err("%s request must have Originator" % (self.method,))
@@ -305,7 +300,7 @@
     def checkCalendarData(self):
         # Must be a valid calendar
         try:
-            self.calendar.validCalendarForCalDAV()
+            self.calendar.validCalendarData()
         except ValueError, e:
             log.err("%s request calendar component is not valid:%s %s" % (self.method, e, self.calendar,))
             raise HTTPError(ErrorResponse(

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/sharing.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/sharing.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -38,6 +38,7 @@
 from twistedcaldav import customxml, caldavxml
 from twistedcaldav.config import config
 from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.directory.wiki import WikiDirectoryService, getWikiAccess
 from twistedcaldav.linkresource import LinkFollowerMixIn
 from twistedcaldav.sql import AbstractSQLDatabase, db_prefix
 
@@ -267,11 +268,28 @@
 
         assert self._isVirtualShare, "Only call this for a virtual share"
 
+        wikiAccessMethod = kwargs.get("wikiAccessMethod", getWikiAccess)
+
         # Direct shares use underlying privileges of shared collection
         if self._share.sharetype == SHARETYPE_DIRECT:
             original = (yield request.locateResource(self._share.hosturl))
-            result = (yield original.accessControlList(request, *args, **kwargs))
-            returnValue(result)
+            owner = yield original.ownerPrincipal(request)
+            if owner.record.recordType == WikiDirectoryService.recordType_wikis:
+                # Access level comes from what the wiki has granted to the
+                # sharee
+                userID = self._shareePrincipal.record.guid
+                wikiID = owner.record.shortNames[0]
+                inviteAccess = (yield wikiAccessMethod(userID, wikiID))
+                if inviteAccess == "read":
+                    inviteAccess = "read-only"
+                elif inviteAccess in ("write", "admin"):
+                    inviteAccess = "read-write"
+                else:
+                    inviteAccess = None
+            else:
+                result = (yield original.accessControlList(request, *args,
+                    **kwargs))
+                returnValue(result)
         else:
             # Invite shares use access mode from the invite
     
@@ -281,68 +299,69 @@
             )
             if invite is None:
                 returnValue(davxml.ACL())
+            inviteAccess = invite.access
             
-            userprivs = [
-            ]
-            if invite.access in ("read-only", "read-write", "read-write-schedule",):
-                userprivs.append(davxml.Privilege(davxml.Read()))
-                userprivs.append(davxml.Privilege(davxml.ReadACL()))
-                userprivs.append(davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()))
-            if invite.access in ("read-only",):
-                userprivs.append(davxml.Privilege(davxml.WriteProperties()))
-            if invite.access in ("read-write", "read-write-schedule",):
-                userprivs.append(davxml.Privilege(davxml.Write()))
-            proxyprivs = list(userprivs)
-            proxyprivs.remove(davxml.Privilege(davxml.ReadACL()))
+        userprivs = [
+        ]
+        if inviteAccess in ("read-only", "read-write", "read-write-schedule",):
+            userprivs.append(davxml.Privilege(davxml.Read()))
+            userprivs.append(davxml.Privilege(davxml.ReadACL()))
+            userprivs.append(davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()))
+        if inviteAccess in ("read-only",):
+            userprivs.append(davxml.Privilege(davxml.WriteProperties()))
+        if inviteAccess in ("read-write", "read-write-schedule",):
+            userprivs.append(davxml.Privilege(davxml.Write()))
+        proxyprivs = list(userprivs)
+        proxyprivs.remove(davxml.Privilege(davxml.ReadACL()))
 
-            aces = (
-                # Inheritable specific access for the resource's associated principal.
+        aces = (
+            # Inheritable specific access for the resource's associated principal.
+            davxml.ACE(
+                davxml.Principal(davxml.HRef(self._shareePrincipal.principalURL())),
+                davxml.Grant(*userprivs),
+                davxml.Protected(),
+                TwistedACLInheritable(),
+            ),
+        )
+
+        if self.isCalendarCollection():
+            aces += (
+                # Inheritable CALDAV:read-free-busy access for authenticated users.
                 davxml.ACE(
-                    davxml.Principal(davxml.HRef(self._shareePrincipal.principalURL())),
-                    davxml.Grant(*userprivs),
-                    davxml.Protected(),
+                    davxml.Principal(davxml.Authenticated()),
+                    davxml.Grant(davxml.Privilege(caldavxml.ReadFreeBusy())),
                     TwistedACLInheritable(),
                 ),
             )
 
-            if self.isCalendarCollection():
-                aces += (
-                    # Inheritable CALDAV:read-free-busy access for authenticated users.
-                    davxml.ACE(
-                        davxml.Principal(davxml.Authenticated()),
-                        davxml.Grant(davxml.Privilege(caldavxml.ReadFreeBusy())),
-                        TwistedACLInheritable(),
-                    ),
-                )
+        # Give read access to config.ReadPrincipals
+        aces += config.ReadACEs
 
-            # Give read access to config.ReadPrincipals
-            aces += config.ReadACEs
+        # Give all access to config.AdminPrincipals
+        aces += config.AdminACEs
 
-            # Give all access to config.AdminPrincipals
-            aces += config.AdminACEs
-
-            if config.EnableProxyPrincipals:
-                aces += (
-                    # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
-                    davxml.ACE(
-                        davxml.Principal(davxml.HRef(joinURL(self._shareePrincipal.principalURL(), "calendar-proxy-read/"))),
-                        davxml.Grant(
-                            davxml.Privilege(davxml.Read()),
-                            davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
-                        ),
-                        davxml.Protected(),
-                        TwistedACLInheritable(),
+        if config.EnableProxyPrincipals:
+            aces += (
+                # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(joinURL(self._shareePrincipal.principalURL(), "calendar-proxy-read/"))),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
                     ),
-                    # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users.
-                    davxml.ACE(
-                        davxml.Principal(davxml.HRef(joinURL(self._shareePrincipal.principalURL(), "calendar-proxy-write/"))),
-                        davxml.Grant(*proxyprivs),
-                        davxml.Protected(),
-                        TwistedACLInheritable(),
-                    ),
-                )
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+                ),
+                # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users.
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(joinURL(self._shareePrincipal.principalURL(), "calendar-proxy-write/"))),
+                    davxml.Grant(*proxyprivs),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+                ),
+            )
 
-            returnValue(davxml.ACL(*aces))
+        returnValue(davxml.ACL(*aces))
 
     def validUserIDForShare(self, userid):
         """

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/stdconfig.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/stdconfig.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -79,6 +79,8 @@
                 "emailSuffix": None, # used only to synthesize email address
                 "filter": None, # additional filter for this type
                 "recordName": "userid", # uniquely identifies user records
+                "loginEnabledAttr" : "loginEnabled", # attribute controlling login
+                "loginEnabledValue" : "yes", # value of above attribute
             },
             "groups": {
                 "rdn": "ou=Group",
@@ -436,9 +438,10 @@
     #
     # Standard (or draft) WebDAV extensions
     #
-    "EnableAddMember"         : True,  # POST ;add-member extension
-    "EnableSyncReport"        : True,  # REPORT collection-sync
-    "EnableWellKnown"         : True,  # /.well-known resource
+    "EnableAddMember"             : True,  # POST ;add-member extension
+    "EnableSyncReport"            : True,  # REPORT collection-sync
+    "EnableWellKnown"             : True,  # /.well-known resource
+    "EnableCalendarQueryExtended" : True,  # Extended calendar-query REPORT
 
     #
     # Non-standard CalDAV extensions
@@ -450,16 +453,16 @@
     "TimezoneService"         : {    # New standard timezone service
         "Enabled"       : True,      # Overall on/off switch
         "Mode"          : "primary", # Can be "primary" or "secondary"
-        "BasePath"      : None,      # Path to zoneinfo - if None use default package path
+        "BasePath"      : "",        # Path to zoneinfo - if None use default package path
                                      # secondary service MUST define its own writeable path
-        "XMLInfoPath"   : None,      # Path to db cache info - if None use default package path
+        "XMLInfoPath"   : "",        # Path to db cache info - if None use default package path
                                      # secondary service MUST define its own writeable path if
                                      # not None
         
         "SecondaryService" : {
             # Only one of these should be used when a secondary service is used
-            "Host"                  : None,      # Domain/IP of secondary service to discover
-            "URI"                   : None,      # HTTP(s) URI to secondary service
+            "Host"                  : "",        # Domain/IP of secondary service to discover
+            "URI"                   : "",        # HTTP(s) URI to secondary service
 
             "UpdateIntervalMinutes" : 24 * 60,
         }
@@ -485,7 +488,7 @@
 
     # CardDAV Features
     "DirectoryAddressBook": {
-        "Enabled": True,
+        "Enabled": False,
         "type":    "twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService",
         "params":  directoryAddressBookBackingServiceDefaultParams["twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService"],
         "name":    "directory",
@@ -495,7 +498,7 @@
     "AnonymousDirectoryAddressBookAccess": False, # Anonymous users may access directory address book
 
     "GlobalAddressBook": {
-        "Enabled":                   True,
+        "Enabled":                   False,
         "Name":                      "global-addressbook",
         "EnableAnonymousReadAccess": False,
     },
@@ -1061,6 +1064,19 @@
 
     log.debug("Nav ACL: %s" % (configDict.ProvisioningResourceACL.toxml(),))
 
+    def principalObjects(urls):
+        for principalURL in urls:
+            yield davxml.Principal(davxml.HRef(principalURL))
+
+    # Should be sets, except WebDAVElement isn't hashable.
+    a = configDict.AdminPrincipalObjects = list(
+        principalObjects(configDict.AdminPrincipals))
+    b = configDict.ReadPrincipalObjects = list(
+        principalObjects(configDict.ReadPrincipals))
+    configDict.AllAdminPrincipalObjects = a + b
+
+
+
 def _updateRejectClients(configDict):
     #
     # Compile RejectClients expressions for speed
@@ -1208,6 +1224,8 @@
             compliance += customxml.calendarserver_sharing_compliance
             # TODO: This is only needed whilst we do not support scheduling in shared calendars
             compliance += customxml.calendarserver_sharing_no_scheduling_compliance
+        if configDict.EnableCalendarQueryExtended:
+            compliance += caldavxml.caldav_query_extended_compliance
     else:
         compliance = ()
 

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/storebridge.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/storebridge.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -25,7 +25,11 @@
 from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL, davXMLFromStream
 from twext.web2.http import HTTPError, StatusResponse, Response
 from twext.web2.http_headers import ETag, MimeType
-from twext.web2.responsecode import FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED, BAD_REQUEST, OK
+from twext.web2.responsecode import (
+    FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED,
+    BAD_REQUEST, OK, INSUFFICIENT_STORAGE_SPACE
+)
+
 from twext.web2.stream import ProducerStream, readStream, MemoryStream
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue, maybeDeferred
 from twisted.internet.protocol import Protocol
@@ -43,13 +47,16 @@
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
 from twistedcaldav.method.put_common import StoreCalendarObjectResource
-from twistedcaldav.notifications import NotificationCollectionResource, NotificationResource
+from twistedcaldav.notifications import (
+    NotificationCollectionResource, NotificationResource
+)
 from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource
 from twistedcaldav.schedule import ScheduleInboxResource
 from twistedcaldav.scheduling.implicit import ImplicitScheduler
 from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
 
 from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.icommondatastore import NoSuchObjectResourceError
 from urlparse import urlsplit
 import time
@@ -938,7 +945,7 @@
         filteredaces = (yield self.inheritedACEsforChildren(request))
 
         tzids = set()
-        isowner = (yield self.isOwner(request, adminprincipals=True, readprincipals=True))
+        isowner = (yield self.isOwner(request))
         accessPrincipal = (yield self.resourceOwnerPrincipal(request))
 
         for name, uid, type in (yield maybeDeferred(self.index().bruteForceSearch)): #@UnusedVariable
@@ -955,9 +962,8 @@
                     continue
 
                 # Get the access filtered view of the data
-                caldata = yield child.iCalendarTextFiltered(isowner, accessPrincipal.principalUID() if accessPrincipal else "")
                 try:
-                    subcalendar = VCalendar.fromString(caldata)
+                    subcalendar = yield child.iCalendarFiltered(isowner, accessPrincipal.principalUID() if accessPrincipal else "")
                 except ValueError:
                     continue
                 assert subcalendar.name() == "VCALENDAR"
@@ -1517,12 +1523,18 @@
 
         creating = (self._newStoreAttachment is None)
         if creating:
-            self._newStoreAttachment = self._newStoreObject =  (yield self._newStoreCalendarObject.createAttachmentWithName(
-                self.attachmentName,
-            ))
+            self._newStoreAttachment = self._newStoreObject = (
+                yield self._newStoreCalendarObject.createAttachmentWithName(
+                    self.attachmentName))
         t = self._newStoreAttachment.store(content_type)
         yield readStream(request.stream, t.write)
-        yield t.loseConnection()
+        try:
+            yield t.loseConnection()
+        except QuotaExceeded:
+            raise HTTPError(
+                ErrorResponse(INSUFFICIENT_STORAGE_SPACE,
+                              (dav_namespace, "quota-not-exceeded"))
+            )
         returnValue(CREATED if creating else NO_CONTENT)
 
 
@@ -1618,9 +1630,6 @@
         return succeed(self._newStoreObject.size())
 
 
-    def text(self):
-        return self._newStoreObject.text()
-
     def component(self):
         return self._newStoreObject.component()
 
@@ -1630,9 +1639,9 @@
             log.debug("Resource not found: %s" % (self,))
             raise HTTPError(responsecode.NOT_FOUND)
 
-        output = yield self.text()
+        output = yield self.component()
 
-        response = Response(200, {}, output)
+        response = Response(200, {}, str(output))
         response.headers.setHeader("content-type", self.contentType())
         returnValue(response)
 
@@ -1709,66 +1718,45 @@
         returnValue(NO_CONTENT)
 
 
-class _CalendarObjectMetaDataMixin(object):
+
+class _MetadataProperty(object):
     """
-    Dynamically create the required meta-data for an object resource 
+    A python property which can be set either on a _newStoreObject or on some
+    metadata if no new store object exists yet.
     """
 
-    def _get_accessMode(self):
-        return self._newStoreObject.accessMode if self._newStoreObject else self._metadata.get("accessMode", None)
+    def __init__(self, name):
+        self.name = name
 
-    def _set_accessMode(self, value):
-        if self._newStoreObject:
-            self._newStoreObject.accessMode = value
-        else:
-            self._metadata["accessMode"] = value
 
-    accessMode = property(_get_accessMode, _set_accessMode)
-
-    def _get_isScheduleObject(self):
-        return self._newStoreObject.isScheduleObject if self._newStoreObject else self._metadata.get("isScheduleObject", None)
-
-    def _set_isScheduleObject(self, value):
-        if self._newStoreObject:
-            self._newStoreObject.isScheduleObject = value
+    def __get__(self, oself, type=None):
+        if oself._newStoreObject:
+            return getattr(oself._newStoreObject, self.name)
         else:
-            self._metadata["isScheduleObject"] = value
+            return oself._metadata.get(self.name, None)
 
-    isScheduleObject = property(_get_isScheduleObject, _set_isScheduleObject)
 
-    def _get_scheduleTag(self):
-        return self._newStoreObject.scheduleTag if self._newStoreObject else self._metadata.get("scheduleTag", None)
-
-    def _set_scheduleTag(self, value):
-        if self._newStoreObject:
-            self._newStoreObject.scheduleTag = value
+    def __set__(self, oself, value):
+        if oself._newStoreObject:
+            setattr(oself._newStoreObject, self.name, value)
         else:
-            self._metadata["scheduleTag"] = value
+            oself._metadata[self.name] = value
 
-    scheduleTag = property(_get_scheduleTag, _set_scheduleTag)
 
-    def _get_scheduleEtags(self):
-        return self._newStoreObject.scheduleEtags if self._newStoreObject else self._metadata.get("scheduleEtags", None)
 
-    def _set_scheduleEtags(self, value):
-        if self._newStoreObject:
-            self._newStoreObject.scheduleEtags = value
-        else:
-            self._metadata["scheduleEtags"] = value
+class _CalendarObjectMetaDataMixin(object):
+    """
+    Dynamically create the required meta-data for an object resource 
+    """
 
-    scheduleEtags = property(_get_scheduleEtags, _set_scheduleEtags)
+    accessMode        = _MetadataProperty("accessMode")
+    isScheduleObject  = _MetadataProperty("isScheduleObject")
+    scheduleTag       = _MetadataProperty("scheduleTag")
+    scheduleEtags     = _MetadataProperty("scheduleEtags")
+    hasPrivateComment = _MetadataProperty("hasPrivateComment")
 
-    def _get_hasPrivateComment(self):
-        return self._newStoreObject.hasPrivateComment if self._newStoreObject else self._metadata.get("hasPrivateComment", None)
 
-    def _set_hasPrivateComment(self, value):
-        if self._newStoreObject:
-            self._newStoreObject.hasPrivateComment = value
-        else:
-            self._metadata["hasPrivateComment"] = value
 
-    hasPrivateComment = property(_get_hasPrivateComment, _set_hasPrivateComment)
-
 class CalendarObjectResource(_CalendarObjectMetaDataMixin, _CommonObjectResource):
     """
     A resource wrapping a calendar object.
@@ -1808,7 +1796,11 @@
         returnValue(txn)
 
 
-    iCalendarText = _CommonObjectResource.text
+    @inlineCallbacks
+    def iCalendarText(self):
+        data = yield self.iCalendar()
+        returnValue(str(data))
+
     iCalendar = _CommonObjectResource.component
 
 
@@ -1864,7 +1856,10 @@
         isinbox = self._newStoreObject._calendar.name() == "inbox"
 
         # Do If-Schedule-Tag-Match behavior first
-        if not isinbox:
+        # Important: this should only ever be done when storeRemove is called
+        # directly as a result of an HTTP DELETE to ensure the proper If-
+        # header is used in this test.
+        if not isinbox and implicitly:
             self.validIfScheduleMatch(request)
 
         scheduler = None
@@ -2059,9 +2054,14 @@
 
     _componentFromStream = VCard.fromString
 
-    vCardText = _CommonObjectResource.text
+    @inlineCallbacks
+    def vCardText(self):
+        data = yield self.vCard()
+        returnValue(str(data))
 
+    vCard = _CommonObjectResource.component
 
+
 class _NotificationChildHelper(object):
     """
     Methods for things which are like notification objects.

Copied: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/AnotherEvent.ics (from rev 7695, CalendarServer/trunk/twistedcaldav/test/data/AnotherEvent.ics)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/AnotherEvent.ics	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/AnotherEvent.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,32 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.4//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:America/New_Yrok
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0500
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+DTSTART:20070311T020000
+TZNAME:EDT
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:-0400
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+DTSTART:20071104T020000
+TZNAME:EST
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20110516T031511Z
+UID:1C4B4547-9D99-446B-B1D7-135AE04A319E
+DTEND;TZID=America/New_Yrok:20110516T130000
+TRANSP:OPAQUE
+SUMMARY:Another Event
+DTSTART;TZID=America/New_Yrok:20110516T114500
+DTSTAMP:20110516T031514Z
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3184A66-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3184A66-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3184A66-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020101
 DTEND;VALUE=DATE:20020102
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20031231;BYMONTH=1

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3184D26-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3184D26-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3184D26-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3184D26-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020121
 DTEND;VALUE=DATE:20020122
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20040118;BYMONTH=1;BYDAY=3MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185326-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185326-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185326-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3185326-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020212
 DTEND;VALUE=DATE:20020213
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31854DA-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31854DA-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31854DA-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C31854DA-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020214
 DTEND;VALUE=DATE:20020215
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31856AC-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31856AC-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31856AC-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C31856AC-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020218
 DTEND;VALUE=DATE:20020219
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20040215;BYMONTH=2;BYDAY=3MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318585A-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318585A-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318585A-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318585A-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020222
 DTEND;VALUE=DATE:20020223
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20040221;BYMONTH=2

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185A14-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185A14-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185A14-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3185A14-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020317
 DTEND;VALUE=DATE:20020318
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=3

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185BBD-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185BBD-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185BBD-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3185BBD-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020331
 DTEND;VALUE=DATE:20020401
 SUMMARY:Easter

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185D63-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185D63-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185D63-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3185D63-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020415
 DTEND;VALUE=DATE:20020416
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=4

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185F20-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185F20-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3185F20-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3185F20-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020512
 DTEND;VALUE=DATE:20020513
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=5;BYDAY=2SU

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31860C8-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31860C8-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31860C8-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C31860C8-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020527
 DTEND;VALUE=DATE:20020528
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20040530;BYMONTH=5;BYDAY=-1MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318627C-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318627C-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318627C-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318627C-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020529
 DTEND;VALUE=DATE:20020530
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=5

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186426-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186426-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186426-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3186426-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020616
 DTEND;VALUE=DATE:20020617
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=6;BYDAY=3SU

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31865E4-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31865E4-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31865E4-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C31865E4-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020704
 DTEND;VALUE=DATE:20020705
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20040703;BYMONTH=7

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186792-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186792-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186792-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3186792-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020902
 DTEND;VALUE=DATE:20020903
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20040905;BYMONTH=9;BYDAY=1MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186938-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186938-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186938-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3186938-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20021014
 DTEND;VALUE=DATE:20021015
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20041010;BYMONTH=10;BYDAY=2MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186ADE-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186ADE-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186ADE-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3186ADE-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20021027
 DTEND;VALUE=DATE:20021028
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=10;BYDAY=-1SU

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186C96-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186C96-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186C96-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3186C96-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20021031
 DTEND;VALUE=DATE:20021101
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=10

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186E3A-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186E3A-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186E3A-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3186E3A-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20021111
 DTEND;VALUE=DATE:20021112
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20041110;BYMONTH=11

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186FE7-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186FE7-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3186FE7-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3186FE7-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20021128
 DTEND;VALUE=DATE:20021129
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20041124;BYMONTH=11;BYDAY=4TH

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318719A-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318719A-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318719A-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318719A-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20021225
 DTEND;VALUE=DATE:20021226
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20041224;BYMONTH=12

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3187343-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3187343-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3187343-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3187343-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20030420
 DTEND;VALUE=DATE:20030421
 SUMMARY:Easter

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188906-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188906-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188906-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3188906-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020407
 DTEND;VALUE=DATE:20020408
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=4;BYDAY=1SU

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188B3A-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188B3A-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188B3A-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3188B3A-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020202
 DURATION:P1D
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188CFF-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188CFF-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188CFF-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3188CFF-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020401
 DURATION:P1D
 SEQUENCE:2

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188EAA-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188EAA-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3188EAA-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3188EAA-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020506
 DURATION:P1D
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20040502;BYMONTH=5;BYDAY=1MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189058-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189058-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189058-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3189058-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020505
 DURATION:P1D
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=5

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189203-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189203-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189203-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3189203-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20020614
 DURATION:P1D
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=6

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31893C2-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31893C2-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31893C2-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C31893C2-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20050327
 DTEND;VALUE=DATE:20050328
 SEQUENCE:3

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189572-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189572-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189572-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3189572-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20040401
 DTEND;VALUE=DATE:20040402
 RRULE:FREQ=YEARLY;INTERVAL=1

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189716-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189716-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189716-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3189716-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20040101
 DTEND;VALUE=DATE:20040102
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=1

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31898D4-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31898D4-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C31898D4-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C31898D4-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20040119
 DTEND;VALUE=DATE:20040120
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=1;BYDAY=3MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189A88-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189A88-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189A88-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3189A88-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20040216
 DTEND;VALUE=DATE:20040217
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYDAY=3MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189C32-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189C32-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189C32-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3189C32-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20040222
 DTEND;VALUE=DATE:20040223
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189DEC-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189DEC-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189DEC-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3189DEC-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20040411
 DTEND;VALUE=DATE:20040412
 SEQUENCE:2

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189F94-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189F94-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C3189F94-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C3189F94-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20060416
 DTEND;VALUE=DATE:20060417
 SEQUENCE:2

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A148-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A148-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A148-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318A148-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20040531
 DTEND;VALUE=DATE:20040601
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=5;BYDAY=-1MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A2F3-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A2F3-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A2F3-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318A2F3-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20040503
 DURATION:P1D
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=5;BYDAY=1MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A4BA-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A4BA-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A4BA-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318A4BA-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20040704
 DTEND;VALUE=DATE:20040705
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=7

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A6E1-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A6E1-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A6E1-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318A6E1-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20040906
 DTEND;VALUE=DATE:20040907
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=9;BYDAY=1MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A898-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A898-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318A898-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318A898-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20041011
 DTEND;VALUE=DATE:20041012
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=10;BYDAY=2MO

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318AA54-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318AA54-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318AA54-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318AA54-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20041111
 DTEND;VALUE=DATE:20041112
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=11

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318ABFE-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318ABFE-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318ABFE-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318ABFE-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20041125
 DTEND;VALUE=DATE:20041127
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=11;BYDAY=4TH

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318ADAA-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318ADAA-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318ADAA-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318ADAA-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20041225
 DTEND;VALUE=DATE:20041226
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=12

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318AF53-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318AF53-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318AF53-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318AF53-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20041231
 DTEND;VALUE=DATE:20050101
 RRULE:FREQ=YEARLY;INTERVAL=1

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318B108-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318B108-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318B108-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318B108-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20040912
 DTEND;VALUE=DATE:20040913
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=9;BYDAY=2SU

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318B2D2-1ED0-11D9-A5E0-000A958A3252.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318B2D2-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/Holidays/C318B2D2-1ED0-11D9-A5E0-000A958A3252.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -4,6 +4,7 @@
 VERSION:2.0
 BEGIN:VEVENT
 UID:C318B2D2-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20041015T171054Z
 DTSTART;VALUE=DATE:20041102
 DTEND;VALUE=DATE:20041103
 DESCRIPTION:Every four years on the first Tuesday after the first Monday in

Copied: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/OneEvent.ics (from rev 7695, CalendarServer/trunk/twistedcaldav/test/data/OneEvent.ics)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/OneEvent.ics	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/OneEvent.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,32 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.4//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:America/New_Yrok
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0500
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+DTSTART:20070311T020000
+TZNAME:EDT
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:-0400
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+DTSTART:20071104T020000
+TZNAME:EST
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20110516T031505Z
+UID:89E0257D-3522-4423-B94E-60FF10BDAAFA
+DTEND;TZID=America/New_Yrok:20110516T110000
+TRANSP:OPAQUE
+SUMMARY:One Event
+DTSTART;TZID=America/New_Yrok:20110516T094500
+DTSTAMP:20110516T031511Z
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR

Copied: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/ThirdEvent.ics (from rev 7695, CalendarServer/trunk/twistedcaldav/test/data/ThirdEvent.ics)
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/ThirdEvent.ics	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/data/ThirdEvent.ics	2011-06-30 20:44:13 UTC (rev 7697)
@@ -0,0 +1,32 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.4//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:US/Pacific
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0800
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+DTSTART:20070311T020000
+TZNAME:PDT
+TZOFFSETTO:-0700
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:-0700
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+DTSTART:20071104T020000
+TZNAME:PST
+TZOFFSETTO:-0800
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20110516T033401Z
+UID:39F43724-3103-4379-9A0A-11FAD4FF542A
+DTEND;TZID=US/Pacific:20110516T114500
+TRANSP:OPAQUE
+SUMMARY:Third Event
+DTSTART;TZID=US/Pacific:20110516T104500
+DTSTAMP:20110516T033404Z
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_calendarquery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_calendarquery.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_calendarquery.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -34,7 +34,6 @@
 from twistedcaldav.config import config
 from twistedcaldav.test.util import HomeTestCase
 from twisted.internet.defer import inlineCallbacks, returnValue
-from twistedcaldav.memcacher import Memcacher
 from txdav.common.datastore.test.util import buildStore, StubNotifierFactory
 
 
@@ -355,9 +354,6 @@
 
     @inlineCallbacks
     def setUp(self):
-        self.patch(config.Memcached.Pools.Default, "ClientEnabled", False)
-        self.patch(config.Memcached.Pools.Default, "ServerEnabled", False)
-        self.patch(Memcacher, "allowTestCache", True)
         self.calendarStore = yield buildStore(self, StubNotifierFactory())
         yield super(DatabaseQueryTests, self).setUp()
 

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_icalendar.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_icalendar.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -26,6 +26,7 @@
 
 from pycalendar.datetime import PyCalendarDateTime
 from pycalendar.timezone import PyCalendarTimezone
+from twistedcaldav.ical import iCalendarProductID
 from pycalendar.duration import PyCalendarDuration
 
 class iCalendar (twistedcaldav.test.util.TestCase):
@@ -50,6 +51,20 @@
             else:
                 SkipTest("test unimplemented")
 
+
+    def test_newCalendar(self):
+        """
+        L{Component.newCalendar} creates a new VCALENDAR L{Component} with
+        appropriate version and product identifiers, and no subcomponents.
+        """
+        calendar = Component.newCalendar()
+        version = calendar.getProperty("VERSION")
+        prodid = calendar.getProperty("PRODID")
+        self.assertEqual(version.value(), "2.0")
+        self.assertEqual(prodid.value(), iCalendarProductID)
+        self.assertEqual(list(calendar.subcomponents()), [])
+
+
     def test_component_equality(self):
 #        for filename in (
 #            os.path.join(self.data_dir, "Holidays", "C318A4BA-1ED0-11D9-A5E0-000A958A3252.ics"),
@@ -72,6 +87,7 @@
 UID:12345-67890
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 SUMMARY:Test
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
@@ -89,6 +105,7 @@
 UID:12345-67890
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 SUMMARY:Test
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
@@ -110,6 +127,7 @@
 UID:12345-67890
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 SUMMARY:Test
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
@@ -123,6 +141,7 @@
 RECURRENCE-ID:20080602T120000Z
 DTSTART:20080602T130000Z
 DTEND:20080602T140000Z
+DTSTAMP:20080601T120000Z
 SUMMARY:Test
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
@@ -138,6 +157,7 @@
 RECURRENCE-ID:20080602T120000Z
 DTSTART:20080602T130000Z
 DTEND:20080602T140000Z
+DTSTAMP:20080601T120000Z
 SUMMARY:Test
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
@@ -147,6 +167,7 @@
 UID:12345-67890
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 SUMMARY:Test
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
@@ -168,6 +189,7 @@
 UID:12345-67890
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 SUMMARY:Test
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
@@ -195,6 +217,7 @@
 UID:12345-67890
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 SUMMARY:Test
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
@@ -233,9 +256,13 @@
         CalDAV resource validation.
         """
         calendar = Component.fromStream(file(os.path.join(self.data_dir, "Holidays.ics")))
-        try: calendar.validateForCalDAV()
-        except ValueError: pass
-        else: self.fail("Monolithic iCalendar shouldn't validate for CalDAV")
+        try:
+            calendar.validCalendarData()
+            calendar.validCalendarForCalDAV(methodAllowed=False)
+        except ValueError:
+            pass
+        else:
+            self.fail("Monolithic iCalendar shouldn't validate for CalDAV")
 
         resource_dir = os.path.join(self.data_dir, "Holidays")
         for filename in resource_dir:
@@ -243,8 +270,11 @@
             filename = os.path.join(resource_dir, filename)
 
             calendar = Component.fromStream(file(filename))
-            try: calendar.validateForCalDAV()
-            except ValueError: self.fail("Resource iCalendar %s didn't validate for CalDAV" % (filename,))
+            try:
+                calendar.validCalendarData()
+                calendar.validCalendarForCalDAV(methodAllowed=False)
+            except ValueError:
+                self.fail("Resource iCalendar %s didn't validate for CalDAV" % (filename,))
 
     def test_component_validate_and_fix(self):
         """
@@ -252,6 +282,7 @@
         """
         data = """BEGIN:VCALENDAR
 VERSION:2.0
+PRODID:-//Apple Computer\, Inc//iCal 2.0//EN
 BEGIN:VTIMEZONE
 TZID:America/Los_Angeles
 BEGIN:DAYLIGHT
@@ -283,20 +314,24 @@
 """
         # Ensure it starts off invalid
         calendar = Component.fromString(data)
-        try: calendar.validateComponentsForCalDAV(False)
-        except InvalidICalendarDataError: pass
-        else: self.fail("Shouldn't validate for CalDAV")
+        try:
+            calendar.validCalendarData(doFix=False)
+        except InvalidICalendarDataError:
+            pass
+        else:
+            self.fail("Shouldn't validate for CalDAV")
 
         # Fix it
-        calendar.validateComponentsForCalDAV(False, fix=True)
+        calendar.validCalendarData(doFix=True)
         self.assertTrue("RRULE:FREQ=DAILY;UNTIL=20110121T203000Z\r\n"
             in str(calendar))
 
         # Now it should pass without fixing
-        calendar.validateComponentsForCalDAV(False, fix=False)
+        calendar.validCalendarData(doFix=False)
 
         data = """BEGIN:VCALENDAR
 VERSION:2.0
+PRODID:-//Apple Computer\, Inc//iCal 2.0//EN
 BEGIN:VTIMEZONE
 TZID:America/Los_Angeles
 BEGIN:DAYLIGHT
@@ -328,16 +363,19 @@
 """
         # Ensure it starts off invalid
         calendar = Component.fromString(data)
-        try: calendar.validateComponentsForCalDAV(False)
-        except InvalidICalendarDataError: pass
-        else: self.fail("Shouldn't validate for CalDAV")
+        try:
+            calendar.validCalendarData(doFix=False)
+        except InvalidICalendarDataError:
+            pass
+        else:
+            self.fail("Shouldn't validate for CalDAV")
 
         # Fix it
-        calendar.validateComponentsForCalDAV(False, fix=True)
+        calendar.validCalendarData(doFix=True)
         self.assertTrue("RRULE:FREQ=DAILY;UNTIL=20110131\r\n" in str(calendar))
 
         # Now it should pass without fixing
-        calendar.validateComponentsForCalDAV(False, fix=False)
+        calendar.validCalendarData(doFix=False)
 
 
     def test_component_timeranges(self):
@@ -476,11 +514,13 @@
         
         data = """BEGIN:VCALENDAR
 VERSION:2.0
+PRODID:-//Apple Computer\, Inc//iCal 2.0//EN
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20071114T000000Z
 END:VEVENT
 END:VCALENDAR
 """
@@ -492,11 +532,13 @@
         
         data = """BEGIN:VCALENDAR
 VERSION:2.0
+DTSTART:20071114T000000Z
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20071114T000000Z
 END:VEVENT
 END:VCALENDAR
 """
@@ -513,6 +555,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -526,6 +569,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
 END:VEVENT
@@ -541,6 +585,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ORGANIZER:mailto:user2 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -555,6 +600,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
 RRULE:FREQ=YEARLY
@@ -563,6 +609,7 @@
 UID:12345-67890
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
 END:VEVENT
@@ -579,6 +626,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
 RRULE:FREQ=YEARLY
@@ -587,6 +635,7 @@
 UID:12345-67890
 RECURRENCE-ID:20091114T000000Z
 DTSTART:20071114T020000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user3 at example.com
 ATTENDEE:mailto:user2 at example.com
 END:VEVENT
@@ -603,6 +652,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
 RRULE:FREQ=YEARLY
@@ -611,6 +661,7 @@
 UID:12345-67890
 RECURRENCE-ID:20091114T000000Z
 DTSTART:20071114T020000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user3 at example.com
 ORGANIZER:mailto:user4 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -636,6 +687,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -648,6 +700,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
 END:VEVENT
@@ -664,6 +717,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
 ATTENDEE:mailto:user3 at example.com
@@ -682,6 +736,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
 RRULE:FREQ=YEARLY
@@ -690,6 +745,7 @@
 UID:12345-67890
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE;SCHEDULE-AGENT=NONE:mailto:user2 at example.com
 ATTENDEE;SCHEDULE-AGENT=CLIENT:mailto:user3 at example.com
@@ -709,6 +765,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE;SCHEDULE-AGENT=NONE:mailto:user2 at example.com
 ATTENDEE:mailto:user3 at example.com
@@ -726,6 +783,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE;SCHEDULE-AGENT=SERVER:mailto:user2 at example.com
 RRULE:FREQ=YEARLY
@@ -734,6 +792,7 @@
 UID:12345-67890
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE;SCHEDULE-AGENT=NONE:mailto:user2 at example.com
 ATTENDEE;SCHEDULE-AGENT=CLIENT:mailto:user3 at example.com
@@ -763,6 +822,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user01 at example.com
 ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user01 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -775,6 +835,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user01 at example.com
 ATTENDEE;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user01 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -796,6 +857,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user01 at example.com
 ATTENDEE;SCHEDULE-STATUS=5.0:mailto:user02 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user01 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -808,6 +870,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user01 at example.com
 ATTENDEE;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user01 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -829,6 +892,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user01 at example.com
 ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user01 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -841,6 +905,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user01 at example.com
 ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;SCHEDULE-STATUS=2.0:mailto:user01 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -862,6 +927,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user01 at example.com
 ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;SCHEDULE-STATUS=5.0:mailto:user01 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -874,6 +940,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user01 at example.com
 ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER;SCHEDULE-STATUS=2.0:mailto:user01 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -902,6 +969,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -911,6 +979,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 REQUEST-STATUS:2.0;Success
 END:VEVENT
 END:VCALENDAR
@@ -924,12 +993,14 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 BEGIN:VEVENT
 UID:12345-67890-1
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T020000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -939,6 +1010,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 REQUEST-STATUS:2.0;Success
 RRULE:FREQ=DAILY
 END:VEVENT
@@ -946,6 +1018,7 @@
 UID:12345-67890-1
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T020000Z
+DTSTAMP:20080601T120000Z
 REQUEST-STATUS:2.0;Success
 END:VEVENT
 END:VCALENDAR
@@ -969,6 +1042,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -979,6 +1053,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -993,6 +1068,7 @@
 BEGIN:VEVENT
 UID:12345-67890-2
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1014,6 +1090,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1026,6 +1103,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1042,6 +1120,7 @@
 UID:12345-67890-4
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1064,6 +1143,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 END:VEVENT
@@ -1072,6 +1152,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1084,6 +1165,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 END:VEVENT
@@ -1092,6 +1174,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1108,6 +1191,7 @@
 UID:12345-67890-4
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 END:VEVENT
@@ -1116,6 +1200,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1138,6 +1223,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 END:VEVENT
@@ -1145,6 +1231,7 @@
 UID:12345-67890
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1157,6 +1244,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 EXDATE:20081114T000000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
@@ -1175,6 +1263,7 @@
 UID:12345-67890-4
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 END:VEVENT
@@ -1182,6 +1271,7 @@
 UID:12345-67890
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1203,6 +1293,7 @@
 BEGIN:VEVENT
 UID:12345-67890-3
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 END:VEVENT
@@ -1211,6 +1302,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1224,6 +1316,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1239,6 +1332,7 @@
 BEGIN:VEVENT
 UID:12345-67890-4
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 END:VEVENT
@@ -1247,6 +1341,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1268,6 +1363,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1278,6 +1374,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1292,6 +1389,7 @@
 BEGIN:VEVENT
 UID:12345-67890-2
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1313,6 +1411,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1325,6 +1424,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1341,6 +1441,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE;SCHEDULE-AGENT=SERVER:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1353,6 +1454,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE;SCHEDULE-AGENT=SERVER:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1369,6 +1471,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE;SCHEDULE-AGENT=CLIENT:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1391,6 +1494,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE;SCHEDULE-AGENT=NONE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1422,6 +1526,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1431,6 +1536,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1446,6 +1552,7 @@
 UID:12345-67890-2
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1456,6 +1563,7 @@
 BEGIN:VEVENT
 UID:12345-67890-2
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1472,6 +1580,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1483,6 +1592,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1500,6 +1610,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
 ATTENDEE:mailto:user3 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1510,6 +1621,7 @@
 BEGIN:VEVENT
 UID:12345-67890-4
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1527,6 +1639,7 @@
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
 ATTENDEE:mailto:user3 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1538,6 +1651,7 @@
 UID:12345-67890-5
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1564,6 +1678,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 SUMMARY:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
@@ -1577,6 +1692,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1594,6 +1710,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 BEGIN:VALARM
@@ -1607,6 +1724,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1618,6 +1736,7 @@
 BEGIN:VEVENT
 UID:12345-67890-3
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 BEGIN:VALARM
 ACTION:DISPLAY
@@ -1629,6 +1748,7 @@
 UID:12345-67890
 RECURRENCE-ID:20081114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1655,6 +1775,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 SUMMARY:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
@@ -1668,6 +1789,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1685,6 +1807,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 BEGIN:VALARM
@@ -1698,6 +1821,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1709,6 +1833,7 @@
 BEGIN:VEVENT
 UID:12345-67890-3
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 BEGIN:VALARM
@@ -1721,6 +1846,7 @@
 UID:12345-67890
 RECURRENCE-ID:20081114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1746,6 +1872,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1755,6 +1882,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1768,6 +1896,7 @@
 BEGIN:VEVENT
 UID:12345-67890-2
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 BEGIN:VALARM
 ACTION:DISPLAY
 DESCRIPTION:Test
@@ -1782,6 +1911,7 @@
 BEGIN:VEVENT
 UID:12345-67890-2
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -1796,6 +1926,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 BEGIN:VALARM
@@ -1809,6 +1940,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1820,6 +1952,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 END:VEVENT
@@ -1828,6 +1961,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1843,6 +1977,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 BEGIN:VALARM
@@ -1856,6 +1991,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 BEGIN:VALARM
 ACTION:DISPLAY
@@ -1872,6 +2008,7 @@
 UID:12345-67890-3
 DTSTART:20071114T000000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 RRULE:FREQ=YEARLY
 END:VEVENT
@@ -1880,6 +2017,7 @@
 RECURRENCE-ID:20081114T000000Z
 DTSTART:20071114T010000Z
 ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
 END:VCALENDAR
@@ -1903,6 +2041,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 DURATION:PT1H
 END:VEVENT
 END:VCALENDAR
@@ -1918,6 +2057,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 DURATION:PT1H
 RRULE:FREQ=DAILY;COUNT=2
 END:VEVENT
@@ -1937,6 +2077,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 DURATION:PT1H
 RRULE:FREQ=DAILY;COUNT=2
 RDATE:20071116T010000Z
@@ -1958,6 +2099,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 DURATION:PT1H
 RRULE:FREQ=DAILY;COUNT=3
 EXDATE:20071115T000000Z
@@ -1978,6 +2120,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 DURATION:PT1H
 RRULE:FREQ=DAILY;COUNT=3
 EXDATE:20071114T000000Z
@@ -1998,6 +2141,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 DURATION:PT1H
 RRULE:FREQ=DAILY;COUNT=2
 END:VEVENT
@@ -2005,6 +2149,7 @@
 UID:12345-67890-1
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
+DTSTAMP:20080601T120000Z
 DURATION:PT1H
 END:VEVENT
 END:VCALENDAR
@@ -2024,6 +2169,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY;COUNT=2
 END:VEVENT
 BEGIN:VEVENT
@@ -2031,6 +2177,7 @@
 RECURRENCE-ID:20071115T010000Z
 DTSTART:20071115T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2046,6 +2193,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY;COUNT=2
 END:VEVENT
 BEGIN:VEVENT
@@ -2053,6 +2201,7 @@
 RECURRENCE-ID:20071115T010000Z
 DTSTART:20071115T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2086,6 +2235,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2101,6 +2251,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2116,6 +2267,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 BEGIN:VEVENT
@@ -2123,6 +2275,7 @@
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2138,6 +2291,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 BEGIN:VEVENT
@@ -2145,6 +2299,7 @@
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2160,6 +2315,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 BEGIN:VEVENT
@@ -2167,6 +2323,7 @@
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2182,6 +2339,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 BEGIN:VEVENT
@@ -2189,6 +2347,7 @@
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2204,6 +2363,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 BEGIN:VEVENT
@@ -2211,6 +2371,7 @@
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2226,12 +2387,14 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 BEGIN:VEVENT
 UID:12345-67890-1
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
+DTSTAMP:20080601T120000Z
 DURATION:PT1H
 END:VEVENT
 END:VCALENDAR
@@ -2257,6 +2420,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM1:True
 END:VEVENT
 END:VCALENDAR
@@ -2268,6 +2432,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM2:True
 END:VEVENT
 END:VCALENDAR
@@ -2279,6 +2444,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM1:True
 X-ITEM2:True
 END:VEVENT
@@ -2295,6 +2461,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM1:True
 END:VEVENT
 END:VCALENDAR
@@ -2306,6 +2473,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM2:True
 X-ITEM3:True
 END:VEVENT
@@ -2318,6 +2486,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM1:True
 X-ITEM2:True
 X-ITEM3:True
@@ -2335,6 +2504,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM1:True
 END:VEVENT
 END:VCALENDAR
@@ -2346,6 +2516,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM2:True
 X-ITEM1:False
 END:VEVENT
@@ -2358,6 +2529,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM1:True
 X-ITEM2:True
 X-ITEM1:False
@@ -2375,6 +2547,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 X-ITEM1:True
 END:VEVENT
@@ -2383,6 +2556,7 @@
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM1:False
 END:VEVENT
 END:VCALENDAR
@@ -2394,6 +2568,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 X-ITEM2:True
 END:VEVENT
@@ -2402,6 +2577,7 @@
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM2:False
 END:VEVENT
 END:VCALENDAR
@@ -2413,6 +2589,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 X-ITEM1:True
 X-ITEM2:True
@@ -2422,6 +2599,7 @@
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM1:False
 X-ITEM2:False
 END:VEVENT
@@ -2438,6 +2616,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 X-ITEM1:True
 END:VEVENT
@@ -2446,6 +2625,7 @@
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM1:False
 END:VEVENT
 END:VCALENDAR
@@ -2457,6 +2637,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 X-ITEM2:True
 END:VEVENT
@@ -2469,6 +2650,7 @@
 UID:12345-67890-1
 DTSTART:20071114T000000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 X-ITEM1:True
 X-ITEM2:True
@@ -2478,6 +2660,7 @@
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
 DURATION:PT1H
+DTSTAMP:20080601T120000Z
 X-ITEM1:False
 X-ITEM2:True
 END:VEVENT
@@ -2505,6 +2688,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART;VALUE=DATE-TIME:20071114T000000Z
+DTSTAMP:20080601T120000Z
 SEQUENCE:0
 END:VEVENT
 END:VCALENDAR
@@ -2515,6 +2699,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2527,6 +2712,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART;VALUE=DATE-TIME:20071114T000000Z
+DTSTAMP:20080601T120000Z
 TRANSP:OPAQUE
 ORGANIZER:mailto:user01 at example.com
 ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
@@ -2542,6 +2728,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user01 at example.com
 ATTENDEE;RSVP=TRUE:mailto:user02 at example.com
 ATTENDEE:mailto:user03 at example.com
@@ -2559,6 +2746,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART;VALUE=DATE-TIME:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=1;BYDAY=MO,WE,FR
 TRANSP:OPAQUE
 ORGANIZER:mailto:user01 at example.com
@@ -2575,6 +2763,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user01 at example.com
 ATTENDEE;RSVP=TRUE:mailto:user02 at example.com
 ATTENDEE:mailto:user03 at example.com
@@ -2610,6 +2799,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART;TZID=US/Pacific:20071114T000000
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=1;BYDAY=MO,WE,FR
 TRANSP:OPAQUE
 ORGANIZER:mailto:user01 at example.com
@@ -2643,6 +2833,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART;_TZID=US/Pacific:20071114T080000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER:mailto:user01 at example.com
 ATTENDEE;RSVP=TRUE:mailto:user02 at example.com
 ATTENDEE:mailto:user03 at example.com
@@ -2675,6 +2866,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART;VALUE=DATE-TIME:20071114
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2684,6 +2876,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART;VALUE=DATE-TIME:20071114
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2697,6 +2890,7 @@
 UID:12345-67890-1
 DTSTART;VALUE=DATE-TIME:20071114
 ATTACH:http://example.com/file.txt
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2707,6 +2901,7 @@
 UID:12345-67890-1
 DTSTART;VALUE=DATE-TIME:20071114
 ATTACH:http://example.com/file.txt
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2720,6 +2915,7 @@
 UID:12345-67890-1
 DTSTART;VALUE=DATE-TIME:20071114
 ATTACH:http://example.com/calendars/user.dropbox/file.txt
+DTSTAMP:20080601T120000Z
 X-APPLE-DROPBOX:/calendars/user.dropbox
 END:VEVENT
 END:VCALENDAR
@@ -2730,6 +2926,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART;VALUE=DATE-TIME:20071114
+DTSTAMP:20080601T120000Z
 X-APPLE-DROPBOX:/calendars/user.dropbox
 END:VEVENT
 END:VCALENDAR
@@ -2744,6 +2941,7 @@
 UID:12345-67890-1
 DTSTART;VALUE=DATE-TIME:20071114
 ATTACH:http://example.com/calendars/user.dropbox/file.txt
+DTSTAMP:20080601T120000Z
 X-APPLE-DROPBOX:/calendars/user1.dropbox
 END:VEVENT
 END:VCALENDAR
@@ -2755,6 +2953,7 @@
 UID:12345-67890-1
 DTSTART;VALUE=DATE-TIME:20071114
 ATTACH:http://example.com/calendars/user.dropbox/file.txt
+DTSTAMP:20080601T120000Z
 X-APPLE-DROPBOX:/calendars/user1.dropbox
 END:VEVENT
 END:VCALENDAR
@@ -2783,6 +2982,7 @@
 UID:12345-67890-1
 DTSTART:20090101T000000Z
 DTEND:20090102T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -2797,6 +2997,7 @@
 UID:12345-67890-1
 DTSTART:20090101T000000Z
 DTEND:20090102T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY;COUNT=2
 END:VEVENT
 END:VCALENDAR
@@ -2812,6 +3013,7 @@
 UID:12345-67890-1
 DTSTART:20090101T000000Z
 DTEND:20090102T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY;UNTIL=20090108T000000Z
 END:VEVENT
 END:VCALENDAR
@@ -2827,6 +3029,7 @@
 UID:12345-67890-1
 DTSTART:20090101T000000Z
 DTEND:20090102T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
@@ -2852,6 +3055,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
@@ -2862,6 +3066,7 @@
 RECURRENCE-ID:20090102T080000Z
 DTSTART:20090102T080000Z
 DTEND:20090102T090000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
             ),
@@ -2874,6 +3079,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 RDATE:20090102T180000Z
 END:VEVENT
@@ -2885,6 +3091,7 @@
 RECURRENCE-ID:20090102T180000Z
 DTSTART:20090102T180000Z
 DTEND:20090102T190000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
             ),
@@ -2897,6 +3104,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 RDATE:20090102T180000Z,20090103T180000Z
 RDATE:20090104T180000Z
@@ -2909,6 +3117,7 @@
 RECURRENCE-ID:20090103T180000Z
 DTSTART:20090103T180000Z
 DTEND:20090103T190000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
             ),
@@ -2921,6 +3130,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
@@ -2937,6 +3147,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 RDATE:20090102T180000Z
 END:VEVENT
@@ -2954,6 +3165,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 RDATE:20090102T180000Z,20090103T180000Z
 RDATE:20090104T180000Z
@@ -2972,6 +3184,7 @@
 UID:12345-67890-1
 DTSTART;VALUE=DATE:20090101
 DTEND;VALUE=DATE:20090102
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY
 END:VEVENT
 END:VCALENDAR
@@ -2982,6 +3195,7 @@
 RECURRENCE-ID;VALUE=DATE:20090108
 DTSTART;VALUE=DATE:20090108
 DTEND;VALUE=DATE:20090109
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
             ),
@@ -2994,6 +3208,7 @@
 UID:12345-67890-1
 DTSTART;VALUE=DATE:20090101
 DTEND;VALUE=DATE:20090102
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY
 RDATE;VALUE=DATE:20090103
 END:VEVENT
@@ -3005,6 +3220,7 @@
 RECURRENCE-ID;VALUE=DATE:20090103
 DTSTART;VALUE=DATE:20090103
 DTEND;VALUE=DATE:20090104
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
             ),
@@ -3017,6 +3233,7 @@
 UID:12345-67890-1
 DTSTART;VALUE=DATE:20090101
 DTEND;VALUE=DATE:20090102
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY
 RDATE;VALUE=DATE:20090103,20090110
 RDATE;VALUE=DATE:20090118
@@ -3029,6 +3246,7 @@
 RECURRENCE-ID;VALUE=DATE:20090110
 DTSTART;VALUE=DATE:20090110
 DTEND;VALUE=DATE:20090111
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
             ),
@@ -3041,6 +3259,7 @@
 UID:12345-67890-1
 DTSTART;VALUE=DATE:20090101
 DTEND;VALUE=DATE:20090102
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY
 END:VEVENT
 END:VCALENDAR
@@ -3057,6 +3276,7 @@
 UID:12345-67890-1
 DTSTART;VALUE=DATE:20090101
 DTEND;VALUE=DATE:20090102
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY
 RDATE;VALUE=DATE:20090104
 END:VEVENT
@@ -3074,6 +3294,7 @@
 UID:12345-67890-1
 DTSTART;VALUE=DATE:20090101
 DTEND;VALUE=DATE:20090102
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY
 RDATE;VALUE=DATE:20090104,20090111
 RDATE;VALUE=DATE:20090118
@@ -3103,6 +3324,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
@@ -3117,6 +3339,7 @@
 RECURRENCE-ID:20090102T080000Z
 DTSTART:20090102T080000Z
 DTEND:20090102T090000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
                     """BEGIN:VEVENT
@@ -3124,6 +3347,7 @@
 RECURRENCE-ID:20090104T080000Z
 DTSTART:20090104T080000Z
 DTEND:20090104T090000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
                 ),
@@ -3137,6 +3361,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 RDATE:20090102T180000Z
 END:VEVENT
@@ -3152,6 +3377,7 @@
 RECURRENCE-ID:20090102T180000Z
 DTSTART:20090102T180000Z
 DTEND:20090102T190000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
                     """BEGIN:VEVENT
@@ -3159,6 +3385,7 @@
 RECURRENCE-ID:20090104T080000Z
 DTSTART:20090104T080000Z
 DTEND:20090104T090000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
                 ),
@@ -3172,6 +3399,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 RDATE:20090102T180000Z,20090103T180000Z
 RDATE:20090104T180000Z
@@ -3188,6 +3416,7 @@
 RECURRENCE-ID:20090103T180000Z
 DTSTART:20090103T180000Z
 DTEND:20090103T190000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
                     """BEGIN:VEVENT
@@ -3195,6 +3424,7 @@
 RECURRENCE-ID:20090105T080000Z
 DTSTART:20090105T080000Z
 DTEND:20090105T090000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
                 ),
@@ -3208,6 +3438,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
@@ -3223,6 +3454,7 @@
 RECURRENCE-ID:20090103T080000Z
 DTSTART:20090103T080000Z
 DTEND:20090103T090000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
                 ),
@@ -3236,6 +3468,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 RDATE:20090102T180000Z
 END:VEVENT
@@ -3252,6 +3485,7 @@
 RECURRENCE-ID:20090103T080000Z
 DTSTART:20090103T080000Z
 DTEND:20090103T090000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
                 ),
@@ -3265,6 +3499,7 @@
 UID:12345-67890-1
 DTSTART:20090101T080000Z
 DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 RDATE:20090102T180000Z,20090103T180000Z
 RDATE:20090104T180000Z
@@ -3282,6 +3517,7 @@
 RECURRENCE-ID:20090103T080000Z
 DTSTART:20090103T080000Z
 DTEND:20090103T090000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 """,
                 ),
@@ -3306,6 +3542,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -3319,6 +3556,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY;COUNT=2
 END:VEVENT
 END:VCALENDAR
@@ -3333,6 +3571,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY;UNTIL=20071128T000000Z
 END:VEVENT
 END:VCALENDAR
@@ -3347,6 +3586,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY;COUNT=2000
 END:VEVENT
 END:VCALENDAR
@@ -3357,6 +3597,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:COUNT=400;FREQ=WEEKLY
 END:VEVENT
 END:VCALENDAR
@@ -3370,6 +3611,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY;UNTIL=20471128T000000Z
 END:VEVENT
 END:VCALENDAR
@@ -3380,6 +3622,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:COUNT=400;FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
@@ -3393,6 +3636,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=WEEKLY;UNTIL=20071128T000000Z
 END:VEVENT
 END:VCALENDAR
@@ -3407,6 +3651,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
@@ -3417,6 +3662,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:COUNT=400;FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
@@ -3452,6 +3698,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -3469,6 +3716,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RDATE:20091004T000000Z
 END:VEVENT
 END:VCALENDAR
@@ -3488,6 +3736,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
@@ -3508,6 +3757,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 RDATE:20091004T010000Z
 END:VEVENT
@@ -3530,6 +3780,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 RDATE:20091004T010000Z
 EXDATE:20091003T000000Z
@@ -3554,6 +3805,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 END:VEVENT
 BEGIN:VEVENT
@@ -3580,6 +3832,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 RRULE:FREQ=DAILY
 RDATE:20071115T010000Z
 END:VEVENT
@@ -3587,6 +3840,7 @@
 UID:12345-67890-1
 RECURRENCE-ID:20071115T010000Z
 DTSTART:20071115T020000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -3609,6 +3863,7 @@
 UID:12345-67890-1
 RECURRENCE-ID:20071115T000000Z
 DTSTART:20071115T010000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -3627,6 +3882,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -3642,6 +3898,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -3657,6 +3914,7 @@
 BEGIN:VEVENT
 UID:12345-67890-1
 DTSTART:20071114T000000Z
+DTSTAMP:20080601T120000Z
 END:VEVENT
 END:VCALENDAR
 """,
@@ -3769,11 +4027,12 @@
 
         for text in invalid:
             calendar = Component.fromString(text)
-            self.assertRaises(InvalidICalendarDataError, calendar.validateForCalDAV)
+            self.assertRaises(InvalidICalendarDataError, calendar.validCalendarData, doFix=False)
         for text in valid:
             calendar = Component.fromString(text)
             try:
-                calendar.validateForCalDAV()
+                calendar.validCalendarData()
+                calendar.validCalendarForCalDAV(methodAllowed=False)
             except:
                 self.fail("Valid calendar should validate")
 
@@ -3783,6 +4042,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
+DTSTAMP:20080601T120000Z
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
@@ -3826,6 +4086,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
+DTSTAMP:20080601T120000Z
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
@@ -3851,6 +4112,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
+DTSTAMP:20080601T120000Z
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
@@ -3888,6 +4150,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
+DTSTAMP:20080601T120000Z
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
@@ -3933,6 +4196,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
+DTSTAMP:20080601T120000Z
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
@@ -3964,6 +4228,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
+DTSTAMP:20080601T120000Z
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
@@ -4009,6 +4274,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
+DTSTAMP:20080601T120000Z
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
@@ -4063,6 +4329,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
+DTSTAMP:20080601T120000Z
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
@@ -4074,6 +4341,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 RECURRENCE-ID:20080602T120000Z
+DTSTAMP:20080601T120000Z
 DTSTART:20080602T130000Z
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
@@ -4117,6 +4385,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
+DTSTAMP:20080601T120000Z
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
@@ -4127,6 +4396,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 RECURRENCE-ID:20080602T120000Z
+DTSTAMP:20080601T120000Z
 DTSTART:20080602T130000Z
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com
@@ -4193,6 +4463,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
+DTSTAMP:20080601T120000Z
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
 ATTENDEE:mailto:user1 at example.com
@@ -4203,6 +4474,7 @@
 BEGIN:VEVENT
 UID:12345-67890
 RECURRENCE-ID:20080602T120000Z
+DTSTAMP:20080601T120000Z
 DTSTART:20080602T130000Z
 DTEND:20080602T140000Z
 ATTENDEE:mailto:user1 at example.com

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_mail.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_mail.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_mail.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -25,6 +25,7 @@
 from twistedcaldav.mail import MailHandler
 from twistedcaldav.mail import MailGatewayTokensDatabase
 import os
+import datetime
 
 
 def echo(*args):
@@ -38,6 +39,32 @@
         self.handler = MailHandler(dataRoot=":memory:")
         self.dataDir = os.path.join(os.path.dirname(__file__), "data", "mail")
 
+
+    def test_purge(self):
+        """
+        Ensure that purge( ) cleans out old tokens
+        """
+
+        # Insert an "old" token
+        token = "test_token"
+        organizer = "me at example.com"
+        attendee = "you at example.com"
+        icaluid = "123"
+        pastDate = datetime.date(2009,1,1)
+        self.handler.db._db_execute(
+            """
+            insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)
+            values (:1, :2, :3, :4, :5)
+            """, token, organizer, attendee, icaluid, pastDate
+        )
+        self.handler.db._db_commit()
+
+        # purge, and make sure we don't see that token anymore
+        self.handler.purge()
+        retrieved = self.handler.db.getToken(organizer, attendee, icaluid)
+        self.assertEquals(retrieved, None)
+
+
     def test_iconPath(self):
         iconPath = self.handler.getIconPath({'day':'1', 'month':'1'}, False, language='en')
         iconDir = "/usr/share/caldavd/share/date_icons"
@@ -216,28 +243,28 @@
         data = (
             # Initial invite
             (
-                """BEGIN:VCALENDAR
+                u"""BEGIN:VCALENDAR
 VERSION:2.0
 METHOD:REQUEST
 BEGIN:VEVENT
 UID:CFDD5E46-4F74-478A-9311-B3FF905449C3
 DTSTART:20100325T154500Z
 DTEND:20100325T164500Z
-ATTENDEE;CN=The Attendee;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:attendee at example.com
+ATTENDEE;CN=Th\xe9 Attendee;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:attendee at example.com
 ATTENDEE;CN=The Organizer;CUTYPE=INDIVIDUAL;EMAIL=organizer at example.com;PARTSTAT=ACCEPTED:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
 ORGANIZER;CN=The Organizer;EMAIL=organizer at example.com:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
-SUMMARY:testing outbound( )
+SUMMARY:t\xe9sting outbound( )
 END:VEVENT
 END:VCALENDAR
 """,
                 "CFDD5E46-4F74-478A-9311-B3FF905449C3",
-                "mailto:organizer at example.com",
+                "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
                 "mailto:attendee at example.com",
                 "new",
                 "organizer at example.com",
                 "The Organizer",
                 [
-                    (u'The Attendee', u'attendee at example.com'),
+                    (u'Th\xe9 Attendee', u'attendee at example.com'),
                     (u'The Organizer', u'organizer at example.com')
                 ],
                 "The Organizer <organizer at example.com>",
@@ -261,7 +288,7 @@
 END:VCALENDAR
 """,
                 "CFDD5E46-4F74-478A-9311-B3FF905449C3",
-                "mailto:organizer at example.com",
+                "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
                 "mailto:attendee at example.com",
                 "update",
                 "organizer at example.com",
@@ -290,7 +317,7 @@
 END:VCALENDAR
 """,
                 None,
-                "mailto:attendee at example.com",
+                "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
                 "mailto:organizer at example.com",
                 "reply",
                 "organizer at example.com",

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_multiget.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_multiget.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_multiget.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -121,6 +121,7 @@
 UID:good
 DTSTART;VALUE=DATE:20020101
 DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T121212Z
 RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20031231;BYMONTH=1
 SUMMARY:New Year's Day
 END:VEVENT
@@ -134,6 +135,7 @@
 UID:bad
 DTSTART;VALUE=DATE:20020214
 DTEND;VALUE=DATE:20020215
+DTSTAMP:20020101T121212Z
 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2
 SUMMARY:Valentine's Day
 END:VEVENT
@@ -154,6 +156,7 @@
 UID:bad
 DTSTART;VALUE=DATE:20020214
 DTEND;VALUE=DATE:20020
+DTSTAMP:20020101T121212Z
 END:VCALENDAR
 """.replace("\n", "\r\n"))
         f.close

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_resource.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_resource.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2008 Apple Inc. All rights reserved.
+# Copyright (c) 2008-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,15 +14,24 @@
 # limitations under the License.
 ##
 
-from twistedcaldav.resource import CalDAVResource, CommonHomeResource, CalendarHomeResource, AddressBookHomeResource
+from twext.web2.dav import davxml
+from twext.web2.dav.davxml import Principal
+from twext.web2.dav.davxml import Unauthenticated
+from twext.web2.dav.element.rfc2518 import HRef
+from twext.web2.http import HTTPError
+from twext.web2.test.test_server import SimpleRequest
 
+from twisted.internet.defer import inlineCallbacks
+
+from twistedcaldav import carddavxml
+from twistedcaldav.config import config
+from twistedcaldav.resource import CalDAVResource, CommonHomeResource, \
+ CalendarHomeResource, AddressBookHomeResource
 from twistedcaldav.test.util import InMemoryPropertyStore
 from twistedcaldav.test.util import TestCase
-from twistedcaldav.config import config
+from twistedcaldav.test.util import patchConfig
 
-from twisted.internet.defer import inlineCallbacks
 
-
 class StubProperty(object):
     def qname(self):
         return "StubQnamespace", "StubQname"
@@ -62,6 +71,7 @@
         self.assertTrue(('http://calendarserver.org/ns/', 'push-transports') in resource.liveProperties())
         self.assertTrue(('http://calendarserver.org/ns/', 'pushkey') in resource.liveProperties())
 
+
     def test_calendarHomeliveProperties(self):
         resource = CalendarHomeResource(None, None, None, StubHome())
         self.assertTrue(('http://calendarserver.org/ns/', 'push-transports') in resource.liveProperties())
@@ -70,6 +80,7 @@
         self.assertTrue(('http://calendarserver.org/ns/', 'xmpp-heartbeat-uri') in resource.liveProperties())
         self.assertTrue(('http://calendarserver.org/ns/', 'xmpp-server') in resource.liveProperties())
 
+
     def test_addressBookHomeliveProperties(self):
         resource = AddressBookHomeResource(None, None, None, StubHome())
         self.assertTrue(('http://calendarserver.org/ns/', 'push-transports') in resource.liveProperties())
@@ -78,6 +89,7 @@
         self.assertTrue(('http://calendarserver.org/ns/', 'xmpp-heartbeat-uri') not in resource.liveProperties())
         self.assertTrue(('http://calendarserver.org/ns/', 'xmpp-server') not in resource.liveProperties())
 
+
     @inlineCallbacks
     def test_push404(self):
         """
@@ -117,3 +129,215 @@
         self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'xmpp-uri'), None)), None)
         self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'xmpp-heartbeat-uri'), None)), None)
         self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'xmpp-server'), None)), None)
+
+
+
+class OwnershipTests(TestCase):
+    """
+    L{CalDAVResource.isOwner} determines if the authenticated principal of the
+    given request is the owner of that resource.
+    """
+
+    @inlineCallbacks
+    def test_isOwnerUnauthenticated(self):
+        """
+        L{CalDAVResource.isOwner} returns C{False} for unauthenticated requests.
+        """
+        site = None
+        request = SimpleRequest(site, "GET", "/not/a/real/url/")
+        request.authzUser = request.authnUser = Principal(Unauthenticated())
+        rsrc = CalDAVResource()
+        rsrc.owner = lambda igreq: HRef("/somebody/")
+        self.assertEquals((yield rsrc.isOwner(request)), False)
+
+
+    @inlineCallbacks
+    def test_isOwnerNo(self):
+        """
+        L{CalDAVResource.isOwner} returns C{True} for authenticated requests
+        with a principal that matches the resource's owner.
+        """
+        site = None
+        request = SimpleRequest(site, "GET", "/not/a/real/url/")
+        theOwner = Principal(HRef("/yes-i-am-the-owner/"))
+        request.authzUser = request.authnUser = theOwner
+        rsrc = CalDAVResource()
+        rsrc.owner = lambda igreq: HRef("/no-i-am-not-the-owner/")
+        self.assertEquals((yield rsrc.isOwner(request)), False)
+
+
+    @inlineCallbacks
+    def test_isOwnerYes(self):
+        """
+        L{CalDAVResource.isOwner} returns C{True} for authenticated requests
+        with a principal that matches the resource's owner.
+        """
+        site = None
+        request = SimpleRequest(site, "GET", "/not/a/real/url/")
+        theOwner = Principal(HRef("/yes-i-am-the-owner/"))
+        request.authzUser = request.authnUser = theOwner
+        rsrc = CalDAVResource()
+        rsrc.owner = lambda igreq: HRef("/yes-i-am-the-owner/")
+        self.assertEquals((yield rsrc.isOwner(request)), True)
+
+
+    @inlineCallbacks
+    def test_isOwnerAdmin(self):
+        """
+        L{CalDAVResource.isOwner} returns C{True} for authenticated requests
+        with a principal that matches any principal configured in the
+        L{AdminPrincipals} list.
+        """
+        theAdmin = "/read-write-admin/"
+        patchConfig(self, AdminPrincipals=[theAdmin])
+        site = None
+        request = SimpleRequest(site, "GET", "/not/a/real/url/")
+        request.authzUser = request.authnUser = Principal(HRef(theAdmin))
+        rsrc = CalDAVResource()
+        rsrc.owner = lambda igreq: HRef("/some-other-user/")
+        self.assertEquals((yield rsrc.isOwner(request)), True)
+
+
+    @inlineCallbacks
+    def test_isOwnerReadPrincipal(self):
+        """
+        L{CalDAVResource.isOwner} returns C{True} for authenticated requests
+        with a principal that matches any principal configured in the
+        L{AdminPrincipals} list.
+        """
+        theAdmin = "/read-only-admin/"
+        patchConfig(self, ReadPrincipals=[theAdmin])
+        site = None
+        request = SimpleRequest(site, "GET", "/not/a/real/url/")
+        request.authzUser = request.authnUser = Principal(HRef(theAdmin))
+        rsrc = CalDAVResource()
+        rsrc.owner = lambda igreq: HRef("/some-other-user/")
+        self.assertEquals((yield rsrc.isOwner(request)), True)
+
+
+class DefaultAddressBook (TestCase):
+
+    def setUp(self):
+        super(DefaultAddressBook, self).setUp()
+        self.createStockDirectoryService()
+        self.setupCalendars()
+
+    @inlineCallbacks
+    def test_pick_default_addressbook(self):
+        """
+        Make calendar
+        """
+        
+
+        request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
+        home = yield request.locateResource("/addressbooks/users/wsanchez")
+
+        # default property initially not present
+        try:
+            home.readDeadProperty(carddavxml.DefaultAddressBookURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("carddavxml.DefaultAddressBookURL is not empty")
+
+        yield home.pickNewDefaultAddressBook(request)
+
+        try:
+            default = home.readDeadProperty(carddavxml.DefaultAddressBookURL)
+        except HTTPError:
+            self.fail("carddavxml.DefaultAddressBookURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/addressbooks/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/addressbook")
+
+        request._newStoreTransaction.abort()
+
+    @inlineCallbacks
+    def test_pick_default_other(self):
+        """
+        Make adbk
+        """
+        
+
+        request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
+        home = yield request.locateResource("/addressbooks/users/wsanchez")
+
+        # default property not present
+        try:
+            home.readDeadProperty(carddavxml.DefaultAddressBookURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("carddavxml.DefaultAddressBookURL is not empty")
+
+        # Create a new default adbk
+        newadbk = yield request.locateResource("/addressbooks/users/wsanchez/newadbk")
+        yield newadbk.createAddressBookCollection()
+        home.writeDeadProperty(carddavxml.DefaultAddressBookURL(
+            davxml.HRef("/addressbooks/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newadbk")
+        ))
+        request._newStoreTransaction.commit()
+        
+        # Delete the normal adbk
+        request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
+        home = yield request.locateResource("/addressbooks/users/wsanchez")
+        adbk = yield request.locateResource("/addressbooks/users/wsanchez/addressbook")
+        yield adbk.storeRemove(request, False, "/addressbooks/users/wsanchez/addressbook")
+
+        home.removeDeadProperty(carddavxml.DefaultAddressBookURL)
+        
+        # default property not present
+        try:
+            home.readDeadProperty(carddavxml.DefaultAddressBookURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("carddavxml.DefaultAddressBookURL is not empty")
+        request._newStoreTransaction.commit()
+
+        request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
+        home = yield request.locateResource("/addressbooks/users/wsanchez")
+        yield home.pickNewDefaultAddressBook(request)
+
+        try:
+            default = home.readDeadProperty(carddavxml.DefaultAddressBookURL)
+        except HTTPError:
+            self.fail("carddavxml.DefaultAddressBookURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/addressbooks/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newadbk")
+
+        request._newStoreTransaction.abort()
+
+    @inlineCallbacks
+    def test_fix_shared_default(self):
+        """
+        Make calendar
+        """
+        
+
+        request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
+        home = yield request.locateResource("/addressbooks/users/wsanchez")
+
+        # Create a new default adbk
+        newadbk = yield request.locateResource("/addressbooks/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newadbk")
+        yield newadbk.createAddressBookCollection()
+        home.writeDeadProperty(carddavxml.DefaultAddressBookURL(
+            davxml.HRef("/addressbooks/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newadbk")
+        ))
+        try:
+            default = yield home.readProperty(carddavxml.DefaultAddressBookURL, request)
+        except HTTPError:
+            self.fail("carddavxml.DefaultAddressBookURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/addressbooks/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newadbk")
+        
+        # Force the new calendar to think it is a virtual share
+        newadbk._isVirtualShare = True
+        
+        try:
+            default = yield home.readProperty(carddavxml.DefaultAddressBookURL, request)
+        except HTTPError:
+            self.fail("carddavxml.DefaultAddressBookURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/addressbooks/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/addressbook")
+
+        request._newStoreTransaction.abort()

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_schedule.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_schedule.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -15,16 +15,18 @@
 ##
 
 from twext.web2 import responsecode
-from twext.web2.iweb import IResponse
 from twext.web2.dav import davxml
 from twext.web2.dav.util import davXMLFromStream
+from twext.web2.http import HTTPError
+from twext.web2.iweb import IResponse
 from twext.web2.stream import MemoryStream
 from twext.web2.test.test_server import SimpleRequest
 
+from twisted.internet.defer import inlineCallbacks
+
 from twistedcaldav import caldavxml
+from twistedcaldav.test.util import HomeTestCase, TestCase
 
-from twistedcaldav.test.util import HomeTestCase
-
 class Properties (HomeTestCase):
     """
     CalDAV properties
@@ -80,3 +82,127 @@
         request = SimpleRequest(self.site, "PROPFIND", inbox_uri)
         request.stream = MemoryStream(query.toxml())
         return self.send(request, propfind_cb)
+
+class DefaultCalendar (TestCase):
+
+    def setUp(self):
+        super(DefaultCalendar, self).setUp()
+        self.createStockDirectoryService()
+        self.setupCalendars()
+
+    @inlineCallbacks
+    def test_pick_default_calendar(self):
+        """
+        Make calendar
+        """
+        
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+
+        # default property initially not present
+        try:
+            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
+
+        yield inbox.pickNewDefaultCalendar(request)
+
+        try:
+            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
+
+        request._newStoreTransaction.abort()
+
+    @inlineCallbacks
+    def test_pick_default_other(self):
+        """
+        Make calendar
+        """
+        
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+
+        # default property not present
+        try:
+            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
+
+        # Create a new default calendar
+        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
+        yield newcalendar.createCalendarCollection()
+        inbox.writeDeadProperty(caldavxml.ScheduleDefaultCalendarURL(
+            davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
+        ))
+        
+        # Delete the normal calendar
+        calendar = yield request.locateResource("/calendars/users/wsanchez/calendar")
+        yield calendar.storeRemove(request, False, "/calendars/users/wsanchez/calendar")
+
+        inbox.removeDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        
+        # default property not present
+        try:
+            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
+        request._newStoreTransaction.commit()
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+        yield inbox.pickNewDefaultCalendar(request)
+
+        try:
+            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
+
+        request._newStoreTransaction.abort()
+
+    @inlineCallbacks
+    def test_fix_shared_default(self):
+        """
+        Make calendar
+        """
+        
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+
+        # Create a new default calendar
+        newcalendar = yield request.locateResource("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
+        yield newcalendar.createCalendarCollection()
+        inbox.writeDeadProperty(caldavxml.ScheduleDefaultCalendarURL(
+            davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
+        ))
+        try:
+            default = yield inbox.readProperty(caldavxml.ScheduleDefaultCalendarURL, request)
+        except HTTPError:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
+        
+        # Force the new calendar to think it is a virtual share
+        newcalendar._isVirtualShare = True
+        
+        try:
+            default = yield inbox.readProperty(caldavxml.ScheduleDefaultCalendarURL, request)
+        except HTTPError:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
+
+        request._newStoreTransaction.abort()

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_sharing.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_sharing.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -18,16 +18,20 @@
 from twext.web2 import responsecode
 from twext.web2.dav import davxml
 from twext.web2.http_headers import MimeType
+from twext.web2.iweb import IResource
 from twext.web2.stream import MemoryStream
 from twext.web2.test.test_server import SimpleRequest
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from twistedcaldav import customxml
 from twistedcaldav.config import config
 from twistedcaldav.test.util import HomeTestCase, norequest
-from twistedcaldav.memcacher import Memcacher
+from twistedcaldav.sharing import SharedCollectionMixin, SHARETYPE_DIRECT, WikiDirectoryService
+
 from twistedcaldav.resource import CalDAVResource
 from txdav.common.datastore.test.util import buildStore, StubNotifierFactory
+from zope.interface import implements
 
+
 sharedOwnerType = davxml.ResourceType.sharedownercalendar #@UndefinedVariable
 regularCalendarType = davxml.ResourceType.calendar #@UndefinedVariable
 
@@ -542,14 +546,89 @@
         self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite())
 
 
+    @inlineCallbacks
+    def test_wikiACL(self):
+        """
+        Ensure shareeAccessControlList( ) honors the access granted by the wiki
+        to the sharee, so that delegates of the sharee get the same level of
+        access.
+        """
 
+        def stubWikiAccessMethod(userID, wikiID):
+            return access
+
+        class StubCollection(object):
+            def __init__(self):
+                self._isVirtualShare = True
+                self._shareePrincipal = StubUserPrincipal()
+            def isCalendarCollection(self):
+                return True
+
+        class StubShare(object):
+            def __init__(self):
+                self.sharetype = SHARETYPE_DIRECT
+                self.hosturl = "/wikifoo"
+
+        class TestCollection(SharedCollectionMixin, StubCollection):
+            pass
+
+        class StubRecord(object):
+            def __init__(self, recordType, name, guid):
+                self.recordType = recordType
+                self.shortNames = [name]
+                self.guid = guid
+
+        class StubUserPrincipal(object):
+            def __init__(self):
+                self.record = StubRecord(
+                    "users",
+                    "testuser",
+                    "4F364813-0415-45CB-9FD4-DBFEF7A0A8E0"
+                )
+            def principalURL(self):
+                return "/principals/__uids__/%s/" % (self.record.guid,)
+
+        class StubWikiPrincipal(object):
+            def __init__(self):
+                self.record = StubRecord(
+                    WikiDirectoryService.recordType_wikis,
+                    "wikifoo",
+                    "foo"
+                )
+
+        class StubWikiResource(object):
+            implements(IResource)
+
+            def locateChild(self, req, segments):
+                pass
+            def renderHTTP(req):
+                pass
+            def ownerPrincipal(self, req):
+                return succeed(StubWikiPrincipal())
+
+
+        collection = TestCollection()
+        collection._share = StubShare()
+        self.site.resource.putChild("wikifoo", StubWikiResource())
+        request = SimpleRequest(self.site, "GET", "/wikifoo")
+
+        # Simulate the wiki server granting Read access
+        access = "read"
+        acl = (yield collection.shareeAccessControlList(request,
+            wikiAccessMethod=stubWikiAccessMethod))
+        self.assertFalse("<write/>" in acl.toxml())
+
+        # Simulate the wiki server granting Read-Write access
+        access = "write"
+        acl = (yield collection.shareeAccessControlList(request,
+            wikiAccessMethod=stubWikiAccessMethod))
+        self.assertTrue("<write/>" in acl.toxml())
+
+
 class DatabaseSharingTests(SharingTests):
 
     @inlineCallbacks
     def setUp(self):
-        self.patch(config.Memcached.Pools.Default, "ClientEnabled", False)
-        self.patch(config.Memcached.Pools.Default, "ServerEnabled", False)
-        self.patch(Memcacher, "allowTestCache", True)
         self.calendarStore = yield buildStore(self, StubNotifierFactory())
         yield super(DatabaseSharingTests, self).setUp()
 

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_timezonestdservice.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_timezonestdservice.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_timezonestdservice.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -30,7 +30,7 @@
     def test_generateXML(self):
         
         hashed = hashlib.md5("test").hexdigest()
-        info = TimezoneInfo("America/New_York", "20110517T120000Z", hashed)
+        info = TimezoneInfo("America/New_York", ("US/Eastern",), "20110517T120000Z", hashed)
         
         node = Element("root")
         info.generateXML(node)
@@ -39,12 +39,13 @@
         self.assertTrue(timezone is not None)
         self.assertEqual(timezone.findtext("tzid"), "America/New_York")
         self.assertEqual(timezone.findtext("dtstamp"), "20110517T120000Z")
+        self.assertEqual(timezone.findtext("alias"), "US/Eastern")
         self.assertEqual(timezone.findtext("md5"), hashed)
 
     def test_parseXML(self):
         
         hashed = hashlib.md5("test").hexdigest()
-        info1 = TimezoneInfo("America/New_York", "20110517T120000Z", hashed)
+        info1 = TimezoneInfo("America/New_York", ("US/Eastern",), "20110517T120000Z", hashed)
         
         node = Element("root")
         info1.generateXML(node)
@@ -53,6 +54,7 @@
         info2 = TimezoneInfo.readXML(timezone)
 
         self.assertEqual(info2.tzid, "America/New_York")
+        self.assertEqual(info2.aliases, ("US/Eastern",))
         self.assertEqual(info2.dtstamp, "20110517T120000Z")
         self.assertEqual(info2.md5, hashed)
 
@@ -98,36 +100,50 @@
         self.assertEqual(db1.dtstamp, db2.dtstamp)
         self.assertEqual(len(db1.timezones), len(db2.timezones))
         
-    def testGetEmpty(self):
+    def testList(self):
         
         xmlfile = self.mktemp()
         db = PrimaryTimezoneDatabase(TimezoneCache.getDBPath(), xmlfile)
         db.createNewDatabase()
         self.assertTrue(os.path.exists(xmlfile))
         
-        tz1 = db.getTimezones(())
-        self.assertTrue(str(tz1).find("VTIMEZONE") == -1)
+        tzids = set([tz.tzid for tz in db.listTimezones(None)])
+        self.assertTrue("America/New_York" in tzids)
+        self.assertTrue("US/Eastern" not in tzids)
         
-    def testGetOne(self):
+    def testListChangedSince(self):
         
         xmlfile = self.mktemp()
         db = PrimaryTimezoneDatabase(TimezoneCache.getDBPath(), xmlfile)
         db.createNewDatabase()
         self.assertTrue(os.path.exists(xmlfile))
         
-        tz1 = db.getTimezones(("America/New_York",))
-        self.assertTrue(str(tz1).find("VTIMEZONE") != -1)
-        self.assertTrue(str(tz1).find("TZID:America/New_York") != -1)
+        tzids = set([tz.tzid for tz in db.listTimezones(db.dtstamp)])
+        self.assertTrue(len(tzids) == 0)
         
-    def testGetMultiple(self):
+    def testGetNone(self):
         
         xmlfile = self.mktemp()
         db = PrimaryTimezoneDatabase(TimezoneCache.getDBPath(), xmlfile)
         db.createNewDatabase()
         self.assertTrue(os.path.exists(xmlfile))
         
-        tz1 = db.getTimezones(("America/New_York","America/Los_Angeles","America/Bogus",))
+        tz = db.getTimezone("Bogus")
+        self.assertEqual(tz, None)
+        
+    def testGetOne(self):
+        
+        xmlfile = self.mktemp()
+        db = PrimaryTimezoneDatabase(TimezoneCache.getDBPath(), xmlfile)
+        db.createNewDatabase()
+        self.assertTrue(os.path.exists(xmlfile))
+        
+        # Original
+        tz1 = db.getTimezone("America/New_York")
         self.assertTrue(str(tz1).find("VTIMEZONE") != -1)
         self.assertTrue(str(tz1).find("TZID:America/New_York") != -1)
-        self.assertTrue(str(tz1).find("TZID:America/Los_Angeles") != -1)
-        self.assertTrue(str(tz1).find("TZID:America/Bogus") == -1)
+        
+        # Alias
+        tz1 = db.getTimezone("US/Eastern")
+        self.assertTrue(str(tz1).find("VTIMEZONE") != -1)
+        self.assertTrue(str(tz1).find("TZID:US/Eastern") != -1)

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_validation.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_validation.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_validation.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -48,8 +48,10 @@
     def _getSampleCalendar(self):
         return Component.fromString("""BEGIN:VCALENDAR
 VERSION:2.0
+PRODID:-//Apple Computer\, Inc//iCal 2.0//EN
 BEGIN:VEVENT
 UID:12345-67890
+DTSTAMP:20071114T000000Z
 DTSTART:20071114T000000Z
 ORGANIZER:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_wrapping.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_wrapping.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_wrapping.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -46,7 +46,12 @@
     StubNotifierFactory
 
 
-from twistedcaldav.memcacher import Memcacher
+from twext.web2.http import HTTPError
+from twext.web2.responsecode import INSUFFICIENT_STORAGE_SPACE
+from twext.web2.stream import MemoryStream
+from txdav.common.datastore.test.util import deriveQuota
+from twistedcaldav.test.util import patchConfig
+from twistedcaldav.directory.test.test_xmlfile import XMLFileBase
 from txdav.caldav.icalendarstore import ICalendarHome
 from txdav.carddav.iaddressbookstore import IAddressBookHome
 
@@ -73,8 +78,10 @@
     def finish(self):
         pass
 
+    remoteAddr = '127.0.0.1'
 
 
+
 class WrappingTests(TestCase):
     """
     Tests for L{twistedcaldav.static.CalDAVResource} creating the appropriate type
@@ -154,7 +161,7 @@
     requestUnderTest = None
 
     @inlineCallbacks
-    def getResource(self, path):
+    def getResource(self, path, method='GET', user=None):
         """
         Retrieve a resource from the site.
 
@@ -162,15 +169,31 @@
             slash)
 
         @type path: C{str}
+
+        @param method: the HTTP method to initialize the request with.
+            Defaults to GET.  (This should I{mostly} be irrelevant to path
+            traversal, but may be interesting to subsequent operations on
+            C{self.requestUnderTest}).
+
+        @param user: the username (shortname in the test XML file) of the user
+            to forcibly authenticate this request as.
+
+        @return: a L{Deferred} that fires with an L{IResource}.
         """
         if self.requestUnderTest is None:
-            req = self.requestForPath(path)
+            req = self.requestForPath(path, method)
             self.requestUnderTest = req
         else:
+            # How should this handle mismatched methods?
             req = self.requestUnderTest
         aResource = yield req.locateResource(
             "http://localhost:8008/" + path
         )
+        if user is not None:
+            guid = XMLFileBase.users[user]["guid"]
+            req.authnUser = req.authzUser = (
+                davxml.Principal(davxml.HRef('/principals/__uids__/' + guid + '/'))
+            )
         returnValue(aResource)
 
 
@@ -183,9 +206,9 @@
         return self.requestUnderTest._newStoreTransaction.commit()
 
 
-    def requestForPath(self, path):
+    def requestForPath(self, path, method='GET'):
         """
-        Get a L{Request} with a L{FakeChanRequest} for a given path.
+        Get a L{Request} with a L{FakeChanRequest} for a given path and method.
         """
         headers = Headers()
         headers.addRawHeader("Host", "localhost:8008")
@@ -193,13 +216,17 @@
         req = Request(
             site=self.site,
             chanRequest=chanReq,
-            command='GET',
+            command=method,
             path=path,
             version=('1', '1'),
             contentLength=0,
             headers=headers
         )
-        req.path = path # normally process( ) sets request.path
+
+        # 'process()' normally sets these.  Shame on web2, having so much
+        # partially-initialized stuff floating around.
+        req.remoteAddr = '127.0.0.1'
+        req.path = path
         req.credentialFactories = {}
         return req
 
@@ -368,6 +395,28 @@
 
 
     @inlineCallbacks
+    def test_attachmentQuotaExceeded(self):
+        """
+        Exceeding quota on an attachment returns an HTTP error code.
+        """
+        patchConfig(testCase=self, EnableDropBox=True)
+        yield self.populateOneObject("1.ics", event4_text)
+        calendarObject = yield self.getResource(
+            "/calendars/users/wsanchez/dropbox/uid4.dropbox/too-big-attachment",
+            "PUT", "wsanchez"
+        )
+        self.requestUnderTest.stream = MemoryStream(
+            "x" * deriveQuota(self.id()) * 2)
+        try:
+            result = yield calendarObject.http_PUT(self.requestUnderTest)
+        except HTTPError, he:
+            self.assertEquals(he.response.code, INSUFFICIENT_STORAGE_SPACE)
+        else:
+            self.fail("Error not raised, %r returned instead." %
+                      (result,))
+
+
+    @inlineCallbacks
     def test_lookupNewCalendarObject(self):
         """
         When a L{CalDAVResource} representing a new calendar object on a
@@ -470,9 +519,6 @@
 
     @inlineCallbacks
     def setUp(self):
-        self.patch(config.Memcached.Pools.Default, "ClientEnabled", False)
-        self.patch(config.Memcached.Pools.Default, "ServerEnabled", False)
-        self.patch(Memcacher, "allowTestCache", True)
         self.calendarStore = yield buildStore(self, StubNotifierFactory())
         super(DatabaseWrappingTests, self).setUp()
 

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_xml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_xml.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/test_xml.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -30,7 +30,8 @@
     calendar_file = os.path.join(os.path.dirname(__file__), "data", "Holidays",
                                  "C3184A66-1ED0-11D9-A5E0-000A958A3252.ics")
     calendar = Component.fromStream(file(calendar_file))
-    calendar.validateForCalDAV()
+    calendar.validCalendarData()
+    calendar.validCalendarForCalDAV(methodAllowed=False)
 
     def test_ComponentFilter(self):
         """

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/util.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/test/util.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -43,6 +43,7 @@
     DirectoryPrincipalProvisioningResource)
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 
+from txdav.common.datastore.test.util import deriveQuota
 from txdav.common.datastore.file import CommonDataStore
 
 
@@ -80,15 +81,13 @@
         self.xmlFile = FilePath(config.DataRoot).child("accounts.xml")
         self.xmlFile.setContent(xmlFile.getContent())
 
-        # *temporarily* set up an augment service so this directory service will
-        # work.
-        self.patch(augment, "AugmentService", augment.AugmentXMLDB(
-                xmlFiles=(augmentsFile.path,)
-            )
-        )
 
         self.directoryService = XMLDirectoryService(
-            {'xmlFile' : "accounts.xml"}
+            {
+                "xmlFile" : "accounts.xml",
+                "augmentService" :
+                    augment.AugmentXMLDB( xmlFiles=(augmentsFile.path,)),
+            }
         )
 
         # FIXME: see FIXME in DirectoryPrincipalProvisioningResource.__init__;
@@ -105,13 +104,15 @@
         addressbooks.)  By default returns a L{CommonDataStore}, but this is a
         hook for subclasses to override to provide different data stores.
         """
-        return CommonDataStore(FilePath(config.DocumentRoot), None, True, False)
+        return CommonDataStore(FilePath(config.DocumentRoot), None, True, False,
+                               quota=deriveQuota(self.id()))
 
 
     def setupCalendars(self):
         """
-        Set up the resource at /calendars (a L{DirectoryCalendarHomeProvisioningResource}),
-        and assign it as C{self.calendarCollection}.
+        Set up the resource at /calendars (a
+        L{DirectoryCalendarHomeProvisioningResource}), and assign it as
+        C{self.calendarCollection}.
         """
 
         # Need a data store
@@ -586,8 +587,21 @@
 
 
 
+def patchConfig(testCase, **kw):
+    """
+    Patch the global configuration (including running the appropriate hooks) for
+    the duration of the given test.
+    """
+    preserved = {}
+    for k in kw:
+        preserved[k] = config.get(k, None)
+    def reUpdate():
+        config.update(preserved)
+    testCase.addCleanup(reUpdate)
+    config.update(kw)
 
 
+
 class ErrorOutput(Exception):
     """
     The process produced some error output and exited with a non-zero exit

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/upgrade.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/upgrade.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/upgrade.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -928,8 +928,7 @@
         owner = LocalCalendarUser(cua, ownerPrincipal,
             inbox, ownerPrincipal.scheduleInboxURL())
 
-        data = yield inboxItem.iCalendarText()
-        calendar = Component.fromString(data)
+        calendar = yield inboxItem.iCalendar()
         try:
             method = calendar.propertyValue("METHOD")
         except ValueError:

Modified: CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/vcard.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/vcard.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/twistedcaldav/vcard.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -27,15 +27,18 @@
 import cStringIO as StringIO
 import codecs
 
+from twext.python.log import Logger
 from twext.web2.stream import IStream
 from twext.web2.dav.util import allDataFromStream
 
 from pycalendar.attribute import PyCalendarAttribute
 from pycalendar.componentbase import PyCalendarComponentBase
-from pycalendar.exceptions import PyCalendarInvalidData
+from pycalendar.exceptions import PyCalendarError
 from pycalendar.vcard.card import Card
 from pycalendar.vcard.property import Property as pyProperty
 
+log = Logger()
+
 vCardProductID = "-//CALENDARSERVER.ORG//NONSGML Version 1//EN"
 
 class InvalidVCardDataError(ValueError):
@@ -197,7 +200,7 @@
         """
         try:
             results = Card.parseMultiple(stream)
-        except PyCalendarInvalidData:
+        except PyCalendarError:
             results = None
         if not results:
             stream.seek(0)
@@ -235,7 +238,7 @@
         cal = Card()
         try:
             result = cal.parse(stream)
-        except PyCalendarInvalidData:
+        except PyCalendarError:
             result = None
         if not result:
             stream.seek(0)
@@ -409,6 +412,26 @@
 
         return self._resource_uid
 
+    def validVCardData(self, doFix=True, doRaise=True):
+        """
+        @return: tuple of fixed, unfixed issues
+        @raise InvalidVCardDataError: if the given vcard data is not valid.
+        """
+        if self.name() != "VCARD":
+            log.debug("Not a vcard: %s" % (self,))
+            raise InvalidVCardDataError("Not a vcard")
+
+        # Do underlying vCard library validation with data fix
+        fixed, unfixed = self._pycard.validate(doFix=doFix)
+        if unfixed:
+            log.debug("vCard data had unfixable problems:\n  %s" % ("\n  ".join(unfixed),))
+            if doRaise:
+                raise InvalidVCardDataError("Calendar data had unfixable problems:\n  %s" % ("\n  ".join(unfixed),))
+        if fixed:
+            log.debug("vCard data had fixable problems:\n  %s" % ("\n  ".join(fixed),))
+        
+        return fixed, unfixed
+
     def validForCardDAV(self):
         """
         @raise ValueError: if the given vcard data is not valid.


Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/base
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/base/datastore
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/base/datastore/subpostgres.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/base/datastore/subpostgres.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/base/datastore/subpostgres.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -87,8 +87,9 @@
 
     def processEnded(self, reason):
         log.msg("postgres process ended %r" % (reason,))
+        result = (reason.value.status == 0)
         self.lineReceiver.connectionLost(reason)
-        self.completionDeferred.callback(None)
+        self.completionDeferred.callback(result)
 
 
 
@@ -362,6 +363,7 @@
         )
         self.monitor = monitor
         def gotReady(result):
+            self.shouldStopDatabase = result
             self.ready()
             self.deactivateDelayedShutdown()
         def reportit(f):
@@ -370,6 +372,7 @@
         self.monitor.completionDeferred.addCallback(
             gotReady).addErrback(reportit)
 
+    shouldStopDatabase = False
 
     def startService(self):
         MultiService.startService(self)
@@ -423,14 +426,17 @@
             d = MultiService.stopService(self)
 
         def superStopped(result):
-            monitor = _PostgresMonitor()
-            pg_ctl = which("pg_ctl")[0]
-            reactor.spawnProcess(monitor, pg_ctl,
-                [pg_ctl, '-l', 'logfile', 'stop'],
-                self.env,
-                uid=self.uid, gid=self.gid,
-            )
-            return monitor.completionDeferred
+            # If pg_ctl's startup wasn't successful, don't bother to stop the
+            # database.  (This also happens in command-line tools.)
+            if self.shouldStopDatabase:
+                monitor = _PostgresMonitor()
+                pg_ctl = which("pg_ctl")[0]
+                reactor.spawnProcess(monitor, pg_ctl,
+                    [pg_ctl, '-l', 'logfile', 'stop'],
+                    self.env,
+                    uid=self.uid, gid=self.gid,
+                )
+                return monitor.completionDeferred
         return d.addCallback(superStopped)
 
 #        def maybeStopSubprocess(result):


Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/base/propertystore
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/base/propertystore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/base/propertystore/test/test_sql.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/base/propertystore/test/test_sql.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -26,10 +26,8 @@
 from txdav.base.propertystore.test.base import (
     PropertyStoreTest, propertyName, propertyValue)
 
-from twistedcaldav import memcacher
 from twisted.internet.defer import gatherResults
 from twext.enterprise.ienterprise import AlreadyFinishedError
-from twistedcaldav.config import config
 
 try:
     from txdav.base.propertystore.sql import PropertyStore
@@ -45,10 +43,6 @@
 
     @inlineCallbacks
     def setUp(self):
-        self.patch(config.Memcached.Pools.Default, "ClientEnabled", False)
-        self.patch(config.Memcached.Pools.Default, "ServerEnabled", False)
-        self.patch(memcacher.Memcacher, "allowTestCache", True)
-
         self.notifierFactory = StubNotifierFactory()
         self.store = yield buildStore(self, self.notifierFactory)
         self.addCleanup(self.maybeCommitLast)


Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/file.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/file.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -14,6 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from twistedcaldav.ical import InvalidICalendarDataError
 
 """
 File calendar store.
@@ -31,13 +32,12 @@
 
 from errno import ENOENT
 
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.internet.interfaces import ITransport
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed, fail
+
 from twisted.python.failure import Failure
 
 from txdav.base.propertystore.xattr import PropertyStore
 
-from twext.python.vcomponent import InvalidICalendarDataError
 from twext.python.vcomponent import VComponent
 from twext.web2.dav import davxml
 from twext.web2.dav.element.rfc2518 import ResourceType, GETContentType
@@ -55,13 +55,14 @@
 from txdav.caldav.datastore.index_file import Index as OldIndex,\
     IndexSchedule as OldInboxIndex
 from txdav.caldav.datastore.util import (
-    validateCalendarComponent, dropboxIDFromCalendarObject
+    validateCalendarComponent, dropboxIDFromCalendarObject, CalendarObjectBase,
+    StorageTransportBase
 )
 
 from txdav.common.datastore.file import (
     CommonDataStore, CommonStoreTransaction, CommonHome, CommonHomeChild,
-    CommonObjectResource
-, CommonStubResource)
+    CommonObjectResource, CommonStubResource)
+from txdav.caldav.icalendarstore import QuotaExceeded
 
 from txdav.common.icommondatastore import (NoSuchObjectResourceError,
     InternalDataStoreError)
@@ -86,7 +87,8 @@
     _notifierPrefix = "CalDAV"
 
     def __init__(self, uid, path, calendarStore, transaction, notifiers):
-        super(CalendarHome, self).__init__(uid, path, calendarStore, transaction, notifiers)
+        super(CalendarHome, self).__init__(uid, path, calendarStore,
+                                           transaction, notifiers)
 
         self._childClass = Calendar
 
@@ -262,7 +264,7 @@
 
 
 
-class CalendarObject(CommonObjectResource):
+class CalendarObject(CommonObjectResource, CalendarObjectBase):
     """
     @ivar _path: The path of the .ics file on disk
 
@@ -300,8 +302,8 @@
             self.name(), component
         )
 
-        self._component = component
-        # FIXME: needs to clear text cache
+        componentText = str(component)
+        self._objectText = componentText
 
         def do():
             # Mark all properties as dirty, so they can be added back
@@ -313,7 +315,6 @@
                 backup = hidden(self._path.temporarySibling())
                 self._path.moveTo(backup)
             
-            componentText = str(component)
             fh = self._path.open("w")
             try:
                 # FIXME: concurrency problem; if this write is interrupted
@@ -340,25 +341,38 @@
 
 
     def component(self):
-        if self._component is not None:
-            return self._component
-        text = self.text()
-
+        """
+        Read calendar data and validate/fix it. Do not raise a store error here if there are unfixable
+        errors as that could prevent the overall request to fail. Instead we will hand bad data off to
+        the caller - that is not ideal but in theory we should have checked everything on the way in and
+        only allowed in good data.
+        """
+        text = self._text()
         try:
             component = VComponent.fromString(text)
-            # Fix any bogus data we can
-            component.validateComponentsForCalDAV(False, fix=True)
         except InvalidICalendarDataError, e:
+            # This is a really bad situation, so do raise
             raise InternalDataStoreError(
                 "File corruption detected (%s) in file: %s"
                 % (e, self._path.path)
             )
+
+        # Fix any bogus data we can
+        fixed, unfixed = component.validCalendarData(doFix=True, doRaise=False)
+
+        if unfixed:
+            self.log_error("Calendar data at %s had unfixable problems:\n  %s" % (self._path.path, "\n  ".join(unfixed),))
+        
+        if fixed:
+            self.log_error("Calendar data at %s had fixable problems:\n  %s" % (self._path.path, "\n  ".join(fixed),))
+
         return component
 
 
-    def text(self):
-        if self._component is not None:
-            return str(self._component)
+    def _text(self):
+        if self._objectText is not None:
+            return self._objectText
+
         try:
             fh = self._path.open()
         except IOError, e:
@@ -388,10 +402,10 @@
                     "File corruption detected (improper start) in file: %s"
                     % (self._path.path,)
                 )
+        
+        self._objectText = text
         return text
 
-    iCalendarText = text
-
     def uid(self):
         if not hasattr(self, "_uid"):
             self._uid = self.component().resourceUID()
@@ -497,14 +511,14 @@
         # FIXME: rollback, tests for rollback
 
         attachment = (yield self.attachmentWithName(name))
-        old_size = attachment.size()
+        oldSize = attachment.size()
 
         (yield self._dropboxPath()).child(name).remove()
         if name in self._attachments:
             del self._attachments[name]
 
         # Adjust quota
-        self._calendar._home.adjustQuotaUsedBytes(-old_size)
+        self._calendar._home.adjustQuotaUsedBytes(-oldSize)
 
 
     @inlineCallbacks
@@ -580,10 +594,8 @@
 
 
 
-class AttachmentStorageTransport(object):
+class AttachmentStorageTransport(StorageTransportBase):
 
-    implements(ITransport)
-
     def __init__(self, attachment, contentType):
         """
         Initialize this L{AttachmentStorageTransport} and open its file for
@@ -592,9 +604,10 @@
         @param attachment: The attachment whose data is being filled out.
         @type attachment: L{Attachment}
         """
-        self._attachment = attachment
-        self._contentType = contentType
-        self._file = self._attachment._path.open("w")
+        super(AttachmentStorageTransport, self).__init__(
+            attachment, contentType)
+        self._path = self._attachment._path.temporarySibling()
+        self._file = self._path.open("w")
 
 
     def write(self, data):
@@ -603,20 +616,30 @@
 
 
     def loseConnection(self):
-        
-        old_size = self._attachment.size()
-
+        home = self._attachment._calendarObject._calendar._home
+        oldSize = self._attachment.size()
+        newSize = self._file.tell()
         # FIXME: do anything
         self._file.close()
 
+        if home.quotaAllowedBytes() < (home.quotaUsedBytes()
+                                       + (newSize - oldSize)):
+            self._path.remove()
+            return fail(QuotaExceeded())
+
+        self._path.moveTo(self._attachment._path)
+
         md5 = hashlib.md5(self._attachment._path.getContent()).hexdigest()
         props = self._attachment.properties()
-        props[contentTypeKey] = GETContentType(generateContentType(self._contentType))
+        props[contentTypeKey] = GETContentType(
+            generateContentType(self._contentType)
+        )
         props[md5key] = TwistedGETContentMD5.fromString(md5)
 
         # Adjust quota
-        self._attachment._calendarObject._calendar._home.adjustQuotaUsedBytes(self._attachment.size() - old_size)
+        home.adjustQuotaUsedBytes(newSize - oldSize)
         props.flush()
+        return succeed(None)
 
 
 
@@ -644,7 +667,7 @@
 
     def properties(self):
         uid = self._calendarObject._parentCollection._home.uid()
-        return PropertyStore(uid, lambda :self._path)
+        return PropertyStore(uid, lambda: self._path)
 
 
     def store(self, contentType):

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/index_file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/index_file.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/index_file.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1046,7 +1046,8 @@
             # FIXME: This is blocking I/O
             try:
                 calendar = Component.fromStream(stream)
-                calendar.validateForCalDAV()
+                calendar.validCalendarData()
+                calendar.validCalendarForCalDAV(methodAllowed=False)
             except ValueError:
                 log.err("Non-calendar resource: %s" % (name,))
             else:
@@ -1149,8 +1150,8 @@
             # FIXME: This is blocking I/O
             try:
                 calendar = Component.fromStream(stream)
-                calendar.validCalendarForCalDAV()
-                calendar.validateComponentsForCalDAV(True)
+                calendar.validCalendarData()
+                calendar.validCalendarForCalDAV(methodAllowed=True)
             except ValueError:
                 log.err("Non-calendar resource: %s" % (name,))
             else:


Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/index_file.py
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation/txdav/caldav/datastore/index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/caldav/datastore/index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/caldav/datastore/index_file.py:6167-6191
/CalendarServer/branches/new-store/txdav/caldav/datastore/index_file.py:5594-5934
/CalendarServer/branches/new-store-no-caldavfile/txdav/caldav/datastore/index_file.py:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/caldav/datastore/index_file.py:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/caldav/datastore/index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/caldav/datastore/index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/caldav/datastore/index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/caldav/datastore/index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/caldav/datastore/index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/pods/txdav/caldav/datastore/index_file.py:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar/txdav/caldav/datastore/index_file.py:7085-7206
/CalendarServer/branches/users/cdaboo/pycard/txdav/caldav/datastore/index_file.py:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/caldav/datastore/index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/caldav/datastore/index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/caldav/datastore/index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/caldav/datastore/index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/caldav/datastore/index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/caldav/datastore/index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/caldav/datastore/index_file.py:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes/txdav/caldav/datastore/index_file.py:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/caldav/datastore/index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/caldav/datastore/index_file.py:6369
/CalendarServer/branches/users/glyph/oracle/txdav/caldav/datastore/index_file.py:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls/txdav/caldav/datastore/index_file.py:7340-7351
/CalendarServer/branches/users/glyph/sendfdport/txdav/caldav/datastore/index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/caldav/datastore/index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/caldav/datastore/index_file.py:5929-6073
/CalendarServer/branches/users/glyph/subtransactions/txdav/caldav/datastore/index_file.py:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/caldav/datastore/index_file.py:5084-5149
/CalendarServer/branches/users/sagen/inboxitems/txdav/caldav/datastore/index_file.py:7380-7381
/CalendarServer/branches/users/sagen/locations-resources/txdav/caldav/datastore/index_file.py:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/caldav/datastore/index_file.py:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events/txdav/caldav/datastore/index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/caldav/datastore/index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/caldav/datastore/index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/caldav/datastore/index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/caldav/datastore/index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/index.py:6322-6394
   + /CalendarServer/branches/config-separation/txdav/caldav/datastore/index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/caldav/datastore/index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/caldav/datastore/index_file.py:6167-6191
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/caldav/datastore/index_file.py:5936-5981
/CalendarServer/branches/new-store-no-caldavfile/txdav/caldav/datastore/index_file.py:5911-5935
/CalendarServer/branches/new-store/txdav/caldav/datastore/index_file.py:5594-5934
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/caldav/datastore/index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/caldav/datastore/index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/caldav/datastore/index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/caldav/datastore/index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/caldav/datastore/index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/pods/txdav/caldav/datastore/index_file.py:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar/txdav/caldav/datastore/index_file.py:7085-7206
/CalendarServer/branches/users/cdaboo/pycard/txdav/caldav/datastore/index_file.py:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/caldav/datastore/index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/caldav/datastore/index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/caldav/datastore/index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/caldav/datastore/index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/caldav/datastore/index_file.py:6932-7023
/CalendarServer/branches/users/glyph/deploybuild/txdav/caldav/datastore/index_file.py:7563-7572
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/caldav/datastore/index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/caldav/datastore/index_file.py:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes/txdav/caldav/datastore/index_file.py:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/caldav/datastore/index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/caldav/datastore/index_file.py:6369
/CalendarServer/branches/users/glyph/new-export/txdav/caldav/datastore/index_file.py:7444-7485
/CalendarServer/branches/users/glyph/oracle-nulls/txdav/caldav/datastore/index_file.py:7340-7351
/CalendarServer/branches/users/glyph/oracle/txdav/caldav/datastore/index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/caldav/datastore/index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/caldav/datastore/index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/caldav/datastore/index_file.py:5929-6073
/CalendarServer/branches/users/glyph/subtransactions/txdav/caldav/datastore/index_file.py:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/caldav/datastore/index_file.py:5084-5149
/CalendarServer/branches/users/sagen/inboxitems/txdav/caldav/datastore/index_file.py:7380-7381
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/caldav/datastore/index_file.py:5052-5061
/CalendarServer/branches/users/sagen/locations-resources/txdav/caldav/datastore/index_file.py:5032-5051
/CalendarServer/branches/users/sagen/purge_old_events/txdav/caldav/datastore/index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/caldav/datastore/index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/caldav/datastore/index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/caldav/datastore/index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/caldav/datastore/index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/index.py:6322-6394
/CalendarServer/trunk/txdav/caldav/datastore/index_file.py:7443-7695

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/scheduling.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/scheduling.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/scheduling.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,6 +1,6 @@
 # -*- test-case-name: txdav.caldav.datastore.test.test_scheduling -*-
 ##
-# Copyright (c) 2010 Apple Inc. All rights reserved.
+# Copyright (c) 2010-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -119,7 +119,6 @@
     implements(ICalendarObject)
     def setComponent(self, component): ""
     def component(self): ""
-    def iCalendarText(self): ""
     def uid(self): ""
     def componentType(self): ""
     def organizer(self): ""

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/sql.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/sql.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -31,7 +31,6 @@
 
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.error import ConnectionLost
-from twisted.internet.interfaces import ITransport
 from twisted.python import hashlib
 from twisted.python.failure import Failure
 
@@ -40,7 +39,7 @@
 from twistedcaldav.config import config
 from twistedcaldav.dateops import normalizeForIndex, datetimeMktime,\
     parseSQLTimestamp, pyCalendarTodatetime
-from twistedcaldav.ical import Component
+from twistedcaldav.ical import Component, InvalidICalendarDataError
 from twistedcaldav.instance import InvalidOverriddenInstanceError
 from twistedcaldav.memcacher import Memcacher
 
@@ -67,8 +66,14 @@
 from twext.enterprise.dal.syntax import Parameter
 from twext.enterprise.dal.syntax import utcNowSQL
 from twext.enterprise.dal.syntax import Len
-from txdav.common.icommondatastore import IndexedSearchException
 
+from txdav.caldav.datastore.util import CalendarObjectBase
+from txdav.caldav.icalendarstore import QuotaExceeded
+
+from txdav.caldav.datastore.util import StorageTransportBase
+from txdav.common.icommondatastore import IndexedSearchException,\
+    InternalDataStoreError
+
 from pycalendar.datetime import PyCalendarDateTime
 from pycalendar.duration import PyCalendarDuration
 from pycalendar.timezone import PyCalendarTimezone
@@ -103,6 +108,7 @@
         super(CalendarHome, self).__init__(transaction, ownerUID, notifiers)
         self._shares = SQLLegacyCalendarShares(self)
 
+
     createCalendarWithName = CommonHome.createChildWithName
     removeCalendarWithName = CommonHome.removeChildWithName
     calendarWithName = CommonHome.childWithName
@@ -113,7 +119,7 @@
 
     @inlineCallbacks
     def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, type):
-        
+
         objectResources = (yield self.objectResourcesWithUID(uid, ("inbox",)))
         for objectResource in objectResources:
             if ok_object and objectResource._resourceID == ok_object._resourceID:
@@ -121,18 +127,18 @@
             matched_type = "schedule" if objectResource.isScheduleObject else "calendar"
             if type == "schedule" or matched_type == "schedule":
                 returnValue(True)
-            
+
         returnValue(False)
 
     @inlineCallbacks
     def getCalendarResourcesForUID(self, uid, allow_shared=False):
-        
+
         results = []
         objectResources = (yield self.objectResourcesWithUID(uid, ("inbox",)))
         for objectResource in objectResources:
             if allow_shared or objectResource._parentCollection._owned:
                 results.append(objectResource)
-            
+
         returnValue(results)
 
 
@@ -305,7 +311,7 @@
 
 
 
-class CalendarObject(CommonObjectResource):
+class CalendarObject(CommonObjectResource, CalendarObjectBase):
     implements(ICalendarObject)
 
     _objectTable = CALENDAR_OBJECT_TABLE
@@ -314,7 +320,7 @@
     def __init__(self, calendar, name, uid, resourceID=None, metadata=None):
 
         super(CalendarObject, self).__init__(calendar, name, uid, resourceID)
-        
+
         if metadata is None:
             metadata = {}
         self.accessMode = metadata.get("accessMode", "")
@@ -406,7 +412,7 @@
             expand = PyCalendarDateTime(2100, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
             doInstanceIndexing = True
         else:
-            
+
             # If migrating or re-creating or config option for delayed indexing is off, always index
             if reCreate or self._txn._migrating or not config.FreeBusyIndexDelayedExpand:
                 doInstanceIndexing = True
@@ -453,11 +459,11 @@
             else:
                 raise
 
-        # Now coerce indexing to off if needed 
+        # Now coerce indexing to off if needed
         if not doInstanceIndexing:
             instances = None
             recurrenceLimit = PyCalendarDateTime(1900, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
-            
+
         co = schema.CALENDAR_OBJECT
         tr = schema.TIME_RANGE
         tpy = schema.TRANSPARENCY
@@ -470,13 +476,13 @@
             organizer = component.getOrganizer()
             if not organizer:
                 organizer = ""
-    
+
             # CALENDAR_OBJECT table update
             self._uid = component.resourceUID()
             self._md5 = hashlib.md5(componentText).hexdigest()
             self._size = len(componentText)
 
-            # Special - if migrating we need to preserve the original md5    
+            # Special - if migrating we need to preserve the original md5
             if self._txn._migrating and hasattr(component, "md5"):
                 self._md5 = component.md5
 
@@ -494,7 +500,7 @@
                     # server dropbox collections and only then set the read mode
                     self._attachment = _ATTACHMENTS_MODE_READ
                     self._dropboxID = (yield self.dropboxID())
-    
+
             values = {
                 co.CALENDAR_RESOURCE_ID            : self._calendar._resourceID,
                 co.RESOURCE_NAME                   : self._name,
@@ -513,7 +519,7 @@
                 co.PRIVATE_COMMENTS                : self._private_comments,
                 co.MD5                             : self._md5
             }
-    
+
             if inserting:
                 self._resourceID, self._created, self._modified = (
                     yield Insert(
@@ -539,12 +545,12 @@
                 co.RECURRANCE_MAX :
                     pyCalendarTodatetime(normalizeForIndex(recurrenceLimit)) if recurrenceLimit else None,
             }
-    
+
             yield Update(
                 values,
                 Where=co.RESOURCE_ID == self._resourceID
             ).on(self._txn)
-            
+
             # Need to wipe the existing time-range for this and rebuild
             yield Delete(
                 From=tr,
@@ -580,7 +586,7 @@
                         tpy.USER_ID                : useruid,
                         tpy.TRANSPARENT            : transp,
                     }).on(self._txn))
-    
+
             # Special - for unbounded recurrence we insert a value for "infinity"
             # that will allow an open-ended time-range to always match it.
             if component.isRecurringUnbounded():
@@ -609,12 +615,35 @@
 
     @inlineCallbacks
     def component(self):
-        returnValue(VComponent.fromString((yield self.iCalendarText())))
+        """
+        Read calendar data and validate/fix it. Do not raise a store error here if there are unfixable
+        errors as that could prevent the overall request to fail. Instead we will hand bad data off to
+        the caller - that is not ideal but in theory we should have checked everything on the way in and
+        only allowed in good data.
+        """
+        text = yield self._text()
 
+        try:
+            component = VComponent.fromString(text)
+        except InvalidICalendarDataError, e:
+            # This is a really bad situation, so do raise
+            raise InternalDataStoreError(
+                "Data corruption detected (%s) in id: %s"
+                % (e, self._resourceID)
+            )
 
-    iCalendarText = CommonObjectResource.text
+        # Fix any bogus data we can
+        fixed, unfixed = component.validCalendarData(doFix=True, doRaise=False)
 
+        if unfixed:
+            self.log_error("Calendar data id=%s had unfixable problems:\n  %s" % (self._resourceID, "\n  ".join(unfixed),))
+        
+        if fixed:
+            self.log_error("Calendar data id=%s had fixable problems:\n  %s" % (self._resourceID, "\n  ".join(fixed),))
 
+        returnValue(component)
+
+
     @inlineCallbacks
     def organizer(self):
         returnValue((yield self.component()).getOrganizer())
@@ -622,7 +651,7 @@
 
     def getMetadata(self):
         metadata = {}
-        metadata["accessMode"] = self.accessMode 
+        metadata["accessMode"] = self.accessMode
         metadata["isScheduleObject"] = self.isScheduleObject
         metadata["scheduleTag"] = self.scheduleTag
         metadata["scheduleEtags"] = self.scheduleEtags
@@ -671,7 +700,7 @@
 
     @inlineCallbacks
     def createAttachmentWithName(self, name):
-        
+
         # We need to know the resource_ID of the home collection of the owner (not sharee)
         # of this event
         sharerHomeID = (yield self._parentCollection.sharerHomeID())
@@ -683,7 +712,7 @@
         yield attachment.remove()
 
     def attachmentWithName(self, name):
-        return Attachment.attachmentWithName(self._txn, self._dropboxID, name)
+        return Attachment.loadWithName(self._txn, self._dropboxID, name)
 
     def attendeesCanManageAttachments(self):
         return self._attachment == _ATTACHMENTS_MODE_WRITE
@@ -731,56 +760,73 @@
         """
         return MimeType.fromString("text/calendar; charset=utf-8")
 
-class AttachmentStorageTransport(object):
 
-    implements(ITransport)
 
-    def __init__(self, attachment, contentType):
-        self.attachment = attachment
-        self.contentType = contentType
-        self.buf = ''
-        self.hash = hashlib.md5()
+class AttachmentStorageTransport(StorageTransportBase):
 
+    def __init__(self, attachment, contentType, creating=False):
+        super(AttachmentStorageTransport, self).__init__(
+            attachment, contentType)
+        self._buf = ''
+        self._hash = hashlib.md5()
+        self._creating = creating
 
+
     @property
     def _txn(self):
-        return self.attachment._txn
+        return self._attachment._txn
 
 
     def write(self, data):
-        self.buf += data
-        self.hash.update(data)
+        if isinstance(data, buffer):
+            data = str(data)
+        self._buf += data
+        self._hash.update(data)
 
 
     @inlineCallbacks
     def loseConnection(self):
 
-        old_size = self.attachment.size()
+        # FIXME: this should be synchronously accessible; IAttachment should
+        # have a method for getting its parent just as CalendarObject/Calendar
+        # do.
 
-        self.attachment._path.setContent(self.buf)
-        self.attachment._contentType = self.contentType
-        self.attachment._md5 = self.hash.hexdigest()
-        self.attachment._size = len(self.buf)
+        # FIXME: If this method isn't called, the transaction should be
+        # prevented from committing successfully.  It's not valid to have an
+        # attachment that doesn't point to a real file.
+
+        home = (yield self._txn.calendarHomeWithResourceID(
+                    self._attachment._ownerHomeID))
+
+        oldSize = self._attachment.size()
+
+        if home.quotaAllowedBytes() < ((yield home.quotaUsedBytes())
+                                       + (len(self._buf) - oldSize)):
+            if self._creating:
+                yield self._attachment._internalRemove()
+            raise QuotaExceeded()
+
+        self._attachment._path.setContent(self._buf)
+        self._attachment._contentType = self._contentType
+        self._attachment._md5 = self._hash.hexdigest()
+        self._attachment._size = len(self._buf)
         att = schema.ATTACHMENT
-        self.attachment._created, self.attachment._modified = map(
+        self._attachment._created, self._attachment._modified = map(
             sqltime,
             (yield Update(
                 {
-                    att.CONTENT_TYPE : generateContentType(self.contentType),
-                    att.SIZE         : self.attachment._size,
-                    att.MD5          : self.attachment._md5,
+                    att.CONTENT_TYPE : generateContentType(self._contentType),
+                    att.SIZE         : self._attachment._size,
+                    att.MD5          : self._attachment._md5,
                     att.MODIFIED     : utcNowSQL
                 },
-                Where=att.PATH == self.attachment.name(),
+                Where=att.PATH == self._attachment.name(),
                 Return=(att.CREATED, att.MODIFIED)).on(self._txn))[0]
         )
 
-        home = (
-            yield self._txn.calendarHomeWithResourceID(
-                self.attachment._ownerHomeID))
         if home:
             # Adjust quota
-            yield home.adjustQuotaUsedBytes(self.attachment.size() - old_size)
+            yield home.adjustQuotaUsedBytes(self._attachment.size() - oldSize)
 
             # Send change notification to home
             yield home.notifyChanged()
@@ -794,12 +840,13 @@
 
     implements(IAttachment)
 
-    def __init__(self, txn, dropboxID, name, ownerHomeID=None):
+    def __init__(self, txn, dropboxID, name, ownerHomeID=None, justCreated=False):
         self._txn = txn
         self._dropboxID = dropboxID
         self._name = name
         self._ownerHomeID = ownerHomeID
         self._size = 0
+        self._justCreated = justCreated
 
 
     @classmethod
@@ -823,7 +870,7 @@
             pass
 
         # Now create the DB entry
-        attachment = cls(txn, dropboxID, name, ownerHomeID)
+        attachment = cls(txn, dropboxID, name, ownerHomeID, True)
         att = schema.ATTACHMENT
         yield Insert({
             att.CALENDAR_HOME_RESOURCE_ID : ownerHomeID,
@@ -838,8 +885,8 @@
 
     @classmethod
     @inlineCallbacks
-    def attachmentWithName(cls, txn, dropboxID, name):
-        attachment = Attachment(txn, dropboxID, name)
+    def loadWithName(cls, txn, dropboxID, name):
+        attachment = cls(txn, dropboxID, name)
         attachment = (yield attachment.initFromStore())
         returnValue(attachment)
 
@@ -886,7 +933,7 @@
 
 
     def store(self, contentType):
-        return AttachmentStorageTransport(self, contentType)
+        return AttachmentStorageTransport(self, contentType, self._justCreated)
 
 
     def retrieve(self, protocol):
@@ -903,19 +950,28 @@
 
     @inlineCallbacks
     def remove(self):
-        old_size = self._size
+        oldSize = self._size
         self._txn.postCommit(self._path.remove)
-        yield self._removeStatement.on(self._txn, dropboxID=self._dropboxID,
-                                       path=self._name)
+        yield self._internalRemove()
         # Adjust quota
         home = (yield self._txn.calendarHomeWithResourceID(self._ownerHomeID))
         if home:
-            yield home.adjustQuotaUsedBytes(-old_size)
+            yield home.adjustQuotaUsedBytes(-oldSize)
 
             # Send change notification to home
             yield home.notifyChanged()
 
 
+    def _internalRemove(self):
+        """
+        Just delete the row; don't do any accounting / bookkeeping.  (This is
+        for attachments that have failed to be created due to errors during
+        storage.)
+        """
+        return self._removeStatement.on(self._txn, dropboxID=self._dropboxID,
+                                        path=self._name)
+
+
     # IDataStoreObject
     def contentType(self):
         return self._contentType

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/common.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/common.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,6 +1,6 @@
 # -*- test-case-name: txdav.caldav.datastore -*-
 ##
-# Copyright (c) 2010 Apple Inc. All rights reserved.
+# Copyright (c) 2010-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -19,6 +19,8 @@
 Tests for common calendar store API functions.
 """
 
+from StringIO import StringIO
+
 from twisted.internet.defer import Deferred, inlineCallbacks, returnValue,\
     maybeDeferred
 from twisted.internet.protocol import Protocol
@@ -49,6 +51,9 @@
 
 
 from twistedcaldav.customxml import InviteNotification, InviteSummary
+from txdav.caldav.icalendarstore import IAttachmentStorageTransport
+from txdav.caldav.icalendarstore import QuotaExceeded
+from txdav.common.datastore.test.util import deriveQuota
 from twistedcaldav.ical import Component
 
 storePath = FilePath(__file__).parent().child("calendar_store")
@@ -860,10 +865,14 @@
         self.assertIsInstance(calendar.created(), int)
         self.assertIsInstance(calendar.modified(), int)
 
-        self.assertEqual(calendar.accessMode, CommonTests.metadata1["accessMode"])
-        self.assertEqual(calendar.isScheduleObject, CommonTests.metadata1["isScheduleObject"])
-        self.assertEqual(calendar.scheduleEtags, CommonTests.metadata1["scheduleEtags"])
-        self.assertEqual(calendar.hasPrivateComment, CommonTests.metadata1["hasPrivateComment"])
+        self.assertEqual(calendar.accessMode,
+                         CommonTests.metadata1["accessMode"])
+        self.assertEqual(calendar.isScheduleObject,
+                         CommonTests.metadata1["isScheduleObject"])
+        self.assertEqual(calendar.scheduleEtags,
+                         CommonTests.metadata1["scheduleEtags"])
+        self.assertEqual(calendar.hasPrivateComment,
+                         CommonTests.metadata1["hasPrivateComment"])
 
         calendar.accessMode = Component.ACCESS_PRIVATE
         calendar.isScheduleObject = True
@@ -912,13 +921,116 @@
         self.assertEquals(component.resourceUID(), "uid1")
 
 
+    perUserComponent = lambda self: VComponent.fromString("""BEGIN:VCALENDAR
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART:20110101T120000Z
+DTEND:20110101T120100Z
+DTSTAMP:20080601T120000Z
+UID:event-with-some-per-user-data
+ATTENDEE:urn:uuid:home1
+ORGANIZER:urn:uuid:home1
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+X-CALENDARSERVER-PERUSER-UID:some-other-user
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:somebody else
+TRIGGER:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+X-CALENDARSERVER-PERUSER-UID:home1
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:the owner
+TRIGGER:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"))
+
+
+    asSeenByOwner = lambda self: VComponent.fromString("""BEGIN:VCALENDAR
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART:20110101T120000Z
+DTEND:20110101T120100Z
+DTSTAMP:20080601T120000Z
+UID:event-with-some-per-user-data
+ATTENDEE:urn:uuid:home1
+ORGANIZER:urn:uuid:home1
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:the owner
+TRIGGER:-PT20M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"))
+
+
+    asSeenByOther = lambda self: VComponent.fromString("""BEGIN:VCALENDAR
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART:20110101T120000Z
+DTEND:20110101T120100Z
+DTSTAMP:20080601T120000Z
+UID:event-with-some-per-user-data
+ATTENDEE:urn:uuid:home1
+ORGANIZER:urn:uuid:home1
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:somebody else
+TRIGGER:-PT20M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"))
+
+
     @inlineCallbacks
+    def setUpPerUser(self):
+        """
+        Set up state for testing of per-user components.
+        """
+        cal = yield self.calendarUnderTest()
+        yield cal.createCalendarObjectWithName(
+            "per-user-stuff.ics",
+            self.perUserComponent())
+        returnValue((yield cal.calendarObjectWithName("per-user-stuff.ics")))
+
+
+    @inlineCallbacks
+    def test_filteredComponent(self):
+        """
+        L{ICalendarObject.filteredComponent} returns a L{VComponent} that has
+        filtered per-user data.
+        """
+        obj = yield self.setUpPerUser()
+        temp = yield obj.component()
+        obj._component = temp.duplicate()
+        otherComp = (yield obj.filteredComponent("some-other-user"))
+        self.assertEquals(otherComp, self.asSeenByOther())
+        obj._component = temp.duplicate()
+        ownerComp = (yield obj.filteredComponent("home1"))
+        self.assertEquals(ownerComp, self.asSeenByOwner())
+
+
+    @inlineCallbacks
     def test_iCalendarText(self):
         """
         L{ICalendarObject.iCalendarText} returns a C{str} describing the same
         data provided by L{ICalendarObject.component}.
         """
-        text = yield (yield self.calendarObjectUnderTest()).iCalendarText()
+        text = yield (yield self.calendarObjectUnderTest())._text()
         self.assertIsInstance(text, str)
         self.failUnless(text.startswith("BEGIN:VCALENDAR\r\n"))
         self.assertIn("\r\nUID:uid1\r\n", text)
@@ -1247,7 +1359,7 @@
                 (yield self.calendarObjectUnderTest()).properties()[propertyName],
                 propertyContent)
             obj = yield self.calendarObjectUnderTest()
-            event1_text = yield obj.iCalendarText()
+            event1_text = yield obj._text()
             event1_text_withDifferentSubject = event1_text.replace(
                 "SUMMARY:CalDAV protocol updates",
                 "SUMMARY:Changed"
@@ -1329,7 +1441,7 @@
         now that logic lives in the protocol layer, so this testing method
         replicates it.
         """
-        uuid, rev = token.split("_", 1)
+        _ignore_uuid, rev = token.split("_", 1)
         rev = int(rev)
         return rev
 
@@ -1442,6 +1554,7 @@
             "new.attachment",
         )
         t = attachment.store(MimeType("text", "x-fixture"))
+        self.assertProvides(IAttachmentStorageTransport, t)
         t.write("new attachment")
         t.write(" text")
         yield t.loseConnection()
@@ -1492,6 +1605,108 @@
         return self.createAttachmentTest(refresh)
 
 
+    @inlineCallbacks
+    def test_quotaAllowedBytes(self):
+        """
+        L{ICalendarHome.quotaAllowedBytes} should return the configuration value
+        passed to the calendar store's constructor.
+        """
+        expected = deriveQuota(self.id())
+        home = yield self.homeUnderTest()
+        actual = home.quotaAllowedBytes()
+        self.assertEquals(expected, actual)
+
+
+    @inlineCallbacks
+    def test_quotaTransportAddress(self):
+        """
+        Since L{IAttachmentStorageTransport} is a subinterface of L{ITransport},
+        it must provide peer and host addresses.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        name = 'a-fun-attachment'
+        attachment = yield obj.createAttachmentWithName(name)
+        transport = attachment.store(MimeType("test", "x-something"))
+        peer = transport.getPeer()
+        host = transport.getHost()
+        self.assertIdentical(peer.attachment, attachment)
+        self.assertIdentical(host.attachment, attachment)
+        self.assertIn(name, repr(peer))
+        self.assertIn(name, repr(host))
+
+
+    @inlineCallbacks
+    def exceedQuotaTest(self, getit):
+        """
+        If too many bytes are passed to the transport returned by
+        L{ICalendarObject.createAttachmentWithName},
+        L{IAttachmentStorageTransport.loseConnection} will return a L{Deferred}
+        that fails with L{QuotaExceeded}.
+        """
+        home = yield self.homeUnderTest()
+        attachment = yield getit() 
+        t = attachment.store(MimeType("text", "x-fixture"))
+        sample = "all work and no play makes jack a dull boy"
+        chunk = (sample * (home.quotaAllowedBytes() / len(sample)))
+
+        t.write(chunk)
+        t.writeSequence([chunk, chunk])
+
+        d = t.loseConnection()
+        yield self.failUnlessFailure(d, QuotaExceeded)
+
+
+    @inlineCallbacks
+    def test_exceedQuotaNew(self):
+        """
+        When quota is exceeded on a new attachment, that attachment will no
+        longer exist.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        yield self.exceedQuotaTest(
+            lambda: obj.createAttachmentWithName("too-big.attachment")
+        )
+        self.assertEquals((yield obj.attachments()), [])
+        yield self.commit()
+        obj = yield self.calendarObjectUnderTest()
+        self.assertEquals((yield obj.attachments()), [])
+
+
+    @inlineCallbacks
+    def test_exceedQuotaReplace(self):
+        """
+        When quota is exceeded while replacing an attachment, that attachment's
+        contents will not be replaced.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        create = lambda: obj.createAttachmentWithName("exists.attachment")
+        get = lambda: obj.attachmentWithName("exists.attachment")
+        attachment = yield create()
+        t = attachment.store(MimeType("text", "x-fixture"))
+        sampleData = "a reasonably sized attachment"
+        t.write(sampleData)
+        yield t.loseConnection()
+        yield self.exceedQuotaTest(get)
+        def checkOriginal():
+            catch = StringIO()
+            catch.dataReceived = catch.write
+            lost = []
+            catch.connectionLost = lost.append
+            attachment.retrieve(catch)
+            expected = sampleData
+            # note: 60 is less than len(expected); trimming is just to make
+            # the error message look sane when the test fails.
+            actual = catch.getvalue()[:60]
+            self.assertEquals(actual, expected)
+        checkOriginal()
+        yield self.commit()
+        # Make sure that things go back to normal after a commit of that
+        # transaction.
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield get()
+        checkOriginal()
+
+
     def test_removeAttachmentWithName(self, refresh=lambda x:x):
         """
         L{ICalendarObject.removeAttachmentWithName} will remove the calendar
@@ -1555,7 +1770,7 @@
         yield self.calendarObjectUnderTest()
         txn = self.lastTransaction
         yield self.commit()
-        
+
         yield self.failUnlessFailure(
             maybeDeferred(txn.commit),
             AlreadyFinishedError

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_file.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_file.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -38,6 +38,7 @@
 from txdav.caldav.datastore.file import CalendarStore, CalendarHome
 from txdav.caldav.datastore.file import Calendar, CalendarObject
 
+from txdav.common.datastore.test.util import deriveQuota
 from txdav.caldav.datastore.test.common import (
     CommonTests, event4_text, event1modified_text)
 
@@ -64,8 +65,10 @@
     calendarPath.parent().makedirs()
     storePath.copyTo(calendarPath)
 
-    test.calendarStore = CalendarStore(storeRootPath, test.notifierFactory)
-    test.txn = test.calendarStore.newTransaction(test.id() + "(old)")
+    testID = test.id()
+    test.calendarStore = CalendarStore(storeRootPath, test.notifierFactory,
+                                       quota=deriveQuota(testID))
+    test.txn = test.calendarStore.newTransaction(testID + "(old)")
     assert test.calendarStore is not None, "No calendar store?"
 
 
@@ -77,6 +80,7 @@
     assert test.home1 is not None, "No calendar home?"
 
 
+
 @inlineCallbacks
 def setUpCalendar1(test):
     yield setUpHome1(test)
@@ -375,23 +379,6 @@
         )
 
 
-    @inlineCallbacks
-    def test_modifyCalendarObjectCaches(self):
-        """
-        Modifying a calendar object should cache the modified component in
-        memory, to avoid unnecessary parsing round-trips.
-        """
-        self.addCleanup(self.txn.commit)
-        modifiedComponent = VComponent.fromString(event1modified_text)
-        (yield self.calendar1.calendarObjectWithName("1.ics")).setComponent(
-            modifiedComponent
-        )
-        self.assertIdentical(
-            modifiedComponent,
-            (yield self.calendar1.calendarObjectWithName("1.ics")).component()
-        )
-
-
     @featureUnimplemented
     def test_removeCalendarObjectWithUID_absent(self):
         """

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_index_file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_index_file.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_index_file.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -49,7 +49,8 @@
         try:
             component = Component.fromString(text)
             # Fix any bogus data we can
-            component.validateComponentsForCalDAV(False, fix=True)
+            component.validCalendarData()
+            component.validCalendarForCalDAV(methodAllowed=False)
         except InvalidICalendarDataError, e:
             raise InternalDataStoreError(
                 "File corruption detected (%s) in file: %s"
@@ -163,6 +164,7 @@
 UID:12345-67890-1.1
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -182,6 +184,7 @@
 UID:12345-67890-2.1
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -202,6 +205,7 @@
 UID:12345-67890-2.2
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -212,6 +216,7 @@
 RECURRENCE-ID:20080608T120000Z
 DTSTART:20080608T120000Z
 DTEND:20080608T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -231,6 +236,7 @@
 UID:12345-67890-2.3
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -241,6 +247,7 @@
 RECURRENCE-ID:20080609T120000Z
 DTSTART:20080608T120000Z
 DTEND:20080608T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -260,6 +267,7 @@
 UID:12345-67890-2.4
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -270,6 +278,7 @@
 RECURRENCE-ID:20080609T120000Z
 DTSTART:20080608T120000Z
 DTEND:20080608T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -322,6 +331,7 @@
 UID:12345-67890-1.1
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -342,6 +352,7 @@
 UID:12345-67890-1.2
 DTSTART:20080602T120000Z
 DTEND:20080602T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -363,6 +374,7 @@
 UID:12345-67890-1.3
 DTSTART:20080603T120000Z
 DTEND:20080603T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -384,6 +396,7 @@
 UID:12345-67890-1.4
 DTSTART:20080604T120000Z
 DTEND:20080604T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -405,6 +418,7 @@
 UID:12345-67890-2.1
 DTSTART:20080605T120000Z
 DTEND:20080605T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -429,6 +443,7 @@
 UID:12345-67890-2.2
 DTSTART:20080607T120000Z
 DTEND:20080607T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -439,6 +454,7 @@
 RECURRENCE-ID:20080608T120000Z
 DTSTART:20080608T140000Z
 DTEND:20080608T150000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -502,6 +518,7 @@
 UID:12345-67890-1.1
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -543,6 +560,7 @@
 UID:12345-67890-1.2
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -599,6 +617,7 @@
 UID:12345-67890-1.1
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -647,6 +666,7 @@
 UID:12345-67890-1.2
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -713,6 +733,7 @@
 UID:12345-67890-1.1
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -723,6 +744,7 @@
 RECURRENCE-ID:20080602T120000Z
 DTSTART:20080602T130000Z
 DTEND:20080602T140000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -776,6 +798,7 @@
 UID:12345-67890-1.2
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -786,6 +809,7 @@
 RECURRENCE-ID:20080602T120000Z
 DTSTART:20080602T130000Z
 DTEND:20080602T140000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -899,6 +923,7 @@
 UID:12345-67890-1.1
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -912,6 +937,7 @@
 UID:12345-67890-2.1
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
@@ -926,6 +952,7 @@
 UID:12345-67890-2.3
 DTSTART:20080601T120000Z
 DTEND:20080601T130000Z
+DTSTAMP:20080601T120000Z
 ORGANIZER;CN="User 01":mailto:user1 at example.com
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com


Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_index_file.py
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation/txdav/caldav/datastore/test/test_index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/caldav/datastore/test/test_index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/caldav/datastore/test/test_index_file.py:6167-6191
/CalendarServer/branches/new-store/txdav/caldav/datastore/test/test_index_file.py:5594-5934
/CalendarServer/branches/new-store-no-caldavfile/txdav/caldav/datastore/test/test_index_file.py:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/caldav/datastore/test/test_index_file.py:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/caldav/datastore/test/test_index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/caldav/datastore/test/test_index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/caldav/datastore/test/test_index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/caldav/datastore/test/test_index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/caldav/datastore/test/test_index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/pods/txdav/caldav/datastore/test/test_index_file.py:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar/txdav/caldav/datastore/test/test_index_file.py:7085-7206
/CalendarServer/branches/users/cdaboo/pycard/txdav/caldav/datastore/test/test_index_file.py:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/caldav/datastore/test/test_index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/caldav/datastore/test/test_index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/caldav/datastore/test/test_index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/caldav/datastore/test/test_index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/caldav/datastore/test/test_index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/caldav/datastore/test/test_index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/caldav/datastore/test/test_index_file.py:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes/txdav/caldav/datastore/test/test_index_file.py:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/caldav/datastore/test/test_index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/caldav/datastore/test/test_index_file.py:6369
/CalendarServer/branches/users/glyph/oracle/txdav/caldav/datastore/test/test_index_file.py:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls/txdav/caldav/datastore/test/test_index_file.py:7340-7351
/CalendarServer/branches/users/glyph/sendfdport/txdav/caldav/datastore/test/test_index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/caldav/datastore/test/test_index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/caldav/datastore/test/test_index_file.py:5929-6073
/CalendarServer/branches/users/glyph/subtransactions/txdav/caldav/datastore/test/test_index_file.py:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/caldav/datastore/test/test_index_file.py:5084-5149
/CalendarServer/branches/users/sagen/inboxitems/txdav/caldav/datastore/test/test_index_file.py:7380-7381
/CalendarServer/branches/users/sagen/locations-resources/txdav/caldav/datastore/test/test_index_file.py:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/caldav/datastore/test/test_index_file.py:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events/txdav/caldav/datastore/test/test_index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/caldav/datastore/test/test_index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/caldav/datastore/test/test_index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/caldav/datastore/test/test_index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/caldav/datastore/test/test_index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/test/test_index.py:6322-6394
   + /CalendarServer/branches/config-separation/txdav/caldav/datastore/test/test_index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/caldav/datastore/test/test_index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/caldav/datastore/test/test_index_file.py:6167-6191
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/caldav/datastore/test/test_index_file.py:5936-5981
/CalendarServer/branches/new-store-no-caldavfile/txdav/caldav/datastore/test/test_index_file.py:5911-5935
/CalendarServer/branches/new-store/txdav/caldav/datastore/test/test_index_file.py:5594-5934
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/caldav/datastore/test/test_index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/caldav/datastore/test/test_index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/caldav/datastore/test/test_index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/caldav/datastore/test/test_index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/caldav/datastore/test/test_index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/pods/txdav/caldav/datastore/test/test_index_file.py:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar/txdav/caldav/datastore/test/test_index_file.py:7085-7206
/CalendarServer/branches/users/cdaboo/pycard/txdav/caldav/datastore/test/test_index_file.py:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/caldav/datastore/test/test_index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/caldav/datastore/test/test_index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/caldav/datastore/test/test_index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/caldav/datastore/test/test_index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/caldav/datastore/test/test_index_file.py:6932-7023
/CalendarServer/branches/users/glyph/deploybuild/txdav/caldav/datastore/test/test_index_file.py:7563-7572
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/caldav/datastore/test/test_index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/caldav/datastore/test/test_index_file.py:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes/txdav/caldav/datastore/test/test_index_file.py:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/caldav/datastore/test/test_index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/caldav/datastore/test/test_index_file.py:6369
/CalendarServer/branches/users/glyph/new-export/txdav/caldav/datastore/test/test_index_file.py:7444-7485
/CalendarServer/branches/users/glyph/oracle-nulls/txdav/caldav/datastore/test/test_index_file.py:7340-7351
/CalendarServer/branches/users/glyph/oracle/txdav/caldav/datastore/test/test_index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/caldav/datastore/test/test_index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/caldav/datastore/test/test_index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/caldav/datastore/test/test_index_file.py:5929-6073
/CalendarServer/branches/users/glyph/subtransactions/txdav/caldav/datastore/test/test_index_file.py:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/caldav/datastore/test/test_index_file.py:5084-5149
/CalendarServer/branches/users/sagen/inboxitems/txdav/caldav/datastore/test/test_index_file.py:7380-7381
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/caldav/datastore/test/test_index_file.py:5052-5061
/CalendarServer/branches/users/sagen/locations-resources/txdav/caldav/datastore/test/test_index_file.py:5032-5051
/CalendarServer/branches/users/sagen/purge_old_events/txdav/caldav/datastore/test/test_index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/caldav/datastore/test/test_index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/caldav/datastore/test/test_index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/caldav/datastore/test/test_index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/caldav/datastore/test/test_index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/test/test_index.py:6322-6394
/CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py:7443-7695

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_scheduling.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_scheduling.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_scheduling.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -53,7 +53,7 @@
 
     def storeUnderTest(self):
         if self.implicitStore is None:
-            sut = FileStorageTests.storeUnderTest(self)
+            sut = super(ImplicitStoreTests, self).storeUnderTest()
             self.implicitStore = ImplicitStore(sut)
         return self.implicitStore
 
@@ -62,3 +62,5 @@
 
     test_calendarObjectsWithDotFile = skipit
     test_init = skipit
+
+del FileStorageTests

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_sql.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/test/test_sql.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -38,8 +38,8 @@
 from txdav.common.datastore.sql_tables import schema
 from txdav.common.datastore.test.util import buildStore, populateCalendarsFrom
 
-from twistedcaldav import memcacher, caldavxml
-from twistedcaldav.config import config
+from twistedcaldav import caldavxml
+
 from twistedcaldav.dateops import datetimeMktime
 from twistedcaldav.sharing import SharedCollectionRecord
 
@@ -52,10 +52,6 @@
 
     @inlineCallbacks
     def setUp(self):
-        self.patch(config.Memcached.Pools.Default, "ClientEnabled", False)
-        self.patch(config.Memcached.Pools.Default, "ServerEnabled", False)
-        self.patch(memcacher.Memcacher, "allowTestCache", True)
-
         yield super(CalendarSQLStorageTests, self).setUp()
         self._sqlCalendarStore = yield buildStore(self, self.notifierFactory)
         yield self.populate()

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/util.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/datastore/util.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -28,6 +28,10 @@
 from txdav.common.icommondatastore import InvalidObjectResourceError, \
     NoSuchObjectResourceError, InternalDataStoreError
 
+from twistedcaldav.datafilters.privateevents import PrivateEventFilter
+from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
+from zope.interface.declarations import implements
+from txdav.caldav.icalendarstore import IAttachmentStorageTransport
 from twext.python.log import Logger
 log = Logger()
 
@@ -60,12 +64,10 @@
         pass
 
     try:
-        # FIXME: This is a bad way to do this test, there should be a
+        # FIXME: This is a bad way to do this test (== 'inbox'), there should be a
         # Calendar-level API for it.
-        if calendar.name() == 'inbox':
-            component.validateComponentsForCalDAV(True)
-        else:
-            component.validateForCalDAV()
+        component.validCalendarData()
+        component.validCalendarForCalDAV(methodAllowed=calendar.name() == 'inbox')
     except InvalidICalendarDataError, e:
         raise InvalidObjectResourceError(e)
 
@@ -215,3 +217,105 @@
     # No migration for notifications, since they weren't present in earlier
     # released versions of CalendarServer.
 
+
+class CalendarObjectBase(object):
+    """
+    Base logic shared between file- and sql-based L{ICalendarObject}
+    implementations.
+    """
+
+    @inlineCallbacks
+    def filteredComponent(self, accessUID, asAdmin=False):
+        """
+        Filter this calendar object's iCalendar component as it would be
+        perceived by a particular user, accounting for per-user iCalendar data
+        and private events, and return a L{Deferred} that fires with that
+        object.
+
+        Unlike the result of C{component()}, which contains storage-specific
+        iCalendar properties, this is a valid iCalendar object which could be
+        serialized and displayed to other iCalendar-processing software.
+
+        @param accessUID: the UID of the principal who is accessing this
+            component.
+        @type accessUID: C{str} (UTF-8 encoded)
+
+        @param asAdmin: should the given UID be treated as an administrator?  If
+            this is C{True}, the resulting component will have an unobscured
+            view of private events, even if the given UID is not actually the
+            owner of said events.  (However, per-instance overridden values will
+            still be seen as the given C{accessUID}.)
+
+        @return: a L{Deferred} which fires with a
+            L{twistedcaldav.ical.Component}.
+        """
+        component = yield self.component()
+        calendar = self.calendar()
+        isOwner = asAdmin or (calendar._owned and
+                              calendar.ownerCalendarHome().uid() == accessUID)
+        for filter in [PrivateEventFilter(self.accessMode, isOwner),
+                       PerUserDataFilter(accessUID)]:
+            component = filter.filter(component)
+        returnValue(component)
+
+
+
+class StorageTransportAddress(object):
+    """
+    Peer / host address for L{IAttachmentStorageTransport} implementations.
+
+    @ivar attachment: the L{IAttachment} being stored.
+
+    @type attachment: L{IAttachment} provider
+
+    @ivar isHost: is this a host address or peer address?
+
+    @type isHost: C{bool}
+    """
+
+    def __init__(self, attachment, isHost):
+        """
+        Initialize with the attachment being stored.
+        """
+        self.attachment = attachment
+        self.isHost = isHost
+
+
+    def __repr__(self):
+        if self.isHost:
+            host = " (host)"
+        else:
+            host = ""
+        return '<Storing Attachment: %r%s>' % (self.attachment.name(), host)
+
+
+
+class StorageTransportBase(object):
+    """
+    Base logic shared between file- and sql-based L{IAttachmentStorageTransport}
+    implementations.
+    """
+
+    implements(IAttachmentStorageTransport)
+
+    def __init__(self, attachment, contentType):
+        """
+        Create a storage transport with a reference to an L{IAttachment} and a
+        L{twext.web2.http_headers.MimeType}.
+        """
+        self._attachment = attachment
+        self._contentType = contentType
+
+
+    def getPeer(self):
+        return StorageTransportAddress(self._attachment, False)
+
+
+    def getHost(self):
+        return StorageTransportAddress(self._attachment, True)
+
+
+    def writeSequence(self, seq):
+        return self.write(''.join(seq))
+
+

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/icalendarstore.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/caldav/icalendarstore.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,6 +1,6 @@
 # -*- test-case-name: txdav.caldav.datastore -*-
 ##
-# Copyright (c) 2010 Apple Inc. All rights reserved.
+# Copyright (c) 2010-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -23,18 +23,30 @@
     IShareableCollection
 from txdav.idav import IDataStoreObject, IDataStore
 
+from twisted.internet.interfaces import ITransport
 from txdav.idav import INotifier
 
 
 __all__ = [
-    # Classes
+    # Interfaces
     "ICalendarTransaction",
     "ICalendarHome",
     "ICalendar",
     "ICalendarObject",
+
+    # Exceptions
+    "QuotaExceeded",
 ]
 
 
+
+class QuotaExceeded(Exception):
+    """
+    The quota for a particular user has been exceeded.
+    """
+
+
+
 class ICalendarTransaction(ICommonTransaction):
     """
     Transaction functionality required to be implemented by calendar stores.
@@ -163,15 +175,34 @@
         """
 
 
+    def quotaAllowedBytes():
+        """
+        The number of bytes of quota that the user is allowed to access.
+
+        Currently this is only enforced / tracked against attachment data.
+
+        @rtype: C{int}
+        """
+
+
     def quotaUsedBytes():
         """
         The number of bytes counted towards the user's quota.
+
+        @rtype: C{int}
         """
 
 
     def adjustQuotaUsedBytes(delta):
         """
-        Increase the number of bytes that count towards the user's quota.
+        Increase or decrease the number of bytes that count towards the user's
+        quota.
+
+        @param delta: The number of bytes to adjust the quota by.
+
+        @type delta: C{int}
+
+        @raise QuotaExceeded: when the quota is exceeded.
         """
 
 
@@ -336,14 +367,6 @@
         @return: a C{VCALENDAR} L{VComponent}.
         """
 
-    def iCalendarText():
-        """
-        Retrieve the iCalendar text data for this calendar object.
-
-        @return: a string containing iCalendar data for a single
-            calendar object.
-        """
-
     def uid():
         """
         Retrieve the UID for this calendar object.
@@ -395,12 +418,18 @@
 
     def attachmentWithName(name):
         """
-        Retrieve an attachment from this calendar object.
+        Asynchronously retrieve an attachment with the given name from this
+        calendar object.
 
         @param name: An identifier, unique to this L{ICalendarObject}, which
             names the attachment for future retrieval.
 
         @type name: C{str}
+
+        @return: a L{Deferred} which fires with an L{IAttachment} with the given
+            name, or L{None} if no such attachment exists.
+
+        @rtype: L{Deferred}
         """
         # FIXME: MIME-type?
 
@@ -432,6 +461,36 @@
 
 
 
+class IAttachmentStorageTransport(ITransport):
+    """
+    An L{IAttachmentStorageTransport} is a transport which stores the bytes
+    written to in a calendar attachment.
+
+    The user of an L{IAttachmentStorageTransport} must call C{loseConnection} on
+    its result to indicate that the attachment upload was successfully
+    completed.  If the transaction associated with this upload is committed or
+    aborted before C{loseConnection} is called, the upload will be presumed to
+    have failed, and no attachment data will be stored.
+    """
+
+    # Note: should also require IConsumer
+
+    def loseConnection():
+        """
+        The attachment has completed being uploaded successfully.
+
+        Unlike L{ITransport.loseConnection}, which returns C{None}, providers of
+        L{IAttachmentStorageTransport} must return a L{Deferred} from
+        C{loseConnection}, which may fire with a few different types of error;
+        for example, it may fail with a L{QuotaExceeded}.
+
+        If the upload fails for some reason, the transaction should be
+        terminated with L{ICalendarTransaction.abort} and this method should
+        never be called.
+        """
+
+
+
 class IAttachment(IDataStoreObject):
     """
     Information associated with an attachment to a calendar object.
@@ -439,17 +498,15 @@
 
     def store(contentType):
         """
+        Store an attachment (of the given MIME content/type).
+
         @param contentType: The content type of the data which will be stored.
+
         @type contentType: L{twext.web2.http_headers.MimeType}
 
-        @return: An L{ITransport}/L{IConsumer} provider that will store the
-            bytes passed to its 'write' method.
+        @return: A transport which stores the contents written to it.
 
-            The caller of C{store} must call C{loseConnection} on its result to
-            indicate that the attachment upload was successfully completed.  If
-            the transaction associated with this upload is committed or aborted
-            before C{loseConnection} is called, the upload will be presumed to
-            have failed, and no attachment data will be stored.
+        @rtype: L{IAttachmentStorageTransport}
         """
         # If you do a big write()/loseConnection(), how do you tell when the
         # data has actually been written?  you don't: commit() ought to return


Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/carddav
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/file.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/file.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -144,9 +144,6 @@
             ),
         )
 
-    def _doValidate(self, component):
-        component.validForCardDAV()
-
     def contentType(self):
         """
         The content type of Addresbook objects is text/vcard.
@@ -181,8 +178,8 @@
             self.name(), component
         )
 
-        self._component = component
-        # FIXME: needs to clear text cache
+        componentText = str(component)
+        self._objectText = componentText
 
         def do():
             # Mark all properties as dirty, so they can be added back
@@ -194,7 +191,6 @@
                 backup = hidden(self._path.temporarySibling())
                 self._path.moveTo(backup)
 
-            componentText = str(component)
             fh = self._path.open("w")
             try:
                 # FIXME: concurrency problem; if this write is interrupted
@@ -221,23 +217,38 @@
 
 
     def component(self):
-        if self._component is not None:
-            return self._component
-        text = self.text()
-
+        """
+        Read address data and validate/fix it. Do not raise a store error here if there are unfixable
+        errors as that could prevent the overall request to fail. Instead we will hand bad data off to
+        the caller - that is not ideal but in theory we should have checked everything on the way in and
+        only allowed in good data.
+        """
+        text = self._text()
         try:
             component = VComponent.fromString(text)
         except InvalidVCardDataError, e:
+            # This is a really bad situation, so do raise
             raise InternalDataStoreError(
                 "File corruption detected (%s) in file: %s"
                 % (e, self._path.path)
             )
+
+        # Fix any bogus data we can
+        fixed, unfixed = component.validVCardData(doFix=True, doRaise=False)
+
+        if unfixed:
+            self.log_error("Address data at %s had unfixable problems:\n  %s" % (self._path.path, "\n  ".join(unfixed),))
+        
+        if fixed:
+            self.log_error("Address data at %s had fixable problems:\n  %s" % (self._path.path, "\n  ".join(fixed),))
+
         return component
 
 
-    def text(self):
-        if self._component is not None:
-            return str(self._component)
+    def _text(self):
+        if self._objectText is not None:
+            return self._objectText
+
         try:
             fh = self._path.open()
         except IOError, e:
@@ -259,10 +270,10 @@
                 "File corruption detected (improper start) in file: %s"
                 % (self._path.path,)
             )
+        
+        self._objectText = text
         return text
 
-    vCardText = text
-
     def uid(self):
         if not hasattr(self, "_uid"):
             self._uid = self.component().resourceUID()

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/index_file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/index_file.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/index_file.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -589,6 +589,7 @@
                 # FIXME: This is blocking I/O
                 try:
                     vcard = Component.fromStream(stream)
+                    vcard.validVCardData()
                     vcard.validForCardDAV()
                 except ValueError:
                     log.err("Non-addressbook resource: %s" % (name,))


Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/index_file.py
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation/txdav/carddav/datastore/index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/carddav/datastore/index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/carddav/datastore/index_file.py:6167-6191
/CalendarServer/branches/new-store/txdav/carddav/datastore/index_file.py:5594-5934
/CalendarServer/branches/new-store-no-caldavfile/txdav/carddav/datastore/index_file.py:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/carddav/datastore/index_file.py:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/carddav/datastore/index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/carddav/datastore/index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/carddav/datastore/index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/carddav/datastore/index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/carddav/datastore/index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/pods/txdav/carddav/datastore/index_file.py:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar/txdav/carddav/datastore/index_file.py:7085-7206
/CalendarServer/branches/users/cdaboo/pycard/txdav/carddav/datastore/index_file.py:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/carddav/datastore/index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/carddav/datastore/index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/carddav/datastore/index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/carddav/datastore/index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/carddav/datastore/index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/carddav/datastore/index_file.py:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes/txdav/carddav/datastore/index_file.py:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/carddav/datastore/index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/carddav/datastore/index_file.py:6369
/CalendarServer/branches/users/glyph/oracle/txdav/carddav/datastore/index_file.py:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls/txdav/carddav/datastore/index_file.py:7340-7351
/CalendarServer/branches/users/glyph/sendfdport/txdav/carddav/datastore/index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/carddav/datastore/index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/carddav/datastore/index_file.py:5929-6073
/CalendarServer/branches/users/glyph/subtransactions/txdav/carddav/datastore/index_file.py:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/carddav/datastore/index_file.py:5084-5149
/CalendarServer/branches/users/sagen/inboxitems/txdav/carddav/datastore/index_file.py:7380-7381
/CalendarServer/branches/users/sagen/locations-resources/txdav/carddav/datastore/index_file.py:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/carddav/datastore/index_file.py:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events/txdav/carddav/datastore/index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/carddav/datastore/index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/carddav/datastore/index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/carddav/datastore/index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/carddav/datastore/index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/vcardindex.py:6322-6394
   + /CalendarServer/branches/config-separation/txdav/carddav/datastore/index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/carddav/datastore/index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/carddav/datastore/index_file.py:6167-6191
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/carddav/datastore/index_file.py:5936-5981
/CalendarServer/branches/new-store-no-caldavfile/txdav/carddav/datastore/index_file.py:5911-5935
/CalendarServer/branches/new-store/txdav/carddav/datastore/index_file.py:5594-5934
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/carddav/datastore/index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/carddav/datastore/index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/carddav/datastore/index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/carddav/datastore/index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/carddav/datastore/index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/pods/txdav/carddav/datastore/index_file.py:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar/txdav/carddav/datastore/index_file.py:7085-7206
/CalendarServer/branches/users/cdaboo/pycard/txdav/carddav/datastore/index_file.py:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/carddav/datastore/index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/carddav/datastore/index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/carddav/datastore/index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/carddav/datastore/index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/index_file.py:6932-7023
/CalendarServer/branches/users/glyph/deploybuild/txdav/carddav/datastore/index_file.py:7563-7572
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/carddav/datastore/index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/carddav/datastore/index_file.py:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes/txdav/carddav/datastore/index_file.py:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/carddav/datastore/index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/carddav/datastore/index_file.py:6369
/CalendarServer/branches/users/glyph/new-export/txdav/carddav/datastore/index_file.py:7444-7485
/CalendarServer/branches/users/glyph/oracle-nulls/txdav/carddav/datastore/index_file.py:7340-7351
/CalendarServer/branches/users/glyph/oracle/txdav/carddav/datastore/index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/carddav/datastore/index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/carddav/datastore/index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/carddav/datastore/index_file.py:5929-6073
/CalendarServer/branches/users/glyph/subtransactions/txdav/carddav/datastore/index_file.py:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/carddav/datastore/index_file.py:5084-5149
/CalendarServer/branches/users/sagen/inboxitems/txdav/carddav/datastore/index_file.py:7380-7381
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/carddav/datastore/index_file.py:5052-5061
/CalendarServer/branches/users/sagen/locations-resources/txdav/carddav/datastore/index_file.py:5032-5051
/CalendarServer/branches/users/sagen/purge_old_events/txdav/carddav/datastore/index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/carddav/datastore/index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/carddav/datastore/index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/carddav/datastore/index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/carddav/datastore/index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/vcardindex.py:6322-6394
/CalendarServer/trunk/txdav/carddav/datastore/index_file.py:7443-7695

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/sql.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/sql.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -14,6 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from txdav.common.icommondatastore import InternalDataStoreError
 
 """
 SQL backend for CardDAV storage.
@@ -35,7 +36,7 @@
 
 from twistedcaldav import carddavxml, customxml
 from twistedcaldav.memcacher import Memcacher
-from twistedcaldav.vcard import Component as VCard
+from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
 
 from txdav.common.datastore.sql_legacy import \
     PostgresLegacyABIndexEmulator, SQLLegacyAddressBookInvites,\
@@ -177,10 +178,6 @@
         )
 
 
-    def _doValidate(self, component):
-        component.validForCardDAV()
-
-
     def contentType(self):
         """
         The content type of Addresbook objects is text/vcard.
@@ -271,16 +268,39 @@
 
     @inlineCallbacks
     def component(self):
-        returnValue(VCard.fromString((yield self.vCardText())))
+        """
+        Read address data and validate/fix it. Do not raise a store error here if there are unfixable
+        errors as that could prevent the overall request to fail. Instead we will hand bad data off to
+        the caller - that is not ideal but in theory we should have checked everything on the way in and
+        only allowed in good data.
+        """
+        text = yield self._text()
 
+        try:
+            component = VCard.fromString(text)
+        except InvalidVCardDataError, e:
+            # This is a really bad situation, so do raise
+            raise InternalDataStoreError(
+                "Data corruption detected (%s) in id: %s"
+                % (e, self._resourceID)
+            )
 
-    vCardText = CommonObjectResource.text
+        # Fix any bogus data we can
+        fixed, unfixed = component.validVCardData(doFix=True, doRaise=False)
 
+        if unfixed:
+            self.log_error("Address data id=%s had unfixable problems:\n  %s" % (self._resourceID, "\n  ".join(unfixed),))
+        
+        if fixed:
+            self.log_error("Address data id=%s had fixable problems:\n  %s" % (self._resourceID, "\n  ".join(fixed),))
 
+        returnValue(component)
+
+
     # IDataStoreObject
     def contentType(self):
         """
-        The content type of Addressbook objects is text/x-vcard.
+        The content type of Addressbook objects is text/vcard.
         """
         return MimeType.fromString("text/vcard; charset=utf-8")
 

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/test/common.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/test/common.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -594,7 +594,7 @@
         L{IAddressBookObject.iAddressBookText} returns a C{str} describing the same
         data provided by L{IAddressBookObject.component}.
         """
-        text = yield (yield self.addressbookObjectUnderTest()).vCardText()
+        text = yield (yield self.addressbookObjectUnderTest())._text()
         self.assertIsInstance(text, str)
         self.failUnless(text.startswith("BEGIN:VCARD\r\n"))
         self.assertIn("\r\nUID:uid1\r\n", text)
@@ -906,7 +906,7 @@
                 ],
                 propertyContent)
             obj = yield self.addressbookObjectUnderTest()
-            vcard1_text = yield obj.vCardText()
+            vcard1_text = yield obj._text()
             vcard1_text_withDifferentNote = vcard1_text.replace(
                 "NOTE:CardDAV protocol updates",
                 "NOTE:Changed"


Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/test/test_index_file.py
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation/txdav/carddav/datastore/test/test_index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/carddav/datastore/test/test_index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/carddav/datastore/test/test_index_file.py:6167-6191
/CalendarServer/branches/new-store/txdav/carddav/datastore/test/test_index_file.py:5594-5934
/CalendarServer/branches/new-store-no-caldavfile/txdav/carddav/datastore/test/test_index_file.py:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/carddav/datastore/test/test_index_file.py:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/carddav/datastore/test/test_index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/carddav/datastore/test/test_index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/carddav/datastore/test/test_index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/carddav/datastore/test/test_index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/carddav/datastore/test/test_index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/pods/txdav/carddav/datastore/test/test_index_file.py:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar/txdav/carddav/datastore/test/test_index_file.py:7085-7206
/CalendarServer/branches/users/cdaboo/pycard/txdav/carddav/datastore/test/test_index_file.py:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/carddav/datastore/test/test_index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/carddav/datastore/test/test_index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/carddav/datastore/test/test_index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/carddav/datastore/test/test_index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/test/test_index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/carddav/datastore/test/test_index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/carddav/datastore/test/test_index_file.py:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes/txdav/carddav/datastore/test/test_index_file.py:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/carddav/datastore/test/test_index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/carddav/datastore/test/test_index_file.py:6369
/CalendarServer/branches/users/glyph/oracle/txdav/carddav/datastore/test/test_index_file.py:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls/txdav/carddav/datastore/test/test_index_file.py:7340-7351
/CalendarServer/branches/users/glyph/sendfdport/txdav/carddav/datastore/test/test_index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/carddav/datastore/test/test_index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/carddav/datastore/test/test_index_file.py:5929-6073
/CalendarServer/branches/users/glyph/subtransactions/txdav/carddav/datastore/test/test_index_file.py:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/carddav/datastore/test/test_index_file.py:5084-5149
/CalendarServer/branches/users/sagen/inboxitems/txdav/carddav/datastore/test/test_index_file.py:7380-7381
/CalendarServer/branches/users/sagen/locations-resources/txdav/carddav/datastore/test/test_index_file.py:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/carddav/datastore/test/test_index_file.py:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events/txdav/carddav/datastore/test/test_index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/carddav/datastore/test/test_index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/carddav/datastore/test/test_index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/carddav/datastore/test/test_index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/carddav/datastore/test/test_index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/test/test_vcardindex.py:6322-6394
   + /CalendarServer/branches/config-separation/txdav/carddav/datastore/test/test_index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/carddav/datastore/test/test_index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/carddav/datastore/test/test_index_file.py:6167-6191
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/carddav/datastore/test/test_index_file.py:5936-5981
/CalendarServer/branches/new-store-no-caldavfile/txdav/carddav/datastore/test/test_index_file.py:5911-5935
/CalendarServer/branches/new-store/txdav/carddav/datastore/test/test_index_file.py:5594-5934
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/carddav/datastore/test/test_index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/carddav/datastore/test/test_index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/carddav/datastore/test/test_index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/carddav/datastore/test/test_index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/carddav/datastore/test/test_index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/pods/txdav/carddav/datastore/test/test_index_file.py:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar/txdav/carddav/datastore/test/test_index_file.py:7085-7206
/CalendarServer/branches/users/cdaboo/pycard/txdav/carddav/datastore/test/test_index_file.py:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/carddav/datastore/test/test_index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/carddav/datastore/test/test_index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/carddav/datastore/test/test_index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/carddav/datastore/test/test_index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/test/test_index_file.py:6932-7023
/CalendarServer/branches/users/glyph/deploybuild/txdav/carddav/datastore/test/test_index_file.py:7563-7572
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/carddav/datastore/test/test_index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/carddav/datastore/test/test_index_file.py:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes/txdav/carddav/datastore/test/test_index_file.py:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/carddav/datastore/test/test_index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/carddav/datastore/test/test_index_file.py:6369
/CalendarServer/branches/users/glyph/new-export/txdav/carddav/datastore/test/test_index_file.py:7444-7485
/CalendarServer/branches/users/glyph/oracle-nulls/txdav/carddav/datastore/test/test_index_file.py:7340-7351
/CalendarServer/branches/users/glyph/oracle/txdav/carddav/datastore/test/test_index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/carddav/datastore/test/test_index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/carddav/datastore/test/test_index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/carddav/datastore/test/test_index_file.py:5929-6073
/CalendarServer/branches/users/glyph/subtransactions/txdav/carddav/datastore/test/test_index_file.py:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/carddav/datastore/test/test_index_file.py:5084-5149
/CalendarServer/branches/users/sagen/inboxitems/txdav/carddav/datastore/test/test_index_file.py:7380-7381
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/carddav/datastore/test/test_index_file.py:5052-5061
/CalendarServer/branches/users/sagen/locations-resources/txdav/carddav/datastore/test/test_index_file.py:5032-5051
/CalendarServer/branches/users/sagen/purge_old_events/txdav/carddav/datastore/test/test_index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/carddav/datastore/test/test_index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/carddav/datastore/test/test_index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/carddav/datastore/test/test_index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/carddav/datastore/test/test_index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/test/test_vcardindex.py:6322-6394
/CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py:7443-7695

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/test/test_sql.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/test/test_sql.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -25,8 +25,8 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.trial import unittest
 
-from twistedcaldav import memcacher, carddavxml
-from twistedcaldav.config import config
+from twistedcaldav import carddavxml
+
 from twistedcaldav.vcard import Component as VCard
 from twistedcaldav.vcard import Component as VComponent
 
@@ -47,10 +47,6 @@
 
     @inlineCallbacks
     def setUp(self):
-        self.patch(config.Memcached.Pools.Default, "ClientEnabled", False)
-        self.patch(config.Memcached.Pools.Default, "ServerEnabled", False)
-        self.patch(memcacher.Memcacher, "allowTestCache", True)
-
         yield super(AddressBookSQLStorageTests, self).setUp()
         self._sqlStore = yield buildStore(self, self.notifierFactory)
         yield self.populate()

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/util.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/datastore/util.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -58,6 +58,7 @@
         pass
 
     try:
+        component.validVCardData()
         component.validForCardDAV()
     except InvalidVCardDataError, e:
         raise InvalidObjectResourceError(e)

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/iaddressbookstore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/iaddressbookstore.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/carddav/iaddressbookstore.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -1,6 +1,6 @@
 # -*- test-case-name: txdav.carddav.datastore,txdav.carddav.datastore.test.test_sql.AddressBookSQLStorageTests -*-
 ##
-# Copyright (c) 2010 Apple Inc. All rights reserved.
+# Copyright (c) 2010-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -257,14 +257,6 @@
         @return: a C{VCARD} L{VComponent}.
         """
 
-    def vCardText():
-        """
-        Retrieve the vCard text data for this addressbook object.
-
-        @return: a string containing vCard data for a single
-            addressbook object.
-        """
-
     def uid():
         """
         Retrieve the UID for this addressbook object.


Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/common
___________________________________________________________________
Added: svn:ignore
   + *.pyc



Property changes on: CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore
___________________________________________________________________
Added: svn:ignore
   + *.pyc


Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/file.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/file.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -75,15 +75,21 @@
 
 class CommonDataStore(DataStore):
     """
-    An implementation of data store.
+    Shared logic for SQL-based data stores, between calendar and addressbook
+    storage.
 
     @ivar _path: A L{CachingFilePath} referencing a directory on disk that
         stores all calendar and addressbook data for a group of UIDs.
+
+    @ivar quota: the amount of space granted to each calendar home (in bytes)
+        for storing attachments.
+
+    @type quota: C{int}
     """
     implements(ICalendarStore)
 
     def __init__(self, path, notifierFactory, enableCalendars=True,
-        enableAddressBooks=True):
+        enableAddressBooks=True, quota=(2 ** 20)):
         """
         Create a store.
 
@@ -96,6 +102,7 @@
         self.enableAddressBooks = enableAddressBooks
         self._notifierFactory = notifierFactory
         self._transactionClass = CommonStoreTransaction
+        self.quota = quota
 
 
     def newTransaction(self, name='no name', migrating=False):
@@ -250,6 +257,10 @@
         self._cachedChildren = {}
 
 
+    def quotaAllowedBytes(self):
+        return self._transaction.store().quota
+
+
     @classmethod
     def homeWithUID(cls, txn, uid, create=False, withNotifications=False):
 
@@ -846,9 +857,6 @@
         """
         pass
 
-    def _doValidate(self, component):
-        raise NotImplementedError
-
     def addNotifier(self, notifier):
         if self._notifiers is None:
             self._notifiers = ()
@@ -895,7 +903,7 @@
         self._name = name
         self._parentCollection = parent
         self._transaction = parent._transaction
-        self._component = None
+        self._objectText = None
 
 
     @property
@@ -916,7 +924,7 @@
         raise NotImplementedError
 
 
-    def text(self):
+    def _text(self):
         raise NotImplementedError
 
 
@@ -1077,12 +1085,7 @@
         name = uid + ".xml"
         self.removeNotificationObjectWithName(name)
 
-    def _doValidate(self, component):
-        # Nothing to do - notifications are always generated internally by the server
-        # so they better be valid all the time!
-        pass
 
-
 class NotificationObject(CommonObjectResource):
     """
     """

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/sql.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/sql.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -93,12 +93,39 @@
 }
 
 class CommonDataStore(Service, object):
+    """
+    Shared logic for SQL-based data stores, between calendar and addressbook
+    storage.
 
+    @ivar sqlTxnFactory: A 0-arg factory callable that produces an
+        L{IAsyncTransaction}.
+
+    @ivar notifierFactory: a L{twistedcaldav.notify.NotifierFactory} (or
+        similar) that produces new notifiers for homes and collections.
+
+    @ivar attachmentsPath: a L{FilePath} indicating a directory where
+        attachments may be stored.
+
+    @ivar enableCalendars: a boolean, C{True} if this data store should provide
+        L{ICalendarStore}, C{False} if not.
+
+    @ivar enableAddressBooks: a boolean, C{True} if this data store should
+        provide L{IAddressbookStore}, C{False} if not.
+
+    @ivar label: A string, used for tagging debug messages in the case where
+        there is more than one store.  (Useful mostly for unit tests.)
+
+    @ivar quota: the amount of space granted to each calendar home (in bytes)
+        for storing attachments.
+
+    @type quota: C{int}
+    """
+
     implements(ICalendarStore)
 
     def __init__(self, sqlTxnFactory, notifierFactory, attachmentsPath,
                  enableCalendars=True, enableAddressBooks=True,
-                 label="unlabeled"):
+                 label="unlabeled", quota=(2 ** 20)):
         assert enableCalendars or enableAddressBooks
 
         self.sqlTxnFactory = sqlTxnFactory
@@ -107,6 +134,7 @@
         self.enableCalendars = enableCalendars
         self.enableAddressBooks = enableAddressBooks
         self.label = label
+        self.quota = quota
 
 
     def eachCalendarHome(self):
@@ -407,7 +435,7 @@
 
         results = (yield self.eventsOlderThan(cutoff, batchSize=batchSize))
         count = 0
-        for uid, calendarName, eventName, maxDate in results:
+        for uid, calendarName, eventName, _ignore_maxDate in results:
             home = (yield self.calendarHomeWithUID(uid))
             calendar = (yield home.childWithName(calendarName))
             (yield calendar.removeObjectResourceWithName(eventName))
@@ -500,6 +528,10 @@
             self._revisionBindJoinTable["BIND:%s" % (key,)] = value
 
 
+    def quotaAllowedBytes(self):
+        return self._txn.store().quota
+
+
     @classproperty
     def _resourceIDFromOwnerQuery(cls): #@NoSelf
         home = cls._homeSchema
@@ -1304,6 +1336,10 @@
 class CommonHomeChild(LoggingMixIn, FancyEqMixin, _SharedSyncLogic):
     """
     Common ancestor class of AddressBooks and Calendars.
+
+    @ivar _owned: Is this calendar or addressbook referencing its sharer (owner)
+        home? (i.e. C{True} if L{ownerCalendarHome} will actually return the
+        sharer, C{False} or if it will return a sharee.)
     """
 
     compareAttributes = (
@@ -2001,10 +2037,6 @@
         """
 
 
-    def _doValidate(self, component):
-        raise NotImplementedError
-
-
     # IDataStoreObject
     def contentType(self):
         raise NotImplementedError()
@@ -2059,9 +2091,7 @@
 
 class CommonObjectResource(LoggingMixIn, FancyEqMixin):
     """
-    @ivar _path: The path of the file on disk
-
-    @type _path: L{FilePath}
+    Base class for object resources.
     """
 
     compareAttributes = (
@@ -2371,7 +2401,7 @@
 
 
     @inlineCallbacks
-    def text(self):
+    def _text(self):
         if self._objectText is None:
             text = (
                 yield self._textByIDQuery.on(self._txn,

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/sql_legacy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/sql_legacy.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/sql_legacy.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -30,13 +30,13 @@
 from twext.python.clsprop import classproperty
 from twext.python.log import Logger, LoggingMixIn
 
-from twistedcaldav import carddavxml
 from twistedcaldav.config import config
 from twistedcaldav.dateops import normalizeForIndex, pyCalendarTodatetime
 from twistedcaldav.memcachepool import CachePoolUserMixIn
 from twistedcaldav.notifications import NotificationRecord
 from twistedcaldav.query import (
-    calendarqueryfilter, calendarquery, addressbookquery)
+    calendarqueryfilter, calendarquery, addressbookquery, expression,
+    addressbookqueryfilter)
 from twistedcaldav.query.sqlgenerator import sqlgenerator
 from twistedcaldav.sharing import Invite
 
@@ -860,8 +860,23 @@
     """
 
     ISOP = " = "
-    CONTAINSOP = " LIKE "
-    NOTCONTAINSOP = " NOT LIKE "
+    STARTSWITHOP = ENDSWITHOP = CONTAINSOP = " LIKE "
+    NOTSTARTSWITHOP = NOTENDSWITHOP = NOTCONTAINSOP = " NOT LIKE "
+
+    def containsArgument(self, arg):
+        return "%%%s%%" % (arg,)
+
+    def startswithArgument(self, arg):
+        return "%s%%" % (arg,)
+
+    def endswithArgument(self, arg):
+        return "%%%s" % (arg,)
+
+class CalDAVSQLBehaviorMixin(RealSQLBehaviorMixin):
+    """
+    Query generator for CalDAV indexed searches.
+    """
+
     FIELDS = {
         "TYPE": "CALENDAR_OBJECT.ICALENDAR_TYPE",
         "UID":  "CALENDAR_OBJECT.ICALENDAR_UID",
@@ -891,6 +906,62 @@
         self.substitutions = []
         self.usedtimespan = False
 
+        # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
+        if self.calendarid:
+            
+            test = expression.isExpression("CALENDAR_OBJECT.CALENDAR_RESOURCE_ID", str(self.calendarid), True)
+
+            # Since timerange expression already have the calendar resource-id test in them, do not
+            # add the additional term to those. When the additional term is added, add it as the first
+            # component in the AND expression to hopefully get the DB to use its index first
+
+            # Top-level timerange expression already has calendar resource-id restriction in it
+            if isinstance(self.expression, expression.timerangeExpression):
+                pass
+            
+            # Top-level OR - check each component
+            elif isinstance(self.expression, expression.orExpression):
+                
+                def _hasTopLevelTimerange(testexpr):
+                    if isinstance(testexpr, expression.timerangeExpression):
+                        return True
+                    elif isinstance(testexpr, expression.andExpression):
+                        return any([isinstance(expr, expression.timerangeExpression) for expr in testexpr.expressions])
+                    else:
+                        return False
+                        
+                hasTimerange = any([_hasTopLevelTimerange(expr) for expr in self.expression.expressions])
+
+                if hasTimerange:
+                    # AND each of the non-timerange expressions
+                    trexpressions = []
+                    orexpressions = []
+                    for expr in self.expression.expressions:
+                        if _hasTopLevelTimerange(expr):
+                            trexpressions.append(expr)
+                        else:
+                            orexpressions.append(expr)
+                    
+                    if orexpressions:
+                        self.expression.expressions = tuple(trexpressions) + (
+                            test.andWith(expression.orExpression(orexpressions)),
+                        )
+                else:
+                    # AND the whole thing
+                    self.expression = test.andWith(self.expression)    
+
+            
+            # Top-level AND - only add additional expression if timerange not present
+            elif isinstance(self.expression, expression.andExpression):
+                hasTimerange = any([isinstance(expr, expression.timerangeExpression) for expr in self.expression.expressions])
+                if not hasTimerange:
+                    # AND the whole thing
+                    self.expression = test.andWith(self.expression)    
+            
+            # Just AND the entire thing
+            else:
+                self.expression = test.andWith(self.expression)
+
         # Generate ' where ...' partial statement
         self.sout.write(self.WHERE)
         self.generateExpression(self.expression)
@@ -911,11 +982,6 @@
         return select, self.arguments
 
 
-    def containsArgument(self, arg):
-        return "%%%s%%" % (arg,)
-
-
-
 class FormatParamStyleMixin(object):
     """
     Mixin for overriding methods on sqlgenerator that generate arguments
@@ -940,7 +1006,7 @@
 
 
 
-class postgresqlgenerator(FormatParamStyleMixin, RealSQLBehaviorMixin,
+class postgresqlgenerator(FormatParamStyleMixin, CalDAVSQLBehaviorMixin,
                           sqlgenerator):
     """
     Query generator for PostgreSQL indexed searches.
@@ -952,17 +1018,17 @@
 
 
 
-class oraclesqlgenerator(RealSQLBehaviorMixin, sqlgenerator):
+class oraclesqlgenerator(CalDAVSQLBehaviorMixin, sqlgenerator):
     """
     Query generator for Oracle indexed searches.
     """
-    TIMESPANTEST = fixbools(RealSQLBehaviorMixin.TIMESPANTEST)
-    TIMESPANTEST_NOEND = fixbools(RealSQLBehaviorMixin.TIMESPANTEST_NOEND)
-    TIMESPANTEST_NOSTART = fixbools(RealSQLBehaviorMixin.TIMESPANTEST_NOSTART)
+    TIMESPANTEST = fixbools(CalDAVSQLBehaviorMixin.TIMESPANTEST)
+    TIMESPANTEST_NOEND = fixbools(CalDAVSQLBehaviorMixin.TIMESPANTEST_NOEND)
+    TIMESPANTEST_NOSTART = fixbools(CalDAVSQLBehaviorMixin.TIMESPANTEST_NOSTART)
     TIMESPANTEST_TAIL_PIECE = fixbools(
-        RealSQLBehaviorMixin.TIMESPANTEST_TAIL_PIECE)
+        CalDAVSQLBehaviorMixin.TIMESPANTEST_TAIL_PIECE)
     TIMESPANTEST_JOIN_ON_PIECE = fixbools(
-        RealSQLBehaviorMixin.TIMESPANTEST_JOIN_ON_PIECE)
+        CalDAVSQLBehaviorMixin.TIMESPANTEST_JOIN_ON_PIECE)
 
 
 
@@ -1259,23 +1325,16 @@
 
 # CARDDAV
 
-class oraclesqladbkgenerator(sqlgenerator):
+class CardDAVSQLBehaviorMixin(RealSQLBehaviorMixin):
     """
-    Query generator for Oracle indexed searches.
+    Query generator for CardDAV indexed searches.
     """
 
-    ISOP = " = "
-    CONTAINSOP = " LIKE "
-    NOTCONTAINSOP = " NOT LIKE "
     FIELDS = {
         "UID":  "ADDRESSBOOK_OBJECT.VCARD_UID",
     }
     RESOURCEDB = "ADDRESSBOOK_OBJECT"
 
-    def containsArgument(self, arg):
-        return "%%%s%%" % (arg,)
-
-
     def generate(self):
         """
         Generate the actual SQL 'where ...' expression from the passed in
@@ -1291,6 +1350,13 @@
         self.arguments = []
         self.substitutions = []
 
+        # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
+        if self.calendarid:
+            
+            # AND the whole thing
+            test = expression.isExpression("ADDRESSBOOK_OBJECT.ADDRESSBOOK_RESOURCE_ID", str(self.calendarid), True)
+            self.expression = test.andWith(self.expression)    
+
         # Generate ' where ...' partial statement
         self.sout.write(self.WHERE)
         self.generateExpression(self.expression)
@@ -1305,15 +1371,18 @@
 
 
 
-class postgresqladbkgenerator(FormatParamStyleMixin, oraclesqladbkgenerator):
+class postgresqladbkgenerator(FormatParamStyleMixin, CardDAVSQLBehaviorMixin, sqlgenerator):
     """
-    Query generator for PostgreSQL indexed searches.  Inherit 'real' database
-    behavior from L{oracleadbkgenerator}, and %s-style formatting from
-    L{FormatParamStyleMixin}.
+    Query generator for PostgreSQL indexed searches.
     """
 
+class oraclesqladbkgenerator(CardDAVSQLBehaviorMixin, sqlgenerator):
+    """
+    Query generator for Oracle indexed searches.
+    """
 
 
+
 class PostgresLegacyABIndexEmulator(LegacyIndexHelper):
     """
     Emulator for L{twistedcaldv.index.Index} and
@@ -1356,7 +1425,7 @@
 
 
     def searchValid(self, filter):
-        if isinstance(filter, carddavxml.Filter):
+        if isinstance(filter, addressbookqueryfilter.Filter):
             qualifiers = addressbookquery.sqladdressbookquery(filter)
         else:
             qualifiers = None
@@ -1379,7 +1448,7 @@
         else:
             generator = postgresqladbkgenerator
         # Make sure we have a proper Filter element and get the partial SQL statement to use.
-        if isinstance(filter, carddavxml.Filter):
+        if isinstance(filter, addressbookqueryfilter.Filter):
             qualifiers = addressbookquery.sqladdressbookquery(
                 filter, self.addressbook._resourceID, generator=generator)
         else:
@@ -1392,10 +1461,10 @@
             )
         else:
             rowiter = yield Select(
-                [schema.ADDRESSBOOK_OBJECT.RESOURCE_NAME,
-                 schema.ADDRESSBOOK_OBJECT.VCARD_UID],
-                From=schema.ADDRESSBOOK_OBJECT,
-                Where=schema.ADDRESSBOOK_OBJECT.ADDRESSBOOK_RESOURCE_ID ==
+                [self._objectSchema.RESOURCE_NAME,
+                 self._objectSchema.VCARD_UID],
+                From=self._objectSchema,
+                Where=self._objectSchema.ADDRESSBOOK_RESOURCE_ID ==
                 self.addressbook._resourceID
             ).on(self.addressbook._txn)
 

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/test/test_util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/test/test_util.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/test/test_util.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -26,9 +26,6 @@
 from twisted.internet.protocol import Protocol
 from twisted.trial.unittest import TestCase
 
-from twistedcaldav.config import config
-from twistedcaldav.memcacher import Memcacher
-
 from txdav.caldav.datastore.test.common import CommonTests
 from txdav.carddav.datastore.test.common import CommonTests as ABCommonTests
 from txdav.common.datastore.file import CommonDataStore
@@ -48,9 +45,7 @@
         Set up two stores to migrate between.
         """
         # Add some files to the file store.
-        self.patch(config.Memcached.Pools.Default, "ClientEnabled", False)
-        self.patch(config.Memcached.Pools.Default, "ServerEnabled", False)
-        self.patch(Memcacher, "allowTestCache", True)
+
         self.filesPath = CachingFilePath(self.mktemp())
         self.filesPath.createDirectory()
         fileStore = self.fileStore = CommonDataStore(

Modified: CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/test/util.py	2011-06-30 16:48:43 UTC (rev 7696)
+++ CalendarServer/branches/users/cdaboo/timezones/txdav/common/datastore/test/util.py	2011-06-30 20:44:13 UTC (rev 7697)
@@ -20,6 +20,8 @@
 """
 
 import gc
+from hashlib import md5
+from random import Random
 from zope.interface.verify import verifyObject
 from zope.interface.exceptions import BrokenMethodImplementation,\
     DoesNotImplement
@@ -76,6 +78,7 @@
 
         @return: a L{Deferred} which fires with an L{IDataStore}.
         """
+        disableMemcacheForTest(testCase)
         dbRoot = CachingFilePath(self.SHARED_DB_PATH)
         attachmentRoot = dbRoot.child("attachments")
         if self.sharedService is None:
@@ -125,11 +128,12 @@
             attachmentRoot.createDirectory()
         except OSError:
             pass
+        currentTestID = testCase.id()
         cp = ConnectionPool(self.sharedService.produceConnection)
+        quota = deriveQuota(currentTestID)
         store = CommonDataStore(
-            cp.connection, notifierFactory, attachmentRoot
+            cp.connection, notifierFactory, attachmentRoot, quota=quota
         )
-        currentTestID = testCase.id()
         store.label = currentTestID
         cp.startService()
         def stopIt():
@@ -182,6 +186,31 @@
 
 
 
+def deriveQuota(testID):
+    """
+    Derive a distinctive quota number for a specific test, based on its ID.
+    This generates a quota which is small enough that tests may trivially exceed
+    it if they wish to do so, but distinctive enough that it may be compared
+    without the risk of testing only a single value for quota.
+
+    Since SQL stores are generally built during test construction, it's awkward
+    to have tests which specifically construct a store to inspect quota-related
+    state; this allows us to have the test and the infrastructure agree on a
+    number.
+
+    @param testID: The identifier for a test, as returned by L{TestCase.id}.
+
+    @type testID: C{str}
+    """
+    h = md5(testID)
+    seed = int(h.hexdigest(), 16)
+    r = Random(seed)
+    baseline = 2000
+    fuzz = r.randint(1, 1000)
+    return baseline + fuzz
+
+
+
 @inlineCallbacks
 def populateCalendarsFrom(requirements, store):
     """
@@ -423,3 +452,24 @@
 
     def reset(self):
         self.history = []
+
+
+
+def disableMemcacheForTest(aTest):
+    """
+    Disable all memcache logic for the duration of a test; we shouldn't be
+    starting or connecting to any memcache stuff for most tests.
+    """
+
+    # These imports are local so that they don't accidentally leak to anything
+    # else in this module; nothing else in this module should ever touch global
+    # configuration. -glyph
+
+    from twistedcaldav.config import config
+    from twistedcaldav.memcacher import Memcacher
+
+    aTest.patch(config.Memcached.Pools.Default, "ClientEnabled", False)
+    aTest.patch(config.Memcached.Pools.Default, "ServerEnabled", False)
+    aTest.patch(Memcacher, "allowTestCache", True)
+
+
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110630/dfb703dc/attachment-0001.html>


More information about the calendarserver-changes mailing list