[CalendarServer-changes] [12211] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Wed Mar 12 11:23:57 PDT 2014
Revision: 12211
http://trac.calendarserver.org//changeset/12211
Author: cdaboo at apple.com
Date: 2014-01-02 09:19:18 -0800 (Thu, 02 Jan 2014)
Log Message:
-----------
Merge cross-pod sharing work to trunk.
Modified Paths:
--------------
CalendarServer/trunk/calendarserver/push/test/test_notifier.py
CalendarServer/trunk/calendarserver/tap/caldav.py
CalendarServer/trunk/calendarserver/tap/util.py
CalendarServer/trunk/calendarserver/tools/dbinspect.py
CalendarServer/trunk/calendarserver/tools/purge.py
CalendarServer/trunk/calendarserver/tools/shell/test/test_vfs.py
CalendarServer/trunk/calendarserver/tools/test/calverify/accounts.xml
CalendarServer/trunk/calendarserver/tools/test/purge/accounts.xml
CalendarServer/trunk/calendarserver/tools/test/test_export.py
CalendarServer/trunk/calendarserver/tools/test/test_purge.py
CalendarServer/trunk/conf/caldavd-test-podB.plist
CalendarServer/trunk/support/build.sh
CalendarServer/trunk/twisted/plugins/caldav.py
CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py
CalendarServer/trunk/twistedcaldav/directory/principal.py
CalendarServer/trunk/twistedcaldav/directory/resource.py
CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml
CalendarServer/trunk/twistedcaldav/directory/test/augments.xml
CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py
CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py
CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py
CalendarServer/trunk/twistedcaldav/method/report_common.py
CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py
CalendarServer/trunk/twistedcaldav/resource.py
CalendarServer/trunk/twistedcaldav/sharing.py
CalendarServer/trunk/twistedcaldav/stdconfig.py
CalendarServer/trunk/twistedcaldav/storebridge.py
CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py
CalendarServer/trunk/twistedcaldav/test/test_sharing.py
CalendarServer/trunk/twistedcaldav/test/test_xml.py
CalendarServer/trunk/txdav/base/datastore/subpostgres.py
CalendarServer/trunk/txdav/base/datastore/util.py
CalendarServer/trunk/txdav/caldav/datastore/index_file.py
CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py
CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py
CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/xml.py
CalendarServer/trunk/txdav/caldav/datastore/sql.py
CalendarServer/trunk/txdav/caldav/datastore/test/common.py
CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py
CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_sharing.py
CalendarServer/trunk/txdav/caldav/datastore/test/test_util.py
CalendarServer/trunk/txdav/caldav/datastore/test/util.py
CalendarServer/trunk/txdav/caldav/icalendardirectoryservice.py
CalendarServer/trunk/txdav/caldav/icalendarstore.py
CalendarServer/trunk/txdav/carddav/datastore/index_file.py
CalendarServer/trunk/txdav/carddav/datastore/sql.py
CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py
CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py
CalendarServer/trunk/txdav/carddav/datastore/test/test_sql_sharing.py
CalendarServer/trunk/txdav/common/datastore/file.py
CalendarServer/trunk/txdav/common/datastore/sql.py
CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
CalendarServer/trunk/txdav/common/datastore/sql_tables.py
CalendarServer/trunk/txdav/common/datastore/test/test_sql.py
CalendarServer/trunk/txdav/common/datastore/test/util.py
CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/util.py
CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py
CalendarServer/trunk/txdav/common/icommondatastore.py
CalendarServer/trunk/txdav/common/idirectoryservice.py
CalendarServer/trunk/txweb2/http.py
Added Paths:
-----------
CalendarServer/trunk/txdav/caldav/datastore/query/
CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py
CalendarServer/trunk/txdav/caldav/datastore/query/builder.py
CalendarServer/trunk/txdav/caldav/datastore/query/filter.py
CalendarServer/trunk/txdav/caldav/datastore/query/generator.py
CalendarServer/trunk/txdav/caldav/datastore/query/test/
CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py
CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py
CalendarServer/trunk/txdav/caldav/datastore/sql_external.py
CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py
CalendarServer/trunk/txdav/carddav/datastore/query/
CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py
CalendarServer/trunk/txdav/carddav/datastore/query/builder.py
CalendarServer/trunk/txdav/carddav/datastore/query/filter.py
CalendarServer/trunk/txdav/carddav/datastore/query/test/
CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py
CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py
CalendarServer/trunk/txdav/carddav/datastore/sql_external.py
CalendarServer/trunk/txdav/common/datastore/podding/
CalendarServer/trunk/txdav/common/datastore/podding/__init__.py
CalendarServer/trunk/txdav/common/datastore/podding/conduit.py
CalendarServer/trunk/txdav/common/datastore/podding/request.py
CalendarServer/trunk/txdav/common/datastore/podding/resource.py
CalendarServer/trunk/txdav/common/datastore/podding/test/
CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py
CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py
CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py
CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py
CalendarServer/trunk/txdav/common/datastore/podding/test/util.py
CalendarServer/trunk/txdav/common/datastore/query/
CalendarServer/trunk/txdav/common/datastore/query/__init__.py
CalendarServer/trunk/txdav/common/datastore/query/expression.py
CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py
CalendarServer/trunk/txdav/common/datastore/query/generator.py
CalendarServer/trunk/txdav/common/datastore/query/test/
CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py
CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py
CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py
CalendarServer/trunk/txdav/common/datastore/sql_external.py
CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v30.sql
CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v30.sql
CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_30_to_31.sql
CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_30_to_31.sql
Removed Paths:
-------------
CalendarServer/trunk/twistedcaldav/query/
CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py
CalendarServer/trunk/txdav/caldav/datastore/query/builder.py
CalendarServer/trunk/txdav/caldav/datastore/query/filter.py
CalendarServer/trunk/txdav/caldav/datastore/query/generator.py
CalendarServer/trunk/txdav/caldav/datastore/query/test/
CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py
CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py
CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py
CalendarServer/trunk/txdav/carddav/datastore/query/builder.py
CalendarServer/trunk/txdav/carddav/datastore/query/filter.py
CalendarServer/trunk/txdav/carddav/datastore/query/test/
CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py
CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py
CalendarServer/trunk/txdav/common/datastore/podding/__init__.py
CalendarServer/trunk/txdav/common/datastore/podding/conduit.py
CalendarServer/trunk/txdav/common/datastore/podding/request.py
CalendarServer/trunk/txdav/common/datastore/podding/resource.py
CalendarServer/trunk/txdav/common/datastore/podding/test/
CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py
CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py
CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py
CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py
CalendarServer/trunk/txdav/common/datastore/podding/test/util.py
CalendarServer/trunk/txdav/common/datastore/query/__init__.py
CalendarServer/trunk/txdav/common/datastore/query/expression.py
CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py
CalendarServer/trunk/txdav/common/datastore/query/generator.py
CalendarServer/trunk/txdav/common/datastore/query/test/
CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py
CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py
CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py
CalendarServer/trunk/txdav/common/datastore/sql_legacy.py
Property Changed:
----------------
CalendarServer/trunk/
Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
- /CalDAVTester/trunk:11193-11198
/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/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/release/CalendarServer-5.1-dev:11846
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/fix-no-ischedule:11607-11871
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/json:11622-11912
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/performance-tweaks:11824-11836
/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/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/reverse-proxy-pods:11875-11900
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/sharing-in-the-store:11935-12016
/CalendarServer/branches/users/cdaboo/store-scheduling:10876-11129
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/gaya/sharedgroupfixes:12120-12142
/CalendarServer/branches/users/gaya/sharedgroups-3:11088-11204
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/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/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/enforce-max-requests:11640-11643
/CalendarServer/branches/users/glyph/hang-fix:11465-11491
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/launchd-wrapper-bis:11413-11436
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/log-cleanups:11691-11731
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/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/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/start-service-start-loop:11060-11065
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/warning-cleanups:11347-11357
/CalendarServer/branches/users/glyph/whenNotProposed:11881-11897
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/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/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593
+ /CalDAVTester/trunk:11193-11198
/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/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/release/CalendarServer-5.1-dev:11846
/CalendarServer/branches/release/CalendarServer-5.2-dev:11972
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/cross-pod-sharing:12038-12191
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/fix-no-ischedule:11607-11871
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/json:11622-11912
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/performance-tweaks:11824-11836
/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/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/reverse-proxy-pods:11875-11900
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/sharing-in-the-store:11935-12016
/CalendarServer/branches/users/cdaboo/store-scheduling:10876-11129
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/gaya/sharedgroupfixes:12120-12142
/CalendarServer/branches/users/gaya/sharedgroups-3:11088-11204
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/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/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/enforce-max-requests:11640-11643
/CalendarServer/branches/users/glyph/hang-fix:11465-11491
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/launchd-wrapper-bis:11413-11436
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/log-cleanups:11691-11731
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/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/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/start-service-start-loop:11060-11065
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/warning-cleanups:11347-11357
/CalendarServer/branches/users/glyph/whenNotProposed:11881-11897
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/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/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593
Modified: CalendarServer/trunk/calendarserver/push/test/test_notifier.py
===================================================================
--- CalendarServer/trunk/calendarserver/push/test/test_notifier.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/push/test/test_notifier.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -184,10 +184,10 @@
class NotifierFactory(StoreTestCase):
requirements = {
- "home1" : {
+ "user01" : {
"calendar_1" : {}
},
- "home2" : {
+ "user02" : {
"calendar_1" : {}
},
}
@@ -208,23 +208,23 @@
@inlineCallbacks
def test_homeNotifier(self):
- home = yield self.homeUnderTest()
+ home = yield self.homeUnderTest(name="user01")
yield home.notifyChanged(category=ChangeCategory.default)
self.assertEquals(self.notifierFactory.history,
- [("/CalDAV/example.com/home1/", PushPriority.high)])
+ [("/CalDAV/example.com/user01/", PushPriority.high)])
yield self.commit()
@inlineCallbacks
def test_calendarNotifier(self):
- calendar = yield self.calendarUnderTest()
+ calendar = yield self.calendarUnderTest(home="user01")
yield calendar.notifyChanged(category=ChangeCategory.default)
self.assertEquals(
set(self.notifierFactory.history),
set([
- ("/CalDAV/example.com/home1/", PushPriority.high),
- ("/CalDAV/example.com/home1/calendar_1/", PushPriority.high)])
+ ("/CalDAV/example.com/user01/", PushPriority.high),
+ ("/CalDAV/example.com/user01/calendar_1/", PushPriority.high)])
)
yield self.commit()
@@ -232,28 +232,28 @@
@inlineCallbacks
def test_shareWithNotifier(self):
- calendar = yield self.calendarUnderTest()
- yield calendar.inviteUserToShare("home2", _BIND_MODE_WRITE, "")
+ calendar = yield self.calendarUnderTest(home="user01")
+ yield calendar.inviteUserToShare("user02", _BIND_MODE_WRITE, "")
self.assertEquals(
set(self.notifierFactory.history),
set([
- ("/CalDAV/example.com/home1/", PushPriority.high),
- ("/CalDAV/example.com/home1/calendar_1/", PushPriority.high),
- ("/CalDAV/example.com/home2/", PushPriority.high),
- ("/CalDAV/example.com/home2/notification/", PushPriority.high),
+ ("/CalDAV/example.com/user01/", PushPriority.high),
+ ("/CalDAV/example.com/user01/calendar_1/", PushPriority.high),
+ ("/CalDAV/example.com/user02/", PushPriority.high),
+ ("/CalDAV/example.com/user02/notification/", PushPriority.high),
])
)
yield self.commit()
- calendar = yield self.calendarUnderTest()
- yield calendar.uninviteUserFromShare("home2")
+ calendar = yield self.calendarUnderTest(home="user01")
+ yield calendar.uninviteUserFromShare("user02")
self.assertEquals(
set(self.notifierFactory.history),
set([
- ("/CalDAV/example.com/home1/", PushPriority.high),
- ("/CalDAV/example.com/home1/calendar_1/", PushPriority.high),
- ("/CalDAV/example.com/home2/", PushPriority.high),
- ("/CalDAV/example.com/home2/notification/", PushPriority.high),
+ ("/CalDAV/example.com/user01/", PushPriority.high),
+ ("/CalDAV/example.com/user01/calendar_1/", PushPriority.high),
+ ("/CalDAV/example.com/user02/", PushPriority.high),
+ ("/CalDAV/example.com/user02/notification/", PushPriority.high),
])
)
yield self.commit()
@@ -262,20 +262,20 @@
@inlineCallbacks
def test_sharedCalendarNotifier(self):
- calendar = yield self.calendarUnderTest()
- shareeView = yield calendar.inviteUserToShare("home2", _BIND_MODE_WRITE, "")
+ calendar = yield self.calendarUnderTest(home="user01")
+ shareeView = yield calendar.inviteUserToShare("user02", _BIND_MODE_WRITE, "")
yield shareeView.acceptShare("")
shareName = shareeView.name()
yield self.commit()
self.notifierFactory.reset()
- shared = yield self.calendarUnderTest(home="home2", name=shareName)
+ shared = yield self.calendarUnderTest(home="user02", name=shareName)
yield shared.notifyChanged(category=ChangeCategory.default)
self.assertEquals(
set(self.notifierFactory.history),
set([
- ("/CalDAV/example.com/home1/", PushPriority.high),
- ("/CalDAV/example.com/home1/calendar_1/", PushPriority.high)])
+ ("/CalDAV/example.com/user01/", PushPriority.high),
+ ("/CalDAV/example.com/user01/calendar_1/", PushPriority.high)])
)
yield self.commit()
@@ -283,12 +283,12 @@
@inlineCallbacks
def test_notificationNotifier(self):
- notifications = yield self.transactionUnderTest().notificationsWithUID("home1")
+ notifications = yield self.transactionUnderTest().notificationsWithUID("user01")
yield notifications.notifyChanged(category=ChangeCategory.default)
self.assertEquals(
set(self.notifierFactory.history),
set([
- ("/CalDAV/example.com/home1/", PushPriority.high),
- ("/CalDAV/example.com/home1/notification/", PushPriority.high)])
+ ("/CalDAV/example.com/user01/", PushPriority.high),
+ ("/CalDAV/example.com/user01/notification/", PushPriority.high)])
)
yield self.commit()
Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1691,11 +1691,9 @@
raise StoreNotAvailable()
from twisted.internet import reactor
- pool = PeerConnectionPool(reactor, store.newTransaction,
- 7654, schema)
+ pool = PeerConnectionPool(reactor, store.newTransaction, config.WorkQueue.ampPort, schema)
store.queuer = store.queuer.transferProposalCallbacks(pool)
- controlSocket.addFactory(_QUEUE_ROUTE,
- pool.workerListenerFactory())
+ controlSocket.addFactory(_QUEUE_ROUTE, pool.workerListenerFactory())
# TODO: now that we have the shared control socket, we should get
# rid of the connection dispenser and make a shared / async
# connection pool implementation that can dispense transactions
Modified: CalendarServer/trunk/calendarserver/tap/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/util.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tap/util.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -92,6 +92,7 @@
from calendarserver.webadmin.resource import WebAdminResource
from calendarserver.webcal.resource import WebCalendarResource
+from txdav.common.datastore.podding.resource import ConduitResource
from txdav.common.datastore.sql import CommonDataStore as CommonSQLDataStore
from txdav.common.datastore.file import CommonDataStore as CommonFileDataStore
from txdav.common.datastore.sql import current_sql_schema
@@ -407,6 +408,7 @@
rootResourceClass = RootResource
calendarResourceClass = DirectoryCalendarHomeProvisioningResource
iScheduleResourceClass = IScheduleInboxResource
+ conduitResourceClass = ConduitResource
timezoneServiceResourceClass = TimezoneServiceResource
timezoneStdServiceResourceClass = TimezoneStdServiceResource
webCalendarResourceClass = WebCalendarResource
@@ -636,7 +638,7 @@
addSystemEventTrigger("after", "startup", timezoneStdService.onStartup)
#
- # iSchedule service for podding
+ # iSchedule/cross-pod service for podding
#
if config.Servers.Enabled:
log.info("Setting up iSchedule podding inbox resource: {cls}", cls=iScheduleResourceClass)
@@ -648,6 +650,14 @@
)
root.putChild(config.Servers.InboxName, ischedule)
+ log.info("Setting up podding conduit resource: {cls}", cls=conduitResourceClass)
+
+ conduit = conduitResourceClass(
+ root,
+ newStore,
+ )
+ root.putChild(config.Servers.ConduitName, conduit)
+
#
# iSchedule service (not used for podding)
#
Modified: CalendarServer/trunk/calendarserver/tools/dbinspect.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/dbinspect.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/dbinspect.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -37,8 +37,8 @@
from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
from twistedcaldav.directory import calendaruserproxy
from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.query import calendarqueryfilter
from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+from txdav.caldav.datastore.query.filter import Filter
from txdav.common.datastore.sql_tables import schema, _BIND_MODE_OWN
from uuid import UUID
import os
@@ -757,10 +757,10 @@
name="VCALENDAR",
)
)
- filter = calendarqueryfilter.Filter(filter)
+ filter = Filter(filter)
filter.settimezone(None)
- matches = yield calendar._index.indexedSearch(filter, useruid=uid, fbtype=False)
+ matches = yield calendar.search(filter, useruid=uid, fbtype=False)
if matches is None:
returnValue(None)
for name, _ignore_uid, _ignore_type in matches:
Modified: CalendarServer/trunk/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/purge.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/purge.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -30,10 +30,9 @@
from twisted.internet.defer import inlineCallbacks, returnValue
from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import TimeRange
from twistedcaldav.directory.directory import DirectoryRecord
-from twistedcaldav.query import calendarqueryfilter
+from txdav.caldav.datastore.query.filter import Filter
from txdav.xml import element as davxml
@@ -817,13 +816,13 @@
query_filter = caldavxml.Filter(
caldavxml.ComponentFilter(
caldavxml.ComponentFilter(
- TimeRange(start=whenString,),
+ caldavxml.TimeRange(start=whenString,),
name=("VEVENT",),
),
name="VCALENDAR",
)
)
- query_filter = calendarqueryfilter.Filter(query_filter)
+ query_filter = Filter(query_filter)
count = 0
txn = self.store.newTransaction()
@@ -844,7 +843,7 @@
childNames.append(childName)
else:
# events matching filter
- for childName, _ignore_childUid, _ignore_childType in (yield calendar._index.indexedSearch(query_filter)):
+ for childName, _ignore_childUid, _ignore_childType in (yield calendar.search(query_filter)):
childNames.append(childName)
yield txn.commit()
Modified: CalendarServer/trunk/calendarserver/tools/shell/test/test_vfs.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/shell/test/test_vfs.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/shell/test/test_vfs.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -28,45 +28,55 @@
class TestListEntry(TestCase):
def test_toString(self):
- self.assertEquals(ListEntry(None, File , "thingo" ).toString(), "thingo" )
- self.assertEquals(ListEntry(None, File , "thingo", Foo="foo").toString(), "thingo" )
- self.assertEquals(ListEntry(None, Folder, "thingo" ).toString(), "thingo/")
+ self.assertEquals(ListEntry(None, File , "thingo").toString(), "thingo")
+ self.assertEquals(ListEntry(None, File , "thingo", Foo="foo").toString(), "thingo")
+ self.assertEquals(ListEntry(None, Folder, "thingo").toString(), "thingo/")
self.assertEquals(ListEntry(None, Folder, "thingo", Foo="foo").toString(), "thingo/")
+
def test_fieldNamesImplicit(self):
# This test assumes File doesn't set list.fieldNames.
assert not hasattr(File.list, "fieldNames")
self.assertEquals(set(ListEntry(File(None, ()), File, "thingo").fieldNames), set(("Name",)))
+
def test_fieldNamesExplicit(self):
def fieldNames(fileClass):
return ListEntry(fileClass(None, ()), fileClass, "thingo", Flavor="Coconut", Style="Hard")
# Full list
- class MyFile(File):
- def list(self): return succeed(())
+ class MyFile1(File):
+ def list(self):
+ return succeed(())
list.fieldNames = ("Name", "Flavor")
- self.assertEquals(fieldNames(MyFile).fieldNames, ("Name", "Flavor"))
+ self.assertEquals(fieldNames(MyFile1).fieldNames, ("Name", "Flavor"))
+
# Full list, different order
- class MyFile(File):
- def list(self): return succeed(())
+ class MyFile2(File):
+ def list(self):
+ return succeed(())
list.fieldNames = ("Flavor", "Name")
- self.assertEquals(fieldNames(MyFile).fieldNames, ("Flavor", "Name"))
+ self.assertEquals(fieldNames(MyFile2).fieldNames, ("Flavor", "Name"))
+
# Omits Name, which is implicitly added
- class MyFile(File):
- def list(self): return succeed(())
+ class MyFile3(File):
+ def list(self):
+ return succeed(())
list.fieldNames = ("Flavor",)
- self.assertEquals(fieldNames(MyFile).fieldNames, ("Name", "Flavor"))
+ self.assertEquals(fieldNames(MyFile3).fieldNames, ("Name", "Flavor"))
+
# Emtpy
- class MyFile(File):
- def list(self): return succeed(())
+ class MyFile4(File):
+ def list(self):
+ return succeed(())
list.fieldNames = ()
- self.assertEquals(fieldNames(MyFile).fieldNames, ("Name",))
+ self.assertEquals(fieldNames(MyFile4).fieldNames, ("Name",))
+
def test_toFieldsImplicit(self):
# This test assumes File doesn't set list.fieldNames.
assert not hasattr(File.list, "fieldNames")
@@ -77,33 +87,41 @@
("thingo", "Coconut", "Hard")
)
+
def test_toFieldsExplicit(self):
def fields(fileClass):
return tuple(ListEntry(fileClass(None, ()), fileClass, "thingo", Flavor="Coconut", Style="Hard").toFields())
# Full list
- class MyFile(File):
- def list(self): return succeed(())
+ class MyFile1(File):
+ def list(self):
+ return succeed(())
list.fieldNames = ("Name", "Flavor")
- self.assertEquals(fields(MyFile), ("thingo", "Coconut"))
+ self.assertEquals(fields(MyFile1), ("thingo", "Coconut"))
+
# Full list, different order
- class MyFile(File):
- def list(self): return succeed(())
+ class MyFile2(File):
+ def list(self):
+ return succeed(())
list.fieldNames = ("Flavor", "Name")
- self.assertEquals(fields(MyFile), ("Coconut", "thingo"))
+ self.assertEquals(fields(MyFile2), ("Coconut", "thingo"))
+
# Omits Name, which is implicitly added
- class MyFile(File):
- def list(self): return succeed(())
+ class MyFile3(File):
+ def list(self):
+ return succeed(())
list.fieldNames = ("Flavor",)
- self.assertEquals(fields(MyFile), ("thingo", "Coconut"))
+ self.assertEquals(fields(MyFile3), ("thingo", "Coconut"))
+
# Emtpy
- class MyFile(File):
- def list(self): return succeed(())
+ class MyFile4(File):
+ def list(self):
+ return succeed(())
list.fieldNames = ()
- self.assertEquals(fields(MyFile), ("thingo",))
+ self.assertEquals(fields(MyFile4), ("thingo",))
@@ -114,6 +132,7 @@
def __init__(self, testCase):
self.testCase = testCase
+
def mktemp(self):
return self.testCase.mktemp()
@@ -129,8 +148,9 @@
"""
Create a L{UIDsFolder}.
"""
- self.svc = ShellService(store=(yield buildStore(self, None)),
- directory=DirectoryStubber(self).service(),
+ directory = DirectoryStubber(self).service()
+ self.svc = ShellService(store=(yield buildStore(self, None, directoryService=directory)),
+ directory=directory,
options=None, reactor=None, config=None)
self.folder = UIDsFolder(self.svc, ())
@@ -156,6 +176,3 @@
{"Record Type": "users", "Short Name": "dreid",
"Full Name": "David Reid", "Name": dreid}]
)
-
-
-
Modified: CalendarServer/trunk/calendarserver/tools/test/calverify/accounts.xml
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/calverify/accounts.xml 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/test/calverify/accounts.xml 2014-01-02 17:19:18 UTC (rev 12211)
@@ -47,4 +47,11 @@
<name>Example User4</name>
<email-address>example4 at example.com</email-address>
</user>
+ <user>
+ <uid>home1</uid>
+ <guid>home1</guid>
+ <password>home1</password>
+ <name>Home 1</name>
+ <email-address>home1 at example.com</email-address>
+ </user>
</accounts>
Modified: CalendarServer/trunk/calendarserver/tools/test/purge/accounts.xml
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/purge/accounts.xml 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/test/purge/accounts.xml 2014-01-02 17:19:18 UTC (rev 12211)
@@ -26,4 +26,25 @@
<name>Example User</name>
<email-address>example at example.com</email-address>
</user>
+ <user>
+ <uid>example2</uid>
+ <guid>37DB0C90-4DB1-4932-BC69-3DAB66F374F5</guid>
+ <password>example2</password>
+ <name>Example User 2</name>
+ <email-address>example2 at example.com</email-address>
+ </user>
+ <user>
+ <uid>home1</uid>
+ <guid>home1</guid>
+ <password>home1</password>
+ <name>Home 1</name>
+ <email-address>home1 at example.com</email-address>
+ </user>
+ <user>
+ <uid>home2</uid>
+ <guid>home2</guid>
+ <password>home2</password>
+ <name>Home 2</name>
+ <email-address>home2 at example.com</email-address>
+ </user>
</accounts>
Modified: CalendarServer/trunk/calendarserver/tools/test/test_export.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_export.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/test/test_export.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -277,7 +277,7 @@
"""
yield populateCalendarsFrom(
{
- "home1": {
+ "user01": {
"calendar1": {
"valentines-day.ics": (valentines, {})
}
@@ -291,7 +291,7 @@
io = StringIO()
yield exportToFile(
- [(yield self.txn().calendarHomeWithUID("home1"))
+ [(yield self.txn().calendarHomeWithUID("user01"))
.calendarWithName("calendar1")], io
)
self.assertEquals(Component.fromString(io.getvalue()),
@@ -306,7 +306,7 @@
"""
yield populateCalendarsFrom(
{
- "home1": {
+ "user01": {
"calendar1": {
"valentines-day.ics": (valentines, {}),
"new-years-day.ics": (newYears, {})
@@ -324,7 +324,7 @@
io = StringIO()
yield exportToFile(
- [(yield self.txn().calendarHomeWithUID("home1"))
+ [(yield self.txn().calendarHomeWithUID("user01"))
.calendarWithName("calendar1")], io
)
self.assertEquals(Component.fromString(io.getvalue()),
@@ -342,7 +342,7 @@
"""
yield populateCalendarsFrom(
{
- "home1": {
+ "user01": {
"calendar1": {
"1.ics": (one, {}), # EST
"2.ics": (another, {}), # EST
@@ -354,7 +354,7 @@
io = StringIO()
yield exportToFile(
- [(yield self.txn().calendarHomeWithUID("home1"))
+ [(yield self.txn().calendarHomeWithUID("user01"))
.calendarWithName("calendar1")], io
)
result = Component.fromString(io.getvalue())
Modified: CalendarServer/trunk/calendarserver/tools/test/test_purge.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_purge.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/test/test_purge.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -806,16 +806,6 @@
@inlineCallbacks
def setUp(self):
- self.patch(config.DirectoryService.params, "xmlFile",
- os.path.join(
- os.path.dirname(__file__), "purge", "accounts.xml"
- )
- )
- self.patch(config.ResourceService.params, "xmlFile",
- os.path.join(
- os.path.dirname(__file__), "purge", "resources.xml"
- )
- )
yield super(PurgePrincipalTests, self).setUp()
txn = self._sqlCalendarStore.newTransaction()
@@ -850,6 +840,20 @@
(yield txn.commit())
+ def configure(self):
+ super(PurgePrincipalTests, self).configure()
+ self.patch(config.DirectoryService.params, "xmlFile",
+ os.path.join(
+ os.path.dirname(__file__), "purge", "accounts.xml"
+ )
+ )
+ self.patch(config.ResourceService.params, "xmlFile",
+ os.path.join(
+ os.path.dirname(__file__), "purge", "resources.xml"
+ )
+ )
+
+
@inlineCallbacks
def populate(self):
yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
Modified: CalendarServer/trunk/conf/caldavd-test-podB.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test-podB.plist 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/conf/caldavd-test-podB.plist 2014-01-02 17:19:18 UTC (rev 12211)
@@ -43,6 +43,13 @@
<array>
</array>
+ <!-- Work Queue -->
+ <key>WorkQueue</key>
+ <dict>
+ <key>ampPort</key>
+ <integer>7655</integer>
+ </dict>
+
<!-- Server root -->
<key>ServerRoot</key>
<string>./data/podB</string>
@@ -94,6 +101,19 @@
</dict>
</dict>
+ <!-- Sqlite ProxyDB Service - must use the same one as Pod A-->
+ <key>ProxyDBService</key>
+ <dict>
+ <key>type</key>
+ <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
+
+ <key>params</key>
+ <dict>
+ <key>dbpath</key>
+ <string>./data/podA/Data/proxies.sqlite</string>
+ </dict>
+ </dict>
+
<key>ProxyLoadFromFile</key>
<string>./conf/auth/proxies-test-pod.xml</string>
Modified: CalendarServer/trunk/support/build.sh
===================================================================
--- CalendarServer/trunk/support/build.sh 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/support/build.sh 2014-01-02 17:19:18 UTC (rev 12211)
@@ -864,7 +864,7 @@
"${n}" "twisted" "${p}" \
"${pypi}/T/${n}/${p}.tar.bz2";
- local v="12171";
+ local v="12210";
local n="twext";
local p="${n}-${v}";
py_dependency -fe -r "${v}" \
Modified: CalendarServer/trunk/twisted/plugins/caldav.py
===================================================================
--- CalendarServer/trunk/twisted/plugins/caldav.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twisted/plugins/caldav.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -23,7 +23,6 @@
from twisted.internet.protocol import Factory
Factory.noisy = False
-
def serviceMakerProperty(propname):
def getProperty(self):
return getattr(reflect.namedClass(self.serviceMakerClass), propname)
Modified: CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -23,46 +23,46 @@
"OpenDirectoryBackingService", "VCardRecord",
]
-import traceback
-import hashlib
+from calendarserver.platform.darwin.od import opendirectory, dsattributes, dsquery
-import os
-import sys
-import time
-
-from os import listdir
-from os.path import join, abspath
-from tempfile import mkstemp, gettempdir
-from random import random
-
+from pycalendar.datetime import DateTime
+from pycalendar.vcard.adr import Adr
from pycalendar.vcard.n import N
-from pycalendar.vcard.adr import Adr
-from pycalendar.datetime import DateTime
-from socket import getfqdn
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue, deferredGenerator, succeed
from twext.python.filepath import CachingFilePath as FilePath
-from txdav.xml import element as davxml
-from txdav.xml.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
+
from txweb2.dav.resource import DAVPropertyMixIn
from txweb2.dav.util import joinURL
from txweb2.http_headers import MimeType, generateContentType, ETag
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, deferredGenerator, succeed
from twistedcaldav import customxml, carddavxml
+from twistedcaldav.config import config
from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.config import config
from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
-from twistedcaldav.query import addressbookqueryfilter
from twistedcaldav.vcard import Component, Property, vCardProductID
+from txdav.carddav.datastore.query.filter import IsNotDefined, ParameterFilter, \
+ TextMatch
+from txdav.xml import element as davxml
+from txdav.xml.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
+
+from os import listdir
+from os.path import join, abspath
+from random import random
+from socket import getfqdn
+from tempfile import mkstemp, gettempdir
from xmlrpclib import datetime
+import hashlib
+import os
+import sys
+import time
+import traceback
-from calendarserver.platform.darwin.od import opendirectory, dsattributes, dsquery
-
class OpenDirectoryBackingService(DirectoryService):
"""
Open Directory implementation of L{IDirectoryService}.
@@ -830,11 +830,11 @@
if not constant and not allAttrStrings:
return (False, [], [])
- if propFilter.qualifier and isinstance(propFilter.qualifier, addressbookqueryfilter.IsNotDefined):
+ if propFilter.qualifier and isinstance(propFilter.qualifier, IsNotDefined):
return definedExpression(False, filterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
- paramFilterElements = [paramFilterElement for paramFilterElement in propFilter.filters if isinstance(paramFilterElement, addressbookqueryfilter.ParameterFilter)]
- textMatchElements = [textMatchElement for textMatchElement in propFilter.filters if isinstance(textMatchElement, addressbookqueryfilter.TextMatch)]
+ paramFilterElements = [paramFilterElement for paramFilterElement in propFilter.filters if isinstance(paramFilterElement, ParameterFilter)]
+ textMatchElements = [textMatchElement for textMatchElement in propFilter.filters if isinstance(textMatchElement, TextMatch)]
propFilterAllOf = propFilter.propfilter_test == "allof"
# handle parameter filter elements
Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -922,6 +922,23 @@
@inlineCallbacks
+ def proxyMode(self, principal):
+ """
+ Determine whether what proxy mode this principal has in relation to the one specified.
+ """
+
+ read_uids = (yield self.proxyFor(False))
+ if principal in read_uids:
+ returnValue("read")
+
+ write_uids = (yield self.proxyFor(True))
+ if principal in write_uids:
+ returnValue("write")
+
+ returnValue("none")
+
+
+ @inlineCallbacks
def proxyFor(self, read_write, resolve_memberships=True):
proxyFors = set()
Modified: CalendarServer/trunk/twistedcaldav/directory/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/resource.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/resource.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -22,6 +22,8 @@
from twistedcaldav.client.reverseproxy import ReverseProxyResource
+from twisted.internet.defer import succeed
+
__all__ = ["DirectoryReverseProxyResource"]
class DirectoryReverseProxyResource(ReverseProxyResource):
@@ -35,3 +37,32 @@
def url(self):
return joinURL(self.parent.url(), self.record.uid)
+
+
+ def hasQuota(self, request):
+ return succeed(False)
+
+
+ def hasQuotaRoot(self, request):
+ return succeed(False)
+
+
+ def quotaRootResource(self, request):
+ """
+ Return the quota root for this resource.
+
+ @return: L{DAVResource} or C{None}
+ """
+
+ return succeed(None)
+
+
+ def checkPrivileges(
+ self, request, privileges, recurse=False,
+ principal=None, inherited_aces=None
+ ):
+ return succeed(None)
+
+
+ def hasProperty(self, property, request):
+ return succeed(False)
Modified: CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml 2014-01-02 17:19:18 UTC (rev 12211)
@@ -139,7 +139,7 @@
<member type="users">delegateviagroup</member>
</members>
</group>
- <user repeat="2">
+ <user repeat="100">
<uid>user%02d</uid>
<guid>user%02d</guid>
<password>%02duser</password>
Modified: CalendarServer/trunk/twistedcaldav/directory/test/augments.xml
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/augments.xml 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/test/augments.xml 2014-01-02 17:19:18 UTC (rev 12211)
@@ -57,7 +57,7 @@
<enable-calendar>false</enable-calendar>
<enable-addressbook>false</enable-addressbook>
</record>
- <record repeat="2">
+ <record repeat="100">
<uid>user%02d</uid>
<enable>true</enable>
<enable-calendar>true</enable-calendar>
Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -999,8 +999,9 @@
Exercise the default recordsMatchingTokens implementation
"""
records = list((yield self.directoryService.recordsMatchingTokens(["Use", "01"])))
- self.assertEquals(len(records), 1)
- self.assertEquals(records[0].shortNames[0], "user01")
+ self.assertNotEquals(len(records), 0)
+ shorts = [record.shortNames[0] for record in records]
+ self.assertTrue("user01" in shorts)
records = list((yield self.directoryService.recordsMatchingTokens(['"quotey"'],
context=self.directoryService.searchContext_attendee)))
Modified: CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -27,7 +27,6 @@
from twext.python.log import Logger
from txweb2 import responsecode
-from txdav.xml import element as davxml
from txweb2.dav.http import ErrorResponse, MultiStatusResponse
from txweb2.dav.method.report import NumberOfMatchesWithinLimits
from txweb2.dav.util import joinURL
@@ -37,9 +36,11 @@
from twistedcaldav.config import config
from twistedcaldav.carddavxml import carddav_namespace, NResults
from twistedcaldav.method import report_common
-from twistedcaldav.query import addressbookqueryfilter
-from txdav.common.icommondatastore import ConcurrentModification
+from txdav.carddav.datastore.query.filter import Filter
+from txdav.common.icommondatastore import ConcurrentModification, \
+ IndexedSearchException
+from txdav.xml import element as davxml
log = Logger()
@@ -62,7 +63,7 @@
responses = []
xmlfilter = addressbook_query.filter
- filter = addressbookqueryfilter.Filter(xmlfilter)
+ filter = Filter(xmlfilter)
query = addressbook_query.props
limit = addressbook_query.limit
@@ -209,7 +210,7 @@
carddavxml.TextMatch.fromString(resource_name[:-4]),
name="UID", # attributes
), ])
- vCardFilter = addressbookqueryfilter.Filter(vCardFilter)
+ vCardFilter = Filter(vCardFilter)
directoryAddressBookLock, limited[0] = (yield directory.cacheVCardsForAddressBookQuery(vCardFilter, query, max_number_of_results[0]))
@@ -230,11 +231,13 @@
# Check for disabled access
if filteredaces is not None:
- # See whether the filter is valid for an index only query
- index_query_ok = addrresource.index().searchValid(filter)
-
- # Get list of children that match the search and have read access
- names = [name for name, ignore_uid in (yield addrresource.index().search(filter))] #@UnusedVariable
+ index_query_ok = True
+ try:
+ # Get list of children that match the search and have read access
+ names = [name for name, ignore_uid in (yield addrresource.search(filter))] #@UnusedVariable
+ except IndexedSearchException:
+ names = yield addrresource.listChildren()
+ index_query_ok = False
if not names:
return
@@ -277,7 +280,7 @@
carddavxml.TextMatch.fromString(resource_name[:-4]),
name="UID", # attributes
), ])
- vCardFilter = addressbookqueryfilter.Filter(vCardFilter)
+ vCardFilter = Filter(vCardFilter)
yield maybeDeferred(queryDirectoryBackedAddressBook, parent, vCardFilter)
handled = True
Modified: CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -20,8 +20,7 @@
__all__ = ["report_urn_ietf_params_xml_ns_caldav_calendar_query"]
-from twisted.internet.defer import inlineCallbacks, returnValue, \
- maybeDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue
from twext.python.log import Logger
from txweb2 import responsecode
@@ -39,8 +38,8 @@
ConcurrentModification
from twistedcaldav.instance import TooManyInstancesError
from twistedcaldav.method import report_common
-from twistedcaldav.query import calendarqueryfilter
+from txdav.caldav.datastore.query.filter import Filter
from txdav.caldav.icalendarstore import TimeRangeLowerLimit, TimeRangeUpperLimit
from txdav.xml import element as davxml
@@ -66,7 +65,7 @@
responses = []
xmlfilter = calendar_query.filter
- filter = calendarqueryfilter.Filter(xmlfilter)
+ filter = Filter(xmlfilter)
props = calendar_query.props
assert props is not None
@@ -190,13 +189,11 @@
if filteredaces is not None:
index_query_ok = True
try:
- # Get list of children that match the search and have read
- # access
- records = yield maybeDeferred(calresource.index().indexedSearch, filter)
+ # Get list of children that match the search and have read access
+ names = [name for name, ignore_uid, ignore_type in (yield calresource.search(filter))]
except IndexedSearchException:
- records = yield maybeDeferred(calresource.index().bruteForceSearch)
+ names = yield calresource.listChildren()
index_query_ok = False
- names = [name for name, ignore_uid, ignore_type in records]
if not names:
returnValue(True)
Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_common.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -36,17 +36,16 @@
except ImportError:
from md5 import new as md5
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.python.failure import Failure
+
from txweb2 import responsecode
-
-from txdav.xml import element
from txweb2.dav.http import statusForFailure
from txweb2.dav.method.propfind import propertyName
from txweb2.dav.method.report import NumberOfMatchesWithinLimits
from txweb2.dav.method.report import max_number_of_matches
from txweb2.dav.resource import AccessDeniedError
-from txweb2.http import HTTPError
+from txweb2.http import HTTPError, StatusResponse
from twext.python.log import Logger
@@ -65,9 +64,9 @@
from twistedcaldav.instance import InstanceList
from twistedcaldav.memcacher import Memcacher
-from twistedcaldav.query import calendarqueryfilter
-
+from txdav.caldav.datastore.query.filter import Filter
from txdav.common.icommondatastore import IndexedSearchException
+from txdav.xml import element
from pycalendar.duration import Duration
from pycalendar.datetime import DateTime
@@ -583,17 +582,18 @@
name="VCALENDAR",
)
)
- filter = calendarqueryfilter.Filter(filter)
+ filter = Filter(filter)
tzinfo = filter.settimezone(tz)
try:
- resources = yield maybeDeferred(calresource.index().indexedSearch,
- filter, useruid=useruid, fbtype=True
- )
+ resources = yield calresource.search(filter, useruid=useruid, fbtype=True)
if caching:
yield FBCacheEntry.makeCacheEntry(calresource, useruid, cache_timerange, resources)
except IndexedSearchException:
- resources = yield maybeDeferred(calresource.index().bruteForceSearch)
+ raise HTTPError(StatusResponse(
+ responsecode.INTERNAL_SERVER_ERROR,
+ "Failed freebusy query"
+ ))
else:
# Log extended item
Modified: CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -23,9 +23,8 @@
from urllib import unquote
from twext.python.log import Logger
+
from txweb2 import responsecode
-from txdav.xml import element as davxml
-from txdav.xml.base import dav_namespace
from txweb2.dav.http import ErrorResponse, MultiStatusResponse
from txweb2.dav.resource import AccessDeniedError
from txweb2.http import HTTPError, StatusResponse
@@ -37,11 +36,14 @@
from twistedcaldav.carddavxml import carddav_namespace
from twistedcaldav.config import config
from twistedcaldav.method import report_common
-from txdav.common.icommondatastore import ConcurrentModification
from twistedcaldav.method.report_common import COLLECTION_TYPE_CALENDAR, \
COLLECTION_TYPE_ADDRESSBOOK
-from twistedcaldav.query import addressbookqueryfilter
+from txdav.carddav.datastore.query.filter import Filter
+from txdav.common.icommondatastore import ConcurrentModification
+from txdav.xml import element as davxml
+from txdav.xml.base import dav_namespace
+
log = Logger()
@inlineCallbacks
@@ -265,7 +267,7 @@
returnValue(None)
addressBookFilter = carddavxml.Filter(*vCardFilters)
- addressBookFilter = addressbookqueryfilter.Filter(addressBookFilter)
+ addressBookFilter = Filter(addressBookFilter)
if self.directory.cacheQuery:
# add vcards to directory address book and run "normal case" below
limit = config.DirectoryAddressBook.MaxQueryResults
@@ -333,11 +335,11 @@
parent = (yield child.locateParent(request, resource_uri))
if collection_type == COLLECTION_TYPE_CALENDAR:
- if not parent.isCalendarCollection() or not (yield parent.index().resourceExists(name)):
+ if not parent.isCalendarCollection() or not (yield parent.resourceExists(name)):
responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
continue
elif collection_type == COLLECTION_TYPE_ADDRESSBOOK:
- if not parent.isAddressBookCollection() or not (yield parent.index().resourceExists(name)):
+ if not parent.isAddressBookCollection() or not (yield parent.resourceExists(name)):
responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
continue
@@ -367,11 +369,11 @@
parent = (yield self.locateParent(request, resource_uri))
if collection_type == COLLECTION_TYPE_CALENDAR:
- if not parent.isPseudoCalendarCollection() or not (yield parent.index().resourceExists(name)):
+ if not parent.isPseudoCalendarCollection() or not (yield parent.resourceExists(name)):
responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
continue
elif collection_type == COLLECTION_TYPE_ADDRESSBOOK:
- if not parent.isAddressBookCollection() or not (yield parent.index().resourceExists(name)):
+ if not parent.isAddressBookCollection() or not (yield parent.resourceExists(name)):
responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
continue
child = self
Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/resource.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -2667,10 +2667,9 @@
@inlineCallbacks
def _indexWhatChanged(self, revision, depth):
# The newstore implementation supports this directly
- changed, deleted = yield self._newStoreHome.resourceNamesSinceToken(
+ changed, deleted, notallowed = yield self._newStoreHome.resourceNamesSinceToken(
revision, depth
)
- notallowed = []
# Need to insert some addition items on first sync
if revision == 0:
@@ -2894,10 +2893,9 @@
@inlineCallbacks
def _indexWhatChanged(self, revision, depth):
# The newstore implementation supports this directly
- changed, deleted = yield self._newStoreHome.resourceNamesSinceToken(
+ changed, deleted, notallowed = yield self._newStoreHome.resourceNamesSinceToken(
revision, depth
)
- notallowed = []
# Need to insert some addition items on first sync
if revision == 0:
Modified: CalendarServer/trunk/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/sharing.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/sharing.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -267,10 +267,10 @@
"""
if self._newStoreObject.direct():
owner = self.principalForUID(self._newStoreObject.ownerHome().uid())
+ sharee = self.principalForUID(self._newStoreObject.viewerHome().uid())
if owner.record.recordType == WikiDirectoryService.recordType_wikis:
# Access level comes from what the wiki has granted to the
# sharee
- sharee = self.principalForUID(self._newStoreObject.viewerHome().uid())
userID = sharee.record.guid
wikiID = owner.record.shortNames[0]
access = (yield getWikiAccess(userID, wikiID))
@@ -281,7 +281,12 @@
else:
returnValue(None)
else:
- returnValue("original")
+ # Check proxy access
+ proxy_mode = yield sharee.proxyMode(owner)
+ if proxy_mode == "none":
+ returnValue("original")
+ else:
+ returnValue("read-write" if proxy_mode == "write" else "read-only")
else:
# Invited shares use access mode from the invite
# Get the access for self
@@ -318,7 +323,7 @@
sharee = self.principalForUID(self._newStoreObject.viewerHome().uid())
access = yield self._checkAccessControl()
- if access == "original":
+ if access == "original" and not self._newStoreObject.ownerHome().external():
original = (yield request.locateResource(self._share_url))
result = (yield original.accessControlList(request, *args, **kwargs))
returnValue(result)
@@ -805,6 +810,12 @@
# Accept the share
shareeView = yield self._newStoreHome.acceptShare(inviteUID, summary)
+ if shareeView is None:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (calendarserver_namespace, "invalid-share"),
+ "Invite UID not valid",
+ ))
# Return the URL of the shared collection
sharedAsURL = joinURL(self.url(), shareeView.shareName())
@@ -820,7 +831,13 @@
def declineShare(self, request, inviteUID):
# Remove it if it is in the DB
- yield self._newStoreHome.declineShare(inviteUID)
+ result = yield self._newStoreHome.declineShare(inviteUID)
+ if not result:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (calendarserver_namespace, "invalid-share"),
+ "Invite UID not valid",
+ ))
returnValue(Response(code=responsecode.NO_CONTENT))
Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -318,6 +318,13 @@
# upgrade.
#
+ # Work queue configuration information
+ #
+ "WorkQueue" : {
+ "ampPort": 7654, # Port used for hosts in a cluster to take to each other
+ },
+
+ #
# Types of service provided
#
"EnableCalDAV" : True, # Enable CalDAV service
@@ -361,7 +368,7 @@
#
# Directory service
#
- # A directory service provides information about principals (eg.
+ # A directory service provides information about principals (e.g.
# users, groups, locations and resources) to the server.
#
"DirectoryService": {
@@ -825,8 +832,9 @@
"Servers" : {
"Enabled": False, # Multiple servers enabled or not
"ConfigFile": "localservers.xml", # File path for server information
- "MaxClients": 5, # Pool size for connections to between servers
+ "MaxClients": 5, # Pool size for connections between servers
"InboxName": "podding", # Name for top-level inbox resource
+ "ConduitName": "conduit", # Name for top-level cross-pod resource
},
#
@@ -1067,8 +1075,11 @@
def _loadImport(childDict):
# Look for an import and read that one as the main config and merge the current one into that
if "ImportConfig" in childDict and childDict.ImportConfig:
- configRoot = os.path.join(childDict.ServerRoot, childDict.ConfigRoot)
- path = _expandPath(fullServerPath(configRoot, childDict.ImportConfig))
+ if childDict.ImportConfig[0] != ".":
+ configRoot = os.path.join(childDict.ServerRoot, childDict.ConfigRoot)
+ path = _expandPath(fullServerPath(configRoot, childDict.ImportConfig))
+ else:
+ path = childDict.ImportConfig
if os.path.exists(path):
importDict = ConfigDict(self._parseConfigFromFile(path))
if importDict:
@@ -1564,6 +1575,7 @@
(direction,))
+
def _updateSharing(configDict, reloading=False):
#
# Sharing
@@ -1574,6 +1586,7 @@
PerUserDataFilter.IGNORE_X_PROPERTIES.append(propertyName)
+
def _updateServers(configDict, reloading=False):
from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
if configDict.Servers.Enabled:
Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -286,13 +286,6 @@
return self._parentResource
- def index(self):
- """
- Retrieve the new-style index wrapper.
- """
- return self._newStoreObject.retrieveOldIndex()
-
-
def exists(self):
# FIXME: tests
return self._newStoreObject is not None
@@ -303,7 +296,6 @@
# The newstore implementation supports this directly
returnValue(
(yield self._newStoreObject.resourceNamesSinceToken(revision))
- + ([],)
)
@@ -347,6 +339,18 @@
return self._newStoreObject.countObjectResources()
+ @inlineCallbacks
+ def resourceExists(self, name):
+ """
+ Indicate whether a resource with the specified name exists.
+
+ @return: C{True} if it exists
+ @rtype: C{bool}
+ """
+ allNames = yield self._newStoreObject.listObjectResources()
+ returnValue(name in allNames)
+
+
def name(self):
return self._name
@@ -462,6 +466,8 @@
if self.isShareeResource():
log.debug("Removing shared collection %s" % (self,))
yield self.removeShareeResource(request)
+ # Re-initialize to get stuff setup again now we have no object
+ self._initializeWithHomeChild(None, self._parentResource)
returnValue(NO_CONTENT)
log.debug("Deleting collection %s" % (self,))
@@ -488,11 +494,6 @@
# Now do normal delete
- # Handle sharing
- wasShared = self.isShared()
- if wasShared:
- yield self.downgradeFromShare(request)
-
# Actually delete it.
yield self._newStoreObject.remove()
@@ -968,6 +969,10 @@
)
+ def search(self, filter, **kwargs):
+ return self._newStoreObject.search(filter, **kwargs)
+
+
def notifierID(self):
return "%s/%s" % self._newStoreObject.notifierID()
@@ -1121,7 +1126,7 @@
isowner = (yield self.isOwner(request))
accessPrincipal = (yield self.resourceOwnerPrincipal(request))
- for name, _ignore_uid, _ignore_type in (yield maybeDeferred(self.index().bruteForceSearch)):
+ for name in (yield self._newStoreObject.listObjectResources()):
try:
child = yield request.locateChildResource(self, name)
except TypeError:
@@ -2421,6 +2426,7 @@
try:
response = (yield self.storeMove(request, destinationparent, destination.name()))
+ self._newStoreObject = None
returnValue(response)
# Handle the various store errors
@@ -3371,6 +3377,8 @@
if self.isShareeResource():
log.debug("Removing shared resource %s" % (self,))
yield self.removeShareeResource(request)
+ # Re-initialize to get stuff setup again now we have no object
+ self._initializeWithObject(None, self._newStoreParent)
returnValue(NO_CONTENT)
elif self._newStoreObject.isGroupForSharedAddressBook():
abCollectionResource = (yield request.locateResource(parentForURL(request.uri)))
@@ -3673,7 +3681,6 @@
# The newstore implementation supports this directly
returnValue(
(yield self._newStoreNotifications.resourceNamesSinceToken(revision))
- + ([],)
)
Modified: CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -27,7 +27,6 @@
from twistedcaldav import caldavxml
from twistedcaldav import ical
-from twistedcaldav.query import calendarqueryfilter
from twistedcaldav.config import config
from twistedcaldav.test.util import StoreTestCase, SimpleStoreRequest
from twisted.internet.defer import inlineCallbacks, returnValue
@@ -36,6 +35,7 @@
from twistedcaldav.ical import Component
from txdav.caldav.icalendarstore import ComponentUpdateState
from twistedcaldav.directory.directory import DirectoryService
+from txdav.caldav.datastore.query.filter import TimeRange
@inlineCallbacks
@@ -167,7 +167,7 @@
cal = property.calendar()
instances = cal.expandTimeRanges(query_timerange.end)
vevents = [x for x in cal.subcomponents() if x.name() == "VEVENT"]
- if not calendarqueryfilter.TimeRange(query_timerange).matchinstance(vevents[0], instances):
+ if not TimeRange(query_timerange).matchinstance(vevents[0], instances):
self.fail("REPORT property %r returned calendar %s outside of request time range %r"
% (property, property.calendar, query_timerange))
Modified: CalendarServer/trunk/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_sharing.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/test/test_sharing.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -30,6 +30,7 @@
from twistedcaldav.test.test_cache import StubResponseCacheResource
from twistedcaldav.test.util import norequest, StoreTestCase, SimpleStoreRequest
+from txdav.caldav.datastore.test.util import buildDirectory
from txdav.common.datastore.sql_tables import _BIND_MODE_DIRECT
from txdav.xml import element as davxml
from txdav.xml.parser import WebDAVDocument
@@ -738,6 +739,8 @@
home is at /. Return the name of the newly shared calendar in the
sharee's home.
"""
+
+ self._sqlCalendarStore._directoryService = buildDirectory(homes=("wiki-testing",))
wcreate = self._sqlCalendarStore.newTransaction("create wiki")
yield wcreate.calendarHomeWithUID("wiki-testing", create=True)
yield wcreate.commit()
Modified: CalendarServer/trunk/twistedcaldav/test/test_xml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_xml.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/test/test_xml.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -17,12 +17,15 @@
import os
from twisted.trial.unittest import SkipTest
+
from twistedcaldav.ical import Component
-from twistedcaldav.query import calendarqueryfilter
import twistedcaldav.test.util
from twistedcaldav.caldavxml import ComponentFilter, PropertyFilter, TextMatch, \
Filter, TimeRange
+from txdav.caldav.datastore.query.filter import Filter as storeFilter
+from txdav.caldav.datastore.query.filter import ComponentFilter as storeComponentFilter
+
class XML (twistedcaldav.test.util.TestCase):
"""
XML tests
@@ -46,7 +49,7 @@
else:
no = ""
- if has != calendarqueryfilter.ComponentFilter(
+ if has != storeComponentFilter(
ComponentFilter(
ComponentFilter(
name=component_name
@@ -70,7 +73,7 @@
else:
no = ""
- if has != calendarqueryfilter.ComponentFilter(
+ if has != storeComponentFilter(
ComponentFilter(
ComponentFilter(
PropertyFilter(
@@ -106,7 +109,7 @@
else:
no = ""
- if has != calendarqueryfilter.ComponentFilter(
+ if has != storeComponentFilter(
ComponentFilter(
ComponentFilter(
PropertyFilter(
@@ -148,7 +151,7 @@
else:
no = ""
- if has != calendarqueryfilter.Filter(
+ if has != storeFilter(
Filter(
ComponentFilter(
ComponentFilter(
Modified: CalendarServer/trunk/txdav/base/datastore/subpostgres.py
===================================================================
--- CalendarServer/trunk/txdav/base/datastore/subpostgres.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/base/datastore/subpostgres.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -228,7 +228,7 @@
# in /tmp and based on a hash of the data store directory
digest = md5(dataStoreDirectory.path).hexdigest()
socketDir = "/tmp/ccs_postgres_" + digest
-
+
self.socketDir = CachingFilePath(socketDir)
self.host = self.socketDir.path
self.port = None
Modified: CalendarServer/trunk/txdav/base/datastore/util.py
===================================================================
--- CalendarServer/trunk/txdav/base/datastore/util.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/base/datastore/util.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -98,6 +98,12 @@
return "objectWithResourceID:%s:%s" % (homeResourceID, resourceID)
+ # Home child objects by external id
+
+ def keyForObjectWithExternalID(self, homeResourceID, externalID):
+ return "objectWithExternalID:%s:%s" % (homeResourceID, externalID)
+
+
# Home metadata (Created/Modified)
def keyForHomeMetaData(self, homeResourceID):
Modified: CalendarServer/trunk/txdav/caldav/datastore/index_file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/index_file.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/index_file.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -15,6 +15,7 @@
# limitations under the License.
##
+
"""
CalDAV Index.
@@ -43,12 +44,14 @@
from twext.python.log import Logger
+from txdav.caldav.datastore.query.builder import buildExpression
+from txdav.caldav.datastore.query.filter import Filter
+from txdav.common.datastore.query.filegenerator import sqllitegenerator
from txdav.common.icommondatastore import SyncTokenValidException, \
ReservationError, IndexedSearchException
from twistedcaldav.dateops import pyCalendarTodatetime
from twistedcaldav.ical import Component
-from twistedcaldav.query import calendarquery, calendarqueryfilter
from twistedcaldav.sql import AbstractSQLDatabase
from twistedcaldav.sql import db_prefix
from twistedcaldav.instance import InvalidOverriddenInstanceError
@@ -275,6 +278,7 @@
changed = []
deleted = []
+ invalid = []
for name, wasdeleted in results:
if name:
if wasdeleted == 'Y':
@@ -285,7 +289,7 @@
else:
raise SyncTokenValidException
- return changed, deleted,
+ return (changed, deleted, invalid)
def lastRevision(self):
@@ -320,7 +324,7 @@
# Make sure we have a proper Filter element and get the partial SQL
# statement to use.
- if isinstance(filter, calendarqueryfilter.Filter):
+ if isinstance(filter, Filter):
if fbtype:
# Lookup the useruid - try the empty (default) one if needed
dbuseruid = self._db_value_for_sql(
@@ -330,7 +334,7 @@
else:
dbuseruid = ""
- qualifiers = calendarquery.sqlcalendarquery(filter, None, dbuseruid, fbtype)
+ qualifiers = sqlcalendarquery(filter, None, dbuseruid, fbtype)
if qualifiers is not None:
# Determine how far we need to extend the current expansion of
# events. If we have an open-ended time-range we will expand one
@@ -437,6 +441,24 @@
+def sqlcalendarquery(filter, calendarid=None, userid=None, freebusy=False):
+ """
+ Convert the supplied calendar-query into a partial SQL statement.
+
+ @param filter: the L{Filter} for the calendar-query to convert.
+ @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+ and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+ Or return C{None} if it is not possible to create an SQL query to fully match the calendar-query.
+ """
+ try:
+ expression = buildExpression(filter, sqllitegenerator.FIELDS)
+ sql = sqllitegenerator(expression, calendarid, userid, freebusy)
+ return sql.generate()
+ except ValueError:
+ return None
+
+
+
class CalendarIndex (AbstractCalendarIndex):
"""
Calendar index - abstract class for indexer that indexes calendar objects in a collection.
Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/__init__.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 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.
-##
Copied: CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 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.
+##
Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/builder.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/builder.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/builder.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,227 +0,0 @@
-##
-# Copyright (c) 2006-2013 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.dateops import floatoffset, pyCalendarTodatetime
-
-from txdav.caldav.datastore.query.filter import ComponentFilter, PropertyFilter, TextMatch, TimeRange
-from txdav.common.datastore.query import expression
-
-
-"""
-SQL statement generator from query expressions.
-"""
-
-__all__ = [
- "buildExpression",
-]
-
-
-
-# SQL Index column (field) names
-
-def buildExpression(filter, fields):
- """
- Convert the supplied calendar-query into an expression tree.
-
- @param filter: the L{Filter} for the calendar-query to convert.
- @return: a L{baseExpression} for the expression tree.
- """
-
- # Lets assume we have a valid filter from the outset.
-
- # Top-level filter contains exactly one comp-filter element
- assert filter.child is not None
- vcalfilter = filter.child
- assert isinstance(vcalfilter, ComponentFilter)
- 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, ComponentFilter)]:
- raise ValueError
-
- return compfilterListExpression(vcalfilter.filters, fields, logical)
- else:
- return expression.allExpression()
-
-
-
-def compfilterListExpression(compfilters, fields, logical):
- """
- Create an expression for a list of comp-filter elements.
-
- @param compfilters: the C{list} of L{ComponentFilter} elements.
- @return: a L{baseExpression} for the expression tree.
- """
-
- if len(compfilters) == 1:
- return compfilterExpression(compfilters[0], fields)
- else:
- return logical([compfilterExpression(c, fields) for c in compfilters])
-
-
-
-def compfilterExpression(compfilter, fields):
- """
- Create an expression for a single comp-filter element.
-
- @param compfilter: the L{ComponentFilter} element.
- @return: a L{baseExpression} for the expression tree.
- """
-
- # Handle is-not-defined case
- 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) or isinstance(compfilter.filter_name, unicode):
- expressions.append(expression.isExpression(fields["TYPE"], compfilter.filter_name, True))
- else:
- expressions.append(expression.inExpression(fields["TYPE"], compfilter.filter_name, True))
-
- # Handle time-range
- if compfilter.qualifier and isinstance(compfilter.qualifier, TimeRange):
- start, end, startfloat, endfloat = getTimerangeArguments(compfilter.qualifier)
- expressions.append(expression.timerangeExpression(start, end, startfloat, endfloat))
-
- # Handle properties - we can only do UID right now
- props = []
- for p in [x for x in compfilter.filters if isinstance(x, PropertyFilter)]:
- props.append(propfilterExpression(p, fields))
- if len(props) > 1:
- propsExpression = logical(props)
- elif len(props) == 1:
- propsExpression = props[0]
- else:
- propsExpression = None
-
- # Handle embedded components - we do not right now as our Index does not handle them
- comps = []
- for _ignore in [x for x in compfilter.filters if isinstance(x, ComponentFilter)]:
- raise ValueError
- if len(comps) > 1:
- compsExpression = logical(comps)
- elif len(comps) == 1:
- compsExpression = comps[0]
- else:
- compsExpression = None
-
- # Now build compound expression
- if ((propsExpression is not None) and (compsExpression is not None)):
- expressions.append(logical([propsExpression, compsExpression]))
- elif propsExpression is not None:
- expressions.append(propsExpression)
- elif compsExpression is not None:
- expressions.append(compsExpression)
-
- # Now build return expression
- return expression.andExpression(expressions)
-
-
-
-def propfilterExpression(propfilter, fields):
- """
- Create an expression for a single prop-filter element.
-
- @param propfilter: the L{PropertyFilter} element.
- @return: a L{baseExpression} for the expression tree.
- """
-
- # Only handle UID right now
- if propfilter.filter_name != "UID":
- raise ValueError
-
- # Handle is-not-defined case
- if not propfilter.defined:
- # 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, TimeRange):
- raise ValueError
-
- # Handle text-match
- tm = None
- if propfilter.qualifier and isinstance(propfilter.qualifier, TextMatch):
- 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 = logical(params)
- elif len(params) == 1:
- paramsExpression = params[0]
- else:
- paramsExpression = None
-
- # Now build return expression
- if (tm is not None) and (paramsExpression is not None):
- return logical([tm, paramsExpression])
- elif tm is not None:
- return tm
- elif paramsExpression is not None:
- return paramsExpression
- else:
- return None
-
-
-
-def getTimerangeArguments(timerange):
- """
- Get start/end and floating start/end (adjusted for timezone offset) values from the
- supplied time-range test.
-
- @param timerange: the L{TimeRange} used in the query.
- @return: C{tuple} of C{str} for start, end, startfloat, endfloat
- """
-
- # Start/end in UTC
- start = timerange.start
- end = timerange.end
-
- # Get timezone
- tzinfo = timerange.tzinfo
-
- # Now force to floating UTC
- startfloat = floatoffset(start, tzinfo) if start else None
- endfloat = floatoffset(end, tzinfo) if end else None
-
- return (
- pyCalendarTodatetime(start) if start else None,
- pyCalendarTodatetime(end) if end else None,
- pyCalendarTodatetime(startfloat) if startfloat else None,
- pyCalendarTodatetime(endfloat) if endfloat else None,
- )
Copied: CalendarServer/trunk/txdav/caldav/datastore/query/builder.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/builder.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/builder.py (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/builder.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,227 @@
+##
+# Copyright (c) 2006-2013 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.dateops import floatoffset, pyCalendarTodatetime
+
+from txdav.caldav.datastore.query.filter import ComponentFilter, PropertyFilter, TextMatch, TimeRange
+from txdav.common.datastore.query import expression
+
+
+"""
+SQL statement generator from query expressions.
+"""
+
+__all__ = [
+ "buildExpression",
+]
+
+
+
+# SQL Index column (field) names
+
+def buildExpression(filter, fields):
+ """
+ Convert the supplied calendar-query into an expression tree.
+
+ @param filter: the L{Filter} for the calendar-query to convert.
+ @return: a L{baseExpression} for the expression tree.
+ """
+
+ # Lets assume we have a valid filter from the outset.
+
+ # Top-level filter contains exactly one comp-filter element
+ assert filter.child is not None
+ vcalfilter = filter.child
+ assert isinstance(vcalfilter, ComponentFilter)
+ 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, ComponentFilter)]:
+ raise ValueError
+
+ return compfilterListExpression(vcalfilter.filters, fields, logical)
+ else:
+ return expression.allExpression()
+
+
+
+def compfilterListExpression(compfilters, fields, logical):
+ """
+ Create an expression for a list of comp-filter elements.
+
+ @param compfilters: the C{list} of L{ComponentFilter} elements.
+ @return: a L{baseExpression} for the expression tree.
+ """
+
+ if len(compfilters) == 1:
+ return compfilterExpression(compfilters[0], fields)
+ else:
+ return logical([compfilterExpression(c, fields) for c in compfilters])
+
+
+
+def compfilterExpression(compfilter, fields):
+ """
+ Create an expression for a single comp-filter element.
+
+ @param compfilter: the L{ComponentFilter} element.
+ @return: a L{baseExpression} for the expression tree.
+ """
+
+ # Handle is-not-defined case
+ 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) or isinstance(compfilter.filter_name, unicode):
+ expressions.append(expression.isExpression(fields["TYPE"], compfilter.filter_name, True))
+ else:
+ expressions.append(expression.inExpression(fields["TYPE"], compfilter.filter_name, True))
+
+ # Handle time-range
+ if compfilter.qualifier and isinstance(compfilter.qualifier, TimeRange):
+ start, end, startfloat, endfloat = getTimerangeArguments(compfilter.qualifier)
+ expressions.append(expression.timerangeExpression(start, end, startfloat, endfloat))
+
+ # Handle properties - we can only do UID right now
+ props = []
+ for p in [x for x in compfilter.filters if isinstance(x, PropertyFilter)]:
+ props.append(propfilterExpression(p, fields))
+ if len(props) > 1:
+ propsExpression = logical(props)
+ elif len(props) == 1:
+ propsExpression = props[0]
+ else:
+ propsExpression = None
+
+ # Handle embedded components - we do not right now as our Index does not handle them
+ comps = []
+ for _ignore in [x for x in compfilter.filters if isinstance(x, ComponentFilter)]:
+ raise ValueError
+ if len(comps) > 1:
+ compsExpression = logical(comps)
+ elif len(comps) == 1:
+ compsExpression = comps[0]
+ else:
+ compsExpression = None
+
+ # Now build compound expression
+ if ((propsExpression is not None) and (compsExpression is not None)):
+ expressions.append(logical([propsExpression, compsExpression]))
+ elif propsExpression is not None:
+ expressions.append(propsExpression)
+ elif compsExpression is not None:
+ expressions.append(compsExpression)
+
+ # Now build return expression
+ return expression.andExpression(expressions)
+
+
+
+def propfilterExpression(propfilter, fields):
+ """
+ Create an expression for a single prop-filter element.
+
+ @param propfilter: the L{PropertyFilter} element.
+ @return: a L{baseExpression} for the expression tree.
+ """
+
+ # Only handle UID right now
+ if propfilter.filter_name != "UID":
+ raise ValueError
+
+ # Handle is-not-defined case
+ if not propfilter.defined:
+ # 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, TimeRange):
+ raise ValueError
+
+ # Handle text-match
+ tm = None
+ if propfilter.qualifier and isinstance(propfilter.qualifier, TextMatch):
+ 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 = logical(params)
+ elif len(params) == 1:
+ paramsExpression = params[0]
+ else:
+ paramsExpression = None
+
+ # Now build return expression
+ if (tm is not None) and (paramsExpression is not None):
+ return logical([tm, paramsExpression])
+ elif tm is not None:
+ return tm
+ elif paramsExpression is not None:
+ return paramsExpression
+ else:
+ return None
+
+
+
+def getTimerangeArguments(timerange):
+ """
+ Get start/end and floating start/end (adjusted for timezone offset) values from the
+ supplied time-range test.
+
+ @param timerange: the L{TimeRange} used in the query.
+ @return: C{tuple} of C{str} for start, end, startfloat, endfloat
+ """
+
+ # Start/end in UTC
+ start = timerange.start
+ end = timerange.end
+
+ # Get timezone
+ tzinfo = timerange.tzinfo
+
+ # Now force to floating UTC
+ startfloat = floatoffset(start, tzinfo) if start else None
+ endfloat = floatoffset(end, tzinfo) if end else None
+
+ return (
+ pyCalendarTodatetime(start) if start else None,
+ pyCalendarTodatetime(end) if end else None,
+ pyCalendarTodatetime(startfloat) if startfloat else None,
+ pyCalendarTodatetime(endfloat) if endfloat else None,
+ )
Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/filter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/filter.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/filter.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,913 +0,0 @@
-##
-# Copyright (c) 2011-2013 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.
-##
-
-"""
-Object model of CALDAV:filter element used in an addressbook-query.
-"""
-
-__all__ = [
- "Filter",
-]
-
-from twext.python.log import Logger
-
-from twistedcaldav.caldavxml import caldav_namespace, CalDAVTimeZoneElement
-from twistedcaldav.dateops import timeRangesOverlap
-from twistedcaldav.ical import Component, Property
-
-from pycalendar.datetime import DateTime
-from pycalendar.timezone import Timezone
-
-log = Logger()
-
-
-class FilterBase(object):
- """
- Determines which matching components are returned.
- """
-
- serialized_name = None
- deserialize_names = {}
-
- @classmethod
- def serialize_register(cls, register):
- cls.deserialize_names[register.serialized_name] = register
-
-
- def __init__(self, xml_element):
- pass
-
-
- @classmethod
- def deserialize(cls, data):
- """
- Convert a JSON compatible serialization of this object into the actual object.
- """
- obj = cls.deserialize_names[data["type"]](None)
- obj._deserialize(data)
- return obj
-
-
- def _deserialize(self, data):
- """
- Convert a JSON compatible serialization of this object into the actual object.
- """
- pass
-
-
- def serialize(self):
- """
- Create a JSON compatible serialization of this object - will be used in a cross-pod request.
- """
- return {
- "type": self.serialized_name,
- }
-
-
- def match(self, item, access=None):
- raise NotImplementedError
-
-
- def valid(self, level=0):
- raise NotImplementedError
-
-
-
-class Filter(FilterBase):
- """
- Determines which matching components are returned.
- """
-
- serialized_name = "Filter"
-
- def __init__(self, xml_element):
-
- super(Filter, self).__init__(xml_element)
- if xml_element is None:
- return
-
- # One comp-filter element must be present
- if len(xml_element.children) != 1 or xml_element.children[0].qname() != (caldav_namespace, "comp-filter"):
- raise ValueError("Invalid CALDAV:filter element: %s" % (xml_element,))
-
- self.child = ComponentFilter(xml_element.children[0])
-
-
- def _deserialize(self, data):
- """
- Convert a JSON compatible serialization of this object into the actual object.
- """
- self.child = FilterBase.deserialize(data["child"])
-
-
- def serialize(self):
- """
- Create a JSON compatible serialization of this object - will be used in a cross-pod request.
- """
- result = super(Filter, self).serialize()
- result.update({
- "child": self.child.serialize(),
- })
- return result
-
-
- def match(self, component, access=None):
- """
- Returns True if the given calendar component matches this filter, False
- otherwise.
- """
-
- # We only care about certain access restrictions.
- if access not in (Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED):
- access = None
-
- # We need to prepare ourselves for a time-range query by pre-calculating
- # the set of instances up to the latest time-range limit. That way we can
- # avoid having to do some form of recurrence expansion for each query sub-part.
- maxend, isStartTime = self.getmaxtimerange()
- if maxend:
- if isStartTime:
- if component.isRecurringUnbounded():
- # Unbounded recurrence is always within a start-only time-range
- instances = None
- else:
- # Expand the instances up to infinity
- instances = component.expandTimeRanges(DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)), ignoreInvalidInstances=True)
- else:
- instances = component.expandTimeRanges(maxend, ignoreInvalidInstances=True)
- else:
- instances = None
- self.child.setInstances(instances)
-
- # <filter> contains exactly one <comp-filter>
- return self.child.match(component, access)
-
-
- def valid(self):
- """
- Indicate whether this filter element's structure is valid wrt iCalendar
- data object model.
-
- @return: True if valid, False otherwise
- """
-
- # Must have one child element for VCALENDAR
- return self.child.valid(0)
-
-
- def settimezone(self, tzelement):
- """
- Set the default timezone to use with this query.
- @param calendar: a L{Component} for the VCALENDAR containing the one
- VTIMEZONE that we want
- @return: the L{Timezone} derived from the VTIMEZONE or utc.
- """
-
- if tzelement is None:
- tz = None
- elif isinstance(tzelement, CalDAVTimeZoneElement):
- tz = tzelement.gettimezone()
- elif isinstance(tzelement, Component):
- tz = tzelement.gettimezone()
- if tz is None:
- tz = Timezone(utc=True)
- self.child.settzinfo(tz)
- return tz
-
-
- def getmaxtimerange(self):
- """
- Get the date farthest into the future in any time-range elements
- """
-
- return self.child.getmaxtimerange(None, False)
-
-
- def getmintimerange(self):
- """
- Get the date farthest into the past in any time-range elements. That is either
- the start date, or if start is not present, the end date.
- """
-
- return self.child.getmintimerange(None, False)
-
-FilterBase.serialize_register(Filter)
-
-
-
-class FilterChildBase(FilterBase):
- """
- CalDAV filter element.
- """
-
- def __init__(self, xml_element):
-
- super(FilterChildBase, self).__init__(xml_element)
- if xml_element is None:
- return
-
- qualifier = None
- filters = []
-
- for child in xml_element.children:
- qname = child.qname()
-
- if qname in (
- (caldav_namespace, "is-not-defined"),
- (caldav_namespace, "time-range"),
- (caldav_namespace, "text-match"),
- ):
- if qualifier is not None:
- raise ValueError("Only one of CalDAV:time-range, CalDAV:text-match allowed")
-
- if qname == (caldav_namespace, "is-not-defined"):
- qualifier = IsNotDefined(child)
- elif qname == (caldav_namespace, "time-range"):
- qualifier = TimeRange(child)
- elif qname == (caldav_namespace, "text-match"):
- qualifier = TextMatch(child)
-
- elif qname == (caldav_namespace, "comp-filter"):
- filters.append(ComponentFilter(child))
- elif qname == (caldav_namespace, "prop-filter"):
- filters.append(PropertyFilter(child))
- elif qname == (caldav_namespace, "param-filter"):
- filters.append(ParameterFilter(child))
- else:
- raise ValueError("Unknown child element: %s" % (qname,))
-
- if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
- raise ValueError("No other tests allowed when CalDAV:is-not-defined is present")
-
- self.qualifier = qualifier
- self.filters = filters
- self.filter_name = xml_element.attributes["name"]
- if isinstance(self.filter_name, unicode):
- 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 _deserialize(self, data):
- """
- Convert a JSON compatible serialization of this object into the actual object.
- """
- self.qualifier = FilterBase.deserialize(data["qualifier"]) if data["qualifier"] else None
- self.filters = [FilterBase.deserialize(filter) for filter in data["filters"]]
- self.filter_name = data["filter_name"]
- self.defined = data["defined"]
- self.filter_test = data["filter_test"]
-
-
- def serialize(self):
- """
- Create a JSON compatible serialization of this object - will be used in a cross-pod request.
- """
- result = super(FilterChildBase, self).serialize()
- result.update({
- "qualifier": self.qualifier.serialize() if self.qualifier else None,
- "filters": [filter.serialize() for filter in self.filters],
- "filter_name": self.filter_name,
- "defined": self.defined,
- "filter_test": self.filter_test,
- })
- return result
-
-
- def match(self, item, access=None):
- """
- Returns True if the given calendar item (either a component, property or parameter value)
- matches this filter, False otherwise.
- """
-
- # Always return True for the is-not-defined case as the result of this will
- # be negated by the caller
- if not self.defined:
- return True
-
- 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 allof != filter._match(item, access):
- return not allof
- return allof
- else:
- return True
-
-
-
-class ComponentFilter (FilterChildBase):
- """
- Limits a search to only the chosen component types.
- """
-
- serialized_name = "ComponentFilter"
-
- def match(self, item, access):
- """
- Returns True if the given calendar item (which is a component)
- matches this filter, False otherwise.
- This specialization uses the instance matching option of the time-range filter
- to minimize instance expansion.
- """
-
- # Always return True for the is-not-defined case as the result of this will
- # be negated by the caller
- if not self.defined:
- return True
-
- 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 allof != filter._match(item, access):
- return not allof
- return allof
- else:
- return True
-
-
- def _match(self, component, access):
- # At least one subcomponent must match (or is-not-defined is set)
- for subcomponent in component.subcomponents():
- # If access restrictions are in force, restrict matching to specific components only.
- # In particular do not match VALARM.
- if access and subcomponent.name() not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE",):
- continue
-
- # Try to match the component name
- if isinstance(self.filter_name, str):
- if subcomponent.name() != self.filter_name:
- continue
- else:
- if subcomponent.name() not in self.filter_name:
- continue
- if self.match(subcomponent, access):
- break
- else:
- return not self.defined
- return self.defined
-
-
- def setInstances(self, instances):
- """
- Give the list of instances to each comp-filter element.
- @param instances: the list of instances.
- """
- self.instances = instances
- for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
- compfilter.setInstances(instances)
-
-
- def valid(self, level):
- """
- Indicate whether this filter element's structure is valid wrt iCalendar
- data object model.
-
- @param level: the nesting level of this filter element, 0 being the top comp-filter.
- @return: True if valid, False otherwise
- """
-
- # Check for time-range
- timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
-
- if level == 0:
- # Must have VCALENDAR at the top
- if (self.filter_name != "VCALENDAR") or timerange:
- log.info("Top-level comp-filter must be VCALENDAR, instead: %s" % (self.filter_name,))
- return False
- elif level == 1:
- # Disallow VCALENDAR, VALARM, STANDARD, DAYLIGHT, AVAILABLE at the top, everything else is OK
- if self.filter_name in ("VCALENDAR", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE"):
- log.info("comp-filter wrong component type: %s" % (self.filter_name,))
- return False
-
- # time-range only on VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY
- if timerange and self.filter_name not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY"):
- log.info("time-range cannot be used with component %s" % (self.filter_name,))
- return False
- elif level == 2:
- # Disallow VCALENDAR, VTIMEZONE, VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY at the top, everything else is OK
- if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY")):
- log.info("comp-filter wrong sub-component type: %s" % (self.filter_name,))
- return False
-
- # time-range only on VALARM, AVAILABLE
- if timerange and self.filter_name not in ("VALARM", "AVAILABLE",):
- log.info("time-range cannot be used with sub-component %s" % (self.filter_name,))
- return False
- else:
- # Disallow all standard iCal components anywhere else
- if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE")) or timerange:
- log.info("comp-filter wrong standard component type: %s" % (self.filter_name,))
- return False
-
- # Test each property
- for propfilter in [x for x in self.filters if isinstance(x, PropertyFilter)]:
- if not propfilter.valid():
- return False
-
- # Test each component
- for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
- if not compfilter.valid(level + 1):
- return False
-
- # Test the time-range
- if timerange:
- if not self.qualifier.valid():
- return False
-
- return True
-
-
- def settzinfo(self, tzinfo):
- """
- Set the default timezone to use with this query.
- @param tzinfo: a L{Timezone} to use.
- """
-
- # Give tzinfo to any TimeRange we have
- if isinstance(self.qualifier, TimeRange):
- self.qualifier.settzinfo(tzinfo)
-
- # Pass down to sub components/properties
- for x in self.filters:
- x.settzinfo(tzinfo)
-
-
- def getmaxtimerange(self, currentMaximum, currentIsStartTime):
- """
- Get the date farthest into the future in any time-range elements
-
- @param currentMaximum: current future value to compare with
- @type currentMaximum: L{DateTime}
- """
-
- # Give tzinfo to any TimeRange we have
- isStartTime = False
- if isinstance(self.qualifier, TimeRange):
- isStartTime = self.qualifier.end is None
- compareWith = self.qualifier.start if isStartTime else self.qualifier.end
- if currentMaximum is None or currentMaximum < compareWith:
- currentMaximum = compareWith
- currentIsStartTime = isStartTime
-
- # Pass down to sub components/properties
- for x in self.filters:
- currentMaximum, currentIsStartTime = x.getmaxtimerange(currentMaximum, currentIsStartTime)
-
- return currentMaximum, currentIsStartTime
-
-
- def getmintimerange(self, currentMinimum, currentIsEndTime):
- """
- Get the date farthest into the past in any time-range elements. That is either
- the start date, or if start is not present, the end date.
- """
-
- # Give tzinfo to any TimeRange we have
- isEndTime = False
- if isinstance(self.qualifier, TimeRange):
- isEndTime = self.qualifier.start is None
- compareWith = self.qualifier.end if isEndTime else self.qualifier.start
- if currentMinimum is None or currentMinimum > compareWith:
- currentMinimum = compareWith
- currentIsEndTime = isEndTime
-
- # Pass down to sub components/properties
- for x in self.filters:
- currentMinimum, currentIsEndTime = x.getmintimerange(currentMinimum, currentIsEndTime)
-
- return currentMinimum, currentIsEndTime
-
-FilterBase.serialize_register(ComponentFilter)
-
-
-
-class PropertyFilter (FilterChildBase):
- """
- Limits a search to specific properties.
- """
-
- serialized_name = "PropertyFilter"
-
- def _match(self, component, access):
- # When access restriction is in force, we need to only allow matches against the properties
- # allowed by the access restriction level.
- if access:
- allowedProperties = Component.confidentialPropertiesMap.get(component.name(), None)
- if allowedProperties and access == Component.ACCESS_RESTRICTED:
- allowedProperties += Component.extraRestrictedProperties
- else:
- allowedProperties = None
-
- # At least one property must match (or is-not-defined is set)
- for property in component.properties():
- # Apply access restrictions, if any.
- if allowedProperties is not None and property.name().upper() not in allowedProperties:
- continue
- if property.name().upper() == self.filter_name.upper() and self.match(property, access):
- break
- else:
- return not self.defined
- return self.defined
-
-
- def valid(self):
- """
- Indicate whether this filter element's structure is valid wrt iCalendar
- data object model.
-
- @return: True if valid, False otherwise
- """
-
- # Check for time-range
- timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
-
- # time-range only on COMPLETED, CREATED, DTSTAMP, LAST-MODIFIED
- if timerange and self.filter_name.upper() not in ("COMPLETED", "CREATED", "DTSTAMP", "LAST-MODIFIED"):
- log.info("time-range cannot be used with property %s" % (self.filter_name,))
- return False
-
- # Test the time-range
- if timerange:
- if not self.qualifier.valid():
- return False
-
- # No other tests
- return True
-
-
- def settzinfo(self, tzinfo):
- """
- Set the default timezone to use with this query.
- @param tzinfo: a L{Timezone} to use.
- """
-
- # Give tzinfo to any TimeRange we have
- if isinstance(self.qualifier, TimeRange):
- self.qualifier.settzinfo(tzinfo)
-
-
- def getmaxtimerange(self, currentMaximum, currentIsStartTime):
- """
- Get the date farthest into the future in any time-range elements
-
- @param currentMaximum: current future value to compare with
- @type currentMaximum: L{DateTime}
- """
-
- # Give tzinfo to any TimeRange we have
- isStartTime = False
- if isinstance(self.qualifier, TimeRange):
- isStartTime = self.qualifier.end is None
- compareWith = self.qualifier.start if isStartTime else self.qualifier.end
- if currentMaximum is None or currentMaximum < compareWith:
- currentMaximum = compareWith
- currentIsStartTime = isStartTime
-
- return currentMaximum, currentIsStartTime
-
-
- def getmintimerange(self, currentMinimum, currentIsEndTime):
- """
- Get the date farthest into the past in any time-range elements. That is either
- the start date, or if start is not present, the end date.
- """
-
- # Give tzinfo to any TimeRange we have
- isEndTime = False
- if isinstance(self.qualifier, TimeRange):
- isEndTime = self.qualifier.start is None
- compareWith = self.qualifier.end if isEndTime else self.qualifier.start
- if currentMinimum is None or currentMinimum > compareWith:
- currentMinimum = compareWith
- currentIsEndTime = isEndTime
-
- return currentMinimum, currentIsEndTime
-
-FilterBase.serialize_register(PropertyFilter)
-
-
-
-class ParameterFilter (FilterChildBase):
- """
- Limits a search to specific parameters.
- """
-
- serialized_name = "ParameterFilter"
-
- def _match(self, property, access):
-
- # At least one parameter must match (or is-not-defined is set)
- result = not self.defined
- for parameterName in property.parameterNames():
- if parameterName.upper() == self.filter_name.upper() and self.match([property.parameterValue(parameterName)], access):
- result = self.defined
- break
-
- return result
-
-FilterBase.serialize_register(ParameterFilter)
-
-
-
-class IsNotDefined (FilterBase):
- """
- Specifies that the named iCalendar item does not exist.
- """
-
- serialized_name = "IsNotDefined"
-
- def match(self, component, access=None):
- # Oddly, this needs always to return True so that it appears there is
- # a match - but we then "negate" the result if is-not-defined is set.
- # Actually this method should never be called as we special case the
- # is-not-defined option.
- return True
-
-FilterBase.serialize_register(IsNotDefined)
-
-
-class TextMatch (FilterBase):
- """
- Specifies a substring match on a property or parameter value.
- (CalDAV-access-09, section 9.6.4)
- """
- serialized_name = "TextMatch"
-
- def __init__(self, xml_element):
-
- super(TextMatch, self).__init__(xml_element)
- if xml_element is None:
- return
-
- self.text = str(xml_element)
- if "caseless" in xml_element.attributes:
- caseless = xml_element.attributes["caseless"]
- if caseless == "yes":
- self.caseless = True
- elif caseless == "no":
- self.caseless = False
- else:
- self.caseless = True
-
- if "negate-condition" in xml_element.attributes:
- negate = xml_element.attributes["negate-condition"]
- if negate == "yes":
- self.negate = True
- elif negate == "no":
- self.negate = False
- 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 _deserialize(self, data):
- """
- Convert a JSON compatible serialization of this object into the actual object.
- """
- self.text = data["text"]
- self.caseless = data["caseless"]
- self.negate = data["negate"]
- self.match_type = data["match_type"]
-
-
- def serialize(self):
- """
- Create a JSON compatible serialization of this object - will be used in a cross-pod request.
- """
- result = super(TextMatch, self).serialize()
- result.update({
- "text": self.text,
- "caseless": self.caseless,
- "negate": self.negate,
- "match_type": self.match_type,
- })
- return result
-
-
- def match(self, item, access):
- """
- Match the text for the item.
- If the item is a property, then match the property value,
- otherwise it may be a list of parameter values - try to match anyone of those
- """
- if item is None:
- return False
-
- if isinstance(item, Property):
- values = [item.strvalue()]
- else:
- values = item
-
- test = unicode(self.text, "utf-8")
- if self.caseless:
- test = test.lower()
-
- def _textCompare(s):
- if self.caseless:
- 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:
- 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:
- if _textCompare(unicode(subvalue, "utf-8")):
- return not self.negate
- else:
- if _textCompare(unicode(value, "utf-8")):
- return not self.negate
-
- return self.negate
-
-FilterBase.serialize_register(TextMatch)
-
-
-
-class TimeRange (FilterBase):
- """
- Specifies a time for testing components against.
- """
-
- serialized_name = "TimeRange"
-
- def __init__(self, xml_element):
-
- super(TimeRange, self).__init__(xml_element)
- if xml_element is None:
- return
-
- # One of start or end must be present
- if "start" not in xml_element.attributes and "end" not in xml_element.attributes:
- raise ValueError("One of 'start' or 'end' must be present in CALDAV:time-range")
-
- self.start = DateTime.parseText(xml_element.attributes["start"]) if "start" in xml_element.attributes else None
- self.end = DateTime.parseText(xml_element.attributes["end"]) if "end" in xml_element.attributes else None
- self.tzinfo = None
-
-
- def _deserialize(self, data):
- """
- Convert a JSON compatible serialization of this object into the actual object.
- """
- self.start = DateTime.parseText(data["start"]) if data["start"] else None
- self.end = DateTime.parseText(data["end"]) if data["end"] else None
- self.tzinfo = Timezone(tzid=data["tzinfo"]) if data["tzinfo"] else None
-
-
- def serialize(self):
- """
- Create a JSON compatible serialization of this object - will be used in a cross-pod request.
- """
- result = super(TimeRange, self).serialize()
- result.update({
- "start": self.start.getText() if self.start else None,
- "end": self.end.getText() if self.end else None,
- "tzinfo": self.tzinfo.getTimezoneID() if self.tzinfo else None,
- })
- return result
-
-
- def settzinfo(self, tzinfo):
- """
- Set the default timezone to use with this query.
- @param tzinfo: a L{Timezone} to use.
- """
-
- # Give tzinfo to any TimeRange we have
- self.tzinfo = tzinfo
-
-
- def valid(self, level=0):
- """
- Indicate whether the time-range is valid (must be date-time in UTC).
-
- @return: True if valid, False otherwise
- """
-
- if self.start is not None and self.start.isDateOnly():
- log.info("start attribute in <time-range> is not a date-time: %s" % (self.start,))
- return False
- if self.end is not None and self.end.isDateOnly():
- log.info("end attribute in <time-range> is not a date-time: %s" % (self.end,))
- return False
- if self.start is not None and not self.start.utc():
- log.info("start attribute in <time-range> is not UTC: %s" % (self.start,))
- return False
- if self.end is not None and not self.end.utc():
- log.info("end attribute in <time-range> is not UTC: %s" % (self.end,))
- return False
-
- # No other tests
- return True
-
-
- def match(self, property, access=None):
- """
- NB This is only called when doing a time-range match on a property.
- """
- if property is None:
- return False
- else:
- return property.containsTimeRange(self.start, self.end, self.tzinfo)
-
-
- def matchinstance(self, component, instances):
- """
- Test whether this time-range element causes a match to the specified component
- using the specified set of instances to determine the expanded time ranges.
- @param component: the L{Component} to test.
- @param instances: the list of expanded instances.
- @return: True if the time-range query matches, False otherwise.
- """
- if component is None:
- return False
-
- assert instances is not None or self.end is None, "Failure to expand instance for time-range filter: %r" % (self,)
-
- # Special case open-ended unbounded
- if instances is None:
- if component.getRecurrenceIDUTC() is None:
- return True
- else:
- # See if the overridden component's start is past the start
- start, _ignore_end = component.getEffectiveStartEnd()
- if start is None:
- return True
- else:
- return start >= self.start
-
- # Handle alarms as a special case
- alarms = (component.name() == "VALARM")
- if alarms:
- testcomponent = component._parent
- else:
- testcomponent = component
-
- for key in instances:
- instance = instances[key]
-
- # First make sure components match
- if not testcomponent.same(instance.component):
- continue
-
- if alarms:
- # Get all the alarm triggers for this instance and test each one
- triggers = instance.getAlarmTriggers()
- for trigger in triggers:
- if timeRangesOverlap(trigger, None, self.start, self.end, self.tzinfo):
- return True
- else:
- # Regular instance overlap test
- if timeRangesOverlap(instance.start, instance.end, self.start, self.end, self.tzinfo):
- return True
-
- return False
-
-FilterBase.serialize_register(TimeRange)
Copied: CalendarServer/trunk/txdav/caldav/datastore/query/filter.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/filter.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/filter.py (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/filter.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,913 @@
+##
+# Copyright (c) 2011-2013 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.
+##
+
+"""
+Object model of CALDAV:filter element used in an addressbook-query.
+"""
+
+__all__ = [
+ "Filter",
+]
+
+from twext.python.log import Logger
+
+from twistedcaldav.caldavxml import caldav_namespace, CalDAVTimeZoneElement
+from twistedcaldav.dateops import timeRangesOverlap
+from twistedcaldav.ical import Component, Property
+
+from pycalendar.datetime import DateTime
+from pycalendar.timezone import Timezone
+
+log = Logger()
+
+
+class FilterBase(object):
+ """
+ Determines which matching components are returned.
+ """
+
+ serialized_name = None
+ deserialize_names = {}
+
+ @classmethod
+ def serialize_register(cls, register):
+ cls.deserialize_names[register.serialized_name] = register
+
+
+ def __init__(self, xml_element):
+ pass
+
+
+ @classmethod
+ def deserialize(cls, data):
+ """
+ Convert a JSON compatible serialization of this object into the actual object.
+ """
+ obj = cls.deserialize_names[data["type"]](None)
+ obj._deserialize(data)
+ return obj
+
+
+ def _deserialize(self, data):
+ """
+ Convert a JSON compatible serialization of this object into the actual object.
+ """
+ pass
+
+
+ def serialize(self):
+ """
+ Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+ """
+ return {
+ "type": self.serialized_name,
+ }
+
+
+ def match(self, item, access=None):
+ raise NotImplementedError
+
+
+ def valid(self, level=0):
+ raise NotImplementedError
+
+
+
+class Filter(FilterBase):
+ """
+ Determines which matching components are returned.
+ """
+
+ serialized_name = "Filter"
+
+ def __init__(self, xml_element):
+
+ super(Filter, self).__init__(xml_element)
+ if xml_element is None:
+ return
+
+ # One comp-filter element must be present
+ if len(xml_element.children) != 1 or xml_element.children[0].qname() != (caldav_namespace, "comp-filter"):
+ raise ValueError("Invalid CALDAV:filter element: %s" % (xml_element,))
+
+ self.child = ComponentFilter(xml_element.children[0])
+
+
+ def _deserialize(self, data):
+ """
+ Convert a JSON compatible serialization of this object into the actual object.
+ """
+ self.child = FilterBase.deserialize(data["child"])
+
+
+ def serialize(self):
+ """
+ Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+ """
+ result = super(Filter, self).serialize()
+ result.update({
+ "child": self.child.serialize(),
+ })
+ return result
+
+
+ def match(self, component, access=None):
+ """
+ Returns True if the given calendar component matches this filter, False
+ otherwise.
+ """
+
+ # We only care about certain access restrictions.
+ if access not in (Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED):
+ access = None
+
+ # We need to prepare ourselves for a time-range query by pre-calculating
+ # the set of instances up to the latest time-range limit. That way we can
+ # avoid having to do some form of recurrence expansion for each query sub-part.
+ maxend, isStartTime = self.getmaxtimerange()
+ if maxend:
+ if isStartTime:
+ if component.isRecurringUnbounded():
+ # Unbounded recurrence is always within a start-only time-range
+ instances = None
+ else:
+ # Expand the instances up to infinity
+ instances = component.expandTimeRanges(DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)), ignoreInvalidInstances=True)
+ else:
+ instances = component.expandTimeRanges(maxend, ignoreInvalidInstances=True)
+ else:
+ instances = None
+ self.child.setInstances(instances)
+
+ # <filter> contains exactly one <comp-filter>
+ return self.child.match(component, access)
+
+
+ def valid(self):
+ """
+ Indicate whether this filter element's structure is valid wrt iCalendar
+ data object model.
+
+ @return: True if valid, False otherwise
+ """
+
+ # Must have one child element for VCALENDAR
+ return self.child.valid(0)
+
+
+ def settimezone(self, tzelement):
+ """
+ Set the default timezone to use with this query.
+ @param calendar: a L{Component} for the VCALENDAR containing the one
+ VTIMEZONE that we want
+ @return: the L{Timezone} derived from the VTIMEZONE or utc.
+ """
+
+ if tzelement is None:
+ tz = None
+ elif isinstance(tzelement, CalDAVTimeZoneElement):
+ tz = tzelement.gettimezone()
+ elif isinstance(tzelement, Component):
+ tz = tzelement.gettimezone()
+ if tz is None:
+ tz = Timezone(utc=True)
+ self.child.settzinfo(tz)
+ return tz
+
+
+ def getmaxtimerange(self):
+ """
+ Get the date farthest into the future in any time-range elements
+ """
+
+ return self.child.getmaxtimerange(None, False)
+
+
+ def getmintimerange(self):
+ """
+ Get the date farthest into the past in any time-range elements. That is either
+ the start date, or if start is not present, the end date.
+ """
+
+ return self.child.getmintimerange(None, False)
+
+FilterBase.serialize_register(Filter)
+
+
+
+class FilterChildBase(FilterBase):
+ """
+ CalDAV filter element.
+ """
+
+ def __init__(self, xml_element):
+
+ super(FilterChildBase, self).__init__(xml_element)
+ if xml_element is None:
+ return
+
+ qualifier = None
+ filters = []
+
+ for child in xml_element.children:
+ qname = child.qname()
+
+ if qname in (
+ (caldav_namespace, "is-not-defined"),
+ (caldav_namespace, "time-range"),
+ (caldav_namespace, "text-match"),
+ ):
+ if qualifier is not None:
+ raise ValueError("Only one of CalDAV:time-range, CalDAV:text-match allowed")
+
+ if qname == (caldav_namespace, "is-not-defined"):
+ qualifier = IsNotDefined(child)
+ elif qname == (caldav_namespace, "time-range"):
+ qualifier = TimeRange(child)
+ elif qname == (caldav_namespace, "text-match"):
+ qualifier = TextMatch(child)
+
+ elif qname == (caldav_namespace, "comp-filter"):
+ filters.append(ComponentFilter(child))
+ elif qname == (caldav_namespace, "prop-filter"):
+ filters.append(PropertyFilter(child))
+ elif qname == (caldav_namespace, "param-filter"):
+ filters.append(ParameterFilter(child))
+ else:
+ raise ValueError("Unknown child element: %s" % (qname,))
+
+ if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
+ raise ValueError("No other tests allowed when CalDAV:is-not-defined is present")
+
+ self.qualifier = qualifier
+ self.filters = filters
+ self.filter_name = xml_element.attributes["name"]
+ if isinstance(self.filter_name, unicode):
+ 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 _deserialize(self, data):
+ """
+ Convert a JSON compatible serialization of this object into the actual object.
+ """
+ self.qualifier = FilterBase.deserialize(data["qualifier"]) if data["qualifier"] else None
+ self.filters = [FilterBase.deserialize(filter) for filter in data["filters"]]
+ self.filter_name = data["filter_name"]
+ self.defined = data["defined"]
+ self.filter_test = data["filter_test"]
+
+
+ def serialize(self):
+ """
+ Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+ """
+ result = super(FilterChildBase, self).serialize()
+ result.update({
+ "qualifier": self.qualifier.serialize() if self.qualifier else None,
+ "filters": [filter.serialize() for filter in self.filters],
+ "filter_name": self.filter_name,
+ "defined": self.defined,
+ "filter_test": self.filter_test,
+ })
+ return result
+
+
+ def match(self, item, access=None):
+ """
+ Returns True if the given calendar item (either a component, property or parameter value)
+ matches this filter, False otherwise.
+ """
+
+ # Always return True for the is-not-defined case as the result of this will
+ # be negated by the caller
+ if not self.defined:
+ return True
+
+ 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 allof != filter._match(item, access):
+ return not allof
+ return allof
+ else:
+ return True
+
+
+
+class ComponentFilter (FilterChildBase):
+ """
+ Limits a search to only the chosen component types.
+ """
+
+ serialized_name = "ComponentFilter"
+
+ def match(self, item, access):
+ """
+ Returns True if the given calendar item (which is a component)
+ matches this filter, False otherwise.
+ This specialization uses the instance matching option of the time-range filter
+ to minimize instance expansion.
+ """
+
+ # Always return True for the is-not-defined case as the result of this will
+ # be negated by the caller
+ if not self.defined:
+ return True
+
+ 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 allof != filter._match(item, access):
+ return not allof
+ return allof
+ else:
+ return True
+
+
+ def _match(self, component, access):
+ # At least one subcomponent must match (or is-not-defined is set)
+ for subcomponent in component.subcomponents():
+ # If access restrictions are in force, restrict matching to specific components only.
+ # In particular do not match VALARM.
+ if access and subcomponent.name() not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE",):
+ continue
+
+ # Try to match the component name
+ if isinstance(self.filter_name, str):
+ if subcomponent.name() != self.filter_name:
+ continue
+ else:
+ if subcomponent.name() not in self.filter_name:
+ continue
+ if self.match(subcomponent, access):
+ break
+ else:
+ return not self.defined
+ return self.defined
+
+
+ def setInstances(self, instances):
+ """
+ Give the list of instances to each comp-filter element.
+ @param instances: the list of instances.
+ """
+ self.instances = instances
+ for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
+ compfilter.setInstances(instances)
+
+
+ def valid(self, level):
+ """
+ Indicate whether this filter element's structure is valid wrt iCalendar
+ data object model.
+
+ @param level: the nesting level of this filter element, 0 being the top comp-filter.
+ @return: True if valid, False otherwise
+ """
+
+ # Check for time-range
+ timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
+
+ if level == 0:
+ # Must have VCALENDAR at the top
+ if (self.filter_name != "VCALENDAR") or timerange:
+ log.info("Top-level comp-filter must be VCALENDAR, instead: %s" % (self.filter_name,))
+ return False
+ elif level == 1:
+ # Disallow VCALENDAR, VALARM, STANDARD, DAYLIGHT, AVAILABLE at the top, everything else is OK
+ if self.filter_name in ("VCALENDAR", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE"):
+ log.info("comp-filter wrong component type: %s" % (self.filter_name,))
+ return False
+
+ # time-range only on VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY
+ if timerange and self.filter_name not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY"):
+ log.info("time-range cannot be used with component %s" % (self.filter_name,))
+ return False
+ elif level == 2:
+ # Disallow VCALENDAR, VTIMEZONE, VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY at the top, everything else is OK
+ if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY")):
+ log.info("comp-filter wrong sub-component type: %s" % (self.filter_name,))
+ return False
+
+ # time-range only on VALARM, AVAILABLE
+ if timerange and self.filter_name not in ("VALARM", "AVAILABLE",):
+ log.info("time-range cannot be used with sub-component %s" % (self.filter_name,))
+ return False
+ else:
+ # Disallow all standard iCal components anywhere else
+ if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE")) or timerange:
+ log.info("comp-filter wrong standard component type: %s" % (self.filter_name,))
+ return False
+
+ # Test each property
+ for propfilter in [x for x in self.filters if isinstance(x, PropertyFilter)]:
+ if not propfilter.valid():
+ return False
+
+ # Test each component
+ for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
+ if not compfilter.valid(level + 1):
+ return False
+
+ # Test the time-range
+ if timerange:
+ if not self.qualifier.valid():
+ return False
+
+ return True
+
+
+ def settzinfo(self, tzinfo):
+ """
+ Set the default timezone to use with this query.
+ @param tzinfo: a L{Timezone} to use.
+ """
+
+ # Give tzinfo to any TimeRange we have
+ if isinstance(self.qualifier, TimeRange):
+ self.qualifier.settzinfo(tzinfo)
+
+ # Pass down to sub components/properties
+ for x in self.filters:
+ x.settzinfo(tzinfo)
+
+
+ def getmaxtimerange(self, currentMaximum, currentIsStartTime):
+ """
+ Get the date farthest into the future in any time-range elements
+
+ @param currentMaximum: current future value to compare with
+ @type currentMaximum: L{DateTime}
+ """
+
+ # Give tzinfo to any TimeRange we have
+ isStartTime = False
+ if isinstance(self.qualifier, TimeRange):
+ isStartTime = self.qualifier.end is None
+ compareWith = self.qualifier.start if isStartTime else self.qualifier.end
+ if currentMaximum is None or currentMaximum < compareWith:
+ currentMaximum = compareWith
+ currentIsStartTime = isStartTime
+
+ # Pass down to sub components/properties
+ for x in self.filters:
+ currentMaximum, currentIsStartTime = x.getmaxtimerange(currentMaximum, currentIsStartTime)
+
+ return currentMaximum, currentIsStartTime
+
+
+ def getmintimerange(self, currentMinimum, currentIsEndTime):
+ """
+ Get the date farthest into the past in any time-range elements. That is either
+ the start date, or if start is not present, the end date.
+ """
+
+ # Give tzinfo to any TimeRange we have
+ isEndTime = False
+ if isinstance(self.qualifier, TimeRange):
+ isEndTime = self.qualifier.start is None
+ compareWith = self.qualifier.end if isEndTime else self.qualifier.start
+ if currentMinimum is None or currentMinimum > compareWith:
+ currentMinimum = compareWith
+ currentIsEndTime = isEndTime
+
+ # Pass down to sub components/properties
+ for x in self.filters:
+ currentMinimum, currentIsEndTime = x.getmintimerange(currentMinimum, currentIsEndTime)
+
+ return currentMinimum, currentIsEndTime
+
+FilterBase.serialize_register(ComponentFilter)
+
+
+
+class PropertyFilter (FilterChildBase):
+ """
+ Limits a search to specific properties.
+ """
+
+ serialized_name = "PropertyFilter"
+
+ def _match(self, component, access):
+ # When access restriction is in force, we need to only allow matches against the properties
+ # allowed by the access restriction level.
+ if access:
+ allowedProperties = Component.confidentialPropertiesMap.get(component.name(), None)
+ if allowedProperties and access == Component.ACCESS_RESTRICTED:
+ allowedProperties += Component.extraRestrictedProperties
+ else:
+ allowedProperties = None
+
+ # At least one property must match (or is-not-defined is set)
+ for property in component.properties():
+ # Apply access restrictions, if any.
+ if allowedProperties is not None and property.name().upper() not in allowedProperties:
+ continue
+ if property.name().upper() == self.filter_name.upper() and self.match(property, access):
+ break
+ else:
+ return not self.defined
+ return self.defined
+
+
+ def valid(self):
+ """
+ Indicate whether this filter element's structure is valid wrt iCalendar
+ data object model.
+
+ @return: True if valid, False otherwise
+ """
+
+ # Check for time-range
+ timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
+
+ # time-range only on COMPLETED, CREATED, DTSTAMP, LAST-MODIFIED
+ if timerange and self.filter_name.upper() not in ("COMPLETED", "CREATED", "DTSTAMP", "LAST-MODIFIED"):
+ log.info("time-range cannot be used with property %s" % (self.filter_name,))
+ return False
+
+ # Test the time-range
+ if timerange:
+ if not self.qualifier.valid():
+ return False
+
+ # No other tests
+ return True
+
+
+ def settzinfo(self, tzinfo):
+ """
+ Set the default timezone to use with this query.
+ @param tzinfo: a L{Timezone} to use.
+ """
+
+ # Give tzinfo to any TimeRange we have
+ if isinstance(self.qualifier, TimeRange):
+ self.qualifier.settzinfo(tzinfo)
+
+
+ def getmaxtimerange(self, currentMaximum, currentIsStartTime):
+ """
+ Get the date farthest into the future in any time-range elements
+
+ @param currentMaximum: current future value to compare with
+ @type currentMaximum: L{DateTime}
+ """
+
+ # Give tzinfo to any TimeRange we have
+ isStartTime = False
+ if isinstance(self.qualifier, TimeRange):
+ isStartTime = self.qualifier.end is None
+ compareWith = self.qualifier.start if isStartTime else self.qualifier.end
+ if currentMaximum is None or currentMaximum < compareWith:
+ currentMaximum = compareWith
+ currentIsStartTime = isStartTime
+
+ return currentMaximum, currentIsStartTime
+
+
+ def getmintimerange(self, currentMinimum, currentIsEndTime):
+ """
+ Get the date farthest into the past in any time-range elements. That is either
+ the start date, or if start is not present, the end date.
+ """
+
+ # Give tzinfo to any TimeRange we have
+ isEndTime = False
+ if isinstance(self.qualifier, TimeRange):
+ isEndTime = self.qualifier.start is None
+ compareWith = self.qualifier.end if isEndTime else self.qualifier.start
+ if currentMinimum is None or currentMinimum > compareWith:
+ currentMinimum = compareWith
+ currentIsEndTime = isEndTime
+
+ return currentMinimum, currentIsEndTime
+
+FilterBase.serialize_register(PropertyFilter)
+
+
+
+class ParameterFilter (FilterChildBase):
+ """
+ Limits a search to specific parameters.
+ """
+
+ serialized_name = "ParameterFilter"
+
+ def _match(self, property, access):
+
+ # At least one parameter must match (or is-not-defined is set)
+ result = not self.defined
+ for parameterName in property.parameterNames():
+ if parameterName.upper() == self.filter_name.upper() and self.match([property.parameterValue(parameterName)], access):
+ result = self.defined
+ break
+
+ return result
+
+FilterBase.serialize_register(ParameterFilter)
+
+
+
+class IsNotDefined (FilterBase):
+ """
+ Specifies that the named iCalendar item does not exist.
+ """
+
+ serialized_name = "IsNotDefined"
+
+ def match(self, component, access=None):
+ # Oddly, this needs always to return True so that it appears there is
+ # a match - but we then "negate" the result if is-not-defined is set.
+ # Actually this method should never be called as we special case the
+ # is-not-defined option.
+ return True
+
+FilterBase.serialize_register(IsNotDefined)
+
+
+class TextMatch (FilterBase):
+ """
+ Specifies a substring match on a property or parameter value.
+ (CalDAV-access-09, section 9.6.4)
+ """
+ serialized_name = "TextMatch"
+
+ def __init__(self, xml_element):
+
+ super(TextMatch, self).__init__(xml_element)
+ if xml_element is None:
+ return
+
+ self.text = str(xml_element)
+ if "caseless" in xml_element.attributes:
+ caseless = xml_element.attributes["caseless"]
+ if caseless == "yes":
+ self.caseless = True
+ elif caseless == "no":
+ self.caseless = False
+ else:
+ self.caseless = True
+
+ if "negate-condition" in xml_element.attributes:
+ negate = xml_element.attributes["negate-condition"]
+ if negate == "yes":
+ self.negate = True
+ elif negate == "no":
+ self.negate = False
+ 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 _deserialize(self, data):
+ """
+ Convert a JSON compatible serialization of this object into the actual object.
+ """
+ self.text = data["text"]
+ self.caseless = data["caseless"]
+ self.negate = data["negate"]
+ self.match_type = data["match_type"]
+
+
+ def serialize(self):
+ """
+ Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+ """
+ result = super(TextMatch, self).serialize()
+ result.update({
+ "text": self.text,
+ "caseless": self.caseless,
+ "negate": self.negate,
+ "match_type": self.match_type,
+ })
+ return result
+
+
+ def match(self, item, access):
+ """
+ Match the text for the item.
+ If the item is a property, then match the property value,
+ otherwise it may be a list of parameter values - try to match anyone of those
+ """
+ if item is None:
+ return False
+
+ if isinstance(item, Property):
+ values = [item.strvalue()]
+ else:
+ values = item
+
+ test = unicode(self.text, "utf-8")
+ if self.caseless:
+ test = test.lower()
+
+ def _textCompare(s):
+ if self.caseless:
+ 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:
+ 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:
+ if _textCompare(unicode(subvalue, "utf-8")):
+ return not self.negate
+ else:
+ if _textCompare(unicode(value, "utf-8")):
+ return not self.negate
+
+ return self.negate
+
+FilterBase.serialize_register(TextMatch)
+
+
+
+class TimeRange (FilterBase):
+ """
+ Specifies a time for testing components against.
+ """
+
+ serialized_name = "TimeRange"
+
+ def __init__(self, xml_element):
+
+ super(TimeRange, self).__init__(xml_element)
+ if xml_element is None:
+ return
+
+ # One of start or end must be present
+ if "start" not in xml_element.attributes and "end" not in xml_element.attributes:
+ raise ValueError("One of 'start' or 'end' must be present in CALDAV:time-range")
+
+ self.start = DateTime.parseText(xml_element.attributes["start"]) if "start" in xml_element.attributes else None
+ self.end = DateTime.parseText(xml_element.attributes["end"]) if "end" in xml_element.attributes else None
+ self.tzinfo = None
+
+
+ def _deserialize(self, data):
+ """
+ Convert a JSON compatible serialization of this object into the actual object.
+ """
+ self.start = DateTime.parseText(data["start"]) if data["start"] else None
+ self.end = DateTime.parseText(data["end"]) if data["end"] else None
+ self.tzinfo = Timezone(tzid=data["tzinfo"]) if data["tzinfo"] else None
+
+
+ def serialize(self):
+ """
+ Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+ """
+ result = super(TimeRange, self).serialize()
+ result.update({
+ "start": self.start.getText() if self.start else None,
+ "end": self.end.getText() if self.end else None,
+ "tzinfo": self.tzinfo.getTimezoneID() if self.tzinfo else None,
+ })
+ return result
+
+
+ def settzinfo(self, tzinfo):
+ """
+ Set the default timezone to use with this query.
+ @param tzinfo: a L{Timezone} to use.
+ """
+
+ # Give tzinfo to any TimeRange we have
+ self.tzinfo = tzinfo
+
+
+ def valid(self, level=0):
+ """
+ Indicate whether the time-range is valid (must be date-time in UTC).
+
+ @return: True if valid, False otherwise
+ """
+
+ if self.start is not None and self.start.isDateOnly():
+ log.info("start attribute in <time-range> is not a date-time: %s" % (self.start,))
+ return False
+ if self.end is not None and self.end.isDateOnly():
+ log.info("end attribute in <time-range> is not a date-time: %s" % (self.end,))
+ return False
+ if self.start is not None and not self.start.utc():
+ log.info("start attribute in <time-range> is not UTC: %s" % (self.start,))
+ return False
+ if self.end is not None and not self.end.utc():
+ log.info("end attribute in <time-range> is not UTC: %s" % (self.end,))
+ return False
+
+ # No other tests
+ return True
+
+
+ def match(self, property, access=None):
+ """
+ NB This is only called when doing a time-range match on a property.
+ """
+ if property is None:
+ return False
+ else:
+ return property.containsTimeRange(self.start, self.end, self.tzinfo)
+
+
+ def matchinstance(self, component, instances):
+ """
+ Test whether this time-range element causes a match to the specified component
+ using the specified set of instances to determine the expanded time ranges.
+ @param component: the L{Component} to test.
+ @param instances: the list of expanded instances.
+ @return: True if the time-range query matches, False otherwise.
+ """
+ if component is None:
+ return False
+
+ assert instances is not None or self.end is None, "Failure to expand instance for time-range filter: %r" % (self,)
+
+ # Special case open-ended unbounded
+ if instances is None:
+ if component.getRecurrenceIDUTC() is None:
+ return True
+ else:
+ # See if the overridden component's start is past the start
+ start, _ignore_end = component.getEffectiveStartEnd()
+ if start is None:
+ return True
+ else:
+ return start >= self.start
+
+ # Handle alarms as a special case
+ alarms = (component.name() == "VALARM")
+ if alarms:
+ testcomponent = component._parent
+ else:
+ testcomponent = component
+
+ for key in instances:
+ instance = instances[key]
+
+ # First make sure components match
+ if not testcomponent.same(instance.component):
+ continue
+
+ if alarms:
+ # Get all the alarm triggers for this instance and test each one
+ triggers = instance.getAlarmTriggers()
+ for trigger in triggers:
+ if timeRangesOverlap(trigger, None, self.start, self.end, self.tzinfo):
+ return True
+ else:
+ # Regular instance overlap test
+ if timeRangesOverlap(instance.start, instance.end, self.start, self.end, self.tzinfo):
+ return True
+
+ return False
+
+FilterBase.serialize_register(TimeRange)
Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/generator.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/generator.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/generator.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,207 +0,0 @@
-##
-# Copyright (c) 2006-2013 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 twext.enterprise.dal.syntax import Select
-
-from txdav.common.datastore.query import expression
-from txdav.common.datastore.query.generator import SQLQueryGenerator
-from txdav.common.datastore.sql_tables import schema
-
-"""
-SQL statement generator from query expressions.
-"""
-
-__all__ = [
- "CalDAVSQLQueryGenerator",
-]
-
-class CalDAVSQLQueryGenerator(SQLQueryGenerator):
-
- _timerange = schema.TIME_RANGE
- _transparency = schema.TRANSPARENCY
-
- def __init__(self, expr, collection, whereid, userid=None, freebusy=False):
- """
-
- @param expr: the query expression object model
- @type expr: L{expression}
- @param collection: the resource targeted by the query
- @type collection: L{CommonHomeChild}
- @param userid: user for whom query is being done - query will be scoped to that user's privileges and their transparency
- @type userid: C{str}
- @param freebusy: whether or not a freebusy query is being done - if it is, additional time range and transparency information is returned
- @type freebusy: C{bool}
- """
- super(CalDAVSQLQueryGenerator, self).__init__(expr, collection, whereid)
- self.userid = userid if userid else ""
- self.freebusy = freebusy
- self.usedtimerange = False
-
-
- def generate(self):
- """
- Generate the actual SQL statement from the passed in expression tree.
-
- @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
- and the C{list} is the list of argument substitutions to use with the SQL API execute method.
- """
-
- # Init state
- self.arguments = {}
- self.argcount = 0
- obj = self.collection._objectSchema
-
- columns = [obj.RESOURCE_NAME, obj.ICALENDAR_UID, obj.ICALENDAR_TYPE]
- if self.freebusy:
- columns.extend([
- obj.ORGANIZER,
- self._timerange.FLOATING,
- self._timerange.START_DATE,
- self._timerange.END_DATE,
- self._timerange.FBTYPE,
- self._timerange.TRANSPARENT,
- self._transparency.TRANSPARENT,
- ])
-
- # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
- if self.whereid:
-
- test = expression.isExpression(obj.CALENDAR_RESOURCE_ID, self.whereid, 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:
- # timerange expression forces a join on calendarid
- pass
- else:
- # AND the whole thing with calendarid
- 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 use the id test
- elif isinstance(self.expression, expression.allExpression):
- self.expression = test
-
- # Just AND the entire thing
- else:
- self.expression = test.andWith(self.expression)
-
- # Generate ' where ...' partial statement
- where = self.generateExpression(self.expression)
-
- if self.usedtimerange:
- where = where.And(self._timerange.CALENDAR_OBJECT_RESOURCE_ID == obj.RESOURCE_ID).And(self._timerange.CALENDAR_RESOURCE_ID == self.whereid)
-
- # Set of tables depends on use of timespan and fb use
- if self.usedtimerange:
- if self.freebusy:
- tables = obj.join(
- self._timerange.join(
- self._transparency,
- on=(self._timerange.INSTANCE_ID == self._transparency.TIME_RANGE_INSTANCE_ID).And(self._transparency.USER_ID == self.userid),
- type="left outer"
- ),
- type=","
- )
- else:
- tables = obj.join(self._timerange, type=",")
- else:
- tables = obj
-
- select = Select(
- columns,
- From=tables,
- Where=where,
- Distinct=True,
- )
-
- return select, self.arguments, self.usedtimerange
-
-
- def generateExpression(self, expr):
- """
- Generate an expression and all it's subexpressions.
-
- @param expr: the L{baseExpression} derived class to write out.
- """
-
- # Generate based on each type of expression we might encounter
- partial = None
-
- # time-range
- if isinstance(expr, expression.timerangeExpression):
- if expr.start and expr.end:
- partial = (
- (self._timerange.FLOATING == False).And(self._timerange.START_DATE < expr.end).And(self._timerange.END_DATE > expr.start)
- ).Or(
- (self._timerange.FLOATING == True).And(self._timerange.START_DATE < expr.endfloat).And(self._timerange.END_DATE > expr.startfloat)
- )
- elif expr.start and expr.end is None:
- partial = (
- (self._timerange.FLOATING == False).And(self._timerange.END_DATE > expr.start)
- ).Or(
- (self._timerange.FLOATING == True).And(self._timerange.END_DATE > expr.startfloat)
- )
- elif not expr.start and expr.end:
- partial = (
- (self._timerange.FLOATING == False).And(self._timerange.START_DATE < expr.end)
- ).Or(
- (self._timerange.FLOATING == True).And(self._timerange.START_DATE < expr.endfloat)
- )
- self.usedtimerange = True
-
- else:
- partial = super(CalDAVSQLQueryGenerator, self).generateExpression(expr)
-
- return partial
-
-
- def addArgument(self, arg):
- """
-
- @param arg: the C{str} of the argument to add
- """
-
- # Append argument to the list and add the appropriate substitution string to the output stream.
- self.argcount += 1
- argname = "arg{}".format(self.argcount)
- self.arguments[argname] = arg
- return argname
Copied: CalendarServer/trunk/txdav/caldav/datastore/query/generator.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/generator.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/generator.py (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/generator.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,207 @@
+##
+# Copyright (c) 2006-2013 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 twext.enterprise.dal.syntax import Select
+
+from txdav.common.datastore.query import expression
+from txdav.common.datastore.query.generator import SQLQueryGenerator
+from txdav.common.datastore.sql_tables import schema
+
+"""
+SQL statement generator from query expressions.
+"""
+
+__all__ = [
+ "CalDAVSQLQueryGenerator",
+]
+
+class CalDAVSQLQueryGenerator(SQLQueryGenerator):
+
+ _timerange = schema.TIME_RANGE
+ _transparency = schema.TRANSPARENCY
+
+ def __init__(self, expr, collection, whereid, userid=None, freebusy=False):
+ """
+
+ @param expr: the query expression object model
+ @type expr: L{expression}
+ @param collection: the resource targeted by the query
+ @type collection: L{CommonHomeChild}
+ @param userid: user for whom query is being done - query will be scoped to that user's privileges and their transparency
+ @type userid: C{str}
+ @param freebusy: whether or not a freebusy query is being done - if it is, additional time range and transparency information is returned
+ @type freebusy: C{bool}
+ """
+ super(CalDAVSQLQueryGenerator, self).__init__(expr, collection, whereid)
+ self.userid = userid if userid else ""
+ self.freebusy = freebusy
+ self.usedtimerange = False
+
+
+ def generate(self):
+ """
+ Generate the actual SQL statement from the passed in expression tree.
+
+ @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+ and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+ """
+
+ # Init state
+ self.arguments = {}
+ self.argcount = 0
+ obj = self.collection._objectSchema
+
+ columns = [obj.RESOURCE_NAME, obj.ICALENDAR_UID, obj.ICALENDAR_TYPE]
+ if self.freebusy:
+ columns.extend([
+ obj.ORGANIZER,
+ self._timerange.FLOATING,
+ self._timerange.START_DATE,
+ self._timerange.END_DATE,
+ self._timerange.FBTYPE,
+ self._timerange.TRANSPARENT,
+ self._transparency.TRANSPARENT,
+ ])
+
+ # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
+ if self.whereid:
+
+ test = expression.isExpression(obj.CALENDAR_RESOURCE_ID, self.whereid, 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:
+ # timerange expression forces a join on calendarid
+ pass
+ else:
+ # AND the whole thing with calendarid
+ 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 use the id test
+ elif isinstance(self.expression, expression.allExpression):
+ self.expression = test
+
+ # Just AND the entire thing
+ else:
+ self.expression = test.andWith(self.expression)
+
+ # Generate ' where ...' partial statement
+ where = self.generateExpression(self.expression)
+
+ if self.usedtimerange:
+ where = where.And(self._timerange.CALENDAR_OBJECT_RESOURCE_ID == obj.RESOURCE_ID).And(self._timerange.CALENDAR_RESOURCE_ID == self.whereid)
+
+ # Set of tables depends on use of timespan and fb use
+ if self.usedtimerange:
+ if self.freebusy:
+ tables = obj.join(
+ self._timerange.join(
+ self._transparency,
+ on=(self._timerange.INSTANCE_ID == self._transparency.TIME_RANGE_INSTANCE_ID).And(self._transparency.USER_ID == self.userid),
+ type="left outer"
+ ),
+ type=","
+ )
+ else:
+ tables = obj.join(self._timerange, type=",")
+ else:
+ tables = obj
+
+ select = Select(
+ columns,
+ From=tables,
+ Where=where,
+ Distinct=True,
+ )
+
+ return select, self.arguments, self.usedtimerange
+
+
+ def generateExpression(self, expr):
+ """
+ Generate an expression and all it's subexpressions.
+
+ @param expr: the L{baseExpression} derived class to write out.
+ """
+
+ # Generate based on each type of expression we might encounter
+ partial = None
+
+ # time-range
+ if isinstance(expr, expression.timerangeExpression):
+ if expr.start and expr.end:
+ partial = (
+ (self._timerange.FLOATING == False).And(self._timerange.START_DATE < expr.end).And(self._timerange.END_DATE > expr.start)
+ ).Or(
+ (self._timerange.FLOATING == True).And(self._timerange.START_DATE < expr.endfloat).And(self._timerange.END_DATE > expr.startfloat)
+ )
+ elif expr.start and expr.end is None:
+ partial = (
+ (self._timerange.FLOATING == False).And(self._timerange.END_DATE > expr.start)
+ ).Or(
+ (self._timerange.FLOATING == True).And(self._timerange.END_DATE > expr.startfloat)
+ )
+ elif not expr.start and expr.end:
+ partial = (
+ (self._timerange.FLOATING == False).And(self._timerange.START_DATE < expr.end)
+ ).Or(
+ (self._timerange.FLOATING == True).And(self._timerange.START_DATE < expr.endfloat)
+ )
+ self.usedtimerange = True
+
+ else:
+ partial = super(CalDAVSQLQueryGenerator, self).generateExpression(expr)
+
+ return partial
+
+
+ def addArgument(self, arg):
+ """
+
+ @param arg: the C{str} of the argument to add
+ """
+
+ # Append argument to the list and add the appropriate substitution string to the output stream.
+ self.argcount += 1
+ argname = "arg{}".format(self.argcount)
+ self.arguments[argname] = arg
+ return argname
Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/test/__init__.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 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.
-##
Copied: CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/test/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 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.
+##
Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/test/test_filter.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,435 +0,0 @@
-##
-# Copyright (c) 2011-2013 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 pycalendar.timezone import Timezone
-
-from twext.enterprise.dal.syntax import SQLFragment, Parameter
-
-from twistedcaldav.test.util import TestCase
-from twistedcaldav import caldavxml
-from twistedcaldav.timezones import TimezoneCache
-
-from txdav.caldav.datastore.index_file import sqlcalendarquery
-from txdav.caldav.datastore.query.builder import buildExpression
-from txdav.caldav.datastore.query.filter import Filter, FilterBase, TimeRange, \
- PropertyFilter, TextMatch
-from txdav.caldav.datastore.query.generator import CalDAVSQLQueryGenerator
-from txdav.common.datastore.sql_tables import schema
-
-from dateutil.tz import tzutc
-import datetime
-from twistedcaldav.ical import Component
-
-class TestQueryFilter(TestCase):
-
- _objectSchema = schema.CALENDAR_OBJECT
- _queryFields = {
- "UID": _objectSchema.UID,
- "TYPE": _objectSchema.ICALENDAR_TYPE,
- }
-
- def setUp(self):
- super(TestQueryFilter, self).setUp()
- TimezoneCache.create()
-
-
- def test_query(self):
- """
- Basic query test - no time range
- """
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[caldavxml.ComponentFilter(
- **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
- )],
- **{"name": "VCALENDAR"}
- )
- )
- filter = Filter(filter)
- filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
- expression = buildExpression(filter, self._queryFields)
- sql = CalDAVSQLQueryGenerator(expression, self, 1234)
- select, args, usedtimerange = sql.generate()
-
- self.assertEqual(select.toSQL(), SQLFragment(
- "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_TYPE in (?, ?, ?)",
- [1234, Parameter('arg1', 3)]
- ))
- self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
- self.assertEqual(usedtimerange, False)
-
-
- def test_query_timerange(self):
- """
- Basic query test - with time range
- """
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[caldavxml.ComponentFilter(
- *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
- **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
- )],
- **{"name": "VCALENDAR"}
- )
- )
- filter = Filter(filter)
- filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
- expression = buildExpression(filter, self._queryFields)
- sql = CalDAVSQLQueryGenerator(expression, self, 1234)
- select, args, usedtimerange = sql.generate()
-
- self.assertEqual(select.toSQL(), SQLFragment(
- "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT, TIME_RANGE where ICALENDAR_TYPE in (?, ?, ?) and (FLOATING = ? and START_DATE < ? and END_DATE > ? or FLOATING = ? and START_DATE < ? and END_DATE > ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
- [Parameter('arg1', 3), False, datetime.datetime(2006, 6, 5, 17, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 13, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 1234]
- ))
- self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
- self.assertEqual(usedtimerange, True)
-
-
- def test_query_freebusy(self):
- """
- Basic query test - with time range
- """
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[caldavxml.ComponentFilter(
- *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
- **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
- )],
- **{"name": "VCALENDAR"}
- )
- )
- filter = Filter(filter)
- filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
- expression = buildExpression(filter, self._queryFields)
- sql = CalDAVSQLQueryGenerator(expression, self, 1234, "user01", True)
- select, args, usedtimerange = sql.generate()
-
- self.assertEqual(select.toSQL(), SQLFragment(
- "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE, ORGANIZER, FLOATING, START_DATE, END_DATE, FBTYPE, TIME_RANGE.TRANSPARENT, TRANSPARENCY.TRANSPARENT from CALENDAR_OBJECT, TIME_RANGE left outer join TRANSPARENCY on INSTANCE_ID = TIME_RANGE_INSTANCE_ID and USER_ID = ? where ICALENDAR_TYPE in (?, ?, ?) and (FLOATING = ? and START_DATE < ? and END_DATE > ? or FLOATING = ? and START_DATE < ? and END_DATE > ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
- ['user01', Parameter('arg1', 3), False, datetime.datetime(2006, 6, 5, 17, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 13, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 1234]
- ))
- self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
- self.assertEqual(usedtimerange, True)
-
-
- def test_query_not_extended(self):
- """
- Query test - two terms not anyof
- """
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[
- caldavxml.ComponentFilter(
- **{"name":("VEVENT")}
- ),
- caldavxml.ComponentFilter(
- **{"name":("VTODO")}
- ),
- ],
- **{"name": "VCALENDAR"}
- )
- )
- filter = Filter(filter)
- filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
- expression = buildExpression(filter, self._queryFields)
- sql = CalDAVSQLQueryGenerator(expression, self, 1234)
- select, args, usedtimerange = sql.generate()
-
- self.assertEqual(select.toSQL(), SQLFragment(
- "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_TYPE = ? and ICALENDAR_TYPE = ?",
- [1234, "VEVENT", "VTODO"]
- ))
- self.assertEqual(args, {})
- self.assertEqual(usedtimerange, False)
-
-
- def test_query_extended(self):
- """
- Extended query test - two terms with anyof
- """
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[
- caldavxml.ComponentFilter(
- *[caldavxml.TimeRange(**{"start":"20060605T160000Z", })],
- **{"name":("VEVENT")}
- ),
- caldavxml.ComponentFilter(
- **{"name":("VTODO")}
- ),
- ],
- **{"name": "VCALENDAR", "test": "anyof"}
- )
- )
- filter = Filter(filter)
- filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
- expression = buildExpression(filter, self._queryFields)
- sql = CalDAVSQLQueryGenerator(expression, self, 1234)
- select, args, usedtimerange = sql.generate()
-
- self.assertEqual(select.toSQL(), SQLFragment(
- "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT, TIME_RANGE where (ICALENDAR_TYPE = ? and (FLOATING = ? and END_DATE > ? or FLOATING = ? and END_DATE > ?) or ICALENDAR_TYPE = ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
- ['VEVENT', False, datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 'VTODO', 1234]
- ))
- self.assertEqual(args, {})
- self.assertEqual(usedtimerange, True)
-
-
- def test_sqllite_query(self):
- """
- Basic query test - single term.
- Only UID can be queried via sql.
- """
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[caldavxml.ComponentFilter(
- **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
- )],
- **{"name": "VCALENDAR"}
- )
- )
- filter = Filter(filter)
- sql, args = sqlcalendarquery(filter, 1234)
-
- self.assertTrue(sql.find("RESOURCE") != -1)
- self.assertTrue(sql.find("TIMESPAN") == -1)
- self.assertTrue(sql.find("TRANSPARENCY") == -1)
- self.assertTrue("VEVENT" in args)
-
-
-
-class TestQueryFilterSerialize(TestCase):
-
- def setUp(self):
- super(TestQueryFilterSerialize, self).setUp()
- TimezoneCache.create()
-
-
- def test_query(self):
- """
- Basic query test - no time range
- """
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[caldavxml.ComponentFilter(
- **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
- )],
- **{"name": "VCALENDAR"}
- )
- )
- filter = Filter(filter)
- filter.child.settzinfo(Timezone(tzid="America/New_York"))
- j = filter.serialize()
- self.assertEqual(j["type"], "Filter")
-
- f = FilterBase.deserialize(j)
- self.assertTrue(isinstance(f, Filter))
-
-
- def test_timerange_query(self):
- """
- Basic query test with time range
- """
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[caldavxml.ComponentFilter(
- *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
- **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
- )],
- **{"name": "VCALENDAR"}
- )
- )
- filter = Filter(filter)
- filter.child.settzinfo(Timezone(tzid="America/New_York"))
- j = filter.serialize()
- self.assertEqual(j["type"], "Filter")
-
- f = FilterBase.deserialize(j)
- self.assertTrue(isinstance(f, Filter))
- self.assertTrue(isinstance(f.child.filters[0].qualifier, TimeRange))
- self.assertTrue(isinstance(f.child.filters[0].qualifier.tzinfo, Timezone))
- self.assertEqual(f.child.filters[0].qualifier.tzinfo.getTimezoneID(), "America/New_York")
-
-
- def test_query_not_extended(self):
- """
- Basic query test with time range
- """
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[
- caldavxml.ComponentFilter(
- **{"name":("VEVENT")}
- ),
- caldavxml.ComponentFilter(
- **{"name":("VTODO")}
- ),
- ],
- **{"name": "VCALENDAR"}
- )
- )
- filter = Filter(filter)
- filter.child.settzinfo(Timezone(tzid="America/New_York"))
- j = filter.serialize()
- self.assertEqual(j["type"], "Filter")
-
- f = FilterBase.deserialize(j)
- self.assertTrue(isinstance(f, Filter))
- self.assertEqual(len(f.child.filters), 2)
-
-
- def test_query_extended(self):
- """
- Basic query test with time range
- """
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[
- caldavxml.ComponentFilter(
- *[caldavxml.TimeRange(**{"start":"20060605T160000Z", })],
- **{"name":("VEVENT")}
- ),
- caldavxml.ComponentFilter(
- **{"name":("VTODO")}
- ),
- ],
- **{"name": "VCALENDAR", "test": "anyof"}
- )
- )
- filter = Filter(filter)
- filter.child.settzinfo(Timezone(tzid="America/New_York"))
- j = filter.serialize()
- self.assertEqual(j["type"], "Filter")
-
- f = FilterBase.deserialize(j)
- self.assertTrue(isinstance(f, Filter))
- self.assertEqual(len(f.child.filters), 2)
- self.assertTrue(isinstance(f.child.filters[0].qualifier, TimeRange))
-
-
- def test_query_text(self):
- """
- Basic query test with time range
- """
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[
- caldavxml.ComponentFilter(
- caldavxml.PropertyFilter(
- caldavxml.TextMatch.fromString("1234", False),
- name="UID",
- ),
- **{"name":("VEVENT")}
- ),
- ],
- **{"name": "VCALENDAR", "test": "anyof"}
- )
- )
- filter = Filter(filter)
- filter.child.settzinfo(Timezone(tzid="America/New_York"))
- j = filter.serialize()
- self.assertEqual(j["type"], "Filter")
-
- f = FilterBase.deserialize(j)
- self.assertTrue(isinstance(f, Filter))
- self.assertTrue(isinstance(f.child.filters[0].filters[0], PropertyFilter))
- self.assertTrue(isinstance(f.child.filters[0].filters[0].qualifier, TextMatch))
- self.assertEqual(f.child.filters[0].filters[0].qualifier.text, "1234")
-
-
-
-class TestQueryFilterMatch(TestCase):
-
- def setUp(self):
- super(TestQueryFilterMatch, self).setUp()
- TimezoneCache.create()
-
-
- def test_vlarm_undefined(self):
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[caldavxml.ComponentFilter(
- *[caldavxml.ComponentFilter(
- caldavxml.IsNotDefined(),
- **{"name":"VALARM"}
- )],
- **{"name":"VEVENT"}
- )],
- **{"name": "VCALENDAR"}
- )
- )
- filter = Filter(filter)
- filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
- self.assertFalse(filter.match(
- Component.fromString("""BEGIN:VCALENDAR
-CALSCALE:GREGORIAN
-PRODID:-//Example Inc.//Example Calendar//EN
-VERSION:2.0
-BEGIN:VTIMEZONE
-LAST-MODIFIED:20040110T032845Z
-TZID:US/Eastern
-BEGIN:DAYLIGHT
-DTSTART:20000404T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20001026T020000
-RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-DTSTAMP:20051222T210412Z
-CREATED:20060102T150000Z
-DTSTART;TZID=US/Eastern:20130102T100000
-DURATION:PT1H
-RRULE:FREQ=DAILY;COUNT=5
-SUMMARY:event 5
-UID:945113826375CBB89184DC36 at ninevah.local
-CATEGORIES:cool,hot
-CATEGORIES:warm
-BEGIN:VALARM
-ACTION:AUDIO
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-"""
- )))
Copied: CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/test/test_filter.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,435 @@
+##
+# Copyright (c) 2011-2013 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 pycalendar.timezone import Timezone
+
+from twext.enterprise.dal.syntax import SQLFragment, Parameter
+
+from twistedcaldav.test.util import TestCase
+from twistedcaldav import caldavxml
+from twistedcaldav.timezones import TimezoneCache
+
+from txdav.caldav.datastore.index_file import sqlcalendarquery
+from txdav.caldav.datastore.query.builder import buildExpression
+from txdav.caldav.datastore.query.filter import Filter, FilterBase, TimeRange, \
+ PropertyFilter, TextMatch
+from txdav.caldav.datastore.query.generator import CalDAVSQLQueryGenerator
+from txdav.common.datastore.sql_tables import schema
+
+from dateutil.tz import tzutc
+import datetime
+from twistedcaldav.ical import Component
+
+class TestQueryFilter(TestCase):
+
+ _objectSchema = schema.CALENDAR_OBJECT
+ _queryFields = {
+ "UID": _objectSchema.UID,
+ "TYPE": _objectSchema.ICALENDAR_TYPE,
+ }
+
+ def setUp(self):
+ super(TestQueryFilter, self).setUp()
+ TimezoneCache.create()
+
+
+ def test_query(self):
+ """
+ Basic query test - no time range
+ """
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[caldavxml.ComponentFilter(
+ **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+ )],
+ **{"name": "VCALENDAR"}
+ )
+ )
+ filter = Filter(filter)
+ filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+ expression = buildExpression(filter, self._queryFields)
+ sql = CalDAVSQLQueryGenerator(expression, self, 1234)
+ select, args, usedtimerange = sql.generate()
+
+ self.assertEqual(select.toSQL(), SQLFragment(
+ "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_TYPE in (?, ?, ?)",
+ [1234, Parameter('arg1', 3)]
+ ))
+ self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
+ self.assertEqual(usedtimerange, False)
+
+
+ def test_query_timerange(self):
+ """
+ Basic query test - with time range
+ """
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[caldavxml.ComponentFilter(
+ *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
+ **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+ )],
+ **{"name": "VCALENDAR"}
+ )
+ )
+ filter = Filter(filter)
+ filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+ expression = buildExpression(filter, self._queryFields)
+ sql = CalDAVSQLQueryGenerator(expression, self, 1234)
+ select, args, usedtimerange = sql.generate()
+
+ self.assertEqual(select.toSQL(), SQLFragment(
+ "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT, TIME_RANGE where ICALENDAR_TYPE in (?, ?, ?) and (FLOATING = ? and START_DATE < ? and END_DATE > ? or FLOATING = ? and START_DATE < ? and END_DATE > ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
+ [Parameter('arg1', 3), False, datetime.datetime(2006, 6, 5, 17, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 13, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 1234]
+ ))
+ self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
+ self.assertEqual(usedtimerange, True)
+
+
+ def test_query_freebusy(self):
+ """
+ Basic query test - with time range
+ """
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[caldavxml.ComponentFilter(
+ *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
+ **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+ )],
+ **{"name": "VCALENDAR"}
+ )
+ )
+ filter = Filter(filter)
+ filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+ expression = buildExpression(filter, self._queryFields)
+ sql = CalDAVSQLQueryGenerator(expression, self, 1234, "user01", True)
+ select, args, usedtimerange = sql.generate()
+
+ self.assertEqual(select.toSQL(), SQLFragment(
+ "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE, ORGANIZER, FLOATING, START_DATE, END_DATE, FBTYPE, TIME_RANGE.TRANSPARENT, TRANSPARENCY.TRANSPARENT from CALENDAR_OBJECT, TIME_RANGE left outer join TRANSPARENCY on INSTANCE_ID = TIME_RANGE_INSTANCE_ID and USER_ID = ? where ICALENDAR_TYPE in (?, ?, ?) and (FLOATING = ? and START_DATE < ? and END_DATE > ? or FLOATING = ? and START_DATE < ? and END_DATE > ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
+ ['user01', Parameter('arg1', 3), False, datetime.datetime(2006, 6, 5, 17, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 13, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 1234]
+ ))
+ self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
+ self.assertEqual(usedtimerange, True)
+
+
+ def test_query_not_extended(self):
+ """
+ Query test - two terms not anyof
+ """
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[
+ caldavxml.ComponentFilter(
+ **{"name":("VEVENT")}
+ ),
+ caldavxml.ComponentFilter(
+ **{"name":("VTODO")}
+ ),
+ ],
+ **{"name": "VCALENDAR"}
+ )
+ )
+ filter = Filter(filter)
+ filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+ expression = buildExpression(filter, self._queryFields)
+ sql = CalDAVSQLQueryGenerator(expression, self, 1234)
+ select, args, usedtimerange = sql.generate()
+
+ self.assertEqual(select.toSQL(), SQLFragment(
+ "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_TYPE = ? and ICALENDAR_TYPE = ?",
+ [1234, "VEVENT", "VTODO"]
+ ))
+ self.assertEqual(args, {})
+ self.assertEqual(usedtimerange, False)
+
+
+ def test_query_extended(self):
+ """
+ Extended query test - two terms with anyof
+ """
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[
+ caldavxml.ComponentFilter(
+ *[caldavxml.TimeRange(**{"start":"20060605T160000Z", })],
+ **{"name":("VEVENT")}
+ ),
+ caldavxml.ComponentFilter(
+ **{"name":("VTODO")}
+ ),
+ ],
+ **{"name": "VCALENDAR", "test": "anyof"}
+ )
+ )
+ filter = Filter(filter)
+ filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+ expression = buildExpression(filter, self._queryFields)
+ sql = CalDAVSQLQueryGenerator(expression, self, 1234)
+ select, args, usedtimerange = sql.generate()
+
+ self.assertEqual(select.toSQL(), SQLFragment(
+ "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT, TIME_RANGE where (ICALENDAR_TYPE = ? and (FLOATING = ? and END_DATE > ? or FLOATING = ? and END_DATE > ?) or ICALENDAR_TYPE = ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
+ ['VEVENT', False, datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 'VTODO', 1234]
+ ))
+ self.assertEqual(args, {})
+ self.assertEqual(usedtimerange, True)
+
+
+ def test_sqllite_query(self):
+ """
+ Basic query test - single term.
+ Only UID can be queried via sql.
+ """
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[caldavxml.ComponentFilter(
+ **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+ )],
+ **{"name": "VCALENDAR"}
+ )
+ )
+ filter = Filter(filter)
+ sql, args = sqlcalendarquery(filter, 1234)
+
+ self.assertTrue(sql.find("RESOURCE") != -1)
+ self.assertTrue(sql.find("TIMESPAN") == -1)
+ self.assertTrue(sql.find("TRANSPARENCY") == -1)
+ self.assertTrue("VEVENT" in args)
+
+
+
+class TestQueryFilterSerialize(TestCase):
+
+ def setUp(self):
+ super(TestQueryFilterSerialize, self).setUp()
+ TimezoneCache.create()
+
+
+ def test_query(self):
+ """
+ Basic query test - no time range
+ """
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[caldavxml.ComponentFilter(
+ **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+ )],
+ **{"name": "VCALENDAR"}
+ )
+ )
+ filter = Filter(filter)
+ filter.child.settzinfo(Timezone(tzid="America/New_York"))
+ j = filter.serialize()
+ self.assertEqual(j["type"], "Filter")
+
+ f = FilterBase.deserialize(j)
+ self.assertTrue(isinstance(f, Filter))
+
+
+ def test_timerange_query(self):
+ """
+ Basic query test with time range
+ """
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[caldavxml.ComponentFilter(
+ *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
+ **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+ )],
+ **{"name": "VCALENDAR"}
+ )
+ )
+ filter = Filter(filter)
+ filter.child.settzinfo(Timezone(tzid="America/New_York"))
+ j = filter.serialize()
+ self.assertEqual(j["type"], "Filter")
+
+ f = FilterBase.deserialize(j)
+ self.assertTrue(isinstance(f, Filter))
+ self.assertTrue(isinstance(f.child.filters[0].qualifier, TimeRange))
+ self.assertTrue(isinstance(f.child.filters[0].qualifier.tzinfo, Timezone))
+ self.assertEqual(f.child.filters[0].qualifier.tzinfo.getTimezoneID(), "America/New_York")
+
+
+ def test_query_not_extended(self):
+ """
+ Basic query test with time range
+ """
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[
+ caldavxml.ComponentFilter(
+ **{"name":("VEVENT")}
+ ),
+ caldavxml.ComponentFilter(
+ **{"name":("VTODO")}
+ ),
+ ],
+ **{"name": "VCALENDAR"}
+ )
+ )
+ filter = Filter(filter)
+ filter.child.settzinfo(Timezone(tzid="America/New_York"))
+ j = filter.serialize()
+ self.assertEqual(j["type"], "Filter")
+
+ f = FilterBase.deserialize(j)
+ self.assertTrue(isinstance(f, Filter))
+ self.assertEqual(len(f.child.filters), 2)
+
+
+ def test_query_extended(self):
+ """
+ Basic query test with time range
+ """
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[
+ caldavxml.ComponentFilter(
+ *[caldavxml.TimeRange(**{"start":"20060605T160000Z", })],
+ **{"name":("VEVENT")}
+ ),
+ caldavxml.ComponentFilter(
+ **{"name":("VTODO")}
+ ),
+ ],
+ **{"name": "VCALENDAR", "test": "anyof"}
+ )
+ )
+ filter = Filter(filter)
+ filter.child.settzinfo(Timezone(tzid="America/New_York"))
+ j = filter.serialize()
+ self.assertEqual(j["type"], "Filter")
+
+ f = FilterBase.deserialize(j)
+ self.assertTrue(isinstance(f, Filter))
+ self.assertEqual(len(f.child.filters), 2)
+ self.assertTrue(isinstance(f.child.filters[0].qualifier, TimeRange))
+
+
+ def test_query_text(self):
+ """
+ Basic query test with time range
+ """
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[
+ caldavxml.ComponentFilter(
+ caldavxml.PropertyFilter(
+ caldavxml.TextMatch.fromString("1234", False),
+ name="UID",
+ ),
+ **{"name":("VEVENT")}
+ ),
+ ],
+ **{"name": "VCALENDAR", "test": "anyof"}
+ )
+ )
+ filter = Filter(filter)
+ filter.child.settzinfo(Timezone(tzid="America/New_York"))
+ j = filter.serialize()
+ self.assertEqual(j["type"], "Filter")
+
+ f = FilterBase.deserialize(j)
+ self.assertTrue(isinstance(f, Filter))
+ self.assertTrue(isinstance(f.child.filters[0].filters[0], PropertyFilter))
+ self.assertTrue(isinstance(f.child.filters[0].filters[0].qualifier, TextMatch))
+ self.assertEqual(f.child.filters[0].filters[0].qualifier.text, "1234")
+
+
+
+class TestQueryFilterMatch(TestCase):
+
+ def setUp(self):
+ super(TestQueryFilterMatch, self).setUp()
+ TimezoneCache.create()
+
+
+ def test_vlarm_undefined(self):
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[caldavxml.ComponentFilter(
+ *[caldavxml.ComponentFilter(
+ caldavxml.IsNotDefined(),
+ **{"name":"VALARM"}
+ )],
+ **{"name":"VEVENT"}
+ )],
+ **{"name": "VCALENDAR"}
+ )
+ )
+ filter = Filter(filter)
+ filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+ self.assertFalse(filter.match(
+ Component.fromString("""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTAMP:20051222T210412Z
+CREATED:20060102T150000Z
+DTSTART;TZID=US/Eastern:20130102T100000
+DURATION:PT1H
+RRULE:FREQ=DAILY;COUNT=5
+SUMMARY:event 5
+UID:945113826375CBB89184DC36 at ninevah.local
+CATEGORIES:cool,hot
+CATEGORIES:warm
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+ )))
Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -32,10 +32,11 @@
from twistedcaldav.ical import Component, Property, iCalendarProductID
from twistedcaldav.instance import InstanceList
from twistedcaldav.memcacher import Memcacher
-from twistedcaldav.query import calendarqueryfilter
+from txdav.caldav.datastore.query.filter import Filter
from txdav.caldav.icalendarstore import QueryMaxResources
-from txdav.common.icommondatastore import IndexedSearchException
+from txdav.common.icommondatastore import IndexedSearchException, \
+ InternalDataStoreError
import uuid
@@ -92,7 +93,6 @@
- at inlineCallbacks
def generateFreeBusyInfo(
calresource,
fbinfo,
@@ -107,6 +107,86 @@
logItems=None,
):
"""
+ Get freebusy information for a calendar. Different behavior for internal vs external calendars.
+
+ See L{_internalGenerateFreeBusyInfo} for argument description.
+ """
+
+ # TODO: this method really should be moved to L{CalendarObject} so the internal/external pieces
+ # can be split across L{CalendarObject} and L{CalendarObjectExternal}
+ if calresource.external():
+ return _externalGenerateFreeBusyInfo(
+ calresource,
+ fbinfo,
+ timerange,
+ matchtotal,
+ excludeuid,
+ organizer,
+ organizerPrincipal,
+ same_calendar_user,
+ servertoserver,
+ event_details,
+ logItems
+ )
+ else:
+ return _internalGenerateFreeBusyInfo(
+ calresource,
+ fbinfo,
+ timerange,
+ matchtotal,
+ excludeuid,
+ organizer,
+ organizerPrincipal,
+ same_calendar_user,
+ servertoserver,
+ event_details,
+ logItems
+ )
+
+
+
+ at inlineCallbacks
+def _externalGenerateFreeBusyInfo(
+ calresource,
+ fbinfo,
+ timerange,
+ matchtotal,
+ excludeuid=None,
+ organizer=None,
+ organizerPrincipal=None,
+ same_calendar_user=False,
+ servertoserver=False,
+ event_details=None,
+ logItems=None,
+):
+ """
+ Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
+ any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.
+
+ See L{_internalGenerateFreeBusyInfo} for argument description.
+ """
+ fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(calresource, timerange, matchtotal, excludeuid, organizer, organizerPrincipal, same_calendar_user, servertoserver, event_details)
+ for i in range(3):
+ fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
+ returnValue(matchtotal)
+
+
+
+ at inlineCallbacks
+def _internalGenerateFreeBusyInfo(
+ calresource,
+ fbinfo,
+ timerange,
+ matchtotal,
+ excludeuid=None,
+ organizer=None,
+ organizerPrincipal=None,
+ same_calendar_user=False,
+ servertoserver=False,
+ event_details=None,
+ logItems=None,
+):
+ """
Run a free busy report on the specified calendar collection
accumulating the free busy info for later processing.
@param calresource: the L{Calendar} for a calendar collection.
@@ -203,23 +283,23 @@
# Create fake filter element to match time-range
filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- caldavxml.ComponentFilter(
- cache_timerange if caching else timerange,
- name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
- ),
- name="VCALENDAR",
- )
- )
- filter = calendarqueryfilter.Filter(filter)
+ caldavxml.ComponentFilter(
+ caldavxml.ComponentFilter(
+ cache_timerange if caching else timerange,
+ name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
+ ),
+ name="VCALENDAR",
+ )
+ )
+ filter = Filter(filter)
tzinfo = filter.settimezone(tz)
try:
- resources = yield calresource._index.indexedSearch(filter, useruid=attendee_uid, fbtype=True)
+ resources = yield calresource.search(filter, useruid=attendee_uid, fbtype=True)
if caching:
yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources)
except IndexedSearchException:
- resources = yield calresource._index.bruteForceSearch()
+ raise InternalDataStoreError("Invalid indexedSearch query")
else:
# Log extended item
Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -84,6 +84,12 @@
self._thisServer = None
+ def addServer(self, server):
+ self._servers[server.id] = server
+ if server.thisServer:
+ self._thisServer = server
+
+
def getServerById(self, id):
return self._servers.get(id)
@@ -125,16 +131,22 @@
Represents a server.
"""
- def __init__(self):
- self.id = None
- self.uri = None
- self.thisServer = False
+ def __init__(self, id=None, uri=None, sharedSecret=None, thisServer=False):
+ self.id = id
+ self.uri = uri
+ self.thisServer = thisServer
self.ips = set()
self.allowed_from_ips = set()
- self.shared_secret = None
+ self.shared_secret = sharedSecret
self.isImplicit = True
+ def details(self):
+ if not hasattr(self, "ssl"):
+ self._parseDetails()
+ return (self.ssl, self.host, self.port, self.path,)
+
+
def check(self, ignoreIPLookupFailures=False):
# Check whether this matches the current server
parsed_uri = urlparse.urlparse(self.uri)
@@ -215,7 +227,28 @@
return (SERVER_SECRET_HEADER, self.shared_secret,)
+ def _parseDetails(self):
+ # Extract scheme, host, port and path
+ if self.uri.startswith("http://"):
+ self.ssl = False
+ rest = self.uri[7:]
+ elif self.uri.startswith("https://"):
+ self.ssl = True
+ rest = self.uri[8:]
+ splits = rest.split("/", 1)
+ hostport = splits[0].split(":")
+ self.host = hostport[0]
+ if len(hostport) > 1:
+ self.port = int(hostport[1])
+ else:
+ self.port = {False: 80, True: 443}[self.ssl]
+ self.path = "/"
+ if len(splits) > 1:
+ self.path += splits[1]
+
+
+
ELEMENT_SERVERS = "servers"
ELEMENT_SERVER = "server"
ELEMENT_ID = "id"
Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/xml.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/xml.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/xml.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -259,14 +259,18 @@
@classmethod
- def fromCalendar(clazz, calendar):
+ def fromCalendar(clazz, calendar, format=None):
+ attrs = {}
+ if format is not None and format != "text/calendar":
+ attrs["content-type"] = format
+
if isinstance(calendar, str):
if not calendar:
raise ValueError("Missing calendar data")
return clazz(PCDATAElement(calendar))
elif isinstance(calendar, iComponent):
assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
- return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
+ return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference, format=format)))
else:
raise ValueError("Not a calendar: %s" % (calendar,))
Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -57,6 +57,9 @@
from twistedcaldav.memcacher import Memcacher
from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.datastore.query.builder import buildExpression
+from txdav.caldav.datastore.query.filter import Filter
+from txdav.caldav.datastore.query.generator import CalDAVSQLQueryGenerator
from txdav.caldav.datastore.scheduling.icalsplitter import iCalSplitter
from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
from txdav.caldav.datastore.util import AttachmentRetrievalTransport, \
@@ -72,12 +75,11 @@
AttendeeAllowedError, InvalidPerUserDataMerge, ComponentUpdateState, \
ValidOrganizerError, ShareeAllowedError, ComponentRemoveState, \
InvalidDefaultCalendar, \
- InvalidAttachmentOperation, DuplicatePrivateCommentsError
+ InvalidAttachmentOperation, DuplicatePrivateCommentsError, \
+ TimeRangeUpperLimit, TimeRangeLowerLimit
from txdav.caldav.icalendarstore import QuotaExceeded
from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
CommonObjectResource, ECALENDARTYPE
-from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator, \
- PostgresLegacyInboxIndexEmulator
from txdav.common.datastore.sql_tables import _ATTACHMENTS_MODE_NONE, \
_ATTACHMENTS_MODE_WRITE, schema, _BIND_MODE_OWN, \
_ATTACHMENTS_MODE_READ, _TRANSP_OPAQUE, _TRANSP_TRANSPARENT
@@ -87,7 +89,8 @@
InvalidObjectResourceError, ObjectResourceNameAlreadyExistsError, \
ObjectResourceNameNotAllowedError, TooManyObjectResourcesError, \
InvalidUIDError, UIDExistsError, UIDExistsElsewhereError, \
- InvalidResourceMove, InvalidComponentForStoreError
+ InvalidResourceMove, InvalidComponentForStoreError, \
+ NoSuchObjectResourceError
from txdav.xml import element
from txdav.idav import ChangeCategory
@@ -433,12 +436,6 @@
"VPOLL": "_default_polls",
}
- def __init__(self, transaction, ownerUID):
-
- self._childClass = Calendar
- super(CalendarHome, self).__init__(transaction, ownerUID)
-
-
@classmethod
def metadataColumns(cls):
"""
@@ -957,6 +954,12 @@
_objectSchema = schema.CALENDAR_OBJECT
_timeRangeSchema = schema.TIME_RANGE
+ # Mapping of iCalendar property name to DB column name
+ _queryFields = {
+ "UID": _objectSchema.UID,
+ "TYPE": _objectSchema.ICALENDAR_TYPE,
+ }
+
_supportedComponents = None
def __init__(self, *args, **kw):
@@ -964,10 +967,6 @@
Initialize a calendar pointing at a record in a database.
"""
super(Calendar, self).__init__(*args, **kw)
- if self.isInbox():
- self._index = PostgresLegacyInboxIndexEmulator(self)
- else:
- self._index = PostgresLegacyIndexEmulator(self)
self._transp = _TRANSP_OPAQUE
@@ -1085,6 +1084,45 @@
self.viewerHome().removedCalendarResource(child.uid())
+ @inlineCallbacks
+ def moveObjectResourceHere(self, name, component):
+ """
+ Create a new child in this collection as part of a move operation. This needs to be split out because
+ behavior differs for sub-classes and cross-pod operations.
+
+ @param name: new name to use in new parent
+ @type name: C{str} or C{None} for existing name
+ @param component: data for new resource
+ @type component: L{Component}
+ """
+
+ # Cross-pod calls come in with component as str or unicode
+ if isinstance(component, str) or isinstance(component, unicode):
+ try:
+ component = self._objectResourceClass._componentClass.fromString(component)
+ except InvalidICalendarDataError as e:
+ raise InvalidComponentForStoreError(str(e))
+
+ yield self._createCalendarObjectWithNameInternal(name, component, internal_state=ComponentUpdateState.RAW)
+
+
+ @inlineCallbacks
+ def moveObjectResourceAway(self, rid, child=None):
+ """
+ Remove the child as the result of a move operation. This needs to be split out because
+ behavior differs for sub-classes and cross-pod operations.
+
+ @param rid: the child resource-id to move
+ @type rid: C{int}
+ @param child: the child resource to move - might be C{None} for cross-pod
+ @type child: L{CommonObjectResource}
+ """
+
+ if child is None:
+ child = yield self.objectResourceWithID(rid)
+ yield child._removeInternal(internal_state=ComponentRemoveState.INTERNAL)
+
+
def calendarObjectsInTimeRange(self, start, end, timeZone):
raise NotImplementedError()
@@ -1302,6 +1340,37 @@
yield self.setDefaultAlarm("empty", False, False)
+ def getInviteCopyProperties(self):
+ """
+ Get a dictionary of property name/values (as strings) for properties that are shadowable and
+ need to be copied to a sharee's collection when an external (cross-pod) share is created.
+ Sub-classes should override to expose the properties they care about.
+ """
+ props = {}
+ for elem in (element.DisplayName, caldavxml.CalendarDescription, caldavxml.CalendarTimeZone, customxml.CalendarColor,):
+ if PropertyName.fromElement(elem) in self.properties():
+ props[elem.sname()] = str(self.properties()[PropertyName.fromElement(elem)])
+ return props
+
+
+ def setInviteCopyProperties(self, props):
+ """
+ Copy a set of shadowable properties (as name/value strings) onto this shared resource when
+ a cross-pod invite is processed. Sub-classes should override to expose the properties they
+ care about.
+ """
+ # Initialize these for all shares
+ for elem in (caldavxml.CalendarDescription, caldavxml.CalendarTimeZone,):
+ if PropertyName.fromElement(elem) not in self.properties() and elem.sname() in props:
+ self.properties()[PropertyName.fromElement(elem)] = elem.fromString(props[elem.sname()])
+
+ # Only initialize these for direct shares
+ if self.direct():
+ for elem in (element.DisplayName, customxml.CalendarColor,):
+ if PropertyName.fromElement(elem) not in self.properties() and elem.sname() in props:
+ self.properties()[PropertyName.fromElement(elem)] = elem.fromString(props[elem.sname()])
+
+
# FIXME: this is DAV-ish. Data store calendar objects don't have
# mime types. -wsv
def contentType(self):
@@ -1312,6 +1381,196 @@
@inlineCallbacks
+ def search(self, filter, useruid=None, fbtype=False):
+ """
+ Finds resources matching the given qualifiers.
+ @param filter: the L{Filter} for the calendar-query to execute.
+ @return: an iterable of tuples for each resource matching the
+ given C{qualifiers}. The tuples are C{(name, uid)}, where
+ C{name} is the resource name, C{uid} is the resource UID.
+ """
+
+ # We might be passed an L{Filter} or a serialization of one
+ if isinstance(filter, dict):
+ try:
+ filter = Filter.deserialize(filter)
+ except Exception:
+ file = None
+
+ # Make sure we have a proper Filter element and get the partial SQL statement to use.
+ sql_stmt = self._sqlquery(filter, useruid, fbtype)
+
+ # No result means it is too complex for us
+ if sql_stmt is None:
+ raise IndexedSearchException()
+ sql_stmt, args, usedtimerange = sql_stmt
+
+ # Check for time-range re-expand
+ if usedtimerange is not None:
+
+ today = DateTime.getToday()
+
+ # Determine how far we need to extend the current expansion of
+ # events. If we have an open-ended time-range we will expand
+ # one year past the start. That should catch bounded
+ # recurrences - unbounded will have been indexed with an
+ # "infinite" value always included.
+ maxDate, isStartDate = filter.getmaxtimerange()
+ if maxDate:
+ maxDate = maxDate.duplicate()
+ maxDate.offsetDay(1)
+ maxDate.setDateOnly(True)
+ upperLimit = today + Duration(days=config.FreeBusyIndexExpandMaxDays)
+ if maxDate > upperLimit:
+ raise TimeRangeUpperLimit(upperLimit)
+ if isStartDate:
+ maxDate += Duration(days=365)
+
+ # Determine if the start date is too early for the restricted range we
+ # are applying. If it is today or later we don't need to worry about truncation
+ # in the past.
+ minDate, _ignore_isEndDate = filter.getmintimerange()
+ if minDate >= today:
+ minDate = None
+ if minDate is not None and config.FreeBusyIndexLowerLimitDays:
+ truncateLowerLimit = today - Duration(days=config.FreeBusyIndexLowerLimitDays)
+ if minDate < truncateLowerLimit:
+ raise TimeRangeLowerLimit(truncateLowerLimit)
+
+ if maxDate is not None or minDate is not None:
+ yield self.testAndUpdateIndex(minDate, maxDate)
+
+ rowiter = yield sql_stmt.on(self._txn, **args)
+
+ # Check result for missing resources
+ results = []
+ for row in rowiter:
+ if fbtype:
+ row = list(row)
+ row[4] = 'Y' if row[4] else 'N'
+ row[7] = indexfbtype_to_icalfbtype[row[7]]
+ if row[9] is not None:
+ row[8] = row[9]
+ row[8] = 'T' if row[8] else 'F'
+ del row[9]
+ results.append(row)
+
+ returnValue(results)
+
+
+ def _sqlquery(self, filter, useruid, fbtype):
+ """
+ Convert the supplied addressbook-query into a partial SQL statement.
+
+ @param filter: the L{Filter} for the addressbook-query to convert.
+ @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+ and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+ Or return C{None} if it is not possible to create an SQL query to fully match the addressbook-query.
+ """
+
+ if not isinstance(filter, Filter):
+ return None
+
+ try:
+ expression = buildExpression(filter, self._queryFields)
+ sql = CalDAVSQLQueryGenerator(expression, self, self.id(), useruid, fbtype)
+ return sql.generate()
+ except ValueError:
+ return None
+
+
+ @classproperty
+ def _notExpandedWithinQuery(cls): #@NoSelf
+ """
+ Query to find resources that need to be re-expanded
+ """
+ co = schema.CALENDAR_OBJECT
+ return Select(
+ [co.RESOURCE_NAME],
+ From=co,
+ Where=((co.RECURRANCE_MIN > Parameter("minDate"))
+ .Or(co.RECURRANCE_MAX < Parameter("maxDate")))
+ .And(co.CALENDAR_RESOURCE_ID == Parameter("resourceID"))
+ )
+
+
+ @inlineCallbacks
+ def notExpandedWithin(self, minDate, maxDate):
+ """
+ Gives all resources which have not been expanded beyond a given date
+ in the database. (Unused; see above L{postgresqlgenerator}.
+ """
+ returnValue([row[0] for row in (
+ yield self._notExpandedWithinQuery.on(
+ self._txn,
+ minDate=pyCalendarTodatetime(normalizeForIndex(minDate)) if minDate is not None else None,
+ maxDate=pyCalendarTodatetime(normalizeForIndex(maxDate)),
+ resourceID=self._resourceID))]
+ )
+
+
+ @inlineCallbacks
+ def reExpandResource(self, name, expand_start, expand_end):
+ """
+ Given a resource name, remove it from the database and re-add it
+ with a longer expansion.
+ """
+ obj = yield self.calendarObjectWithName(name)
+
+ # Use a new transaction to do this update quickly without locking the row for too long. However, the original
+ # transaction may have the row locked, so use wait=False and if that fails, fall back to using the original txn.
+
+ newTxn = obj.transaction().store().newTransaction()
+ try:
+ yield obj.lock(wait=False, txn=newTxn)
+ except NoSuchObjectResourceError:
+ yield newTxn.commit()
+ returnValue(None)
+ except:
+ yield newTxn.abort()
+ newTxn = None
+
+ # Now do the re-expand using the appropriate transaction
+ try:
+ doExpand = False
+ if newTxn is None:
+ doExpand = True
+ else:
+ # We repeat this check because the resource may have been re-expanded by someone else
+ rmin, rmax = (yield obj.recurrenceMinMax(txn=newTxn))
+
+ # If the resource is not fully expanded, see if within the required range or not.
+ # Note that expand_start could be None if no lower limit is applied, but expand_end will
+ # never be None
+ if rmax is not None and rmax < expand_end:
+ doExpand = True
+ if rmin is not None and expand_start is not None and rmin > expand_start:
+ doExpand = True
+
+ if doExpand:
+ yield obj.updateDatabase(
+ (yield obj.component()),
+ expand_until=expand_end,
+ reCreate=True,
+ txn=newTxn,
+ )
+ finally:
+ if newTxn is not None:
+ yield newTxn.commit()
+
+
+ @inlineCallbacks
+ def testAndUpdateIndex(self, minDate, maxDate):
+ # Find out if the index is expanded far enough
+ names = yield self.notExpandedWithin(minDate, maxDate)
+
+ # Actually expand recurrence max
+ for name in names:
+ self.log.info("Search falls outside range of index for %s %s to %s" % (name, minDate, maxDate))
+ yield self.reExpandResource(name, minDate, maxDate)
+
+
+ @inlineCallbacks
def splitCollectionByComponentTypes(self):
"""
If the calendar contains iCalendar data with different component types, then split it into separate collections
@@ -1524,6 +1783,7 @@
implements(ICalendarObject)
_objectSchema = schema.CALENDAR_OBJECT
+ _componentClass = VComponent
def __init__(self, calendar, name, uid, resourceID=None, options=None):
@@ -1542,36 +1802,20 @@
self._cachedComponent = None
self._cachedCommponentPerUser = {}
- _allColumns = [
- _objectSchema.RESOURCE_ID,
- _objectSchema.RESOURCE_NAME,
- _objectSchema.UID,
- _objectSchema.MD5,
- Len(_objectSchema.TEXT),
- _objectSchema.ATTACHMENTS_MODE,
- _objectSchema.DROPBOX_ID,
- _objectSchema.ACCESS,
- _objectSchema.SCHEDULE_OBJECT,
- _objectSchema.SCHEDULE_TAG,
- _objectSchema.SCHEDULE_ETAGS,
- _objectSchema.PRIVATE_COMMENTS,
- _objectSchema.CREATED,
- _objectSchema.MODIFIED
- ]
-
@classmethod
@inlineCallbacks
def _createInternal(cls, parent, name, component, internal_state, options=None, split_details=None):
- child = (yield cls.objectWithName(parent, name, None))
+ child = (yield cls.objectWithName(parent, name))
if child:
raise ObjectResourceNameAlreadyExistsError(name)
if name.startswith("."):
raise ObjectResourceNameNotAllowedError(name)
- objectResource = cls(parent, name, None, None, options=options)
+ c = cls._externalClass if parent.external() else cls
+ objectResource = c(parent, name, None, None, options=options)
yield objectResource._setComponentInternal(component, inserting=True, internal_state=internal_state, split_details=split_details)
yield objectResource._loadPropertyStore(created=True)
@@ -1581,27 +1825,51 @@
returnValue(objectResource)
- def _initFromRow(self, row):
+ @classmethod
+ def _allColumns(cls): #@NoSelf
"""
- Given a select result using the columns from L{_allColumns}, initialize
- the calendar object resource state.
+ Full set of columns in the object table that need to be loaded to
+ initialize the object resource state.
"""
- (self._resourceID,
- self._name,
- self._uid,
- self._md5,
- self._size,
- self._attachment,
- self._dropboxID,
- self._access,
- self._schedule_object,
- self._schedule_tag,
- self._schedule_etags,
- self._private_comments,
- self._created,
- self._modified,) = tuple(row)
+ obj = cls._objectSchema
+ return [
+ obj.RESOURCE_ID,
+ obj.RESOURCE_NAME,
+ obj.UID,
+ obj.MD5,
+ Len(obj.TEXT),
+ obj.ATTACHMENTS_MODE,
+ obj.DROPBOX_ID,
+ obj.ACCESS,
+ obj.SCHEDULE_OBJECT,
+ obj.SCHEDULE_TAG,
+ obj.SCHEDULE_ETAGS,
+ obj.PRIVATE_COMMENTS,
+ obj.CREATED,
+ obj.MODIFIED
+ ]
+ @classmethod
+ def _rowAttributes(cls): #@NoSelf
+ return (
+ "_resourceID",
+ "_name",
+ "_uid",
+ "_md5",
+ "_size",
+ "_attachment",
+ "_dropboxID",
+ "_access",
+ "_schedule_object",
+ "_schedule_tag",
+ "_schedule_etags",
+ "_private_comments",
+ "_created",
+ "_modified",
+ )
+
+
@property
def _calendar(self):
return self._parentCollection
@@ -2183,6 +2451,12 @@
Scheduling will be done automatically.
"""
+ # Cross-pod calls come in with component as str or unicode
+ if isinstance(component, str) or isinstance(component, unicode):
+ try:
+ component = self._componentClass.fromString(component)
+ except InvalidICalendarDataError as e:
+ raise InvalidComponentForStoreError(str(e))
return self._setComponentInternal(component, inserting, ComponentUpdateState.NORMAL, smart_merge)
@@ -2734,9 +3008,10 @@
yield NamedLock.acquire(self._txn, "ImplicitUIDLock:%s" % (hashlib.md5(calendar.resourceUID()).hexdigest(),))
# Need to also remove attachments
- if self._dropboxID:
- yield DropBoxAttachment.resourceRemoved(self._txn, self._resourceID, self._dropboxID)
- yield ManagedAttachment.resourceRemoved(self._txn, self._resourceID)
+ if internal_state != ComponentRemoveState.INTERNAL:
+ if self._dropboxID:
+ yield DropBoxAttachment.resourceRemoved(self._txn, self._resourceID, self._dropboxID)
+ yield ManagedAttachment.resourceRemoved(self._txn, self._resourceID)
yield super(CalendarObject, self).remove()
# Do scheduling
@@ -4419,4 +4694,10 @@
returnValue(location)
+# Hook-up class relationships at the end after they have all been defined
+from txdav.caldav.datastore.sql_external import CalendarHomeExternal, CalendarExternal, CalendarObjectExternal
+CalendarHome._externalClass = CalendarHomeExternal
+CalendarHome._childClass = Calendar
+Calendar._externalClass = CalendarExternal
Calendar._objectResourceClass = CalendarObject
+CalendarObject._externalClass = CalendarObjectExternal
Copied: CalendarServer/trunk/txdav/caldav/datastore/sql_external.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql_external.py (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql_external.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,217 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 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.
+##
+"""
+SQL backend for CalDAV storage when resources are external.
+"""
+
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
+
+from twext.python.log import Logger
+
+from txdav.caldav.datastore.sql import CalendarHome, Calendar, CalendarObject
+from txdav.caldav.icalendarstore import ComponentUpdateState, ComponentRemoveState
+from txdav.common.datastore.sql_external import CommonHomeExternal, CommonHomeChildExternal, \
+ CommonObjectResourceExternal
+
+log = Logger()
+
+class CalendarHomeExternal(CommonHomeExternal, CalendarHome):
+ """
+ Wrapper for a CalendarHome that is external and only supports a limited set of operations.
+ """
+
+ def __init__(self, transaction, ownerUID, resourceID):
+
+ CalendarHome.__init__(self, transaction, ownerUID)
+ CommonHomeExternal.__init__(self, transaction, ownerUID, resourceID)
+
+
+ def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, mode):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getCalendarResourcesForUID(self, uid):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def calendarObjectWithDropboxID(self, dropboxID):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getAllDropboxIDs(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getAllAttachmentNames(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getAllManagedIDs(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def createdHome(self):
+ """
+ No children - make this a no-op.
+ """
+ return succeed(None)
+
+
+ def splitCalendars(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def ensureDefaultCalendarsExist(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def setDefaultCalendar(self, calendar, componentType):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def defaultCalendar(self, componentType, create=True):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def isDefaultCalendar(self, calendar):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getDefaultAlarm(self, vevent, timed):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def setDefaultAlarm(self, alarm, vevent, timed):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getAvailability(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def setAvailability(self, availability):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class CalendarExternal(CommonHomeChildExternal, Calendar):
+ """
+ SQL-based implementation of L{ICalendar}.
+ """
+ pass
+
+
+
+class CalendarObjectExternal(CommonObjectResourceExternal, CalendarObject):
+ """
+ SQL-based implementation of L{ICalendarObject}.
+ """
+
+ @classmethod
+ def _createInternal(cls, parent, name, component, internal_state, options=None, split_details=None):
+ raise AssertionError("CalendarObjectExternal: not supported")
+
+
+ def _setComponentInternal(self, component, inserting=False, internal_state=ComponentUpdateState.NORMAL, smart_merge=False, split_details=None):
+ raise AssertionError("CalendarObjectExternal: not supported")
+
+
+ def _removeInternal(self, internal_state=ComponentRemoveState.NORMAL):
+ raise AssertionError("CalendarObjectExternal: not supported")
+
+
+ @inlineCallbacks
+ def addAttachment(self, rids, content_type, filename, stream):
+ result = yield self._txn.store().conduit.send_add_attachment(self, rids, content_type, filename, stream)
+ managedID, location = result
+ returnValue((ManagedAttachmentExternal(str(managedID)), str(location),))
+
+
+ @inlineCallbacks
+ def updateAttachment(self, managed_id, content_type, filename, stream):
+ result = yield self._txn.store().conduit.send_update_attachment(self, managed_id, content_type, filename, stream)
+ managedID, location = result
+ returnValue((ManagedAttachmentExternal(str(managedID)), str(location),))
+
+
+ @inlineCallbacks
+ def removeAttachment(self, rids, managed_id):
+ yield self._txn.store().conduit.send_remove_attachment(self, rids, managed_id)
+ returnValue(None)
+
+
+
+class ManagedAttachmentExternal(object):
+ """
+ Fake managed attachment object returned from L{CalendarObjectExternal.addAttachment} and
+ L{CalendarObjectExternal.updateAttachment}.
+ """
+
+ def __init__(self, managedID):
+ self._managedID = managedID
+
+
+ def managedID(self):
+ return self._managedID
+
+
+CalendarExternal._objectResourceClass = CalendarObjectExternal
Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/common.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -416,12 +416,14 @@
yield coll.removeNotificationObjectWithUID("1")
st2 = yield coll.syncToken()
rev2 = self.token2revision(st2)
- changed, deleted = yield coll.resourceNamesSinceToken(rev)
+ changed, deleted, invalid = yield coll.resourceNamesSinceToken(rev)
self.assertEquals(set(changed), set(["2.xml"]))
self.assertEquals(set(deleted), set(["1.xml"]))
- changed, deleted = yield coll.resourceNamesSinceToken(rev2)
+ self.assertEquals(len(invalid), 0)
+ changed, deleted, invalid = yield coll.resourceNamesSinceToken(rev2)
self.assertEquals(set(changed), set([]))
self.assertEquals(set(deleted), set([]))
+ self.assertEquals(len(invalid), 0)
@inlineCallbacks
@@ -1602,7 +1604,7 @@
home = yield self.homeUnderTest()
- changed, deleted = yield home.resourceNamesSinceToken(
+ changed, deleted, invalid = yield home.resourceNamesSinceToken(
self.token2revision(st), "infinity")
self.assertEquals(set(changed), set(["calendar_1/",
@@ -1610,11 +1612,13 @@
"calendar_1/2.ics",
"other-calendar/"]))
self.assertEquals(set(deleted), set(["calendar_1/2.ics"]))
+ self.assertEquals(invalid, [])
- changed, deleted = yield home.resourceNamesSinceToken(
+ changed, deleted, invalid = yield home.resourceNamesSinceToken(
self.token2revision(st2), "infinity")
self.assertEquals(changed, [])
self.assertEquals(deleted, [])
+ self.assertEquals(invalid, [])
@inlineCallbacks
@@ -1634,12 +1638,14 @@
yield obj1.remove()
st2 = yield cal.syncToken()
rev2 = self.token2revision(st2)
- changed, deleted = yield cal.resourceNamesSinceToken(rev)
+ changed, deleted, invalid = yield cal.resourceNamesSinceToken(rev)
self.assertEquals(set(changed), set(["new.ics"]))
self.assertEquals(set(deleted), set(["2.ics"]))
- changed, deleted = yield cal.resourceNamesSinceToken(rev2)
+ self.assertEquals(len(invalid), 0)
+ changed, deleted, invalid = yield cal.resourceNamesSinceToken(rev2)
self.assertEquals(set(changed), set([]))
self.assertEquals(set(deleted), set([]))
+ self.assertEquals(len(invalid), 0)
@inlineCallbacks
@@ -1703,7 +1709,7 @@
L{ICalendarStore.withEachCalendarHomeDo} executes its C{action}
argument repeatedly with all homes that have been created.
"""
- additionalUIDs = set('alpha-uid home2 home3 beta-uid'.split())
+ additionalUIDs = set('user01 home2 home3 uid1'.split())
txn = self.transactionUnderTest()
for name in additionalUIDs:
yield txn.calendarHomeWithUID(name, create=True)
Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -19,6 +19,7 @@
from twisted.internet.task import deferLater
from txdav.caldav.datastore.index_file import Index, MemcachedUIDReserver
+from txdav.caldav.datastore.query.filter import Filter
from txdav.common.icommondatastore import ReservationError, \
InternalDataStoreError
@@ -26,7 +27,6 @@
from twistedcaldav.caldavxml import TimeRange
from twistedcaldav.ical import Component, InvalidICalendarDataError
from twistedcaldav.instance import InvalidOverriddenInstanceError
-from twistedcaldav.query import calendarqueryfilter
from twistedcaldav.test.util import InMemoryMemcacheProtocol
import twistedcaldav.test.util
@@ -480,7 +480,7 @@
name="VCALENDAR",
)
)
- filter = calendarqueryfilter.Filter(filter)
+ filter = Filter(filter)
resources = yield self.db.indexedSearch(filter)
index_results = set()
@@ -666,7 +666,7 @@
name="VCALENDAR",
)
)
- filter = calendarqueryfilter.Filter(filter)
+ filter = Filter(filter)
resources = yield self.db.indexedSearch(filter, fbtype=True)
index_results = set()
@@ -1073,7 +1073,7 @@
name="VCALENDAR",
)
)
- filter = calendarqueryfilter.Filter(filter)
+ filter = Filter(filter)
for useruid, instances in peruserinstances:
resources = yield self.db.indexedSearch(filter, useruid=useruid, fbtype=True)
@@ -1142,12 +1142,12 @@
self.db.deleteResource("data3.ics")
tests = (
- (0, (["data1.ics", "data2.ics", ], [],)),
- (1, (["data2.ics", ], ["data3.ics", ],)),
- (2, ([], ["data3.ics", ],)),
- (3, ([], ["data3.ics", ],)),
- (4, ([], [],)),
- (5, ([], [],)),
+ (0, (["data1.ics", "data2.ics", ], [], [],)),
+ (1, (["data2.ics", ], ["data3.ics", ], [],)),
+ (2, ([], ["data3.ics", ], [],)),
+ (3, ([], ["data3.ics", ], [],)),
+ (4, ([], [], [],)),
+ (5, ([], [], [],)),
)
for revision, results in tests:
Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -13,14 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
-from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor
-from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser, \
- LocalCalendarUser
-from txdav.caldav.datastore.scheduling.caldav.scheduler import CalDAVScheduler
-from txdav.caldav.datastore.scheduling.scheduler import ScheduleResponseQueue
-from txweb2 import responsecode
-from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.instance import InvalidOverriddenInstanceError
"""
Tests for txdav.caldav.datastore.postgres, mostly based on
@@ -32,7 +24,8 @@
from twext.enterprise.dal.syntax import Select, Parameter, Insert, Delete, \
Update
-from twistedcaldav.ical import Component as VComponent
+
+from txweb2 import responsecode
from txweb2.http_headers import MimeType
from txweb2.stream import MemoryStream
@@ -47,9 +40,16 @@
from twistedcaldav.config import config
from twistedcaldav.dateops import datetimeMktime
from twistedcaldav.ical import Component, normalize_iCalStr, diff_iCalStrs
-from twistedcaldav.query import calendarqueryfilter
+from twistedcaldav.instance import InvalidOverriddenInstanceError
from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.datastore.query.filter import Filter
+from txdav.caldav.datastore.scheduling.caldav.scheduler import CalDAVScheduler
+from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser, \
+ LocalCalendarUser
+from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
+from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor
+from txdav.caldav.datastore.scheduling.scheduler import ScheduleResponseQueue
from txdav.caldav.datastore.test.common import CommonTests as CalendarCommonTests, \
test_event_text
from txdav.caldav.datastore.test.test_file import setUpCalendarStore
@@ -57,14 +57,13 @@
from txdav.caldav.datastore.util import _migrateCalendar, migrateHome
from txdav.caldav.icalendarstore import ComponentUpdateState, InvalidDefaultCalendar
from txdav.common.datastore.sql import ECALENDARTYPE, CommonObjectResource
-from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator
from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
_BIND_STATUS_ACCEPTED
from txdav.common.datastore.test.util import populateCalendarsFrom, \
CommonCommonTests
from txdav.common.icommondatastore import NoSuchObjectResourceError
-from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
from txdav.idav import ChangeCategory
+from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
import datetime
@@ -407,10 +406,10 @@
name="VCALENDAR",
)
)
- filter = calendarqueryfilter.Filter(filter)
+ filter = Filter(filter)
filter.settimezone(None)
- results = yield toCalendar._index.indexedSearch(filter, 'user01', True)
+ results = yield toCalendar.search(filter, 'user01', True)
self.assertEquals(len(results), 1)
_ignore_name, uid, _ignore_type, _ignore_organizer, _ignore_float, _ignore_start, _ignore_end, _ignore_fbtype, transp = results[0]
self.assertEquals(uid, "uid4")
@@ -618,7 +617,7 @@
@inlineCallbacks
def _defer1():
- yield cal1.createObjectResourceWithName("1.ics", VComponent.fromString(
+ yield cal1.createObjectResourceWithName("1.ics", Component.fromString(
"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -664,7 +663,7 @@
@inlineCallbacks
def _defer2():
- yield cal2.createObjectResourceWithName("2.ics", VComponent.fromString(
+ yield cal2.createObjectResourceWithName("2.ics", Component.fromString(
"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -825,7 +824,7 @@
# Create calendar object
calendar1 = yield self.calendarUnderTest()
name = "test.ics"
- component = VComponent.fromString(test_event_text)
+ component = Component.fromString(test_event_text)
metadata = {
"accessMode": "PUBLIC",
"isScheduleObject": True,
@@ -873,7 +872,7 @@
inbox = yield home.createCalendarWithName("inbox")
name = "test.ics"
- component = VComponent.fromString(test_event_text)
+ component = Component.fromString(test_event_text)
metadata = {
"accessMode": "PUBLIC",
"isScheduleObject": True,
@@ -922,7 +921,7 @@
"""
home = yield self.homeUnderTest()
inbox = yield home.createCalendarWithName("inbox")
- component = VComponent.fromString(test_event_text)
+ component = Component.fromString(test_event_text)
inboxItem = yield inbox.createCalendarObjectWithName("inbox.ics", component)
self.assertEquals(ChangeCategory.inbox, inboxItem.removeNotifyCategory())
yield self.commit()
@@ -935,7 +934,7 @@
"""
home = yield self.homeUnderTest()
nonInbox = yield home.createCalendarWithName("noninbox")
- component = VComponent.fromString(test_event_text)
+ component = Component.fromString(test_event_text)
nonInboxItem = yield nonInbox.createCalendarObjectWithName("inbox.ics", component)
self.assertEquals(ChangeCategory.default, nonInboxItem.removeNotifyCategory())
yield self.commit()
@@ -1105,9 +1104,10 @@
self.assertTrue(calendar2_vtodo is not None)
children = yield calendar2_vtodo.listCalendarObjects()
self.assertEqual(len(children), 2)
- changed, deleted = yield calendar2_vtodo.resourceNamesSinceToken(None)
+ changed, deleted, invalid = yield calendar2_vtodo.resourceNamesSinceToken(None)
self.assertEqual(sorted(changed), ["3.ics", "5.ics"])
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
result = yield calendar2_vtodo.getSupportedComponents()
self.assertEquals(result, "VTODO")
self.assertTrue(pkey in calendar2_vtodo.properties())
@@ -1118,9 +1118,10 @@
self.assertEqual(len(children), 3)
new_sync_token2 = yield calendar2.syncToken()
self.assertNotEqual(new_sync_token2, original_sync_token2)
- changed, deleted = yield calendar2.resourceNamesSinceToken(original_sync_token2)
+ changed, deleted, invalid = yield calendar2.resourceNamesSinceToken(original_sync_token2)
self.assertEqual(len(changed), 0)
self.assertEqual(sorted(deleted), ["3.ics", "5.ics"])
+ self.assertEqual(len(invalid), 0)
result = yield calendar2.getSupportedComponents()
self.assertEquals(result, "VEVENT")
self.assertTrue(pkey in calendar2.properties())
@@ -1369,7 +1370,7 @@
@inlineCallbacks
def test_notExpandedWithin(self):
"""
- Test PostgresLegacyIndexEmulator.notExpandedWithin to make sure it returns the correct
+ Test Calendar.notExpandedWithin to make sure it returns the correct
result based on the ranges passed in.
"""
@@ -1378,7 +1379,6 @@
# Create the index on a new calendar
home = yield self.homeUnderTest()
newcalendar = yield home.createCalendarWithName("index_testing")
- index = PostgresLegacyIndexEmulator(newcalendar)
# Create the calendar object to use for testing
nowYear = self.nowYear["now"]
@@ -1406,37 +1406,37 @@
# Fully within range
testMin = DateTime(nowYear, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
testMax = DateTime(nowYear + 1, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
- result = yield index.notExpandedWithin(testMin, testMax)
+ result = yield newcalendar.notExpandedWithin(testMin, testMax)
self.assertEqual(result, [])
# Upper bound exceeded
testMin = DateTime(nowYear, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
testMax = DateTime(nowYear + 5, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
- result = yield index.notExpandedWithin(testMin, testMax)
+ result = yield newcalendar.notExpandedWithin(testMin, testMax)
self.assertEqual(result, ["indexing.ics"])
# Lower bound exceeded
testMin = DateTime(nowYear - 5, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
testMax = DateTime(nowYear + 1, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
- result = yield index.notExpandedWithin(testMin, testMax)
+ result = yield newcalendar.notExpandedWithin(testMin, testMax)
self.assertEqual(result, ["indexing.ics"])
# Lower and upper bounds exceeded
testMin = DateTime(nowYear - 5, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
testMax = DateTime(nowYear + 5, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
- result = yield index.notExpandedWithin(testMin, testMax)
+ result = yield newcalendar.notExpandedWithin(testMin, testMax)
self.assertEqual(result, ["indexing.ics"])
# Lower none within range
testMin = None
testMax = DateTime(nowYear + 1, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
- result = yield index.notExpandedWithin(testMin, testMax)
+ result = yield newcalendar.notExpandedWithin(testMin, testMax)
self.assertEqual(result, [])
# Lower none and upper bounds exceeded
testMin = None
testMax = DateTime(nowYear + 5, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
- result = yield index.notExpandedWithin(testMin, testMax)
+ result = yield newcalendar.notExpandedWithin(testMin, testMax)
self.assertEqual(result, ["indexing.ics"])
@@ -1542,8 +1542,8 @@
# Tests on inbox - resources with properties
txn = self.transactionUnderTest()
- yield txn.homeWithUID(ECALENDARTYPE, "byNameTest", create=True)
- inbox = yield self.calendarUnderTest(txn=txn, name="inbox", home="byNameTest")
+ yield txn.homeWithUID(ECALENDARTYPE, "user01", create=True)
+ inbox = yield self.calendarUnderTest(txn=txn, name="inbox", home="user01")
caldata = """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
@@ -1574,7 +1574,7 @@
yield _createInboxItem("4.ics", "p4")
yield self.commit()
- inbox = yield self.calendarUnderTest(name="inbox", home="byNameTest")
+ inbox = yield self.calendarUnderTest(name="inbox", home="user01")
yield _tests(inbox)
resources = yield inbox.objectResourcesWithNames(("1.ics",))
@@ -2152,7 +2152,6 @@
-
class CalendarObjectSplitting(CommonCommonTests, unittest.TestCase):
"""
CalendarObject splitting tests
Copied: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_external.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,682 @@
+##
+# Copyright (c) 2013 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 twisted.internet.defer import inlineCallbacks
+
+from twext.python.clsprop import classproperty
+from txdav.common.datastore.test.util import populateCalendarsFrom
+from txdav.common.datastore.sql_tables import _BIND_MODE_READ, \
+ _BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED
+from txdav.common.datastore.podding.test.util import MultiStoreConduitTest
+
+
+class BaseSharingTests(MultiStoreConduitTest):
+
+ """
+ Test store-based calendar sharing.
+ """
+
+ @inlineCallbacks
+ def setUp(self):
+ yield super(BaseSharingTests, self).setUp()
+ yield self.populate()
+
+
+ @inlineCallbacks
+ def populate(self):
+ yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+ self.notifierFactory.reset()
+
+ cal1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:20131122T140000
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+SUMMARY:event 1
+END:VEVENT
+END:VCALENDAR
+"""
+
+ @classproperty(cache=False)
+ def requirements(cls): #@NoSelf
+ return {
+ "user01": {
+ "calendar": {
+ "cal1.ics": (cls.cal1, None,),
+ },
+ "inbox": {
+ },
+ },
+ "user02": {
+ "calendar": {
+ },
+ "inbox": {
+ },
+ },
+ "user03": {
+ "calendar": {
+ },
+ "inbox": {
+ },
+ },
+ }
+
+
+
+class CalendarSharing(BaseSharingTests):
+
+ @inlineCallbacks
+ def test_no_shares(self):
+ """
+ Test that initially there are no shares.
+ """
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+
+ @inlineCallbacks
+ def test_invite_sharee(self):
+ """
+ Test invite/uninvite creates/removes shares and notifications.
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+ self.assertEqual(invites[0].uid, shareeView.shareUID())
+ self.assertEqual(invites[0].ownerUID, "user01")
+ self.assertEqual(invites[0].shareeUID, "puser02")
+ self.assertEqual(invites[0].mode, _BIND_MODE_READ)
+ self.assertEqual(invites[0].status, _BIND_STATUS_INVITED)
+ self.assertEqual(invites[0].summary, "summary")
+
+ inviteUID = shareeView.shareUID()
+ sharedName = shareeView.name()
+
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+
+ notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID, ])
+ yield self.otherCommit()
+
+ # Uninvite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+
+ yield calendar.uninviteUserFromShare("puser02")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [])
+ yield self.otherCommit()
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+ yield calendar.setShared(False)
+ self.assertFalse(calendar.isShared())
+
+
+ @inlineCallbacks
+ def test_accept_share(self):
+ """
+ Test that invite+accept creates shares and notifications.
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+
+ inviteUID = shareeView.shareUID()
+ sharedName = shareeView.name()
+
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+
+ notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 1)
+ yield self.otherCommit()
+
+ # Accept
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.acceptShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is not None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ # Re-accept
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.acceptShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is not None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+
+ @inlineCallbacks
+ def test_decline_share(self):
+ """
+ Test that invite+decline does not create shares but does create notifications.
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+
+ inviteUID = shareeView.shareUID()
+ sharedName = shareeView.name()
+
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+
+ notifyHome = yield txn2.notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 1)
+ yield self.otherCommit()
+
+ # Decline
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.declineShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ # Redecline
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.declineShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+
+ @inlineCallbacks
+ def test_accept_decline_share(self):
+ """
+ Test that invite+accept/decline creates/removes shares and notifications.
+ Decline via the home.
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+ inviteUID = shareeView.shareUID()
+
+ sharedName = shareeView.name()
+
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+
+ notifyHome = yield txn2.notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 1)
+ yield self.otherCommit()
+
+ # Accept
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.acceptShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is not None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ # Decline
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.declineShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+
+ @inlineCallbacks
+ def test_accept_remove_share(self):
+ """
+ Test that invite+accept/decline creates/removes shares and notifications.
+ Decline via the shared collection (removal).
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+
+ inviteUID = shareeView.shareUID()
+ sharedName = shareeView.name()
+
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+
+ notifyHome = yield txn2.notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 1)
+ yield self.otherCommit()
+
+ # Accept
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.acceptShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is not None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ yield self.commit()
+
+ # Delete
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ yield shared.deleteShare()
+ yield self.otherCommit()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+
+ @inlineCallbacks
+ def test_accept_remove_accept(self):
+ yield self.createShare()
+ yield self.removeShare()
+ shared_name = yield self.createShare()
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+ self.assertTrue(otherCal is not None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_accept_remove_accept_newcalendar(self):
+ """
+ Test that deleting and re-creating a share with the same sharer name works.
+ """
+
+ home = yield self.homeUnderTest(name="user01", create=True)
+ yield home.createCalendarWithName("shared")
+ yield self.commit()
+
+ shared_name = yield self.createShare(name="shared")
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+ self.assertTrue(otherCal is not None)
+ yield self.otherCommit()
+
+ yield self.removeShare(name="shared")
+ home = yield self.homeUnderTest(name="user01", create=True)
+ yield home.removeCalendarWithName("shared")
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+ self.assertTrue(otherCal is None)
+ yield self.otherCommit()
+
+ home = yield self.homeUnderTest(name="user01", create=True)
+ yield home.createCalendarWithName("shared")
+ yield self.commit()
+
+ shared_name = yield self.createShare(name="shared")
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+ self.assertTrue(otherCal is not None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_inviteProperties(self):
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar.setUsedForFreeBusy(True)
+ yield self.commit()
+
+ shared_name = yield self.createShare()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+ self.assertFalse(shared.isUsedForFreeBusy())
+
+
+ @inlineCallbacks
+ def test_direct_sharee(self):
+ """
+ Test invite/uninvite creates/removes shares and notifications.
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+ shareeView = yield calendar.directShareWithUser("puser02")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+ self.assertEqual(invites[0].uid, shareeView.shareUID())
+ self.assertEqual(invites[0].ownerUID, "user01")
+ self.assertEqual(invites[0].shareeUID, "puser02")
+ self.assertEqual(invites[0].mode, _BIND_MODE_DIRECT)
+ self.assertEqual(invites[0].status, _BIND_STATUS_ACCEPTED)
+
+ sharedName = shareeView.name()
+
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="user02", name=sharedName)
+ self.assertTrue(shared is not None)
+
+ notifyHome = yield txn2.notificationsWithUID("user02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 0)
+ yield self.otherCommit()
+
+ # Remove
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ yield shared.deleteShare()
+ yield self.otherCommit()
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 0)
+
+ test_direct_sharee.skip = True
+
+ @inlineCallbacks
+ def test_sharedNotifierID(self):
+ shared_name = yield self.createShare()
+
+ home = yield self.homeUnderTest(name="user01")
+ self.assertEquals(home.notifierID(), ("CalDAV", "user01",))
+ calendar = yield home.calendarWithName("calendar")
+ self.assertEquals(calendar.notifierID(), ("CalDAV", "user01/calendar",))
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ home = yield self.homeUnderTest(txn=txn2, name="puser02")
+ self.assertEquals(home.notifierID(), ("CalDAV", "puser02",))
+ calendar = yield home.calendarWithName(shared_name)
+ self.assertEquals(calendar.notifierID(), ("CalDAV", "user01/calendar",))
+
+
+ @inlineCallbacks
+ def test_sharedWithTwo(self):
+ shared_name1 = yield self.createShare(shareeGUID="puser02")
+ shared_name2 = yield self.createShare(shareeGUID="puser03")
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name1)
+ self.assertTrue(otherCal is not None)
+ yield self.otherCommit()
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser03", name=shared_name2)
+ self.assertTrue(otherCal is not None)
+ yield self.otherCommit()
+
+
+
+class SharingRevisions(BaseSharingTests):
+ """
+ Test store-based sharing and interaction with revision table.
+ """
+
+ @inlineCallbacks
+ def test_shareWithRevision(self):
+ """
+ Verify that bindRevision on calendars and shared calendars has the correct value.
+ """
+ sharedName = yield self.createShare()
+
+ normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertEqual(normalCal._bindRevision, 0)
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+ @inlineCallbacks
+ def test_updateShareRevision(self):
+ """
+ Verify that bindRevision on calendars and shared calendars has the correct value.
+ """
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ newCalName = shareeView.shareUID()
+ yield self.commit()
+
+ normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertEqual(normalCal._bindRevision, 0)
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ otherCal = yield otherHome.anyObjectWithShareUID(newCalName)
+ self.assertEqual(otherCal._bindRevision, 0)
+ yield self.otherCommit()
+
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ shareeView = yield shareeHome.acceptShare(newCalName)
+ sharedName = shareeView.name()
+ yield self.otherCommit()
+
+ normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertEqual(normalCal._bindRevision, 0)
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+ @inlineCallbacks
+ def test_sharedRevisions(self):
+ """
+ Verify that resourceNamesSinceRevision returns all resources after initial bind and sync.
+ """
+ sharedName = yield self.createShare()
+
+ normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertEqual(normalCal._bindRevision, 0)
+
+ txn2 = self.newOtherTransaction()
+ otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertNotEqual(otherCal._bindRevision, 0)
+
+ sync_token = yield otherCal.syncToken()
+ revision = otherCal.revisionFromToken(sync_token)
+
+ changed, deleted, invalid = yield otherCal.resourceNamesSinceRevision(0)
+ self.assertNotEqual(len(changed), 0)
+ self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
+
+ changed, deleted, invalid = yield otherCal.resourceNamesSinceRevision(revision)
+ self.assertEqual(len(changed), 0)
+ self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
+
+ sync_token = yield otherHome.syncToken()
+ revision = otherHome.revisionFromToken(sync_token)
+
+ for depth in ("1", "infinity",):
+ changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision - 1, depth)
+ self.assertEqual(len(changed), 0 if depth == "infinity" else 1)
+ self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 1 if depth == "infinity" else 0)
+
+ changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision, depth)
+ self.assertEqual(len(changed), 0)
+ self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 1 if depth == "infinity" else 0)
+
+ yield self.otherCommit()
+
+ yield self.removeShare()
+
+ txn2 = self.newOtherTransaction()
+ otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+
+ for depth in ("1", "infinity",):
+ changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision, depth)
+ self.assertEqual(len(changed), 0)
+ self.assertEqual(len(deleted), 1)
+ self.assertEqual(len(invalid), 0)
Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_sharing.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_sharing.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_sharing.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -554,19 +554,23 @@
otherCal = yield self.calendarUnderTest(home="user02", name=sharedName)
self.assertNotEqual(otherCal._bindRevision, 0)
- changed, deleted = yield otherCal.resourceNamesSinceRevision(0)
+ changed, deleted, invalid = yield otherCal.resourceNamesSinceRevision(0)
self.assertNotEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
- changed, deleted = yield otherCal.resourceNamesSinceRevision(otherCal._bindRevision)
+ changed, deleted, invalid = yield otherCal.resourceNamesSinceRevision(otherCal._bindRevision)
self.assertEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
for depth in ("1", "infinity",):
- changed, deleted = yield otherHome.resourceNamesSinceRevision(0, depth)
+ changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(0, depth)
self.assertNotEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
- changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision, depth)
+ changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision, depth)
self.assertEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_util.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_util.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_util.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -13,7 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
-from txdav.caldav.datastore.test.util import buildCalendarStore
"""
Tests for txdav.caldav.datastore.util.
@@ -30,6 +29,7 @@
from twistedcaldav.test.util import TestCase
from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
+from txdav.caldav.datastore.test.util import buildCalendarStore
from txdav.caldav.datastore.util import dropboxIDFromCalendarObject, \
StorageTransportBase, migrateHome
@@ -323,7 +323,12 @@
@inlineCallbacks
def setUp(self):
yield super(HomeMigrationTests, self).setUp()
- self.theStore = yield buildCalendarStore(self, self.notifierFactory, homes=("conflict1", "conflict2",))
+ self.theStore = yield buildCalendarStore(self, self.notifierFactory, homes=(
+ "conflict1",
+ "conflict2",
+ "empty_home",
+ "non_empty_home",
+ ))
def storeUnderTest(self):
Modified: CalendarServer/trunk/txdav/caldav/datastore/test/util.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/util.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/util.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -62,18 +62,13 @@
calendarUserAddresses,
cutype="INDIVIDUAL",
thisServer=True,
+ server=None,
extras={},
):
- super(TestCalendarStoreDirectoryRecord, self).__init__(uid, shortNames,
- fullName, extras=extras)
- self.uid = uid
- self.shortNames = shortNames
- self.fullName = fullName
- self.displayName = self.fullName if self.fullName else self.shortNames[0]
+ super(TestCalendarStoreDirectoryRecord, self).__init__(uid, shortNames, fullName, thisServer, server, extras=extras)
self.calendarUserAddresses = calendarUserAddresses
self.cutype = cutype
- self._thisServer = thisServer
def canonicalCalendarUserAddress(self):
@@ -92,10 +87,6 @@
return cua
- def thisServer(self):
- return self._thisServer
-
-
def calendarsEnabled(self):
return True
@@ -150,12 +141,19 @@
homes.update((
"home1",
"home2",
- "Home_attachments",
+ "home3",
+ "home_attachments",
"home_bad",
"home_defaults",
"home_no_splits",
+ "home_provision1",
+ "home_provision2",
"home_splits",
"home_splits_shared",
+ "uid1",
+ "uid2",
+ "new-home",
+ "xyzzy",
))
for uid in homes:
directory.addRecord(buildDirectoryRecord(uid))
Modified: CalendarServer/trunk/txdav/caldav/icalendardirectoryservice.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/icalendardirectoryservice.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/icalendardirectoryservice.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -60,14 +60,6 @@
@rtype: C{str}
"""
- def thisServer(): #@NoSelf
- """
- Indicates whether the record is hosted on this server "pod".
-
- @return: C{True} if hosted by this service.
- @rtype: C{bool}
- """
-
def calendarsEnabled(): #@NoSelf
"""
Indicates whether the record enabled for using the calendar service.
Modified: CalendarServer/trunk/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/icalendarstore.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/icalendarstore.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -907,7 +907,7 @@
NORMAL_NO_IMPLICIT - this is an application layer (user) generated remove that deliberately turns
off implicit scheduling operations.
- INTERNAL - remove the resource without implicit scheduling.
+ INTERNAL - remove the resource without implicit scheduling or attachment processing.
"""
NORMAL = NamedConstant()
Modified: CalendarServer/trunk/txdav/carddav/datastore/index_file.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/index_file.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/carddav/datastore/index_file.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -37,10 +37,6 @@
from twisted.internet.defer import maybeDeferred
-from twistedcaldav import carddavxml
-from txdav.common.icommondatastore import SyncTokenValidException, \
- ReservationError
-from twistedcaldav.query import addressbookquery
from twistedcaldav.sql import AbstractSQLDatabase
from twistedcaldav.sql import db_prefix
from twistedcaldav.vcard import Component
@@ -49,6 +45,12 @@
from twistedcaldav.config import config
from twistedcaldav.memcachepool import CachePoolUserMixIn
+from txdav.carddav.datastore.query.builder import buildExpression
+from txdav.carddav.datastore.query.filter import Filter
+from txdav.common.datastore.query.filegenerator import sqllitegenerator
+from txdav.common.icommondatastore import SyncTokenValidException, \
+ ReservationError
+
log = Logger()
db_basename = db_prefix + "sqlite"
@@ -218,6 +220,24 @@
+def sqladdressbookquery(filter, addressbookid=None):
+ """
+ Convert the supplied addressbook-query into a partial SQL statement.
+
+ @param filter: the L{Filter} for the addressbook-query to convert.
+ @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+ and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+ Or return C{None} if it is not possible to create an SQL query to fully match the addressbook-query.
+ """
+ try:
+ expression = buildExpression(filter, sqllitegenerator.FIELDS)
+ sql = sqllitegenerator(expression, addressbookid, None)
+ return sql.generate()
+ except ValueError:
+ return None
+
+
+
class AddressBookIndex(AbstractSQLDatabase):
"""
AddressBook collection index abstract base class that defines the apis for the index.
@@ -410,6 +430,7 @@
changed = []
deleted = []
+ invalid = []
for name, wasdeleted in results:
if name:
if wasdeleted == 'Y':
@@ -420,7 +441,7 @@
else:
raise SyncTokenValidException
- return changed, deleted,
+ return (changed, deleted, invalid)
def lastRevision(self):
@@ -445,8 +466,8 @@
def searchValid(self, filter):
- if isinstance(filter, carddavxml.Filter):
- qualifiers = addressbookquery.sqladdressbookquery(filter)
+ if isinstance(filter, Filter):
+ qualifiers = sqladdressbookquery(filter)
else:
qualifiers = None
@@ -466,8 +487,8 @@
# start caching...
# Make sure we have a proper Filter element and get the partial SQL statement to use.
- if isinstance(filter, carddavxml.Filter):
- qualifiers = addressbookquery.sqladdressbookquery(filter)
+ if isinstance(filter, Filter):
+ qualifiers = sqladdressbookquery(filter)
else:
qualifiers = None
if qualifiers is not None:
Deleted: CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/__init__.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 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.
-##
Copied: CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 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.
+##
Deleted: CalendarServer/trunk/txdav/carddav/datastore/query/builder.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/builder.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/builder.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,107 +0,0 @@
-##
-# Copyright (c) 2006-2013 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 txdav.common.datastore.query import expression
-from txdav.carddav.datastore.query.filter import TextMatch
-
-"""
-SQL statement generator from query expressions.
-"""
-
-__all__ = [
- "buildExpression",
-]
-
-
-
-# SQL Index column (field) names
-
-def buildExpression(filter, fields):
- """
- Convert the supplied addressbook-query into an expression tree.
-
- @param filter: the L{Filter} for the addressbook-query to convert.
- @return: a L{baseExpression} for the expression tree.
- """
- # Lets assume we have a valid filter from the outset.
-
- # Top-level filter contains zero or more prop-filter element
- if len(filter.children) > 0:
- return propfilterListExpression(filter.children, fields)
- else:
- return expression.allExpression()
-
-
-
-def propfilterListExpression(propfilters, fields):
- """
- Create an expression for a list of prop-filter elements.
-
- @param propfilters: the C{list} of L{ComponentFilter} elements.
- @return: a L{baseExpression} for the expression tree.
- """
-
- if len(propfilters) == 1:
- return propfilterExpression(propfilters[0], fields)
- else:
- return expression.orExpression([propfilterExpression(c, fields) for c in propfilters])
-
-
-
-def propfilterExpression(propfilter, fields):
- """
- Create an expression for a single prop-filter element.
-
- @param propfilter: the L{PropertyFilter} element.
- @return: a L{baseExpression} for the expression tree.
- """
-
- # Only handle UID right now
- if propfilter.filter_name != "UID":
- raise ValueError
-
- # Handle is-not-defined case
- if not propfilter.defined:
- # Test for <<field>> != "*"
- return expression.isExpression(fields["UID"], "", True)
-
- # Handle embedded parameters/text-match
- params = []
- for filter in propfilter.filters:
- if isinstance(filter, 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:
- if propfilter.propfilter_test == "anyof":
- return expression.orExpression(params)
- else:
- return expression.andExpression(params)
- elif len(params) == 1:
- return params[0]
- else:
- return None
Copied: CalendarServer/trunk/txdav/carddav/datastore/query/builder.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/builder.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/query/builder.py (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/builder.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,107 @@
+##
+# Copyright (c) 2006-2013 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 txdav.common.datastore.query import expression
+from txdav.carddav.datastore.query.filter import TextMatch
+
+"""
+SQL statement generator from query expressions.
+"""
+
+__all__ = [
+ "buildExpression",
+]
+
+
+
+# SQL Index column (field) names
+
+def buildExpression(filter, fields):
+ """
+ Convert the supplied addressbook-query into an expression tree.
+
+ @param filter: the L{Filter} for the addressbook-query to convert.
+ @return: a L{baseExpression} for the expression tree.
+ """
+ # Lets assume we have a valid filter from the outset.
+
+ # Top-level filter contains zero or more prop-filter element
+ if len(filter.children) > 0:
+ return propfilterListExpression(filter.children, fields)
+ else:
+ return expression.allExpression()
+
+
+
+def propfilterListExpression(propfilters, fields):
+ """
+ Create an expression for a list of prop-filter elements.
+
+ @param propfilters: the C{list} of L{ComponentFilter} elements.
+ @return: a L{baseExpression} for the expression tree.
+ """
+
+ if len(propfilters) == 1:
+ return propfilterExpression(propfilters[0], fields)
+ else:
+ return expression.orExpression([propfilterExpression(c, fields) for c in propfilters])
+
+
+
+def propfilterExpression(propfilter, fields):
+ """
+ Create an expression for a single prop-filter element.
+
+ @param propfilter: the L{PropertyFilter} element.
+ @return: a L{baseExpression} for the expression tree.
+ """
+
+ # Only handle UID right now
+ if propfilter.filter_name != "UID":
+ raise ValueError
+
+ # Handle is-not-defined case
+ if not propfilter.defined:
+ # Test for <<field>> != "*"
+ return expression.isExpression(fields["UID"], "", True)
+
+ # Handle embedded parameters/text-match
+ params = []
+ for filter in propfilter.filters:
+ if isinstance(filter, 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:
+ if propfilter.propfilter_test == "anyof":
+ return expression.orExpression(params)
+ else:
+ return expression.andExpression(params)
+ elif len(params) == 1:
+ return params[0]
+ else:
+ return None
Deleted: CalendarServer/trunk/txdav/carddav/datastore/query/filter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/filter.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/filter.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,444 +0,0 @@
-##
-# Copyright (c) 2011-2013 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.
-##
-
-"""
-Object model of CARDDAV:filter element used in an addressbook-query.
-"""
-
-__all__ = [
- "Filter",
-]
-
-from twext.python.log import Logger
-
-from twistedcaldav.carddavxml import carddav_namespace
-from twistedcaldav.vcard import Property
-
-log = Logger()
-
-class FilterBase(object):
- """
- Determines which matching components are returned.
- """
-
- serialized_name = None
- deserialize_names = {}
-
- @classmethod
- def serialize_register(cls, register):
- cls.deserialize_names[register.serialized_name] = register
-
-
- def __init__(self, xml_element):
- pass
-
-
- @classmethod
- def deserialize(cls, data):
- """
- Convert a JSON compatible serialization of this object into the actual object.
- """
- obj = cls.deserialize_names[data["type"]](None)
- obj._deserialize(data)
- return obj
-
-
- def _deserialize(self, data):
- """
- Convert a JSON compatible serialization of this object into the actual object.
- """
- pass
-
-
- def serialize(self):
- """
- Create a JSON compatible serialization of this object - will be used in a cross-pod request.
- """
- return {
- "type": self.serialized_name,
- }
-
-
- def match(self, item, access=None):
- raise NotImplementedError
-
-
- def valid(self, level=0):
- raise NotImplementedError
-
-
-
-class Filter(FilterBase):
- """
- Determines which matching components are returned.
- """
-
- serialized_name = "Filter"
-
- def __init__(self, xml_element):
-
- super(Filter, self).__init__(xml_element)
- if xml_element is None:
- return
-
- filter_test = xml_element.attributes.get("test", "anyof")
- if filter_test not in ("anyof", "allof"):
- raise ValueError("Test must be only one of anyof, allof")
-
- self.filter_test = filter_test
-
- self.children = [PropertyFilter(child) for child in xml_element.children]
-
-
- def _deserialize(self, data):
- """
- Convert a JSON compatible serialization of this object into the actual object.
- """
- self.filter_test = data["filter_test"]
- self.child = [FilterBase.deserialize(child) for child in data["children"]]
-
-
- def serialize(self):
- """
- Create a JSON compatible serialization of this object - will be used in a cross-pod request.
- """
- result = super(Filter, self).serialize()
- result.update({
- "filter_test": self.filter_test,
- "children": [child.serialize() for child in self.children],
- })
- return result
-
-
- def match(self, vcard):
- """
- Returns True if the given address property matches this filter, False
- otherwise. Empty element means always match.
- """
-
- if len(self.children) > 0:
- allof = self.filter_test == "allof"
- for propfilter in self.children:
- if allof != propfilter._match(vcard):
- return not allof
- return allof
- else:
- return True
-
-
- def valid(self):
- """
- Indicate whether this filter element's structure is valid wrt vCard
- data object model.
-
- @return: True if valid, False otherwise
- """
-
- # Test each property
- for propfilter in self.children:
- if not propfilter.valid():
- return False
- else:
- return True
-
-FilterBase.serialize_register(Filter)
-
-
-
-class FilterChildBase(FilterBase):
- """
- CardDAV filter element.
- """
-
- def __init__(self, xml_element):
-
- super(FilterChildBase, self).__init__(xml_element)
- if xml_element is None:
- return
-
- qualifier = None
- filters = []
-
- for child in xml_element.children:
- qname = child.qname()
-
- if qname in (
- (carddav_namespace, "is-not-defined"),
- ):
- if qualifier is not None:
- raise ValueError("Only one of CardDAV:is-not-defined allowed")
- qualifier = IsNotDefined(child)
-
- elif qname == (carddav_namespace, "text-match"):
- filters.append(TextMatch(child))
-
- elif qname == (carddav_namespace, "param-filter"):
- filters.append(ParameterFilter(child))
- else:
- raise ValueError("Unknown child element: %s" % (qname,))
-
- if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
- raise ValueError("No other tests allowed when CardDAV:is-not-defined is present")
-
- if xml_element.qname() == (carddav_namespace, "prop-filter"):
- propfilter_test = xml_element.attributes.get("test", "anyof")
- if propfilter_test not in ("anyof", "allof"):
- raise ValueError("Test must be only one of anyof, allof")
- else:
- propfilter_test = "anyof"
-
- self.propfilter_test = propfilter_test
- self.qualifier = qualifier
- self.filters = filters
- self.filter_name = xml_element.attributes["name"]
- if isinstance(self.filter_name, unicode):
- self.filter_name = self.filter_name.encode("utf-8")
- self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined)
-
-
- def _deserialize(self, data):
- """
- Convert a JSON compatible serialization of this object into the actual object.
- """
- self.propfilter_test = data["propfilter_test"]
- self.qualifier = FilterBase.deserialize(data["qualifier"]) if data["qualifier"] else None
- self.filters = [FilterBase.deserialize(filter) for filter in data["filters"]]
- self.filter_name = data["filter_name"]
- self.defined = data["defined"]
-
-
- def serialize(self):
- """
- Create a JSON compatible serialization of this object - will be used in a cross-pod request.
- """
- result = super(FilterChildBase, self).serialize()
- result.update({
- "propfilter_test": self.propfilter_test,
- "qualifier": self.qualifier.serialize() if self.qualifier else None,
- "filters": [filter.serialize() for filter in self.filters],
- "filter_name": self.filter_name,
- "defined": self.defined,
- })
- return result
-
-
- def match(self, item):
- """
- Returns True if the given address book item (either a property or parameter value)
- matches this filter, False otherwise.
- """
-
- # Always return True for the is-not-defined case as the result of this will
- # be negated by the caller
- if not self.defined:
- return True
-
- if self.qualifier and not self.qualifier.match(item):
- return False
-
- if len(self.filters) > 0:
- allof = self.propfilter_test == "allof"
- for filter in self.filters:
- if allof != filter._match(item):
- return not allof
- return allof
- else:
- return True
-
-
-
-class PropertyFilter (FilterChildBase):
- """
- Limits a search to specific properties.
- """
-
- serialized_name = "PropertyFilter"
-
- 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
- else:
- return not self.defined
- return self.defined
-
-
- def valid(self):
- """
- Indicate whether this filter element's structure is valid wrt vCard
- data object model.
-
- @return: True if valid, False otherwise
- """
-
- # No tests
- return True
-
-FilterBase.serialize_register(PropertyFilter)
-
-
-
-class ParameterFilter (FilterChildBase):
- """
- Limits a search to specific parameters.
- """
-
- serialized_name = "ParameterFilter"
-
- def _match(self, property):
-
- # At least one parameter must match (or is-not-defined is set)
- result = not self.defined
- for parameterName in property.parameterNames():
- if parameterName.upper() == self.filter_name.upper() and self.match([property.parameterValues(parameterName)]):
- result = self.defined
- break
-
- return result
-
-FilterBase.serialize_register(ParameterFilter)
-
-
-
-class IsNotDefined (FilterBase):
- """
- Specifies that the named iCalendar item does not exist.
- """
-
- serialized_name = "IsNotDefined"
-
- def match(self, component, access=None):
- # Oddly, this needs always to return True so that it appears there is
- # a match - but we then "negate" the result if is-not-defined is set.
- # Actually this method should never be called as we special case the
- # is-not-defined option.
- return True
-
-FilterBase.serialize_register(IsNotDefined)
-
-
-
-class TextMatch (FilterBase):
- """
- Specifies a substring match on a property or parameter value.
- """
- serialized_name = "TextMatch"
-
- def __init__(self, xml_element):
-
- super(TextMatch, self).__init__(xml_element)
- if xml_element is None:
- return
-
- self.text = str(xml_element)
-
- if "collation" in xml_element.attributes:
- self.collation = xml_element.attributes["collation"]
- else:
- self.collation = "i;unicode-casemap"
-
- if "negate-condition" in xml_element.attributes:
- self.negate = xml_element.attributes["negate-condition"]
- if self.negate not in ("yes", "no"):
- self.negate = "no"
- self.negate = {"yes": True, "no": False}[self.negate]
- 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 _deserialize(self, data):
- """
- Convert a JSON compatible serialization of this object into the actual object.
- """
- self.text = data["text"]
- self.collation = data["collation"]
- self.negate = data["negate"]
- self.match_type = data["match_type"]
-
-
- def serialize(self):
- """
- Create a JSON compatible serialization of this object - will be used in a cross-pod request.
- """
- result = super(TextMatch, self).serialize()
- result.update({
- "text": self.text,
- "collation": self.collation,
- "negate": self.negate,
- "match_type": self.match_type,
- })
- return result
-
-
- def _match(self, item):
- """
- Match the text for the item.
- If the item is a property, then match the property value,
- otherwise it may be a list of parameter values - try to match anyone of those
- """
- if item is None:
- return False
-
- if isinstance(item, Property):
- values = [item.strvalue()]
- else:
- values = item
-
- test = unicode(self.text, "utf-8").lower()
-
-
- def _textCompare(s):
- # Currently ignores the collation and does caseless matching
- 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:
- 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:
- if _textCompare(unicode(subvalue, "utf-8")):
- return not self.negate
- else:
- if _textCompare(unicode(value, "utf-8")):
- return not self.negate
-
- return self.negate
-
-FilterBase.serialize_register(TextMatch)
Copied: CalendarServer/trunk/txdav/carddav/datastore/query/filter.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/filter.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/query/filter.py (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/filter.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,444 @@
+##
+# Copyright (c) 2011-2013 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.
+##
+
+"""
+Object model of CARDDAV:filter element used in an addressbook-query.
+"""
+
+__all__ = [
+ "Filter",
+]
+
+from twext.python.log import Logger
+
+from twistedcaldav.carddavxml import carddav_namespace
+from twistedcaldav.vcard import Property
+
+log = Logger()
+
+class FilterBase(object):
+ """
+ Determines which matching components are returned.
+ """
+
+ serialized_name = None
+ deserialize_names = {}
+
+ @classmethod
+ def serialize_register(cls, register):
+ cls.deserialize_names[register.serialized_name] = register
+
+
+ def __init__(self, xml_element):
+ pass
+
+
+ @classmethod
+ def deserialize(cls, data):
+ """
+ Convert a JSON compatible serialization of this object into the actual object.
+ """
+ obj = cls.deserialize_names[data["type"]](None)
+ obj._deserialize(data)
+ return obj
+
+
+ def _deserialize(self, data):
+ """
+ Convert a JSON compatible serialization of this object into the actual object.
+ """
+ pass
+
+
+ def serialize(self):
+ """
+ Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+ """
+ return {
+ "type": self.serialized_name,
+ }
+
+
+ def match(self, item, access=None):
+ raise NotImplementedError
+
+
+ def valid(self, level=0):
+ raise NotImplementedError
+
+
+
+class Filter(FilterBase):
+ """
+ Determines which matching components are returned.
+ """
+
+ serialized_name = "Filter"
+
+ def __init__(self, xml_element):
+
+ super(Filter, self).__init__(xml_element)
+ if xml_element is None:
+ return
+
+ filter_test = xml_element.attributes.get("test", "anyof")
+ if filter_test not in ("anyof", "allof"):
+ raise ValueError("Test must be only one of anyof, allof")
+
+ self.filter_test = filter_test
+
+ self.children = [PropertyFilter(child) for child in xml_element.children]
+
+
+ def _deserialize(self, data):
+ """
+ Convert a JSON compatible serialization of this object into the actual object.
+ """
+ self.filter_test = data["filter_test"]
+ self.child = [FilterBase.deserialize(child) for child in data["children"]]
+
+
+ def serialize(self):
+ """
+ Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+ """
+ result = super(Filter, self).serialize()
+ result.update({
+ "filter_test": self.filter_test,
+ "children": [child.serialize() for child in self.children],
+ })
+ return result
+
+
+ def match(self, vcard):
+ """
+ Returns True if the given address property matches this filter, False
+ otherwise. Empty element means always match.
+ """
+
+ if len(self.children) > 0:
+ allof = self.filter_test == "allof"
+ for propfilter in self.children:
+ if allof != propfilter._match(vcard):
+ return not allof
+ return allof
+ else:
+ return True
+
+
+ def valid(self):
+ """
+ Indicate whether this filter element's structure is valid wrt vCard
+ data object model.
+
+ @return: True if valid, False otherwise
+ """
+
+ # Test each property
+ for propfilter in self.children:
+ if not propfilter.valid():
+ return False
+ else:
+ return True
+
+FilterBase.serialize_register(Filter)
+
+
+
+class FilterChildBase(FilterBase):
+ """
+ CardDAV filter element.
+ """
+
+ def __init__(self, xml_element):
+
+ super(FilterChildBase, self).__init__(xml_element)
+ if xml_element is None:
+ return
+
+ qualifier = None
+ filters = []
+
+ for child in xml_element.children:
+ qname = child.qname()
+
+ if qname in (
+ (carddav_namespace, "is-not-defined"),
+ ):
+ if qualifier is not None:
+ raise ValueError("Only one of CardDAV:is-not-defined allowed")
+ qualifier = IsNotDefined(child)
+
+ elif qname == (carddav_namespace, "text-match"):
+ filters.append(TextMatch(child))
+
+ elif qname == (carddav_namespace, "param-filter"):
+ filters.append(ParameterFilter(child))
+ else:
+ raise ValueError("Unknown child element: %s" % (qname,))
+
+ if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
+ raise ValueError("No other tests allowed when CardDAV:is-not-defined is present")
+
+ if xml_element.qname() == (carddav_namespace, "prop-filter"):
+ propfilter_test = xml_element.attributes.get("test", "anyof")
+ if propfilter_test not in ("anyof", "allof"):
+ raise ValueError("Test must be only one of anyof, allof")
+ else:
+ propfilter_test = "anyof"
+
+ self.propfilter_test = propfilter_test
+ self.qualifier = qualifier
+ self.filters = filters
+ self.filter_name = xml_element.attributes["name"]
+ if isinstance(self.filter_name, unicode):
+ self.filter_name = self.filter_name.encode("utf-8")
+ self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined)
+
+
+ def _deserialize(self, data):
+ """
+ Convert a JSON compatible serialization of this object into the actual object.
+ """
+ self.propfilter_test = data["propfilter_test"]
+ self.qualifier = FilterBase.deserialize(data["qualifier"]) if data["qualifier"] else None
+ self.filters = [FilterBase.deserialize(filter) for filter in data["filters"]]
+ self.filter_name = data["filter_name"]
+ self.defined = data["defined"]
+
+
+ def serialize(self):
+ """
+ Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+ """
+ result = super(FilterChildBase, self).serialize()
+ result.update({
+ "propfilter_test": self.propfilter_test,
+ "qualifier": self.qualifier.serialize() if self.qualifier else None,
+ "filters": [filter.serialize() for filter in self.filters],
+ "filter_name": self.filter_name,
+ "defined": self.defined,
+ })
+ return result
+
+
+ def match(self, item):
+ """
+ Returns True if the given address book item (either a property or parameter value)
+ matches this filter, False otherwise.
+ """
+
+ # Always return True for the is-not-defined case as the result of this will
+ # be negated by the caller
+ if not self.defined:
+ return True
+
+ if self.qualifier and not self.qualifier.match(item):
+ return False
+
+ if len(self.filters) > 0:
+ allof = self.propfilter_test == "allof"
+ for filter in self.filters:
+ if allof != filter._match(item):
+ return not allof
+ return allof
+ else:
+ return True
+
+
+
+class PropertyFilter (FilterChildBase):
+ """
+ Limits a search to specific properties.
+ """
+
+ serialized_name = "PropertyFilter"
+
+ 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
+ else:
+ return not self.defined
+ return self.defined
+
+
+ def valid(self):
+ """
+ Indicate whether this filter element's structure is valid wrt vCard
+ data object model.
+
+ @return: True if valid, False otherwise
+ """
+
+ # No tests
+ return True
+
+FilterBase.serialize_register(PropertyFilter)
+
+
+
+class ParameterFilter (FilterChildBase):
+ """
+ Limits a search to specific parameters.
+ """
+
+ serialized_name = "ParameterFilter"
+
+ def _match(self, property):
+
+ # At least one parameter must match (or is-not-defined is set)
+ result = not self.defined
+ for parameterName in property.parameterNames():
+ if parameterName.upper() == self.filter_name.upper() and self.match([property.parameterValues(parameterName)]):
+ result = self.defined
+ break
+
+ return result
+
+FilterBase.serialize_register(ParameterFilter)
+
+
+
+class IsNotDefined (FilterBase):
+ """
+ Specifies that the named iCalendar item does not exist.
+ """
+
+ serialized_name = "IsNotDefined"
+
+ def match(self, component, access=None):
+ # Oddly, this needs always to return True so that it appears there is
+ # a match - but we then "negate" the result if is-not-defined is set.
+ # Actually this method should never be called as we special case the
+ # is-not-defined option.
+ return True
+
+FilterBase.serialize_register(IsNotDefined)
+
+
+
+class TextMatch (FilterBase):
+ """
+ Specifies a substring match on a property or parameter value.
+ """
+ serialized_name = "TextMatch"
+
+ def __init__(self, xml_element):
+
+ super(TextMatch, self).__init__(xml_element)
+ if xml_element is None:
+ return
+
+ self.text = str(xml_element)
+
+ if "collation" in xml_element.attributes:
+ self.collation = xml_element.attributes["collation"]
+ else:
+ self.collation = "i;unicode-casemap"
+
+ if "negate-condition" in xml_element.attributes:
+ self.negate = xml_element.attributes["negate-condition"]
+ if self.negate not in ("yes", "no"):
+ self.negate = "no"
+ self.negate = {"yes": True, "no": False}[self.negate]
+ 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 _deserialize(self, data):
+ """
+ Convert a JSON compatible serialization of this object into the actual object.
+ """
+ self.text = data["text"]
+ self.collation = data["collation"]
+ self.negate = data["negate"]
+ self.match_type = data["match_type"]
+
+
+ def serialize(self):
+ """
+ Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+ """
+ result = super(TextMatch, self).serialize()
+ result.update({
+ "text": self.text,
+ "collation": self.collation,
+ "negate": self.negate,
+ "match_type": self.match_type,
+ })
+ return result
+
+
+ def _match(self, item):
+ """
+ Match the text for the item.
+ If the item is a property, then match the property value,
+ otherwise it may be a list of parameter values - try to match anyone of those
+ """
+ if item is None:
+ return False
+
+ if isinstance(item, Property):
+ values = [item.strvalue()]
+ else:
+ values = item
+
+ test = unicode(self.text, "utf-8").lower()
+
+
+ def _textCompare(s):
+ # Currently ignores the collation and does caseless matching
+ 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:
+ 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:
+ if _textCompare(unicode(subvalue, "utf-8")):
+ return not self.negate
+ else:
+ if _textCompare(unicode(value, "utf-8")):
+ return not self.negate
+
+ return self.negate
+
+FilterBase.serialize_register(TextMatch)
Deleted: CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/test/__init__.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 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.
-##
Copied: CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/test/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 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.
+##
Deleted: CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/test/test_filter.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,96 +0,0 @@
-##
-# Copyright (c) 2011-2013 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 twext.enterprise.dal.syntax import SQLFragment
-
-from twisted.trial.unittest import TestCase
-
-from twistedcaldav import carddavxml
-
-from txdav.carddav.datastore.query.filter import Filter, FilterBase
-from txdav.common.datastore.sql_tables import schema
-from txdav.carddav.datastore.query.builder import buildExpression
-from txdav.common.datastore.query.generator import SQLQueryGenerator
-from txdav.carddav.datastore.index_file import sqladdressbookquery
-
-class TestQueryFilter(TestCase):
-
- _objectSchema = schema.ADDRESSBOOK_OBJECT
- _queryFields = {
- "UID": _objectSchema.UID
- }
-
- def test_query(self):
- """
- Basic query test - single term.
- Only UID can be queried via sql.
- """
-
- filter = carddavxml.Filter(
- *[carddavxml.PropertyFilter(
- carddavxml.TextMatch.fromString("Example"),
- **{"name":"UID"}
- )]
- )
- filter = Filter(filter)
-
- expression = buildExpression(filter, self._queryFields)
- sql = SQLQueryGenerator(expression, self, 1234)
- select, args = sql.generate()
-
- self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, VCARD_UID from ADDRESSBOOK_OBJECT where ADDRESSBOOK_HOME_RESOURCE_ID = ? and VCARD_UID like (? || (? || ?))", [1234, "%", "Example", "%"]))
- self.assertEqual(args, {})
-
-
- def test_sqllite_query(self):
- """
- Basic query test - single term.
- Only UID can be queried via sql.
- """
-
- filter = carddavxml.Filter(
- *[carddavxml.PropertyFilter(
- carddavxml.TextMatch.fromString("Example"),
- **{"name":"UID"}
- )]
- )
- filter = Filter(filter)
- sql, args = sqladdressbookquery(filter, 1234)
-
- self.assertEqual(sql, " from RESOURCE where RESOURCE.UID GLOB :1")
- self.assertEqual(args, ["*Example*"])
-
-
-
-class TestQueryFilterSerialize(TestCase):
-
- def test_query(self):
- """
- Basic query test - no time range
- """
-
- filter = carddavxml.Filter(
- *[carddavxml.PropertyFilter(
- carddavxml.TextMatch.fromString("Example"),
- **{"name":"UID"}
- )]
- )
- filter = Filter(filter)
- j = filter.serialize()
- self.assertEqual(j["type"], "Filter")
-
- f = FilterBase.deserialize(j)
- self.assertTrue(isinstance(f, Filter))
Copied: CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/test/test_filter.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,96 @@
+##
+# Copyright (c) 2011-2013 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 twext.enterprise.dal.syntax import SQLFragment
+
+from twisted.trial.unittest import TestCase
+
+from twistedcaldav import carddavxml
+
+from txdav.carddav.datastore.query.filter import Filter, FilterBase
+from txdav.common.datastore.sql_tables import schema
+from txdav.carddav.datastore.query.builder import buildExpression
+from txdav.common.datastore.query.generator import SQLQueryGenerator
+from txdav.carddav.datastore.index_file import sqladdressbookquery
+
+class TestQueryFilter(TestCase):
+
+ _objectSchema = schema.ADDRESSBOOK_OBJECT
+ _queryFields = {
+ "UID": _objectSchema.UID
+ }
+
+ def test_query(self):
+ """
+ Basic query test - single term.
+ Only UID can be queried via sql.
+ """
+
+ filter = carddavxml.Filter(
+ *[carddavxml.PropertyFilter(
+ carddavxml.TextMatch.fromString("Example"),
+ **{"name":"UID"}
+ )]
+ )
+ filter = Filter(filter)
+
+ expression = buildExpression(filter, self._queryFields)
+ sql = SQLQueryGenerator(expression, self, 1234)
+ select, args = sql.generate()
+
+ self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, VCARD_UID from ADDRESSBOOK_OBJECT where ADDRESSBOOK_HOME_RESOURCE_ID = ? and VCARD_UID like (? || (? || ?))", [1234, "%", "Example", "%"]))
+ self.assertEqual(args, {})
+
+
+ def test_sqllite_query(self):
+ """
+ Basic query test - single term.
+ Only UID can be queried via sql.
+ """
+
+ filter = carddavxml.Filter(
+ *[carddavxml.PropertyFilter(
+ carddavxml.TextMatch.fromString("Example"),
+ **{"name":"UID"}
+ )]
+ )
+ filter = Filter(filter)
+ sql, args = sqladdressbookquery(filter, 1234)
+
+ self.assertEqual(sql, " from RESOURCE where RESOURCE.UID GLOB :1")
+ self.assertEqual(args, ["*Example*"])
+
+
+
+class TestQueryFilterSerialize(TestCase):
+
+ def test_query(self):
+ """
+ Basic query test - no time range
+ """
+
+ filter = carddavxml.Filter(
+ *[carddavxml.PropertyFilter(
+ carddavxml.TextMatch.fromString("Example"),
+ **{"name":"UID"}
+ )]
+ )
+ filter = Filter(filter)
+ j = filter.serialize()
+ self.assertEqual(j["type"], "Filter")
+
+ f = FilterBase.deserialize(j)
+ self.assertTrue(isinstance(f, Filter))
Modified: CalendarServer/trunk/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/sql.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/carddav/datastore/sql.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -14,8 +14,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# #
-from txdav.xml import element
+
"""
SQL backend for CardDAV storage.
"""
@@ -47,12 +47,14 @@
from txdav.base.propertystore.base import PropertyName
from txdav.base.propertystore.sql import PropertyStore
+from txdav.carddav.datastore.query.builder import buildExpression
+from txdav.carddav.datastore.query.filter import Filter
from txdav.carddav.iaddressbookstore import IAddressBookHome, IAddressBook, \
IAddressBookObject, GroupWithUnsharedAddressNotAllowedError, \
KindChangeNotAllowedError
+from txdav.common.datastore.query.generator import SQLQueryGenerator
from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
CommonObjectResource, EADDRESSBOOKTYPE, SharingMixIn, SharingInvitation
-from txdav.common.datastore.sql_legacy import PostgresLegacyABIndexEmulator
from txdav.common.datastore.sql_tables import _ABO_KIND_PERSON, \
_ABO_KIND_GROUP, _ABO_KIND_RESOURCE, _ABO_KIND_LOCATION, schema, \
_BIND_MODE_OWN, _BIND_MODE_WRITE, _BIND_STATUS_ACCEPTED, \
@@ -61,7 +63,8 @@
InvalidUIDError, UIDExistsError, ObjectResourceTooBigError, \
InvalidObjectResourceError, InvalidComponentForStoreError, \
AllRetriesFailed, ObjectResourceNameAlreadyExistsError, \
- SyncTokenValidException
+ SyncTokenValidException, IndexedSearchException
+from txdav.xml import element
from zope.interface.declarations import implements
@@ -86,7 +89,6 @@
def __init__(self, transaction, ownerUID):
- self._childClass = AddressBook
super(AddressBookHome, self).__init__(transaction, ownerUID)
self._addressbookPropertyStoreID = None
self._addressbook = None
@@ -450,6 +452,8 @@
"""
implements(IAddressBook)
+ _homeType = EADDRESSBOOKTYPE
+
# structured tables. (new, preferred)
_homeSchema = schema.ADDRESSBOOK_HOME
_bindSchema = schema.SHARED_ADDRESSBOOK_BIND
@@ -458,14 +462,87 @@
_revisionsSchema = schema.ADDRESSBOOK_OBJECT_REVISIONS
_objectSchema = schema.ADDRESSBOOK_OBJECT
+ # Mapping of vCard property name to DB column name
+ _queryFields = {
+ "UID": _objectSchema.UID,
+ }
- def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None):
+
+ @classmethod
+ @inlineCallbacks
+ def _getDBDataIndirect(cls, home, name, resourceID, externalID):
+
+ # Get the bind row data
+ row = None
+
+ # TODO: query cacher
+
+ rows = None
+ ownerHome = None
+
+ # TODO: add queryCacher support
+
+ if rows is None:
+ # No cached copy
+ if name:
+ ownerHome = yield home._txn.addressbookHomeWithUID(name)
+ if ownerHome is None:
+ returnValue(None)
+ resourceID = ownerHome.addressbook()._resourceID
+ rows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
+ home._txn, homeID=home._resourceID, addressbookID=resourceID
+ )
+
+ if not rows:
+ returnValue(None)
+
+ groupID = None
+ overallBindStatus = _BIND_STATUS_INVITED
+ minBindRevision = None
+ for row in rows:
+ bindMode, homeID, resourceGroupID, externalID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
+ if groupID is None:
+ groupID = resourceGroupID
+ minBindRevision = min(minBindRevision, bindRevision) if minBindRevision is not None else bindRevision
+ if bindStatus == _BIND_STATUS_ACCEPTED:
+ overallBindStatus = _BIND_STATUS_ACCEPTED
+
+ if ownerHome is None:
+ ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, groupID)
+ ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
+
+ bindData = row[:cls.bindColumnCount]
+ additionalBindData = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+
+ # Adjust for aggregate values
+ bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)] = resourceID
+ bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] = ownerHome.uid()
+ bindData[cls.bindColumns().index(cls._bindSchema.BIND_MODE)] = _BIND_MODE_INDIRECT
+ bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)] = overallBindStatus
+ bindData[cls.bindColumns().index(cls._bindSchema.BIND_REVISION)] = minBindRevision
+ bindData[cls.bindColumns().index(cls._bindSchema.MESSAGE)] = ""
+
+ # Get the matching metadata data
+ metadataData = None
+ queryCacher = home._txn._queryCacher
+ if queryCacher:
+ # Retrieve from cache
+ cacheKey = queryCacher.keyForHomeChildMetaData(resourceID)
+ metadataData = yield queryCacher.get(cacheKey)
+
+ if metadataData is None:
+ # No cached copy
+ metadataData = (yield cls._metadataByIDQuery.on(home._txn, resourceID=resourceID))[0]
+ if queryCacher:
+ # Cache the results
+ yield queryCacher.setAfterCommit(home._txn, cacheKey, metadataData)
+
+ returnValue((bindData, additionalBindData, metadataData, ownerHome,))
+
+
+ def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, externalID=None):
ownerName = ownerHome.addressbook().name() if ownerHome else None
- super(AddressBook, self).__init__(
- home, name, resourceID, mode, status, revision=revision,
- message=message, ownerHome=ownerHome, ownerName=ownerName
- )
- self._index = PostgresLegacyABIndexEmulator(self)
+ super(AddressBook, self).__init__(home, name, resourceID, mode, status, revision=revision, message=message, ownerHome=ownerHome, ownerName=ownerName, externalID=externalID)
def __repr__(self):
@@ -568,13 +645,14 @@
returnValue((yield super(AddressBook, self).resourceNamesSinceRevision(revision)))
# call sharedChildResourceNamesSinceRevision() and filter results
- sharedChildChanged, sharedChildDeleted = yield self.sharedChildResourceNamesSinceRevision(revision, "infinity")
+ sharedChildChanged, sharedChildDeleted, sharedChildInvalid = yield self.sharedChildResourceNamesSinceRevision(revision, "infinity")
selfPath = self.name() + '/'
lenpath = len(selfPath)
changed = [item[lenpath:] for item in sharedChildChanged if item.startswith(selfPath) and item != selfPath]
deleted = [item[lenpath:] for item in sharedChildDeleted if item.startswith(selfPath) and item != selfPath]
- returnValue((changed, deleted,))
+ invalid = [item[lenpath:] for item in sharedChildInvalid if item.startswith(selfPath) and item != selfPath]
+ returnValue((changed, deleted, invalid))
@inlineCallbacks
@@ -611,7 +689,7 @@
self._txn, homeID=self._home._resourceID, addressbookID=self._resourceID
)
if groupBindRows:
- bindRevisions += [groupBindRow[5] for groupBindRow in groupBindRows]
+ bindRevisions += [groupBindRow[self.bindColumns().index(self._bindSchema.BIND_REVISION)] for groupBindRow in groupBindRows]
if revision != 0 and revision < max(bindRevisions):
if depth != '1':
@@ -623,16 +701,17 @@
if self.fullyShared():
# add change for addressbook group
- changed, deleted = yield super(AddressBook, self).sharedChildResourceNamesSinceRevision(revision, depth)
+ changed, deleted, invalid = yield super(AddressBook, self).sharedChildResourceNamesSinceRevision(revision, depth)
if revision == 0 and depth != "1":
changed.add("%s/%s" % (path, self._groupForSharedAddressBookName(),))
- returnValue((changed, deleted))
+ returnValue((changed, deleted, invalid))
changed = set()
deleted = set()
- acceptedGroupIDs = set([groupBindRow[2] for groupBindRow in groupBindRows])
+ invalid = set()
+ acceptedGroupIDs = set([groupBindRow[self.bindColumns().index(self._bindSchema.RESOURCE_ID)] for groupBindRow in groupBindRows])
allowedObjectIDs = set((yield self.expandGroupIDs(self._txn, acceptedGroupIDs)))
oldAllowedObjectIDs = set((yield self.expandGroupIDs(self._txn, acceptedGroupIDs, revision)))
@@ -700,7 +779,7 @@
for addedObjectID in allowedObjectIDs:
changed.add("%s/%s" % (path, idToNameMap[addedObjectID],))
- returnValue((changed, deleted))
+ returnValue((changed, deleted, invalid,))
@inlineCallbacks
@@ -728,6 +807,37 @@
)
+ def getInviteCopyProperties(self):
+ """
+ Get a dictionary of property name/values (as strings) for properties that are shadowable and
+ need to be copied to a sharee's collection when an external (cross-pod) share is created.
+ Sub-classes should override to expose the properties they care about.
+ """
+ props = {}
+ for elem in (element.DisplayName, carddavxml.AddressBookDescription,):
+ if PropertyName.fromElement(elem) in self.properties():
+ props[elem.sname()] = str(self.properties()[PropertyName.fromElement(elem)])
+ return props
+
+
+ def setInviteCopyProperties(self, props):
+ """
+ Copy a set of shadowable properties (as name/value strings) onto this shared resource when
+ a cross-pod invite is processed. Sub-classes should override to expose the properties they
+ care about.
+ """
+ # Initialize these for all shares
+ for elem in (carddavxml.AddressBookDescription,):
+ if PropertyName.fromElement(elem) not in self.properties() and elem.sname() in props:
+ self.properties()[PropertyName.fromElement(elem)] = elem.fromString(props[elem.sname()])
+
+ # Only initialize these for direct shares
+ if self.direct():
+ for elem in (element.DisplayName,):
+ if PropertyName.fromElement(elem) not in self.properties() and elem.sname() in props:
+ self.properties()[PropertyName.fromElement(elem)] = elem.fromString(props[elem.sname()])
+
+
def contentType(self):
"""
The content type of addressbook objects is text/vcard.
@@ -736,7 +846,7 @@
@classmethod
- def create(cls, home, name):
+ def create(cls, home, name, externalID=None):
if name == home.addressbook().name():
# raise HomeChildNameAlreadyExistsError
pass
@@ -762,13 +872,15 @@
def remove(self):
if self._resourceID == self._home._resourceID:
+
+ # Stop sharing first
+ yield self.ownerDeleteShare()
+
# Allow remove, as a way to reset the address book to an empty state
for abo in (yield self.objectResources()):
yield abo.remove()
yield self.removedObjectResource(abo)
- yield self.unshare() # storebridge should already have done this
-
yield self.properties()._removeResource()
yield self._loadPropertyStore()
@@ -925,6 +1037,57 @@
returnValue((yield super(AddressBook, self).bumpModified()))
+ @inlineCallbacks
+ def search(self, filter):
+ """
+ Finds resources matching the given qualifiers.
+ @param filter: the L{Filter} for the addressbook-query to execute.
+ @return: an iterable of tuples for each resource matching the
+ given C{qualifiers}. The tuples are C{(name, uid)}, where
+ C{name} is the resource name, C{uid} is the resource UID.
+ """
+
+ # We might be passed an L{Filter} or a serialization of one
+ if isinstance(filter, dict):
+ try:
+ filter = Filter.deserialize(filter)
+ except Exception:
+ filter = None
+
+ # Make sure we have a proper Filter element and get the partial SQL statement to use.
+ sql_stmt = self._sqlquery(filter)
+
+ # No result means it is too complex for us
+ if sql_stmt is None:
+ raise IndexedSearchException()
+
+ sql_stmt, args = sql_stmt
+ rowiter = yield sql_stmt.on(self._txn, **args)
+
+ returnValue(list(rowiter))
+
+
+ def _sqlquery(self, filter):
+ """
+ Convert the supplied addressbook-query into a partial SQL statement.
+
+ @param filter: the L{Filter} for the addressbook-query to convert.
+ @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+ and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+ Or return C{None} if it is not possible to create an SQL query to fully match the addressbook-query.
+ """
+
+ if not isinstance(filter, Filter):
+ return None
+
+ try:
+ expression = buildExpression(filter, self._queryFields)
+ sql = SQLQueryGenerator(expression, self, self.id())
+ return sql.generate()
+ except ValueError:
+ return None
+
+
@classmethod
@inlineCallbacks
def listObjects(cls, home):
@@ -946,7 +1109,7 @@
home._txn, homeID=home._resourceID
)
for groupRow in groupRows:
- bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+ bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerAddressBookID, create=True)
names |= set([ownerHome.uid()])
@@ -974,7 +1137,7 @@
)
# get ownerHomeIDs
for dataRow in dataRows:
- bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
+ bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
ownerHome = yield home.ownerHomeWithChildID(resourceID)
ownerHomeToDataRowMap[ownerHome] = dataRow
@@ -983,7 +1146,7 @@
home._txn, homeID=home._resourceID
)
for groupBindRow in groupBindRows:
- bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+ bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
if ownerHome not in ownerHomeToDataRowMap:
@@ -1004,32 +1167,17 @@
# Create the actual objects merging in properties
for ownerHome, dataRow in ownerHomeToDataRowMap.iteritems():
- bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
- additionalBind = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
- metadata = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+ bindData = dataRow[:cls.bindColumnCount]
+ additionalBindData = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+ metadataData = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+ propstore = propertyStores.get(ownerHome._addressbookPropertyStoreID, None)
- child = cls(
- home=home,
- name=ownerHome.uid(),
- resourceID=ownerHome._resourceID,
- mode=bindMode,
- status=bindStatus,
- revision=bindRevision,
- message=bindMessage,
- ownerHome=ownerHome,
- )
+ # Some adjustments for addressbook share model
+ bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)] = ownerHome._resourceID
+ bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] = ownerHome.uid()
- for attr, value in zip(cls.additionalBindAttributes(), additionalBind):
- setattr(child, attr, value)
- for attr, value in zip(cls.metadataAttributes(), metadata):
- setattr(child, attr, value)
+ child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, propstore, ownerHome)
child._syncTokenRevision = revisions[child._resourceID]
- propstore = propertyStores.get(ownerHome._addressbookPropertyStoreID, None)
- # We have to re-adjust the property store object to account for possible shared
- # collections as previously we loaded them all as if they were owned
- if propstore:
- propstore._setDefaultUserUID(ownerHome.uid())
- yield child._loadPropertyStore(propstore)
results.append(child)
returnValue(results)
@@ -1092,7 +1240,7 @@
@classmethod
@inlineCallbacks
- def _indirectObjectWithNameOrID(cls, home, name=None, resourceID=None, accepted=True):
+ def _indirectObjectWithNameOrID(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
# replaces objectWithName()
"""
Synthesize and indirect child for matching name or id based on whether shared groups exist.
@@ -1104,56 +1252,17 @@
@return: an L{CommonHomeChild} or C{None} if no such child
exists.
"""
- rows = None
- ownerHome = None
- # TODO: add queryCacher support
-
- if rows is None:
- # No cached copy
- if name:
- ownerHome = yield home._txn.addressbookHomeWithUID(name)
- if ownerHome is None:
- returnValue(None)
- resourceID = ownerHome.addressbook()._resourceID
- rows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
- home._txn, homeID=home._resourceID, addressbookID=resourceID
- )
-
- if not rows:
+ dbData = yield cls._getDBDataIndirect(home, name, resourceID, externalID)
+ if dbData is None:
returnValue(None)
+ bindData, additionalBindData, metadataData, ownerHome = dbData
- groupID = None
- overallBindStatus = _BIND_STATUS_INVITED
- minBindRevision = None
- for row in rows:
- bindMode, homeID, resourceGroupID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
- if groupID is None:
- groupID = resourceGroupID
- minBindRevision = min(minBindRevision, bindRevision) if minBindRevision is not None else bindRevision
- if bindStatus == _BIND_STATUS_ACCEPTED:
- overallBindStatus = _BIND_STATUS_ACCEPTED
-
- if accepted is not None and (overallBindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
+ bindStatus = bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)]
+ if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
returnValue(None)
- additionalBind = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
- if ownerHome is None:
- ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, groupID)
- ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
-
- child = cls(
- home=home,
- name=ownerHome.uid(),
- resourceID=resourceID,
- mode=_BIND_MODE_INDIRECT,
- status=overallBindStatus,
- revision=minBindRevision,
- message="",
- ownerHome=ownerHome,
- ownerName=ownerHome.uid()
- )
- yield child.initFromStore(additionalBind)
+ child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, None, ownerHome)
returnValue(child)
@@ -1247,7 +1356,7 @@
groupBindRows = yield AddressBookObject._unacceptedBindForHomeIDAndAddressBookID.on(
self._txn, homeID=self._home._resourceID, addressbookID=self._resourceID
)
- returnValue([groupBindRow[2] for groupBindRow in groupBindRows])
+ returnValue([groupBindRow[self.bindColumns().index(self._bindSchema.RESOURCE_ID)] for groupBindRow in groupBindRows])
@inlineCallbacks
@@ -1261,7 +1370,7 @@
groupBindRows = yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
self._txn, homeID=self._home._resourceID, addressbookID=self._resourceID
)
- returnValue([groupBindRow[2] for groupBindRow in groupBindRows])
+ returnValue([groupBindRow[self.bindColumns().index(self._bindSchema.RESOURCE_ID)] for groupBindRow in groupBindRows])
@inlineCallbacks
@@ -1280,7 +1389,7 @@
readWriteGroupIDs = set()
readOnlyGroupIDs = set()
for groupBindRow in groupBindRows:
- bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+ bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
if bindMode == _BIND_MODE_WRITE:
readWriteGroupIDs.add(resourceID)
else:
@@ -1341,7 +1450,7 @@
readWriteGroupIDs = []
readOnlyGroupIDs = []
for groupBindRow in groupBindRows:
- bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+ bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
if bindMode == _BIND_MODE_WRITE:
readWriteGroupIDs.append(resourceID)
else:
@@ -1426,7 +1535,7 @@
@inlineCallbacks
- def shareWith(self, shareeHome, mode, status=None, summary=None):
+ def shareWith(self, shareeHome, mode, status=None, summary=None, shareName=None):
"""
Share this (owned) L{AddressBookObject} with another home.
@@ -1454,11 +1563,12 @@
@inlineCallbacks
def doInsert(subt):
- newName = self.newShareName()
+ newName = shareName if shareName is not None else self.newShareName()
yield self._bindInsertQuery.on(
subt,
homeID=shareeHome._resourceID,
resourceID=self._resourceID,
+ externalID=None,
name=newName,
mode=mode,
bindStatus=status,
@@ -1495,14 +1605,14 @@
@inlineCallbacks
- def createShare(self, shareeUID, mode, summary=None):
+ def createShare(self, shareeUID, mode, summary=None, shareName=None):
"""
Create a new shared resource. If the mode is direct, the share is created in accepted state,
otherwise the share is created in invited state.
"""
if self._kind == _ABO_KIND_GROUP:
- shareeView = yield super(AddressBookObjectSharingMixIn, self).createShare(shareeUID, mode, summary)
+ shareeView = yield super(AddressBookObjectSharingMixIn, self).createShare(shareeUID, mode, summary, shareName)
returnValue(shareeView)
else:
returnValue(None)
@@ -1598,8 +1708,9 @@
# update revision in all remaining bind table rows for this address book
yield shareeView.addressbook().notifyPropertyChanged()
for groupBindRow in groupBindRows:
- if groupBindRow[2] != shareeView._resourceID:
- groupObject = yield shareeView.addressbook().objectResourceWithID(groupBindRow[2])
+ resid = groupBindRow[self.bindColumns().index(self._bindSchema.RESOURCE_ID)]
+ if resid != shareeView._resourceID:
+ groupObject = yield shareeView.addressbook().objectResourceWithID(resid)
yield groupObject._initBindRevision()
if shareeView.addressbook().fullyShared():
yield shareeView.addressbook()._initBindRevision()
@@ -1651,7 +1762,9 @@
yield addressbookAsShared.notifyPropertyChanged()
#update revision in all remaining bind table rows for this address book
for groupBindRow in groupBindRows:
- groupObject = yield addressbookAsShared.objectResourceWithID(groupBindRow[2])
+ groupObject = yield addressbookAsShared.objectResourceWithID(
+ groupBindRow[self.bindColumns().index(self._bindSchema.RESOURCE_ID)]
+ )
yield groupObject._initBindRevision()
addressbookAsShared._objects = {}
addressbookAsShared._objectNames = None
@@ -1731,25 +1844,147 @@
_objectSchema = schema.ADDRESSBOOK_OBJECT
_bindSchema = schema.SHARED_GROUP_BIND
+ _componentClass = VCard
+
# used by CommonHomeChild._childrenAndMetadataForHomeID() only
# _homeChildSchema = schema.ADDRESSBOOK_OBJECT
# _homeChildMetaDataSchema = schema.ADDRESSBOOK_OBJECT
+ @classmethod
+ @inlineCallbacks
+ def makeClass(cls, parent, objectData, groupBindData=None, propstore=None):
+ """
+ Given the various database rows, build the actual class.
+
+ @param parent: the parent collection object
+ @type parent: L{AddressBook}
+ @param objectData: the standard set of object columns
+ @type objectData: C{list}
+ @param groupBindData: additional group bind data
+ @type groupBindData: C{list}
+ @param propstore: a property store to use, or C{None} to load it automatically
+ @type propstore: L{PropertyStore}
+
+ @return: the constructed child class
+ @rtype: L{CommonHomeChild}
+ """
+
+ c = cls._externalClass if parent.external() else cls
+ child = c(
+ parent,
+ objectData[cls._allColumns().index(cls._objectSchema.RESOURCE_NAME)],
+ objectData[cls._allColumns().index(cls._objectSchema.UID)],
+ )
+
+ for attr, value in zip(child._rowAttributes(), objectData):
+ setattr(child, attr, value)
+
+ yield child._loadPropertyStore(propstore)
+
+ if groupBindData:
+ bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupBindData[:AddressBookObject.bindColumnCount] #@UnusedVariable
+ child._bindMode = bindMode
+ child._bindStatus = bindStatus
+ child._bindMessage = bindMessage
+ child._bindName = bindName
+ child._bindRevision = bindRevision
+ else:
+ invites = yield child.sharingInvites()
+ if len(invites):
+ child._bindMessage = "shared"
+
+ returnValue(child)
+
+
+ @classmethod
+ @inlineCallbacks
+ def _getDBData(cls, parent, name, uid, resourceID):
+ """
+ Given a set of identifying information, load the data rows for the object. Only one of
+ L{name}, L{uid} or L{resourceID} is specified - others are C{None}.
+
+ @param parent: the parent collection object
+ @type parent: L{AddressBook}
+ @param name: the resource name
+ @type name: C{str}
+ @param uid: the UID of the data
+ @type uid: C{str}
+ @param resourceID: the resource ID
+ @type resourceID: C{int}
+ """
+
+ row = None
+ groupBindRow = None
+
+ if parent.owned() or parent.fullyShared(): # owned or fully shared
+ row = yield super(AddressBookObject, cls)._getDBData(parent, name, uid, resourceID)
+
+ # Might be special group
+ if row is None and parent.fullyShared():
+ if name:
+ if name == parent._groupForSharedAddressBookName():
+ row = parent._groupForSharedAddressBookRow()
+ elif uid:
+ if uid == (yield parent._groupForSharedAddressBookUID()):
+ row = parent._groupForSharedAddressBookRow()
+ elif resourceID:
+ if resourceID == parent.id():
+ rows = parent._groupForSharedAddressBookRow()
+
+ else:
+ acceptedGroupIDs = yield parent.acceptedGroupIDs()
+ allowedObjectIDs = yield parent.expandGroupIDs(parent._txn, acceptedGroupIDs)
+ rows = None
+ if name:
+ if allowedObjectIDs:
+ rows = (yield cls._allColumnsWithResourceIDsAndName(allowedObjectIDs).on(
+ parent._txn,
+ name=name,
+ resourceIDs=allowedObjectIDs,
+ ))
+ elif uid:
+ if allowedObjectIDs:
+ rows = (yield cls._allColumnsWithResourceIDsAndUID(allowedObjectIDs).on(
+ parent._txn,
+ uid=uid,
+ resourceIDs=allowedObjectIDs,
+ ))
+ elif resourceID:
+ # Also allow invited groups
+ if resourceID in allowedObjectIDs or resourceID in (yield parent.unacceptedGroupIDs()):
+ rows = (yield cls._allColumnsWithResourceID.on(
+ parent._txn,
+ resourceID=resourceID,
+ ))
+ if rows:
+ row = rows[0]
+
+ if row is not None:
+ if row[cls._allColumns().index(cls._objectSchema.KIND)] == _ABO_KIND_GROUP:
+
+ resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+ groupBindRows = yield AddressBookObject._bindForResourceIDAndHomeID.on(
+ parent._txn, resourceID=resourceID, homeID=parent._home._resourceID
+ )
+
+ if groupBindRows:
+ groupBindRow = groupBindRows[0]
+
+ returnValue((row, groupBindRow,))
+
+
def __init__(self, addressbook, name, uid, resourceID=None, options=None):
self._kind = None
self._ownerAddressBookResourceID = None
- # _self._component is the cached, current component
- # super._objectText now contains the text as read of the database only,
- # not including group member text
- self._component = None
self._bindMode = None
self._bindStatus = None
self._bindMessage = None
self._bindName = None
self._bindRevision = None
super(AddressBookObject, self).__init__(addressbook, name, uid, resourceID, options)
+ self._externalID = None
self._options = {} if options is None else options
@@ -1774,6 +2009,15 @@
return self._resourceID == self.addressbook()._resourceID
+ def external(self):
+ """
+ Is this an external object.
+
+ @return: a string.
+ """
+ return self.addressbook().external()
+
+
@inlineCallbacks
def remove(self):
@@ -1872,7 +2116,8 @@
yield super(AddressBookObject, self).remove()
self._kind = None
self._ownerAddressBookResourceID = None
- self._component = None
+ self._objectText = None
+ self._cachedComponent = None
@inlineCallbacks
@@ -1901,7 +2146,7 @@
"""
obj = cls._objectSchema
return Select(
- cls._allColumns, From=obj,
+ cls._allColumns(), From=obj,
Where=(column == Parameter(paramName)).And(
obj.RESOURCE_ID.In(Parameter("resourceIDs", len(resourceIDs)))),
)
@@ -1921,7 +2166,7 @@
def _allColumnsWithResourceID(cls): #@NoSelf
obj = cls._objectSchema
return Select(
- cls._allColumns, From=obj,
+ cls._allColumns(), From=obj,
Where=obj.RESOURCE_ID == Parameter("resourceID"),)
@@ -1944,7 +2189,7 @@
)
if groupBindRows:
groupBindRow = groupBindRows[0]
- bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+ bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
returnValue(None)
@@ -1957,93 +2202,20 @@
returnValue(None)
+ @classmethod
@inlineCallbacks
- def initFromStore(self):
- """
- Initialise this object from the store. We read in and cache all the
- extra metadata from the DB to avoid having to do DB queries for those
- individually later. Either the name or uid is present, so we have to
- tweak the query accordingly.
+ def objectWith(cls, parent, name=None, uid=None, resourceID=None):
- @return: L{self} if object exists in the DB, else C{None}
- """
- abo = None
- if self.owned() or self.addressbook().fullyShared(): # owned or fully shared
- abo = yield super(AddressBookObject, self).initFromStore()
+ row, groupBindRow = yield cls._getDBData(parent, name, uid, resourceID)
- # Might be special group
- if abo is None and self.addressbook().fullyShared():
- rows = None
- if self._name:
- if self._name == self.addressbook()._groupForSharedAddressBookName():
- rows = [self.addressbook()._groupForSharedAddressBookRow()]
- elif self._uid:
- if self._uid == (yield self.addressbook()._groupForSharedAddressBookUID()):
- rows = [self.addressbook()._groupForSharedAddressBookRow()]
- elif self._resourceID:
- if self.isGroupForSharedAddressBook():
- rows = [self.addressbook()._groupForSharedAddressBookRow()]
-
- if rows:
- self._initFromRow(tuple(rows[0]))
- yield self._loadPropertyStore()
- abo = self
-
+ if row:
+ child = yield cls.makeClass(parent, row, groupBindRow)
+ returnValue(child)
else:
- acceptedGroupIDs = yield self.addressbook().acceptedGroupIDs()
- allowedObjectIDs = yield self.addressbook().expandGroupIDs(self._txn, acceptedGroupIDs)
- rows = None
- if self._name:
- if allowedObjectIDs:
- rows = (yield self._allColumnsWithResourceIDsAndName(allowedObjectIDs).on(
- self._txn, name=self._name,
- resourceIDs=allowedObjectIDs,
- ))
- elif self._uid:
- if allowedObjectIDs:
- rows = (yield self._allColumnsWithResourceIDsAndUID(allowedObjectIDs).on(
- self._txn, uid=self._uid,
- resourceIDs=allowedObjectIDs,
- ))
- elif self._resourceID:
- if (self._resourceID in allowedObjectIDs or
- self._resourceID in (yield self.addressbook().unacceptedGroupIDs())): # allow invited groups
- rows = (yield self._allColumnsWithResourceID.on(
- self._txn, resourceID=self._resourceID,
- ))
- if rows:
- self._initFromRow(tuple(rows[0]))
- yield self._loadPropertyStore()
- abo = self
-
- if abo is not None:
- if self._kind == _ABO_KIND_GROUP:
-
- groupBindRows = yield AddressBookObject._bindForResourceIDAndHomeID.on(
- self._txn, resourceID=self._resourceID, homeID=self._home._resourceID
- )
-
- if groupBindRows:
- groupBindRow = groupBindRows[0]
- bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
- self._bindMode = bindMode
- self._bindStatus = bindStatus
- self._bindMessage = bindMessage
- self._bindName = bindName
- self._bindRevision = bindRevision
- else:
- invites = yield self.sharingInvites()
- if len(invites):
- self._bindMessage = "shared"
-
- yield self._loadPropertyStore()
-
- returnValue(self)
- else:
returnValue(None)
- @classproperty
+ @classmethod
def _allColumns(cls): #@NoSelf
"""
Full set of columns in the object table that need to be loaded to
@@ -2059,24 +2231,23 @@
obj.MD5,
Len(obj.TEXT),
obj.CREATED,
- obj.MODIFIED,
+ obj.MODIFIED
]
- def _initFromRow(self, row):
- """
- Given a select result using the columns from L{_allColumns}, initialize
- the object resource state.
- """
- (self._ownerAddressBookResourceID,
- self._resourceID,
- self._name,
- self._uid,
- self._kind,
- self._md5,
- self._size,
- self._created,
- self._modified,) = tuple(row)
+ @classmethod
+ def _rowAttributes(cls): #@NoSelf
+ return (
+ "_ownerAddressBookResourceID",
+ "_resourceID",
+ "_name",
+ "_uid",
+ "_kind",
+ "_md5",
+ "_size",
+ "_created",
+ "_modified",
+ )
@classmethod
@@ -2099,7 +2270,7 @@
else:
acceptedGroupIDs = yield addressbook.acceptedGroupIDs()
allowedObjectIDs = yield addressbook.expandGroupIDs(addressbook._txn, acceptedGroupIDs)
- rows = yield cls._columnsWithResourceIDsQuery(cls._allColumns, allowedObjectIDs).on(
+ rows = yield cls._columnsWithResourceIDsQuery(cls._allColumns(), allowedObjectIDs).on(
addressbook._txn, resourceIDs=allowedObjectIDs
)
returnValue(rows)
@@ -2108,7 +2279,7 @@
@classmethod
def _allColumnsWithResourceIDsAndNamesQuery(cls, resourceIDs, names):
obj = cls._objectSchema
- return Select(cls._allColumns, From=obj,
+ return Select(cls._allColumns(), From=obj,
Where=(obj.RESOURCE_ID.In(Parameter("resourceIDs", len(resourceIDs))).And(
obj.RESOURCE_NAME.In(Parameter("names", len(names))))),)
@@ -2224,6 +2395,13 @@
@inlineCallbacks
def setComponent(self, component, inserting=False):
+ if isinstance(component, str) or isinstance(component, unicode):
+ component = self._componentClass.fromString(component)
+ try:
+ component = self._componentClass.fromString(component)
+ except InvalidVCardDataError as e:
+ raise InvalidComponentForStoreError(str(e))
+
self._componentChanged = False
if "coaddedUIDs" not in self._options:
@@ -2407,7 +2585,7 @@
self._objectText = componentText
self._size = len(self._objectText)
- self._component = component
+ self._cachedComponent = component
self._md5 = hashlib.md5(componentText).hexdigest()
self._componentChanged = originalComponentText != componentText
@@ -2548,7 +2726,7 @@
only allowed in good data.
"""
- if self._component is None:
+ if self._cachedComponent is None:
if self.isGroupForSharedAddressBook():
component = yield self.addressbook()._groupForSharedAddressBookComponent()
@@ -2608,9 +2786,9 @@
component.addProperty(Property("X-ADDRESSBOOKSERVER-KIND", "group"))
component.addProperty(Property("UID", self._uid))
- self._component = component
+ self._cachedComponent = component
- returnValue(self._component)
+ returnValue(self._cachedComponent)
def moveValidation(self, destination, name):
@@ -2725,4 +2903,10 @@
)
+# Hook-up class relationships at the end after they have all been defined
+from txdav.carddav.datastore.sql_external import AddressBookHomeExternal, AddressBookExternal, AddressBookObjectExternal
+AddressBookHome._externalClass = AddressBookHomeExternal
+AddressBookHome._childClass = AddressBook
+AddressBook._externalClass = AddressBookExternal
AddressBook._objectResourceClass = AddressBookObject
+AddressBookObject._externalClass = AddressBookObjectExternal
Copied: CalendarServer/trunk/txdav/carddav/datastore/sql_external.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/sql_external.py (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/sql_external.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,83 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 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.
+##
+"""
+SQL backend for CardDAV storage when resources are external.
+"""
+
+from twisted.internet.defer import succeed
+
+from twext.python.log import Logger
+
+from txdav.carddav.datastore.sql import AddressBookHome, AddressBook, \
+ AddressBookObject
+from txdav.common.datastore.sql_external import CommonHomeExternal, CommonHomeChildExternal, \
+ CommonObjectResourceExternal
+
+log = Logger()
+
+class AddressBookHomeExternal(CommonHomeExternal, AddressBookHome):
+
+ def __init__(self, transaction, ownerUID, resourceID):
+
+ AddressBookHome.__init__(self, transaction, ownerUID)
+ CommonHomeExternal.__init__(self, transaction, ownerUID, resourceID)
+
+
+ def hasAddressBookResourceUIDSomewhereElse(self, uid, ok_object, mode):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getAddressBookResourcesForUID(self, uid):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def createdHome(self):
+ """
+ No children - make this a no-op.
+ """
+ return succeed(None)
+
+
+ def addressbook(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class AddressBookExternal(CommonHomeChildExternal, AddressBook):
+ """
+ SQL-based implementation of L{IAddressBook}.
+ """
+ pass
+
+
+
+class AddressBookObjectExternal(CommonObjectResourceExternal, AddressBookObject):
+ """
+ SQL-based implementation of L{IAddressBookObject}.
+ """
+ pass
+
+AddressBookExternal._objectResourceClass = AddressBookObjectExternal
Modified: CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -189,12 +189,12 @@
self.db.deleteResource("data3.vcf")
tests = (
- (0, (["data1.vcf", "data2.vcf", ], [],)),
- (1, (["data2.vcf", ], ["data3.vcf", ],)),
- (2, ([], ["data3.vcf", ],)),
- (3, ([], ["data3.vcf", ],)),
- (4, ([], [],)),
- (5, ([], [],)),
+ (0, (["data1.vcf", "data2.vcf", ], [], [],)),
+ (1, (["data2.vcf", ], ["data3.vcf", ], [],)),
+ (2, ([], ["data3.vcf", ], [],)),
+ (3, ([], ["data3.vcf", ], [],)),
+ (4, ([], [], [],)),
+ (5, ([], [], [],)),
)
for revision, results in tests:
Modified: CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -41,7 +41,7 @@
from txdav.common.icommondatastore import NoSuchObjectResourceError
from txdav.common.datastore.sql import EADDRESSBOOKTYPE, CommonObjectResource
from txdav.common.datastore.sql_tables import _ABO_KIND_PERSON, _ABO_KIND_GROUP, schema
-from txdav.common.datastore.test.util import buildStore
+from txdav.common.datastore.test.util import buildStore, cleanStore
from txdav.carddav.datastore.sql import AddressBook
from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
@@ -56,7 +56,22 @@
@inlineCallbacks
def setUp(self):
yield super(AddressBookSQLStorageTests, self).setUp()
- self._sqlStore = yield buildStore(self, self.notifierFactory)
+ self._sqlStore = yield buildStore(
+ self,
+ self.notifierFactory,
+ homes=(
+ "home1",
+ "home2",
+ "home3",
+ "home_bad",
+ "home_empty",
+ "homeNew",
+ "new-home",
+ "uid1",
+ "uid2",
+ "xyzzy",
+ )
+ )
yield self.populate()
@@ -289,7 +304,7 @@
Test that two concurrent attempts to PUT different address book object resources to the
same address book home does not cause a deadlock.
"""
- addressbookStore = yield buildStore(self, self.notifierFactory)
+ addressbookStore = self._sqlStore
# Provision the home and addressbook now
txn = addressbookStore.newTransaction()
@@ -395,7 +410,7 @@
"""
Test that kind property UID is stored correctly in database
"""
- addressbookStore = yield buildStore(self, self.notifierFactory)
+ addressbookStore = self._sqlStore
# Provision the home and addressbook, one user and one group
txn = addressbookStore.newTransaction()
@@ -441,7 +456,7 @@
"""
Test that kind property vCard is stored correctly in database
"""
- addressbookStore = yield buildStore(self, self.notifierFactory)
+ addressbookStore = self._sqlStore
# Provision the home and addressbook, one user and one group
txn = addressbookStore.newTransaction()
@@ -532,7 +547,8 @@
"""
Test that kind property vCard is stored correctly in database
"""
- addressbookStore = yield buildStore(self, self.notifierFactory)
+ addressbookStore = self._sqlStore
+ cleanStore(self, addressbookStore)
# Provision the home and addressbook, one user and one group
txn = addressbookStore.newTransaction()
Modified: CalendarServer/trunk/txdav/carddav/datastore/test/test_sql_sharing.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/test/test_sql_sharing.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/test_sql_sharing.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1134,23 +1134,27 @@
otherAB = yield self.addressbookUnderTest(home="user02", name="user01")
self.assertNotEqual(otherAB._bindRevision, 0)
- changed, deleted = yield otherAB.resourceNamesSinceRevision(0)
+ changed, deleted, invalid = yield otherAB.resourceNamesSinceRevision(0)
self.assertNotEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
- changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
+ changed, deleted, invalid = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
self.assertEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
otherHome = yield self.addressbookHomeUnderTest(name="user02")
for depth in ("1", "infinity",):
- changed, deleted = yield otherHome.resourceNamesSinceRevision(0, depth)
+ changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(0, depth)
self.assertNotEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
- changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
+ changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
self.assertEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
@inlineCallbacks
@@ -1166,13 +1170,15 @@
otherAB = yield self.addressbookUnderTest(home="user02", name="user01")
self.assertNotEqual(otherAB._bindRevision, 0)
- changed, deleted = yield otherAB.resourceNamesSinceRevision(0)
+ changed, deleted, invalid = yield otherAB.resourceNamesSinceRevision(0)
self.assertEqual(set(changed), set(['card1.vcf', 'card2.vcf', 'group1.vcf']))
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
- changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
+ changed, deleted, invalid = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
self.assertEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
for depth, result in (
("1", ['addressbook/',
@@ -1184,13 +1190,15 @@
'user01/card2.vcf',
'user01/group1.vcf']
)):
- changed, deleted = yield otherAB.viewerHome().resourceNamesSinceRevision(0, depth)
+ changed, deleted, invalid = yield otherAB.viewerHome().resourceNamesSinceRevision(0, depth)
self.assertEqual(set(changed), set(result))
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
- changed, deleted = yield otherAB.viewerHome().resourceNamesSinceRevision(otherAB._bindRevision, depth)
+ changed, deleted, invalid = yield otherAB.viewerHome().resourceNamesSinceRevision(otherAB._bindRevision, depth)
self.assertEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 0)
@inlineCallbacks
Modified: CalendarServer/trunk/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/file.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/file.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -852,7 +852,8 @@
def resourceNamesSinceToken(self, token, depth):
deleted = []
changed = []
- return succeed((changed, deleted))
+ invalid = []
+ return succeed((changed, deleted, invalid))
# @cached
Deleted: CalendarServer/trunk/txdav/common/datastore/podding/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/__init__.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 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.
-##
Copied: CalendarServer/trunk/txdav/common/datastore/podding/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/__init__.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 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.
+##
Deleted: CalendarServer/trunk/txdav/common/datastore/podding/conduit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/conduit.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,845 +0,0 @@
-##
-# Copyright (c) 2013 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 twext.python.log import Logger
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-
-from txdav.common.datastore.podding.request import ConduitRequest
-from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
-from txdav.common.icommondatastore import ExternalShareFailed
-from twisted.python.reflect import namedClass
-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
-from twistedcaldav.caldavxml import TimeRange
-
-
-__all__ = [
- "PoddingConduitResource",
-]
-
-log = Logger()
-
-
-class FailedCrossPodRequestError(RuntimeError):
- """
- Request returned an error.
- """
- pass
-
-
-
-class PoddingConduit(object):
- """
- This class is the API/RPC bridge between cross-pod requests and the store.
-
- Each cross-pod request/response is described by a Python C{dict} that is serialized
- to JSON for the HTTP request/response.
-
- Each request C{dict} has an "action" key that indicates what call is being made, and
- the other keys are arguments to that call.
-
- Each response C{dict} has a "result" key that indicates the call result, and other
- optional keys for any values returned by the call.
-
- The conduit provides two methods for each action: one for the sending side and one for
- the receiving side, called "send_{action}" and "recv_{action}", respectively, where
- {action} is the action value.
-
- The "send_{action}" calls each have a set of arguments specific to the call itself. The
- code takes care of packing that into a C{dict} and sending to the appropriate pod.
-
- The "recv_{action}" calls take a single C{dict} argument that is the deserialized JSON
- data from the incoming request. The return value is a C{dict} with the result.
-
- Some simple forms of send_/recv_ methods can be auto-generated to simplify coding.
-
- Right now this conduit is used for cross-pod sharing operations. In the future we will
- likely use it for cross-pod migration.
- """
-
- conduitRequestClass = ConduitRequest
-
- def __init__(self, store):
- """
- @param store: the L{CommonDataStore} in use.
- """
- self.store = store
-
-
- def validRequst(self, source_guid, destination_guid):
- """
- Verify that the specified GUIDs are valid for the request and return the
- matching directory records.
-
- @param source_guid: GUID for the user on whose behalf the request is being made
- @type source_guid: C{str}
- @param destination_guid: GUID for the user to whom the request is being sent
- @type destination_guid: C{str}
-
- @return: C{tuple} of L{IStoreDirectoryRecord}
- """
-
- source = self.store.directoryService().recordWithUID(source_guid)
- if source is None:
- raise DirectoryRecordNotFoundError("Cross-pod source: {}".format(source_guid))
- if not source.thisServer():
- raise FailedCrossPodRequestError("Cross-pod source not on this server: {}".format(source_guid))
-
- destination = self.store.directoryService().recordWithUID(destination_guid)
- if destination is None:
- raise DirectoryRecordNotFoundError("Cross-pod destination: {}".format(destination_guid))
- if destination.thisServer():
- raise FailedCrossPodRequestError("Cross-pod destination on this server: {}".format(destination_guid))
-
- return (source, destination,)
-
-
- @inlineCallbacks
- def sendRequest(self, txn, recipient, data, stream=None, streamType=None):
-
- request = self.conduitRequestClass(recipient.server(), data, stream, streamType)
- try:
- response = (yield request.doRequest(txn))
- except Exception as e:
- raise FailedCrossPodRequestError("Failed cross-pod request: {}".format(e))
- returnValue(response)
-
-
- @inlineCallbacks
- def processRequest(self, data):
- """
- Process the request.
-
- @param data: the JSON data to process
- @type data: C{dict}
- """
- # Must have a dict with an "action" key
- try:
- action = data["action"]
- except (KeyError, TypeError) as e:
- log.error("JSON data must have an object as its root with an 'action' attribute: {ex}\n{json}", ex=e, json=data)
- raise FailedCrossPodRequestError("JSON data must have an object as its root with an 'action' attribute: {}\n{}".format(e, data,))
-
- if action == "ping":
- result = {"result": "ok"}
- returnValue(result)
-
- method = "recv_{}".format(action.replace("-", "_"))
- if not hasattr(self, method):
- log.error("Unsupported action: {action}", action=action)
- raise FailedCrossPodRequestError("Unsupported action: {}".format(action))
-
- # Need a transaction to work with
- txn = self.store.newTransaction(repr("Conduit request"))
-
- # Do the actual request processing
- try:
- result = (yield getattr(self, method)(txn, data))
- except Exception as e:
- yield txn.abort()
- log.error("Failed action: {action}, {ex}", action=action, ex=e)
- raise FailedCrossPodRequestError("Failed action: {}, {}".format(action, e))
-
- yield txn.commit()
-
- returnValue(result)
-
-
- #
- # Invite related apis
- #
-
- @inlineCallbacks
- def send_shareinvite(self, txn, homeType, ownerUID, ownerID, ownerName, shareeUID, shareUID, bindMode, summary, copy_properties, supported_components):
- """
- Send a sharing invite cross-pod message.
-
- @param homeType: Type of home being shared.
- @type homeType: C{int}
- @param ownerUID: GUID of the sharer.
- @type ownerUID: C{str}
- @param ownerID: resource ID of the sharer calendar
- @type ownerID: C{int}
- @param ownerName: owner's name of the sharer calendar
- @type ownerName: C{str}
- @param shareeUID: GUID of the sharee
- @type shareeUID: C{str}
- @param shareUID: Resource/invite ID for sharee
- @type shareUID: C{str}
- @param bindMode: bind mode for the share
- @type bindMode: C{str}
- @param summary: sharing message
- @type summary: C{str}
- @param copy_properties: C{str} name/value for properties to be copied
- @type copy_properties: C{dict}
- @param supported_components: supproted components, may be C{None}
- @type supported_components: C{str}
- """
-
- _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
-
- action = {
- "action": "shareinvite",
- "type": homeType,
- "owner": ownerUID,
- "owner_id": ownerID,
- "owner_name": ownerName,
- "sharee": shareeUID,
- "share_id": shareUID,
- "mode": bindMode,
- "summary": summary,
- "properties": copy_properties,
- }
- if supported_components is not None:
- action["supported-components"] = supported_components
-
- result = yield self.sendRequest(txn, recipient, action)
- returnValue(result)
-
-
- @inlineCallbacks
- def recv_shareinvite(self, txn, message):
- """
- Process a sharing invite cross-pod message. Message arguments as per L{send_shareinvite}.
-
- @param message: message arguments
- @type message: C{dict}
- """
-
- if message["action"] != "shareinvite":
- raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareinvite".format(message["action"]))
-
- # Create a share
- shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
- if shareeHome is None or shareeHome.external():
- raise FailedCrossPodRequestError("Invalid sharee UID specified")
-
- try:
- yield shareeHome.processExternalInvite(
- message["owner"],
- message["owner_id"],
- message["owner_name"],
- message["share_id"],
- message["mode"],
- message["summary"],
- message["properties"],
- supported_components=message.get("supported-components")
- )
- except ExternalShareFailed as e:
- raise FailedCrossPodRequestError(str(e))
-
- returnValue({
- "result": "ok",
- })
-
-
- @inlineCallbacks
- def send_shareuninvite(self, txn, homeType, ownerUID, ownerID, shareeUID, shareUID):
- """
- Send a sharing uninvite cross-pod message.
-
- @param homeType: Type of home being shared.
- @type homeType: C{int}
- @param ownerUID: GUID of the sharer.
- @type ownerUID: C{str}
- @param ownerID: resource ID of the sharer calendar
- @type ownerID: C{int}
- @param shareeUID: GUID of the sharee
- @type shareeUID: C{str}
- @param shareUID: Resource/invite ID for sharee
- @type shareUID: C{str}
- """
-
- _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
-
- action = {
- "action": "shareuninvite",
- "type": homeType,
- "owner": ownerUID,
- "owner_id": ownerID,
- "sharee": shareeUID,
- "share_id": shareUID,
- }
-
- result = yield self.sendRequest(txn, recipient, action)
- returnValue(result)
-
-
- @inlineCallbacks
- def recv_shareuninvite(self, txn, message):
- """
- Process a sharing uninvite cross-pod message. Message arguments as per L{send_shareuninvite}.
-
- @param message: message arguments
- @type message: C{dict}
- """
-
- if message["action"] != "shareuninvite":
- raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareuninvite".format(message["action"]))
-
- # Create a share
- shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
- if shareeHome is None or shareeHome.external():
- FailedCrossPodRequestError("Invalid sharee UID specified")
-
- try:
- yield shareeHome.processExternalUninvite(
- message["owner"],
- message["owner_id"],
- message["share_id"],
- )
- except ExternalShareFailed as e:
- FailedCrossPodRequestError(str(e))
-
- returnValue({
- "result": "ok",
- })
-
-
- @inlineCallbacks
- def send_sharereply(self, txn, homeType, ownerUID, shareeUID, shareUID, bindStatus, summary=None):
- """
- Send a sharing reply cross-pod message.
-
- @param homeType: Type of home being shared.
- @type homeType: C{int}
- @param ownerUID: GUID of the sharer.
- @type ownerUID: C{str}
- @param shareeUID: GUID of the recipient
- @type shareeUID: C{str}
- @param shareUID: Resource/invite ID for recipient
- @type shareUID: C{str}
- @param bindStatus: bind mode for the share
- @type bindStatus: C{str}
- @param summary: sharing message
- @type summary: C{str}
- """
-
- _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
-
- action = {
- "action": "sharereply",
- "type": homeType,
- "owner": ownerUID,
- "sharee": shareeUID,
- "share_id": shareUID,
- "status": bindStatus,
- }
- if summary is not None:
- action["summary"] = summary
-
- result = yield self.sendRequest(txn, recipient, action)
- returnValue(result)
-
-
- @inlineCallbacks
- def recv_sharereply(self, txn, message):
- """
- Process a sharing reply cross-pod message. Message arguments as per L{send_sharereply}.
-
- @param message: message arguments
- @type message: C{dict}
- """
-
- if message["action"] != "sharereply":
- raise FailedCrossPodRequestError("Wrong action '{}' for recv_sharereply".format(message["action"]))
-
- # Create a share
- ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
- if ownerHome is None or ownerHome.external():
- FailedCrossPodRequestError("Invalid owner UID specified")
-
- try:
- yield ownerHome.processExternalReply(
- message["owner"],
- message["sharee"],
- message["share_id"],
- message["status"],
- summary=message.get("summary")
- )
- except ExternalShareFailed as e:
- FailedCrossPodRequestError(str(e))
-
- returnValue({
- "result": "ok",
- })
-
-
- #
- # Managed attachment related apis
- #
-
- @inlineCallbacks
- def send_add_attachment(self, objectResource, rids, content_type, filename, stream):
- """
- Managed attachment addAttachment call.
-
- @param objectResource: child resource having an attachment added
- @type objectResource: L{CalendarObject}
- @param rids: list of recurrence ids
- @type rids: C{list}
- @param content_type: content type of attachment data
- @type content_type: L{MimeType}
- @param filename: name of attachment
- @type filename: C{str}
- @param stream: attachment data stream
- @type stream: L{IStream}
- """
-
- actionName = "add-attachment"
- shareeView = objectResource._parentCollection
- action, recipient = self._send(actionName, shareeView, objectResource)
- action["rids"] = rids
- action["filename"] = filename
- result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
- if result["result"] == "ok":
- returnValue(result["value"])
- elif result["result"] == "exception":
- raise namedClass(result["class"])(result["message"])
-
-
- @inlineCallbacks
- def recv_add_attachment(self, txn, message):
- """
- Process an addAttachment cross-pod message. Message arguments as per L{send_add_attachment}.
-
- @param message: message arguments
- @type message: C{dict}
- """
-
- actionName = "add-attachment"
- _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
- try:
- attachment, location = yield objectResource.addAttachment(
- message["rids"],
- message["streamType"],
- message["filename"],
- message["stream"],
- )
- except Exception as e:
- returnValue({
- "result": "exception",
- "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
- "message": str(e),
- })
-
- returnValue({
- "result": "ok",
- "value": (attachment.managedID(), location,),
- })
-
-
- @inlineCallbacks
- def send_update_attachment(self, objectResource, managed_id, content_type, filename, stream):
- """
- Managed attachment updateAttachment call.
-
- @param objectResource: child resource having an attachment added
- @type objectResource: L{CalendarObject}
- @param managed_id: managed-id to update
- @type managed_id: C{str}
- @param content_type: content type of attachment data
- @type content_type: L{MimeType}
- @param filename: name of attachment
- @type filename: C{str}
- @param stream: attachment data stream
- @type stream: L{IStream}
- """
-
- actionName = "update-attachment"
- shareeView = objectResource._parentCollection
- action, recipient = self._send(actionName, shareeView, objectResource)
- action["managedID"] = managed_id
- action["filename"] = filename
- result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
- if result["result"] == "ok":
- returnValue(result["value"])
- elif result["result"] == "exception":
- raise namedClass(result["class"])(result["message"])
-
-
- @inlineCallbacks
- def recv_update_attachment(self, txn, message):
- """
- Process an updateAttachment cross-pod message. Message arguments as per L{send_update_attachment}.
-
- @param message: message arguments
- @type message: C{dict}
- """
-
- actionName = "update-attachment"
- _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
- try:
- attachment, location = yield objectResource.updateAttachment(
- message["managedID"],
- message["streamType"],
- message["filename"],
- message["stream"],
- )
- except Exception as e:
- returnValue({
- "result": "exception",
- "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
- "message": str(e),
- })
-
- returnValue({
- "result": "ok",
- "value": (attachment.managedID(), location,),
- })
-
-
- @inlineCallbacks
- def send_remove_attachment(self, objectResource, rids, managed_id):
- """
- Managed attachment removeAttachment call.
-
- @param objectResource: child resource having an attachment added
- @type objectResource: L{CalendarObject}
- @param rids: list of recurrence ids
- @type rids: C{list}
- @param managed_id: managed-id to update
- @type managed_id: C{str}
- """
-
- actionName = "remove-attachment"
- shareeView = objectResource._parentCollection
- action, recipient = self._send(actionName, shareeView, objectResource)
- action["rids"] = rids
- action["managedID"] = managed_id
- result = yield self.sendRequest(shareeView._txn, recipient, action)
- if result["result"] == "ok":
- returnValue(result["value"])
- elif result["result"] == "exception":
- raise namedClass(result["class"])(result["message"])
-
-
- @inlineCallbacks
- def recv_remove_attachment(self, txn, message):
- """
- Process an removeAttachment cross-pod message. Message arguments as per L{send_remove_attachment}.
-
- @param message: message arguments
- @type message: C{dict}
- """
-
- actionName = "remove-attachment"
- _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
- try:
- yield objectResource.removeAttachment(
- message["rids"],
- message["managedID"],
- )
- except Exception as e:
- returnValue({
- "result": "exception",
- "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
- "message": str(e),
- })
-
- returnValue({
- "result": "ok",
- "value": None,
- })
-
-
- #
- # Sharer data access related apis
- #
-
- def _send(self, action, parent, child=None):
- """
- Base behavior for an operation on a L{CommonHomeChild}.
-
- @param shareeView: sharee resource being operated on.
- @type shareeView: L{CommonHomeChildExternal}
- """
-
- homeType = parent.ownerHome()._homeType
- ownerUID = parent.ownerHome().uid()
- ownerID = parent.external_id()
- shareeUID = parent.viewerHome().uid()
-
- _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
-
- result = {
- "action": action,
- "type": homeType,
- "owner": ownerUID,
- "owner_id": ownerID,
- "sharee": shareeUID,
- }
- if child is not None:
- result["resource_id"] = child.id()
- return result, recipient
-
-
- @inlineCallbacks
- def _recv(self, txn, message, expected_action):
- """
- Base behavior for sharer data access.
-
- @param message: message arguments
- @type message: C{dict}
- """
-
- if message["action"] != expected_action:
- raise FailedCrossPodRequestError("Wrong action '{}' for recv_{}".format(message["action"], expected_action))
-
- # Get a share
- ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
- if ownerHome is None or ownerHome.external():
- FailedCrossPodRequestError("Invalid owner UID specified")
-
- shareeHome = yield txn.homeWithUID(message["type"], message["sharee"])
- if shareeHome is None or not shareeHome.external():
- FailedCrossPodRequestError("Invalid sharee UID specified")
-
- shareeView = yield shareeHome.childWithID(message["owner_id"])
- if shareeView is None:
- FailedCrossPodRequestError("Invalid shared resource specified")
-
- resourceID = message.get("resource_id", None)
- if resourceID is not None:
- objectResource = yield shareeView.objectResourceWithID(resourceID)
- if objectResource is None:
- FailedCrossPodRequestError("Invalid owner shared object resource specified")
- else:
- objectResource = None
-
- returnValue((shareeView, objectResource,))
-
-
- #
- # Simple calls are ones where there is no argument and a single return value. We can simplify
- # code generation for these by dynamically generating the appropriate class methods.
- #
-
- @inlineCallbacks
- def _simple_send(self, actionName, shareeView, objectResource=None, transform=None, args=None, kwargs=None):
- """
- A simple send operation that returns a value.
-
- @param actionName: name of the action.
- @type actionName: C{str}
- @param shareeView: sharee resource being operated on.
- @type shareeView: L{CommonHomeChildExternal}
- @param objectResource: the resource being operated on, or C{None} for classmethod.
- @type objectResource: L{CommonObjectResourceExternal}
- @param transform: a function used to convert the JSON result into return values.
- @type transform: C{callable}
- @param args: list of optional arguments.
- @type args: C{list}
- @param kwargs: optional keyword arguments.
- @type kwargs: C{dict}
- """
-
- action, recipient = self._send(actionName, shareeView, objectResource)
- if args is not None:
- action["arguments"] = args
- if kwargs is not None:
- action["keywords"] = kwargs
- result = yield self.sendRequest(shareeView._txn, recipient, action)
- if result["result"] == "ok":
- returnValue(result["value"] if transform is None else transform(result["value"], shareeView, objectResource))
- elif result["result"] == "exception":
- raise namedClass(result["class"])(result["message"])
-
-
- @inlineCallbacks
- def _simple_recv(self, txn, actionName, message, method, onHomeChild=True, transform=None):
- """
- A simple recv operation that returns a value. We also look for an optional set of arguments/keywords
- and include those only if present.
-
- @param actionName: name of the action.
- @type actionName: C{str}
- @param message: message arguments
- @type message: C{dict}
- @param method: name of the method to execute on the shared resource to get the result.
- @type method: C{str}
- @param transform: method to call on returned JSON value to convert it to something useful.
- @type transform: C{callable}
- """
-
- shareeView, objectResource = yield self._recv(txn, message, actionName)
- try:
- if onHomeChild:
- # Operate on the L{CommonHomeChild}
- value = yield getattr(shareeView, method)(*message.get("arguments", ()), **message.get("keywords", {}))
- else:
- # Operate on the L{CommonObjectResource}
- if objectResource is not None:
- value = yield getattr(objectResource, method)(*message.get("arguments", ()), **message.get("keywords", {}))
- else:
- # classmethod call
- value = yield getattr(shareeView._objectResourceClass, method)(shareeView, *message.get("arguments", ()), **message.get("keywords", {}))
- except Exception as e:
- returnValue({
- "result": "exception",
- "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
- "message": str(e),
- })
-
- returnValue({
- "result": "ok",
- "value": transform(value, shareeView, objectResource) if transform is not None else value,
- })
-
-
- @inlineCallbacks
- def send_freebusy(
- self,
- calresource,
- timerange,
- matchtotal,
- excludeuid,
- organizer,
- organizerPrincipal,
- same_calendar_user,
- servertoserver,
- event_details,
- ):
- action, recipient = self._send("freebusy", calresource)
- action["timerange"] = [timerange.start.getText(), timerange.end.getText()]
- action["matchtotal"] = matchtotal
- action["excludeuid"] = excludeuid
- action["organizer"] = organizer
- action["organizerPrincipal"] = organizerPrincipal
- action["same_calendar_user"] = same_calendar_user
- action["servertoserver"] = servertoserver
- action["event_details"] = event_details
- result = yield self.sendRequest(calresource._txn, recipient, action)
- if result["result"] == "ok":
- returnValue((result["fbresults"], result["matchtotal"],))
- elif result["result"] == "exception":
- raise namedClass(result["class"])(result["message"])
-
-
- @inlineCallbacks
- def recv_freebusy(self, txn, message):
- """
- Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}.
-
- @param message: message arguments
- @type message: C{dict}
- """
-
- shareeView, _ignore_objectResource = yield self._recv(txn, message, "freebusy")
- try:
- # Operate on the L{CommonHomeChild}
- fbinfo = [[], [], []]
- matchtotal = yield generateFreeBusyInfo(
- shareeView,
- fbinfo,
- TimeRange(start=message["timerange"][0], end=message["timerange"][1]),
- message["matchtotal"],
- message["excludeuid"],
- message["organizer"],
- message["organizerPrincipal"],
- message["same_calendar_user"],
- message["servertoserver"],
- message["event_details"],
- logItems=None
- )
- except Exception as e:
- returnValue({
- "result": "exception",
- "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
- "message": str(e),
- })
-
- for i in range(3):
- for j in range(len(fbinfo[i])):
- fbinfo[i][j] = fbinfo[i][j].getText()
-
- returnValue({
- "result": "ok",
- "fbresults": fbinfo,
- "matchtotal": matchtotal,
- })
-
-
- @staticmethod
- def _to_tuple(value, shareeView, objectResource):
- return tuple(value)
-
-
- @staticmethod
- def _to_string(value, shareeView, objectResource):
- return str(value)
-
-
- @staticmethod
- def _to_externalize(value, shareeView, objectResource):
- if isinstance(value, shareeView._objectResourceClass):
- value = value.externalize()
- elif value is not None:
- value = [v.externalize() for v in value]
- return value
-
-
- @classmethod
- def _make_simple_homechild_action(cls, action, method, transform_recv=None, transform_send=None):
- setattr(
- cls,
- "send_{}".format(action),
- lambda self, shareeView, *args, **kwargs:
- self._simple_send(action, shareeView, transform=transform_send, args=args, kwargs=kwargs)
- )
- setattr(
- cls,
- "recv_{}".format(action),
- lambda self, txn, message:
- self._simple_recv(txn, action, message, method, transform=transform_recv)
- )
-
-
- @classmethod
- def _make_simple_object_action(cls, action, method, transform_recv=None, transform_send=None):
- setattr(
- cls,
- "send_{}".format(action),
- lambda self, shareeView, objectResource, *args, **kwargs:
- self._simple_send(action, shareeView, objectResource, transform=transform_send, args=args, kwargs=kwargs)
- )
- setattr(
- cls,
- "recv_{}".format(action),
- lambda self, txn, message:
- self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_recv)
- )
-
-
-# Calls on L{CommonHomeChild} objects
-PoddingConduit._make_simple_homechild_action("countobjects", "countObjectResources")
-PoddingConduit._make_simple_homechild_action("listobjects", "listObjectResources")
-PoddingConduit._make_simple_homechild_action("resourceuidforname", "resourceUIDForName")
-PoddingConduit._make_simple_homechild_action("resourcenameforuid", "resourceNameForUID")
-PoddingConduit._make_simple_homechild_action("movehere", "moveObjectResourceHere")
-PoddingConduit._make_simple_homechild_action("moveaway", "moveObjectResourceAway")
-PoddingConduit._make_simple_homechild_action("synctoken", "syncToken")
-PoddingConduit._make_simple_homechild_action("resourcenamessincerevision", "resourceNamesSinceRevision", transform_send=PoddingConduit._to_tuple)
-PoddingConduit._make_simple_homechild_action("search", "search")
-
-# Calls on L{CommonObjectResource} objects
-PoddingConduit._make_simple_object_action("loadallobjects", "loadAllObjects", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("loadallobjectswithnames", "loadAllObjectsWithNames", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("objectwith", "objectWith", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("create", "create", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("setcomponent", "setComponent")
-PoddingConduit._make_simple_object_action("component", "component", transform_recv=PoddingConduit._to_string)
-PoddingConduit._make_simple_object_action("remove", "remove")
Copied: CalendarServer/trunk/txdav/common/datastore/podding/conduit.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/conduit.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/conduit.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,846 @@
+##
+# Copyright (c) 2013 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 twext.python.log import Logger
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.reflect import namedClass
+
+from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+from txdav.common.datastore.podding.request import ConduitRequest
+from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
+from txdav.common.icommondatastore import ExternalShareFailed
+
+from twistedcaldav.caldavxml import TimeRange
+
+
+__all__ = [
+ "PoddingConduitResource",
+]
+
+log = Logger()
+
+
+class FailedCrossPodRequestError(RuntimeError):
+ """
+ Request returned an error.
+ """
+ pass
+
+
+
+class PoddingConduit(object):
+ """
+ This class is the API/RPC bridge between cross-pod requests and the store.
+
+ Each cross-pod request/response is described by a Python C{dict} that is serialized
+ to JSON for the HTTP request/response.
+
+ Each request C{dict} has an "action" key that indicates what call is being made, and
+ the other keys are arguments to that call.
+
+ Each response C{dict} has a "result" key that indicates the call result, and other
+ optional keys for any values returned by the call.
+
+ The conduit provides two methods for each action: one for the sending side and one for
+ the receiving side, called "send_{action}" and "recv_{action}", respectively, where
+ {action} is the action value.
+
+ The "send_{action}" calls each have a set of arguments specific to the call itself. The
+ code takes care of packing that into a C{dict} and sending to the appropriate pod.
+
+ The "recv_{action}" calls take a single C{dict} argument that is the deserialized JSON
+ data from the incoming request. The return value is a C{dict} with the result.
+
+ Some simple forms of send_/recv_ methods can be auto-generated to simplify coding.
+
+ Right now this conduit is used for cross-pod sharing operations. In the future we will
+ likely use it for cross-pod migration.
+ """
+
+ conduitRequestClass = ConduitRequest
+
+ def __init__(self, store):
+ """
+ @param store: the L{CommonDataStore} in use.
+ """
+ self.store = store
+
+
+ def validRequst(self, source_guid, destination_guid):
+ """
+ Verify that the specified GUIDs are valid for the request and return the
+ matching directory records.
+
+ @param source_guid: GUID for the user on whose behalf the request is being made
+ @type source_guid: C{str}
+ @param destination_guid: GUID for the user to whom the request is being sent
+ @type destination_guid: C{str}
+
+ @return: C{tuple} of L{IStoreDirectoryRecord}
+ """
+
+ source = self.store.directoryService().recordWithUID(source_guid)
+ if source is None:
+ raise DirectoryRecordNotFoundError("Cross-pod source: {}".format(source_guid))
+ if not source.thisServer():
+ raise FailedCrossPodRequestError("Cross-pod source not on this server: {}".format(source_guid))
+
+ destination = self.store.directoryService().recordWithUID(destination_guid)
+ if destination is None:
+ raise DirectoryRecordNotFoundError("Cross-pod destination: {}".format(destination_guid))
+ if destination.thisServer():
+ raise FailedCrossPodRequestError("Cross-pod destination on this server: {}".format(destination_guid))
+
+ return (source, destination,)
+
+
+ @inlineCallbacks
+ def sendRequest(self, txn, recipient, data, stream=None, streamType=None):
+
+ request = self.conduitRequestClass(recipient.server(), data, stream, streamType)
+ try:
+ response = (yield request.doRequest(txn))
+ except Exception as e:
+ raise FailedCrossPodRequestError("Failed cross-pod request: {}".format(e))
+ returnValue(response)
+
+
+ @inlineCallbacks
+ def processRequest(self, data):
+ """
+ Process the request.
+
+ @param data: the JSON data to process
+ @type data: C{dict}
+ """
+ # Must have a dict with an "action" key
+ try:
+ action = data["action"]
+ except (KeyError, TypeError) as e:
+ log.error("JSON data must have an object as its root with an 'action' attribute: {ex}\n{json}", ex=e, json=data)
+ raise FailedCrossPodRequestError("JSON data must have an object as its root with an 'action' attribute: {}\n{}".format(e, data,))
+
+ if action == "ping":
+ result = {"result": "ok"}
+ returnValue(result)
+
+ method = "recv_{}".format(action.replace("-", "_"))
+ if not hasattr(self, method):
+ log.error("Unsupported action: {action}", action=action)
+ raise FailedCrossPodRequestError("Unsupported action: {}".format(action))
+
+ # Need a transaction to work with
+ txn = self.store.newTransaction(repr("Conduit request"))
+
+ # Do the actual request processing
+ try:
+ result = (yield getattr(self, method)(txn, data))
+ except Exception as e:
+ yield txn.abort()
+ log.error("Failed action: {action}, {ex}", action=action, ex=e)
+ raise FailedCrossPodRequestError("Failed action: {}, {}".format(action, e))
+
+ yield txn.commit()
+
+ returnValue(result)
+
+
+ #
+ # Invite related apis
+ #
+
+ @inlineCallbacks
+ def send_shareinvite(self, txn, homeType, ownerUID, ownerID, ownerName, shareeUID, shareUID, bindMode, summary, copy_properties, supported_components):
+ """
+ Send a sharing invite cross-pod message.
+
+ @param homeType: Type of home being shared.
+ @type homeType: C{int}
+ @param ownerUID: GUID of the sharer.
+ @type ownerUID: C{str}
+ @param ownerID: resource ID of the sharer calendar
+ @type ownerID: C{int}
+ @param ownerName: owner's name of the sharer calendar
+ @type ownerName: C{str}
+ @param shareeUID: GUID of the sharee
+ @type shareeUID: C{str}
+ @param shareUID: Resource/invite ID for sharee
+ @type shareUID: C{str}
+ @param bindMode: bind mode for the share
+ @type bindMode: C{str}
+ @param summary: sharing message
+ @type summary: C{str}
+ @param copy_properties: C{str} name/value for properties to be copied
+ @type copy_properties: C{dict}
+ @param supported_components: supproted components, may be C{None}
+ @type supported_components: C{str}
+ """
+
+ _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
+
+ action = {
+ "action": "shareinvite",
+ "type": homeType,
+ "owner": ownerUID,
+ "owner_id": ownerID,
+ "owner_name": ownerName,
+ "sharee": shareeUID,
+ "share_id": shareUID,
+ "mode": bindMode,
+ "summary": summary,
+ "properties": copy_properties,
+ }
+ if supported_components is not None:
+ action["supported-components"] = supported_components
+
+ result = yield self.sendRequest(txn, recipient, action)
+ returnValue(result)
+
+
+ @inlineCallbacks
+ def recv_shareinvite(self, txn, message):
+ """
+ Process a sharing invite cross-pod message. Message arguments as per L{send_shareinvite}.
+
+ @param message: message arguments
+ @type message: C{dict}
+ """
+
+ if message["action"] != "shareinvite":
+ raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareinvite".format(message["action"]))
+
+ # Create a share
+ shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
+ if shareeHome is None or shareeHome.external():
+ raise FailedCrossPodRequestError("Invalid sharee UID specified")
+
+ try:
+ yield shareeHome.processExternalInvite(
+ message["owner"],
+ message["owner_id"],
+ message["owner_name"],
+ message["share_id"],
+ message["mode"],
+ message["summary"],
+ message["properties"],
+ supported_components=message.get("supported-components")
+ )
+ except ExternalShareFailed as e:
+ raise FailedCrossPodRequestError(str(e))
+
+ returnValue({
+ "result": "ok",
+ })
+
+
+ @inlineCallbacks
+ def send_shareuninvite(self, txn, homeType, ownerUID, ownerID, shareeUID, shareUID):
+ """
+ Send a sharing uninvite cross-pod message.
+
+ @param homeType: Type of home being shared.
+ @type homeType: C{int}
+ @param ownerUID: GUID of the sharer.
+ @type ownerUID: C{str}
+ @param ownerID: resource ID of the sharer calendar
+ @type ownerID: C{int}
+ @param shareeUID: GUID of the sharee
+ @type shareeUID: C{str}
+ @param shareUID: Resource/invite ID for sharee
+ @type shareUID: C{str}
+ """
+
+ _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
+
+ action = {
+ "action": "shareuninvite",
+ "type": homeType,
+ "owner": ownerUID,
+ "owner_id": ownerID,
+ "sharee": shareeUID,
+ "share_id": shareUID,
+ }
+
+ result = yield self.sendRequest(txn, recipient, action)
+ returnValue(result)
+
+
+ @inlineCallbacks
+ def recv_shareuninvite(self, txn, message):
+ """
+ Process a sharing uninvite cross-pod message. Message arguments as per L{send_shareuninvite}.
+
+ @param message: message arguments
+ @type message: C{dict}
+ """
+
+ if message["action"] != "shareuninvite":
+ raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareuninvite".format(message["action"]))
+
+ # Create a share
+ shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
+ if shareeHome is None or shareeHome.external():
+ FailedCrossPodRequestError("Invalid sharee UID specified")
+
+ try:
+ yield shareeHome.processExternalUninvite(
+ message["owner"],
+ message["owner_id"],
+ message["share_id"],
+ )
+ except ExternalShareFailed as e:
+ FailedCrossPodRequestError(str(e))
+
+ returnValue({
+ "result": "ok",
+ })
+
+
+ @inlineCallbacks
+ def send_sharereply(self, txn, homeType, ownerUID, shareeUID, shareUID, bindStatus, summary=None):
+ """
+ Send a sharing reply cross-pod message.
+
+ @param homeType: Type of home being shared.
+ @type homeType: C{int}
+ @param ownerUID: GUID of the sharer.
+ @type ownerUID: C{str}
+ @param shareeUID: GUID of the recipient
+ @type shareeUID: C{str}
+ @param shareUID: Resource/invite ID for recipient
+ @type shareUID: C{str}
+ @param bindStatus: bind mode for the share
+ @type bindStatus: C{str}
+ @param summary: sharing message
+ @type summary: C{str}
+ """
+
+ _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
+
+ action = {
+ "action": "sharereply",
+ "type": homeType,
+ "owner": ownerUID,
+ "sharee": shareeUID,
+ "share_id": shareUID,
+ "status": bindStatus,
+ }
+ if summary is not None:
+ action["summary"] = summary
+
+ result = yield self.sendRequest(txn, recipient, action)
+ returnValue(result)
+
+
+ @inlineCallbacks
+ def recv_sharereply(self, txn, message):
+ """
+ Process a sharing reply cross-pod message. Message arguments as per L{send_sharereply}.
+
+ @param message: message arguments
+ @type message: C{dict}
+ """
+
+ if message["action"] != "sharereply":
+ raise FailedCrossPodRequestError("Wrong action '{}' for recv_sharereply".format(message["action"]))
+
+ # Create a share
+ ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
+ if ownerHome is None or ownerHome.external():
+ FailedCrossPodRequestError("Invalid owner UID specified")
+
+ try:
+ yield ownerHome.processExternalReply(
+ message["owner"],
+ message["sharee"],
+ message["share_id"],
+ message["status"],
+ summary=message.get("summary")
+ )
+ except ExternalShareFailed as e:
+ FailedCrossPodRequestError(str(e))
+
+ returnValue({
+ "result": "ok",
+ })
+
+
+ #
+ # Managed attachment related apis
+ #
+
+ @inlineCallbacks
+ def send_add_attachment(self, objectResource, rids, content_type, filename, stream):
+ """
+ Managed attachment addAttachment call.
+
+ @param objectResource: child resource having an attachment added
+ @type objectResource: L{CalendarObject}
+ @param rids: list of recurrence ids
+ @type rids: C{list}
+ @param content_type: content type of attachment data
+ @type content_type: L{MimeType}
+ @param filename: name of attachment
+ @type filename: C{str}
+ @param stream: attachment data stream
+ @type stream: L{IStream}
+ """
+
+ actionName = "add-attachment"
+ shareeView = objectResource._parentCollection
+ action, recipient = self._send(actionName, shareeView, objectResource)
+ action["rids"] = rids
+ action["filename"] = filename
+ result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
+ if result["result"] == "ok":
+ returnValue(result["value"])
+ elif result["result"] == "exception":
+ raise namedClass(result["class"])(result["message"])
+
+
+ @inlineCallbacks
+ def recv_add_attachment(self, txn, message):
+ """
+ Process an addAttachment cross-pod message. Message arguments as per L{send_add_attachment}.
+
+ @param message: message arguments
+ @type message: C{dict}
+ """
+
+ actionName = "add-attachment"
+ _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
+ try:
+ attachment, location = yield objectResource.addAttachment(
+ message["rids"],
+ message["streamType"],
+ message["filename"],
+ message["stream"],
+ )
+ except Exception as e:
+ returnValue({
+ "result": "exception",
+ "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+ "message": str(e),
+ })
+
+ returnValue({
+ "result": "ok",
+ "value": (attachment.managedID(), location,),
+ })
+
+
+ @inlineCallbacks
+ def send_update_attachment(self, objectResource, managed_id, content_type, filename, stream):
+ """
+ Managed attachment updateAttachment call.
+
+ @param objectResource: child resource having an attachment added
+ @type objectResource: L{CalendarObject}
+ @param managed_id: managed-id to update
+ @type managed_id: C{str}
+ @param content_type: content type of attachment data
+ @type content_type: L{MimeType}
+ @param filename: name of attachment
+ @type filename: C{str}
+ @param stream: attachment data stream
+ @type stream: L{IStream}
+ """
+
+ actionName = "update-attachment"
+ shareeView = objectResource._parentCollection
+ action, recipient = self._send(actionName, shareeView, objectResource)
+ action["managedID"] = managed_id
+ action["filename"] = filename
+ result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
+ if result["result"] == "ok":
+ returnValue(result["value"])
+ elif result["result"] == "exception":
+ raise namedClass(result["class"])(result["message"])
+
+
+ @inlineCallbacks
+ def recv_update_attachment(self, txn, message):
+ """
+ Process an updateAttachment cross-pod message. Message arguments as per L{send_update_attachment}.
+
+ @param message: message arguments
+ @type message: C{dict}
+ """
+
+ actionName = "update-attachment"
+ _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
+ try:
+ attachment, location = yield objectResource.updateAttachment(
+ message["managedID"],
+ message["streamType"],
+ message["filename"],
+ message["stream"],
+ )
+ except Exception as e:
+ returnValue({
+ "result": "exception",
+ "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+ "message": str(e),
+ })
+
+ returnValue({
+ "result": "ok",
+ "value": (attachment.managedID(), location,),
+ })
+
+
+ @inlineCallbacks
+ def send_remove_attachment(self, objectResource, rids, managed_id):
+ """
+ Managed attachment removeAttachment call.
+
+ @param objectResource: child resource having an attachment added
+ @type objectResource: L{CalendarObject}
+ @param rids: list of recurrence ids
+ @type rids: C{list}
+ @param managed_id: managed-id to update
+ @type managed_id: C{str}
+ """
+
+ actionName = "remove-attachment"
+ shareeView = objectResource._parentCollection
+ action, recipient = self._send(actionName, shareeView, objectResource)
+ action["rids"] = rids
+ action["managedID"] = managed_id
+ result = yield self.sendRequest(shareeView._txn, recipient, action)
+ if result["result"] == "ok":
+ returnValue(result["value"])
+ elif result["result"] == "exception":
+ raise namedClass(result["class"])(result["message"])
+
+
+ @inlineCallbacks
+ def recv_remove_attachment(self, txn, message):
+ """
+ Process an removeAttachment cross-pod message. Message arguments as per L{send_remove_attachment}.
+
+ @param message: message arguments
+ @type message: C{dict}
+ """
+
+ actionName = "remove-attachment"
+ _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
+ try:
+ yield objectResource.removeAttachment(
+ message["rids"],
+ message["managedID"],
+ )
+ except Exception as e:
+ returnValue({
+ "result": "exception",
+ "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+ "message": str(e),
+ })
+
+ returnValue({
+ "result": "ok",
+ "value": None,
+ })
+
+
+ #
+ # Sharer data access related apis
+ #
+
+ def _send(self, action, parent, child=None):
+ """
+ Base behavior for an operation on a L{CommonHomeChild}.
+
+ @param shareeView: sharee resource being operated on.
+ @type shareeView: L{CommonHomeChildExternal}
+ """
+
+ homeType = parent.ownerHome()._homeType
+ ownerUID = parent.ownerHome().uid()
+ ownerID = parent.external_id()
+ shareeUID = parent.viewerHome().uid()
+
+ _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
+
+ result = {
+ "action": action,
+ "type": homeType,
+ "owner": ownerUID,
+ "owner_id": ownerID,
+ "sharee": shareeUID,
+ }
+ if child is not None:
+ result["resource_id"] = child.id()
+ return result, recipient
+
+
+ @inlineCallbacks
+ def _recv(self, txn, message, expected_action):
+ """
+ Base behavior for sharer data access.
+
+ @param message: message arguments
+ @type message: C{dict}
+ """
+
+ if message["action"] != expected_action:
+ raise FailedCrossPodRequestError("Wrong action '{}' for recv_{}".format(message["action"], expected_action))
+
+ # Get a share
+ ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
+ if ownerHome is None or ownerHome.external():
+ FailedCrossPodRequestError("Invalid owner UID specified")
+
+ shareeHome = yield txn.homeWithUID(message["type"], message["sharee"])
+ if shareeHome is None or not shareeHome.external():
+ FailedCrossPodRequestError("Invalid sharee UID specified")
+
+ shareeView = yield shareeHome.childWithID(message["owner_id"])
+ if shareeView is None:
+ FailedCrossPodRequestError("Invalid shared resource specified")
+
+ resourceID = message.get("resource_id", None)
+ if resourceID is not None:
+ objectResource = yield shareeView.objectResourceWithID(resourceID)
+ if objectResource is None:
+ FailedCrossPodRequestError("Invalid owner shared object resource specified")
+ else:
+ objectResource = None
+
+ returnValue((shareeView, objectResource,))
+
+
+ #
+ # Simple calls are ones where there is no argument and a single return value. We can simplify
+ # code generation for these by dynamically generating the appropriate class methods.
+ #
+
+ @inlineCallbacks
+ def _simple_send(self, actionName, shareeView, objectResource=None, transform=None, args=None, kwargs=None):
+ """
+ A simple send operation that returns a value.
+
+ @param actionName: name of the action.
+ @type actionName: C{str}
+ @param shareeView: sharee resource being operated on.
+ @type shareeView: L{CommonHomeChildExternal}
+ @param objectResource: the resource being operated on, or C{None} for classmethod.
+ @type objectResource: L{CommonObjectResourceExternal}
+ @param transform: a function used to convert the JSON result into return values.
+ @type transform: C{callable}
+ @param args: list of optional arguments.
+ @type args: C{list}
+ @param kwargs: optional keyword arguments.
+ @type kwargs: C{dict}
+ """
+
+ action, recipient = self._send(actionName, shareeView, objectResource)
+ if args is not None:
+ action["arguments"] = args
+ if kwargs is not None:
+ action["keywords"] = kwargs
+ result = yield self.sendRequest(shareeView._txn, recipient, action)
+ if result["result"] == "ok":
+ returnValue(result["value"] if transform is None else transform(result["value"], shareeView, objectResource))
+ elif result["result"] == "exception":
+ raise namedClass(result["class"])(result["message"])
+
+
+ @inlineCallbacks
+ def _simple_recv(self, txn, actionName, message, method, onHomeChild=True, transform=None):
+ """
+ A simple recv operation that returns a value. We also look for an optional set of arguments/keywords
+ and include those only if present.
+
+ @param actionName: name of the action.
+ @type actionName: C{str}
+ @param message: message arguments
+ @type message: C{dict}
+ @param method: name of the method to execute on the shared resource to get the result.
+ @type method: C{str}
+ @param transform: method to call on returned JSON value to convert it to something useful.
+ @type transform: C{callable}
+ """
+
+ shareeView, objectResource = yield self._recv(txn, message, actionName)
+ try:
+ if onHomeChild:
+ # Operate on the L{CommonHomeChild}
+ value = yield getattr(shareeView, method)(*message.get("arguments", ()), **message.get("keywords", {}))
+ else:
+ # Operate on the L{CommonObjectResource}
+ if objectResource is not None:
+ value = yield getattr(objectResource, method)(*message.get("arguments", ()), **message.get("keywords", {}))
+ else:
+ # classmethod call
+ value = yield getattr(shareeView._objectResourceClass, method)(shareeView, *message.get("arguments", ()), **message.get("keywords", {}))
+ except Exception as e:
+ returnValue({
+ "result": "exception",
+ "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+ "message": str(e),
+ })
+
+ returnValue({
+ "result": "ok",
+ "value": transform(value, shareeView, objectResource) if transform is not None else value,
+ })
+
+
+ @inlineCallbacks
+ def send_freebusy(
+ self,
+ calresource,
+ timerange,
+ matchtotal,
+ excludeuid,
+ organizer,
+ organizerPrincipal,
+ same_calendar_user,
+ servertoserver,
+ event_details,
+ ):
+ action, recipient = self._send("freebusy", calresource)
+ action["timerange"] = [timerange.start.getText(), timerange.end.getText()]
+ action["matchtotal"] = matchtotal
+ action["excludeuid"] = excludeuid
+ action["organizer"] = organizer
+ action["organizerPrincipal"] = organizerPrincipal
+ action["same_calendar_user"] = same_calendar_user
+ action["servertoserver"] = servertoserver
+ action["event_details"] = event_details
+ result = yield self.sendRequest(calresource._txn, recipient, action)
+ if result["result"] == "ok":
+ returnValue((result["fbresults"], result["matchtotal"],))
+ elif result["result"] == "exception":
+ raise namedClass(result["class"])(result["message"])
+
+
+ @inlineCallbacks
+ def recv_freebusy(self, txn, message):
+ """
+ Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}.
+
+ @param message: message arguments
+ @type message: C{dict}
+ """
+
+ shareeView, _ignore_objectResource = yield self._recv(txn, message, "freebusy")
+ try:
+ # Operate on the L{CommonHomeChild}
+ fbinfo = [[], [], []]
+ matchtotal = yield generateFreeBusyInfo(
+ shareeView,
+ fbinfo,
+ TimeRange(start=message["timerange"][0], end=message["timerange"][1]),
+ message["matchtotal"],
+ message["excludeuid"],
+ message["organizer"],
+ message["organizerPrincipal"],
+ message["same_calendar_user"],
+ message["servertoserver"],
+ message["event_details"],
+ logItems=None
+ )
+ except Exception as e:
+ returnValue({
+ "result": "exception",
+ "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+ "message": str(e),
+ })
+
+ for i in range(3):
+ for j in range(len(fbinfo[i])):
+ fbinfo[i][j] = fbinfo[i][j].getText()
+
+ returnValue({
+ "result": "ok",
+ "fbresults": fbinfo,
+ "matchtotal": matchtotal,
+ })
+
+
+ @staticmethod
+ def _to_tuple(value, shareeView, objectResource):
+ return tuple(value)
+
+
+ @staticmethod
+ def _to_string(value, shareeView, objectResource):
+ return str(value)
+
+
+ @staticmethod
+ def _to_externalize(value, shareeView, objectResource):
+ if isinstance(value, shareeView._objectResourceClass):
+ value = value.externalize()
+ elif value is not None:
+ value = [v.externalize() for v in value]
+ return value
+
+
+ @classmethod
+ def _make_simple_homechild_action(cls, action, method, transform_recv=None, transform_send=None):
+ setattr(
+ cls,
+ "send_{}".format(action),
+ lambda self, shareeView, *args, **kwargs:
+ self._simple_send(action, shareeView, transform=transform_send, args=args, kwargs=kwargs)
+ )
+ setattr(
+ cls,
+ "recv_{}".format(action),
+ lambda self, txn, message:
+ self._simple_recv(txn, action, message, method, transform=transform_recv)
+ )
+
+
+ @classmethod
+ def _make_simple_object_action(cls, action, method, transform_recv=None, transform_send=None):
+ setattr(
+ cls,
+ "send_{}".format(action),
+ lambda self, shareeView, objectResource, *args, **kwargs:
+ self._simple_send(action, shareeView, objectResource, transform=transform_send, args=args, kwargs=kwargs)
+ )
+ setattr(
+ cls,
+ "recv_{}".format(action),
+ lambda self, txn, message:
+ self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_recv)
+ )
+
+
+# Calls on L{CommonHomeChild} objects
+PoddingConduit._make_simple_homechild_action("countobjects", "countObjectResources")
+PoddingConduit._make_simple_homechild_action("listobjects", "listObjectResources")
+PoddingConduit._make_simple_homechild_action("resourceuidforname", "resourceUIDForName")
+PoddingConduit._make_simple_homechild_action("resourcenameforuid", "resourceNameForUID")
+PoddingConduit._make_simple_homechild_action("movehere", "moveObjectResourceHere")
+PoddingConduit._make_simple_homechild_action("moveaway", "moveObjectResourceAway")
+PoddingConduit._make_simple_homechild_action("synctoken", "syncToken")
+PoddingConduit._make_simple_homechild_action("resourcenamessincerevision", "resourceNamesSinceRevision", transform_send=PoddingConduit._to_tuple)
+PoddingConduit._make_simple_homechild_action("search", "search")
+
+# Calls on L{CommonObjectResource} objects
+PoddingConduit._make_simple_object_action("loadallobjects", "loadAllObjects", transform_recv=PoddingConduit._to_externalize)
+PoddingConduit._make_simple_object_action("loadallobjectswithnames", "loadAllObjectsWithNames", transform_recv=PoddingConduit._to_externalize)
+PoddingConduit._make_simple_object_action("objectwith", "objectWith", transform_recv=PoddingConduit._to_externalize)
+PoddingConduit._make_simple_object_action("create", "create", transform_recv=PoddingConduit._to_externalize)
+PoddingConduit._make_simple_object_action("setcomponent", "setComponent")
+PoddingConduit._make_simple_object_action("component", "component", transform_recv=PoddingConduit._to_string)
+PoddingConduit._make_simple_object_action("remove", "remove")
Deleted: CalendarServer/trunk/txdav/common/datastore/podding/request.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/request.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,191 +0,0 @@
-##
-# Copyright (c) 2013 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 calendarserver.version import version
-
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.python.log import Logger
-from twext.web2 import responsecode
-from twext.web2.client.http import HTTPClientProtocol, ClientRequest
-from twext.web2.dav.util import allDataFromStream
-from twext.web2.http_headers import Headers, MimeType
-from twext.web2.stream import MemoryStream
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.internet.protocol import Factory
-
-from twistedcaldav.accounting import accountingEnabledForCategory, \
- emitAccounting
-from twistedcaldav.client.pool import _configuredClientContextFactory
-from twistedcaldav.config import config
-from twistedcaldav.util import utf8String
-
-from cStringIO import StringIO
-import base64
-import json
-
-
-log = Logger()
-
-
-
-class ConduitRequest(object):
- """
- An HTTP request between pods. This is typically used to send and receive JSON data. However,
- for attachments, we need to send the actual attachment data as the request body, so in that
- case the JSON data is sent in an HTTP header.
- """
-
- def __init__(self, server, data, stream=None, stream_type=None):
- self.server = server
- self.data = json.dumps(data)
- self.stream = stream
- self.streamType = stream_type
-
-
- @inlineCallbacks
- def doRequest(self, txn):
-
- # Generate an HTTP client request
- try:
- if "xpod" not in txn.logItems:
- txn.logItems["xpod"] = 0
- txn.logItems["xpod"] += 1
-
- response = (yield self._processRequest())
-
- if accountingEnabledForCategory("xPod"):
- self.loggedResponse = yield self.logResponse(response)
- emitAccounting("xPod", "", self.loggedRequest + "\n" + self.loggedResponse, "POST")
-
- if response.code in (responsecode.OK,):
- data = (yield allDataFromStream(response.stream))
- data = json.loads(data)
- else:
- raise ValueError("Incorrect cross-pod response status code: {}".format(response.code))
-
- except Exception as e:
- # Request failed
- log.error("Could not do cross-pod request : {request} {ex}", request=self, ex=e)
- raise ValueError("Failed cross-pod request: {}".format(e))
-
- returnValue(data)
-
-
- @inlineCallbacks
- def logRequest(self, request):
- """
- Log an HTTP request.
- """
-
- iostr = StringIO()
- iostr.write(">>>> Request start\n\n")
- if hasattr(request, "clientproto"):
- protocol = "HTTP/{:d}.{:d}".format(request.clientproto[0], request.clientproto[1])
- else:
- protocol = "HTTP/1.1"
- iostr.write("{} {} {}\n".format(request.method, request.uri, protocol))
- for name, valuelist in request.headers.getAllRawHeaders():
- for value in valuelist:
- # Do not log authorization details
- if name not in ("Authorization",):
- iostr.write("{}: {}\n".format(name, value))
- else:
- iostr.write("{}: xxxxxxxxx\n".format(name))
- iostr.write("\n")
-
- # We need to play a trick with the request stream as we can only read it once. So we
- # read it, store the value in a MemoryStream, and replace the request's stream with that,
- # so the data can be read again. Note if we are sending an attachment, we won't log
- # the attachment data as we do not want to read it all into memory.
- if self.stream is None:
- data = (yield allDataFromStream(request.stream))
- iostr.write(data)
- request.stream = MemoryStream(data if data is not None else "")
- request.stream.doStartReading = None
- else:
- iostr.write("<<Stream Type: {}>>\n".format(self.streamType))
-
- iostr.write("\n\n>>>> Request end\n")
- returnValue(iostr.getvalue())
-
-
- @inlineCallbacks
- def logResponse(self, response):
- """
- Log an HTTP request.
- """
- iostr = StringIO()
- iostr.write(">>>> Response start\n\n")
- code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
- iostr.write("HTTP/1.1 {:d} {}\n".format(response.code, code_message))
- for name, valuelist in response.headers.getAllRawHeaders():
- for value in valuelist:
- # Do not log authorization details
- if name not in ("WWW-Authenticate",):
- iostr.write("{}: {}\n".format(name, value))
- else:
- iostr.write("{}: xxxxxxxxx\n".format(name))
- iostr.write("\n")
-
- # We need to play a trick with the response stream to ensure we don't mess it up. So we
- # read it, store the value in a MemoryStream, and replace the response's stream with that,
- # so the data can be read again.
- data = (yield allDataFromStream(response.stream))
- iostr.write(data)
- response.stream = MemoryStream(data if data is not None else "")
- response.stream.doStartReading = None
-
- iostr.write("\n\n>>>> Response end\n")
- returnValue(iostr.getvalue())
-
-
- @inlineCallbacks
- def _processRequest(self):
- """
- Process the request by sending it to the relevant server.
-
- @return: the HTTP response.
- @rtype: L{Response}
- """
- ssl, host, port, _ignore_path = self.server.details()
- path = "/" + config.Servers.ConduitName
-
- headers = Headers()
- headers.setHeader("Host", utf8String(host + ":{}".format(port)))
- if self.streamType:
- # For attachments we put the base64-encoded JSON data into a header
- headers.setHeader("Content-Type", self.streamType)
- headers.addRawHeader("XPOD", base64.b64encode(self.data))
- else:
- headers.setHeader("Content-Type", MimeType("application", "json", params={"charset": "utf-8", }))
- headers.setHeader("User-Agent", "CalendarServer/{}".format(version))
- headers.addRawHeader(*self.server.secretHeader())
-
- from twisted.internet import reactor
- f = Factory()
- f.protocol = HTTPClientProtocol
- ep = GAIEndpoint(reactor, host, port, _configuredClientContextFactory() if ssl else None)
- proto = (yield ep.connect(f))
-
- request = ClientRequest("POST", path, headers, self.stream if self.stream is not None else self.data)
-
- if accountingEnabledForCategory("xPod"):
- self.loggedRequest = yield self.logRequest(request)
-
- response = (yield proto.submitRequest(request))
-
- returnValue(response)
Copied: CalendarServer/trunk/txdav/common/datastore/podding/request.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/request.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/request.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,192 @@
+##
+# Copyright (c) 2013 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 calendarserver.version import version
+
+from twext.internet.gaiendpoint import GAIEndpoint
+from twext.python.log import Logger
+
+from txweb2 import responsecode
+from txweb2.client.http import HTTPClientProtocol, ClientRequest
+from txweb2.dav.util import allDataFromStream
+from txweb2.http_headers import Headers, MimeType
+from txweb2.stream import MemoryStream
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.protocol import Factory
+
+from twistedcaldav.accounting import accountingEnabledForCategory, \
+ emitAccounting
+from twistedcaldav.client.pool import _configuredClientContextFactory
+from twistedcaldav.config import config
+from twistedcaldav.util import utf8String
+
+from cStringIO import StringIO
+import base64
+import json
+
+
+log = Logger()
+
+
+
+class ConduitRequest(object):
+ """
+ An HTTP request between pods. This is typically used to send and receive JSON data. However,
+ for attachments, we need to send the actual attachment data as the request body, so in that
+ case the JSON data is sent in an HTTP header.
+ """
+
+ def __init__(self, server, data, stream=None, stream_type=None):
+ self.server = server
+ self.data = json.dumps(data)
+ self.stream = stream
+ self.streamType = stream_type
+
+
+ @inlineCallbacks
+ def doRequest(self, txn):
+
+ # Generate an HTTP client request
+ try:
+ if "xpod" not in txn.logItems:
+ txn.logItems["xpod"] = 0
+ txn.logItems["xpod"] += 1
+
+ response = (yield self._processRequest())
+
+ if accountingEnabledForCategory("xPod"):
+ self.loggedResponse = yield self.logResponse(response)
+ emitAccounting("xPod", "", self.loggedRequest + "\n" + self.loggedResponse, "POST")
+
+ if response.code in (responsecode.OK,):
+ data = (yield allDataFromStream(response.stream))
+ data = json.loads(data)
+ else:
+ raise ValueError("Incorrect cross-pod response status code: {}".format(response.code))
+
+ except Exception as e:
+ # Request failed
+ log.error("Could not do cross-pod request : {request} {ex}", request=self, ex=e)
+ raise ValueError("Failed cross-pod request: {}".format(e))
+
+ returnValue(data)
+
+
+ @inlineCallbacks
+ def logRequest(self, request):
+ """
+ Log an HTTP request.
+ """
+
+ iostr = StringIO()
+ iostr.write(">>>> Request start\n\n")
+ if hasattr(request, "clientproto"):
+ protocol = "HTTP/{:d}.{:d}".format(request.clientproto[0], request.clientproto[1])
+ else:
+ protocol = "HTTP/1.1"
+ iostr.write("{} {} {}\n".format(request.method, request.uri, protocol))
+ for name, valuelist in request.headers.getAllRawHeaders():
+ for value in valuelist:
+ # Do not log authorization details
+ if name not in ("Authorization",):
+ iostr.write("{}: {}\n".format(name, value))
+ else:
+ iostr.write("{}: xxxxxxxxx\n".format(name))
+ iostr.write("\n")
+
+ # We need to play a trick with the request stream as we can only read it once. So we
+ # read it, store the value in a MemoryStream, and replace the request's stream with that,
+ # so the data can be read again. Note if we are sending an attachment, we won't log
+ # the attachment data as we do not want to read it all into memory.
+ if self.stream is None:
+ data = (yield allDataFromStream(request.stream))
+ iostr.write(data)
+ request.stream = MemoryStream(data if data is not None else "")
+ request.stream.doStartReading = None
+ else:
+ iostr.write("<<Stream Type: {}>>\n".format(self.streamType))
+
+ iostr.write("\n\n>>>> Request end\n")
+ returnValue(iostr.getvalue())
+
+
+ @inlineCallbacks
+ def logResponse(self, response):
+ """
+ Log an HTTP request.
+ """
+ iostr = StringIO()
+ iostr.write(">>>> Response start\n\n")
+ code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
+ iostr.write("HTTP/1.1 {:d} {}\n".format(response.code, code_message))
+ for name, valuelist in response.headers.getAllRawHeaders():
+ for value in valuelist:
+ # Do not log authorization details
+ if name not in ("WWW-Authenticate",):
+ iostr.write("{}: {}\n".format(name, value))
+ else:
+ iostr.write("{}: xxxxxxxxx\n".format(name))
+ iostr.write("\n")
+
+ # We need to play a trick with the response stream to ensure we don't mess it up. So we
+ # read it, store the value in a MemoryStream, and replace the response's stream with that,
+ # so the data can be read again.
+ data = (yield allDataFromStream(response.stream))
+ iostr.write(data)
+ response.stream = MemoryStream(data if data is not None else "")
+ response.stream.doStartReading = None
+
+ iostr.write("\n\n>>>> Response end\n")
+ returnValue(iostr.getvalue())
+
+
+ @inlineCallbacks
+ def _processRequest(self):
+ """
+ Process the request by sending it to the relevant server.
+
+ @return: the HTTP response.
+ @rtype: L{Response}
+ """
+ ssl, host, port, _ignore_path = self.server.details()
+ path = "/" + config.Servers.ConduitName
+
+ headers = Headers()
+ headers.setHeader("Host", utf8String(host + ":{}".format(port)))
+ if self.streamType:
+ # For attachments we put the base64-encoded JSON data into a header
+ headers.setHeader("Content-Type", self.streamType)
+ headers.addRawHeader("XPOD", base64.b64encode(self.data))
+ else:
+ headers.setHeader("Content-Type", MimeType("application", "json", params={"charset": "utf-8", }))
+ headers.setHeader("User-Agent", "CalendarServer/{}".format(version))
+ headers.addRawHeader(*self.server.secretHeader())
+
+ from twisted.internet import reactor
+ f = Factory()
+ f.protocol = HTTPClientProtocol
+ ep = GAIEndpoint(reactor, host, port, _configuredClientContextFactory() if ssl else None)
+ proto = (yield ep.connect(f))
+
+ request = ClientRequest("POST", path, headers, self.stream if self.stream is not None else self.data)
+
+ if accountingEnabledForCategory("xPod"):
+ self.loggedRequest = yield self.logRequest(request)
+
+ response = (yield proto.submitRequest(request))
+
+ returnValue(response)
Deleted: CalendarServer/trunk/txdav/common/datastore/podding/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/resource.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,193 +0,0 @@
-##
-# Copyright (c) 2013 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 twext.web2 import responsecode
-from twext.web2.dav.noneprops import NonePropertyStore
-from twext.web2.dav.util import allDataFromStream
-from twext.web2.http import Response, HTTPError, StatusResponse, JSONResponse
-from twext.web2.http_headers import MimeType
-
-from twisted.internet.defer import succeed, returnValue, inlineCallbacks
-
-from twistedcaldav.extensions import DAVResource, \
- DAVResourceWithoutChildrenMixin
-from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
-from twistedcaldav.scheduling_store.caldav.resource import \
- deliverSchedulePrivilegeSet
-
-from txdav.xml import element as davxml
-from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
-from txdav.common.datastore.podding.conduit import FailedCrossPodRequestError
-
-import base64
-import json
-
-__all__ = [
- "ConduitResource",
-]
-
-class ConduitResource(ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource):
- """
- Podding cross-pod RPC conduit resource.
-
- Extends L{DAVResource} to provide cross-pod RPC functionality.
- """
-
- def __init__(self, parent, store):
- """
- @param parent: the parent resource of this one.
- """
- assert parent is not None
-
- DAVResource.__init__(self, principalCollections=parent.principalCollections())
-
- self.parent = parent
- self.store = store
-
-
- def deadProperties(self):
- if not hasattr(self, "_dead_properties"):
- self._dead_properties = NonePropertyStore(self)
- return self._dead_properties
-
-
- def etag(self):
- return succeed(None)
-
-
- def checkPreconditions(self, request):
- return None
-
-
- def resourceType(self):
- return davxml.ResourceType.ischeduleinbox
-
-
- def contentType(self):
- return MimeType.fromString("text/html; charset=utf-8")
-
-
- def isCollection(self):
- return False
-
-
- def isCalendarCollection(self):
- return False
-
-
- def isPseudoCalendarCollection(self):
- return False
-
-
- def principalForCalendarUserAddress(self, address):
- for principalCollection in self.principalCollections():
- principal = principalCollection.principalForCalendarUserAddress(address)
- if principal is not None:
- return principal
- return None
-
-
- def render(self, request):
- output = """<html>
-<head>
-<title>Podding Conduit Resource</title>
-</head>
-<body>
-<h1>Podding Conduit Resource.</h1>
-</body
-</html>"""
-
- response = Response(200, {}, output)
- response.headers.setHeader("content-type", MimeType("text", "html"))
- return response
-
-
- @inlineCallbacks
- def http_POST(self, request):
- """
- The server-to-server POST method.
- """
-
- # Check shared secret
- if not Servers.getThisServer().checkSharedSecret(request.headers):
- self.log.error("Invalid shared secret header in cross-pod request")
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not authorized to make this request"))
-
- # Look for XPOD header
- xpod = request.headers.getRawHeaders("XPOD")
- contentType = request.headers.getHeader("content-type")
- if xpod is not None:
- # Attachments are sent in the request body with the JSON data in a header. We
- # decode the header and add the request.stream as an attribute of the JSON object.
- xpod = xpod[0]
- try:
- j = json.loads(base64.b64decode(xpod))
- except (TypeError, ValueError) as e:
- self.log.error("Invalid JSON header in request: {ex}\n{xpod}", ex=e, xpod=xpod)
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON header in request: {}\n{}".format(e, xpod)))
- j["stream"] = request.stream
- j["streamType"] = contentType
- else:
- # Check content first
- if "{}/{}".format(contentType.mediaType, contentType.mediaSubtype) != "application/json":
- self.log.error("MIME type {mime} not allowed in request", mime=contentType)
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "MIME type {} not allowed in request".format(contentType)))
-
- body = (yield allDataFromStream(request.stream))
- try:
- j = json.loads(body)
- except ValueError as e:
- self.log.error("Invalid JSON data in request: {ex}\n{body}", ex=e, body=body)
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON data in request: {}\n{}".format(e, body)))
-
- # Log extended item
- if not hasattr(request, "extendedLogItems"):
- request.extendedLogItems = {}
- request.extendedLogItems["xpod"] = j["action"] if "action" in j else "unknown"
-
- # Get the conduit to process the data
- try:
- result = yield self.store.conduit.processRequest(j)
- except FailedCrossPodRequestError as e:
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
- except Exception as e:
- raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, str(e)))
-
- response = JSONResponse(responsecode.OK, result)
- returnValue(response)
-
-
- ##
- # ACL
- ##
-
- def supportedPrivileges(self, request):
- return succeed(deliverSchedulePrivilegeSet)
-
-
- def defaultAccessControlList(self):
- privs = (
- davxml.Privilege(davxml.Read()),
- )
-
- return davxml.ACL(
- # DAV:Read for all principals (includes anonymous)
- davxml.ACE(
- davxml.Principal(davxml.All()),
- davxml.Grant(*privs),
- davxml.Protected(),
- ),
- )
Copied: CalendarServer/trunk/txdav/common/datastore/podding/resource.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/resource.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/resource.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,193 @@
+##
+# Copyright (c) 2013 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 txweb2 import responsecode
+from txweb2.dav.noneprops import NonePropertyStore
+from txweb2.dav.util import allDataFromStream
+from txweb2.http import Response, HTTPError, StatusResponse, JSONResponse
+from txweb2.http_headers import MimeType
+
+from twisted.internet.defer import succeed, returnValue, inlineCallbacks
+
+from twistedcaldav.extensions import DAVResource, \
+ DAVResourceWithoutChildrenMixin
+from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
+from twistedcaldav.scheduling_store.caldav.resource import \
+ deliverSchedulePrivilegeSet
+
+from txdav.xml import element as davxml
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
+from txdav.common.datastore.podding.conduit import FailedCrossPodRequestError
+
+import base64
+import json
+
+__all__ = [
+ "ConduitResource",
+]
+
+class ConduitResource(ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource):
+ """
+ Podding cross-pod RPC conduit resource.
+
+ Extends L{DAVResource} to provide cross-pod RPC functionality.
+ """
+
+ def __init__(self, parent, store):
+ """
+ @param parent: the parent resource of this one.
+ """
+ assert parent is not None
+
+ DAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+ self.parent = parent
+ self.store = store
+
+
+ def deadProperties(self):
+ if not hasattr(self, "_dead_properties"):
+ self._dead_properties = NonePropertyStore(self)
+ return self._dead_properties
+
+
+ def etag(self):
+ return succeed(None)
+
+
+ def checkPreconditions(self, request):
+ return None
+
+
+ def resourceType(self):
+ return davxml.ResourceType.ischeduleinbox
+
+
+ def contentType(self):
+ return MimeType.fromString("text/html; charset=utf-8")
+
+
+ def isCollection(self):
+ return False
+
+
+ def isCalendarCollection(self):
+ return False
+
+
+ def isPseudoCalendarCollection(self):
+ return False
+
+
+ def principalForCalendarUserAddress(self, address):
+ for principalCollection in self.principalCollections():
+ principal = principalCollection.principalForCalendarUserAddress(address)
+ if principal is not None:
+ return principal
+ return None
+
+
+ def render(self, request):
+ output = """<html>
+<head>
+<title>Podding Conduit Resource</title>
+</head>
+<body>
+<h1>Podding Conduit Resource.</h1>
+</body
+</html>"""
+
+ response = Response(200, {}, output)
+ response.headers.setHeader("content-type", MimeType("text", "html"))
+ return response
+
+
+ @inlineCallbacks
+ def http_POST(self, request):
+ """
+ The server-to-server POST method.
+ """
+
+ # Check shared secret
+ if not Servers.getThisServer().checkSharedSecret(request.headers):
+ self.log.error("Invalid shared secret header in cross-pod request")
+ raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not authorized to make this request"))
+
+ # Look for XPOD header
+ xpod = request.headers.getRawHeaders("XPOD")
+ contentType = request.headers.getHeader("content-type")
+ if xpod is not None:
+ # Attachments are sent in the request body with the JSON data in a header. We
+ # decode the header and add the request.stream as an attribute of the JSON object.
+ xpod = xpod[0]
+ try:
+ j = json.loads(base64.b64decode(xpod))
+ except (TypeError, ValueError) as e:
+ self.log.error("Invalid JSON header in request: {ex}\n{xpod}", ex=e, xpod=xpod)
+ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON header in request: {}\n{}".format(e, xpod)))
+ j["stream"] = request.stream
+ j["streamType"] = contentType
+ else:
+ # Check content first
+ if "{}/{}".format(contentType.mediaType, contentType.mediaSubtype) != "application/json":
+ self.log.error("MIME type {mime} not allowed in request", mime=contentType)
+ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "MIME type {} not allowed in request".format(contentType)))
+
+ body = (yield allDataFromStream(request.stream))
+ try:
+ j = json.loads(body)
+ except ValueError as e:
+ self.log.error("Invalid JSON data in request: {ex}\n{body}", ex=e, body=body)
+ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON data in request: {}\n{}".format(e, body)))
+
+ # Log extended item
+ if not hasattr(request, "extendedLogItems"):
+ request.extendedLogItems = {}
+ request.extendedLogItems["xpod"] = j["action"] if "action" in j else "unknown"
+
+ # Get the conduit to process the data
+ try:
+ result = yield self.store.conduit.processRequest(j)
+ except FailedCrossPodRequestError as e:
+ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
+ except Exception as e:
+ raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, str(e)))
+
+ response = JSONResponse(responsecode.OK, result)
+ returnValue(response)
+
+
+ ##
+ # ACL
+ ##
+
+ def supportedPrivileges(self, request):
+ return succeed(deliverSchedulePrivilegeSet)
+
+
+ def defaultAccessControlList(self):
+ privs = (
+ davxml.Privilege(davxml.Read()),
+ )
+
+ return davxml.ACL(
+ # DAV:Read for all principals (includes anonymous)
+ davxml.ACE(
+ davxml.Principal(davxml.All()),
+ davxml.Grant(*privs),
+ davxml.Protected(),
+ ),
+ )
Deleted: CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/__init__.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 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.
-##
Copied: CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 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.
+##
Deleted: CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,1082 +0,0 @@
-##
-# Copyright (c) 2005-2013 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 pycalendar.datetime import DateTime
-from pycalendar.period import Period
-
-from twext.python.clsprop import classproperty
-import twext.web2.dav.test.util
-from twext.web2.http_headers import MimeType
-from twext.web2.stream import MemoryStream
-
-from twisted.internet.defer import inlineCallbacks, succeed, returnValue
-
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import TimeRange
-from twistedcaldav.ical import Component, normalize_iCalStr
-
-from txdav.caldav.datastore.query.filter import Filter
-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
-from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, Server
-from txdav.caldav.datastore.sql import ManagedAttachment
-from txdav.caldav.datastore.test.common import CaptureProtocol
-from txdav.caldav.datastore.test.util import buildCalendarStore, \
- TestCalendarStoreDirectoryRecord
-from txdav.common.datastore.podding.conduit import PoddingConduit, \
- FailedCrossPodRequestError
-from txdav.common.datastore.podding.resource import ConduitResource
-from txdav.common.datastore.podding.test.util import MultiStoreConduitTest, \
- FakeConduitRequest
-from txdav.common.datastore.sql_tables import _BIND_STATUS_ACCEPTED
-from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
-from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError, \
- ObjectResourceNameNotAllowedError
-from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
-
-
-class TestConduit (CommonCommonTests, twext.web2.dav.test.util.TestCase):
-
- class FakeConduit(object):
-
- def recv_fake(self, j):
- return succeed({
- "result": "ok",
- "back2u": j["echo"],
- "more": "bits",
- })
-
-
- @inlineCallbacks
- def setUp(self):
- yield super(TestConduit, self).setUp()
- self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
- self.directory = self._sqlCalendarStore.directoryService()
-
- for ctr in range(1, 100):
- self.directory.addRecord(TestCalendarStoreDirectoryRecord(
- "puser{:02d}".format(ctr),
- ("puser{:02d}".format(ctr),),
- "Puser {:02d}".format(ctr),
- frozenset((
- "urn:uuid:puser{:02d}".format(ctr),
- "mailto:puser{:02d}@example.com".format(ctr),
- )),
- thisServer=False,
- ))
-
- self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
-
- self.thisServer = Server("A", "http://127.0.0.1", "A", True)
- Servers.addServer(self.thisServer)
-
- yield self.populate()
-
-
- def storeUnderTest(self):
- """
- Return a store for testing.
- """
- return self._sqlCalendarStore
-
-
- @inlineCallbacks
- def populate(self):
- yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
- self.notifierFactory.reset()
-
-
- @classproperty(cache=False)
- def requirements(cls): #@NoSelf
- return {
- "user01": {
- "calendar_1": {
- },
- "inbox": {
- },
- },
- "user02": {
- "calendar_1": {
- },
- "inbox": {
- },
- },
- "user03": {
- "calendar_1": {
- },
- "inbox": {
- },
- },
- }
-
-
- def test_validRequst(self):
- """
- Cross-pod request fails when there is no shared secret header present.
- """
-
- conduit = PoddingConduit(self.storeUnderTest())
- r1, r2 = conduit.validRequst("user01", "puser02")
- self.assertTrue(r1 is not None)
- self.assertTrue(r2 is not None)
-
- self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "bogus01", "user02")
- self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "user01", "bogus02")
- self.assertRaises(FailedCrossPodRequestError, conduit.validRequst, "user01", "user02")
-
-
-
-class TestConduitToConduit(MultiStoreConduitTest):
-
- class FakeConduit(PoddingConduit):
-
- @inlineCallbacks
- def send_fake(self, txn, ownerUID, shareeUID):
- _ignore_owner, sharee = self.validRequst(ownerUID, shareeUID)
- action = {
- "action": "fake",
- "echo": "bravo"
- }
-
- result = yield self.sendRequest(txn, sharee, action)
- returnValue(result)
-
-
- def recv_fake(self, txn, j):
- return succeed({
- "result": "ok",
- "back2u": j["echo"],
- "more": "bits",
- })
-
-
- def makeConduit(self, store):
- """
- Use our own variant.
- """
- conduit = self.FakeConduit(store)
- conduit.conduitRequestClass = FakeConduitRequest
- return conduit
-
-
- @inlineCallbacks
- def test_fake_action(self):
- """
- Cross-pod request works when conduit does support the action.
- """
-
- txn = self.transactionUnderTest()
- store1 = self.storeUnderTest()
- response = yield store1.conduit.send_fake(txn, "user01", "puser01")
- self.assertTrue("result" in response)
- self.assertEqual(response["result"], "ok")
- self.assertTrue("back2u" in response)
- self.assertEqual(response["back2u"], "bravo")
- self.assertTrue("more" in response)
- self.assertEqual(response["more"], "bits")
- yield txn.commit()
-
- store2 = self.otherStoreUnderTest()
- txn = store2.newTransaction()
- response = yield store2.conduit.send_fake(txn, "puser01", "user01")
- self.assertTrue("result" in response)
- self.assertEqual(response["result"], "ok")
- self.assertTrue("back2u" in response)
- self.assertEqual(response["back2u"], "bravo")
- self.assertTrue("more" in response)
- self.assertEqual(response["more"], "bits")
- yield txn.commit()
-
-
-
-class TestConduitAPI(MultiStoreConduitTest):
- """
- Test that the conduit api works.
- """
-
- nowYear = {"now": DateTime.getToday().getYear()}
-
- caldata1 = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:uid1
-DTSTART:{now:04d}0102T140000Z
-DURATION:PT1H
-CREATED:20060102T190000Z
-DTSTAMP:20051222T210507Z
-RRULE:FREQ=WEEKLY
-SUMMARY:instance
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n").format(**nowYear)
-
- caldata1_changed = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:uid1
-DTSTART:{now:04d}0102T150000Z
-DURATION:PT1H
-CREATED:20060102T190000Z
-DTSTAMP:20051222T210507Z
-RRULE:FREQ=WEEKLY
-SUMMARY:instance changed
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n").format(**nowYear)
-
- caldata2 = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:uid2
-DTSTART:{now:04d}0102T160000Z
-DURATION:PT1H
-CREATED:20060102T190000Z
-DTSTAMP:20051222T210507Z
-RRULE:FREQ=WEEKLY
-SUMMARY:instance
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n").format(**nowYear)
-
- caldata3 = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:uid3
-DTSTART:{now:04d}0102T160000Z
-DURATION:PT1H
-CREATED:20060102T190000Z
-DTSTAMP:20051222T210507Z
-RRULE:FREQ=WEEKLY
-SUMMARY:instance
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n").format(**nowYear)
-
- @inlineCallbacks
- def test_basic_share(self):
- """
- Test that basic invite/uninvite works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- shared = yield calendar1.shareeView("puser01")
- self.assertEqual(shared.shareStatus(), _BIND_STATUS_ACCEPTED)
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- self.assertTrue(shared is not None)
- self.assertTrue(shared.external())
- yield self.otherCommit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.uninviteUserFromShare("puser01")
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- self.assertTrue(shared is None)
- yield self.otherCommit()
-
-
- @inlineCallbacks
- def test_countobjects(self):
- """
- Test that action=countobjects works.
- """
-
- yield self.createShare("user01", "puser01")
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- count = yield shared.countObjectResources()
- self.assertEqual(count, 0)
- yield self.otherCommit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- count = yield calendar1.countObjectResources()
- self.assertEqual(count, 1)
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- count = yield shared.countObjectResources()
- self.assertEqual(count, 1)
- yield self.otherCommit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- yield object1.remove()
- count = yield calendar1.countObjectResources()
- self.assertEqual(count, 0)
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- count = yield shared.countObjectResources()
- self.assertEqual(count, 0)
- yield self.otherCommit()
-
-
- @inlineCallbacks
- def test_listobjects(self):
- """
- Test that action=listobjects works.
- """
-
- yield self.createShare("user01", "puser01")
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- objects = yield shared.listObjectResources()
- self.assertEqual(set(objects), set())
- yield self.otherCommit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
- objects = yield calendar1.listObjectResources()
- self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- objects = yield shared.listObjectResources()
- self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
- yield self.otherCommit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- yield object1.remove()
- objects = yield calendar1.listObjectResources()
- self.assertEqual(set(objects), set(("2.ics",)))
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- objects = yield shared.listObjectResources()
- self.assertEqual(set(objects), set(("2.ics",)))
- yield self.otherCommit()
-
-
- @inlineCallbacks
- def test_synctoken(self):
- """
- Test that action=synctoken works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- token1_1 = yield calendar1.syncToken()
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- token2_1 = yield shared.syncToken()
- yield self.otherCommit()
-
- self.assertEqual(token1_1, token2_1)
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- token1_2 = yield calendar1.syncToken()
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- token2_2 = yield shared.syncToken()
- yield self.otherCommit()
-
- self.assertNotEqual(token1_1, token1_2)
- self.assertEqual(token1_2, token2_2)
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- yield object1.remove()
- count = yield calendar1.countObjectResources()
- self.assertEqual(count, 0)
- yield self.commit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- token1_3 = yield calendar1.syncToken()
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- token2_3 = yield shared.syncToken()
- yield self.otherCommit()
-
- self.assertNotEqual(token1_1, token1_3)
- self.assertNotEqual(token1_2, token1_3)
- self.assertEqual(token1_3, token2_3)
-
-
- @inlineCallbacks
- def test_resourcenamessincerevision(self):
- """
- Test that action=synctoken works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- token1_1 = yield calendar1.syncToken()
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- token2_1 = yield shared.syncToken()
- yield self.otherCommit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- token1_2 = yield calendar1.syncToken()
- names1 = yield calendar1.resourceNamesSinceToken(token1_1)
- self.assertEqual(names1, ([u"1.ics"], [],))
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- token2_2 = yield shared.syncToken()
- names2 = yield shared.resourceNamesSinceToken(token2_1)
- self.assertEqual(names2, ([u"1.ics"], [],))
- yield self.otherCommit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- yield object1.remove()
- count = yield calendar1.countObjectResources()
- self.assertEqual(count, 0)
- yield self.commit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- token1_3 = yield calendar1.syncToken()
- names1 = yield calendar1.resourceNamesSinceToken(token1_2)
- self.assertEqual(names1, ([], [u"1.ics"],))
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- token2_3 = yield shared.syncToken()
- names2 = yield shared.resourceNamesSinceToken(token2_2)
- self.assertEqual(names2, ([], [u"1.ics"],))
- yield self.otherCommit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- names1 = yield calendar1.resourceNamesSinceToken(token1_3)
- self.assertEqual(names1, ([], [],))
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- names2 = yield shared.resourceNamesSinceToken(token2_3)
- self.assertEqual(names2, ([], [],))
- yield self.otherCommit()
-
-
- @inlineCallbacks
- def test_resourceuidforname(self):
- """
- Test that action=resourceuidforname works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- uid = yield calendar1.resourceUIDForName("1.ics")
- self.assertEqual(uid, "uid1")
- uid = yield calendar1.resourceUIDForName("2.ics")
- self.assertTrue(uid is None)
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- uid = yield shared.resourceUIDForName("1.ics")
- self.assertEqual(uid, "uid1")
- uid = yield shared.resourceUIDForName("2.ics")
- self.assertTrue(uid is None)
- yield self.otherCommit()
-
-
- @inlineCallbacks
- def test_resourcenameforuid(self):
- """
- Test that action=resourcenameforuid works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- name = yield calendar1.resourceNameForUID("uid1")
- self.assertEqual(name, "1.ics")
- name = yield calendar1.resourceNameForUID("uid2")
- self.assertTrue(name is None)
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- name = yield shared.resourceNameForUID("uid1")
- self.assertEqual(name, "1.ics")
- name = yield shared.resourceNameForUID("uid2")
- self.assertTrue(name is None)
- yield self.otherCommit()
-
-
- @inlineCallbacks
- def test_search(self):
- """
- Test that action=resourcenameforuid works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[caldavxml.ComponentFilter(
- **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
- )],
- **{"name": "VCALENDAR"}
- )
- )
- filter = Filter(filter)
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- names = [item[0] for item in (yield calendar1.search(filter))]
- self.assertEqual(names, ["1.ics", ])
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- names = [item[0] for item in (yield shared.search(filter))]
- self.assertEqual(names, ["1.ics", ])
- yield self.otherCommit()
-
-
- @inlineCallbacks
- def test_loadallobjects(self):
- """
- Test that action=loadallobjects works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- resource1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- resource_id1 = resource1.id()
- resource2 = yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
- resource_id2 = resource2.id()
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resources = yield shared.objectResources()
- byname = dict([(resource.name(), resource) for resource in resources])
- byuid = dict([(resource.uid(), resource) for resource in resources])
- self.assertEqual(len(resources), 2)
- self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "2.ics",)))
- self.assertEqual(set([resource.uid() for resource in resources]), set(("uid1", "uid2",)))
- self.assertEqual(set([resource.id() for resource in resources]), set((resource_id1, resource_id2,)))
- resource = yield shared.objectResourceWithName("1.ics")
- self.assertTrue(resource is byname["1.ics"])
- resource = yield shared.objectResourceWithName("2.ics")
- self.assertTrue(resource is byname["2.ics"])
- resource = yield shared.objectResourceWithName("Missing.ics")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithUID("uid1")
- self.assertTrue(resource is byuid["uid1"])
- resource = yield shared.objectResourceWithUID("uid2")
- self.assertTrue(resource is byuid["uid2"])
- resource = yield shared.objectResourceWithUID("uid-missing")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithID(resource_id1)
- self.assertTrue(resource is byname["1.ics"])
- resource = yield shared.objectResourceWithID(resource_id2)
- self.assertTrue(resource is byname["2.ics"])
- resource = yield shared.objectResourceWithID(0)
- self.assertTrue(resource is None)
- yield self.otherCommit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- yield object1.remove()
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resources = yield shared.objectResources()
- byname = dict([(resource.name(), resource) for resource in resources])
- byuid = dict([(resource.uid(), resource) for resource in resources])
- self.assertEqual(len(resources), 1)
- self.assertEqual(set([resource.name() for resource in resources]), set(("2.ics",)))
- self.assertEqual(set([resource.uid() for resource in resources]), set(("uid2",)))
- self.assertEqual(set([resource.id() for resource in resources]), set((resource_id2,)))
- resource = yield shared.objectResourceWithName("1.ics")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithName("2.ics")
- self.assertTrue(resource is byname["2.ics"])
- resource = yield shared.objectResourceWithName("Missing.ics")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithUID("uid1")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithUID("uid2")
- self.assertTrue(resource is byuid["uid2"])
- resource = yield shared.objectResourceWithUID("uid-missing")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithID(resource_id1)
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithID(resource_id2)
- self.assertTrue(resource is byname["2.ics"])
- resource = yield shared.objectResourceWithID(0)
- self.assertTrue(resource is None)
- yield self.otherCommit()
-
-
- @inlineCallbacks
- def test_loadallobjectswithnames(self):
- """
- Test that action=loadallobjectswithnames works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- resource1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- resource_id1 = resource1.id()
- yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
- resource3 = yield calendar1.createCalendarObjectWithName("3.ics", Component.fromString(self.caldata3))
- resource_id3 = resource3.id()
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resources = yield shared.objectResources()
- self.assertEqual(len(resources), 3)
- yield self.otherCommit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
- byname = dict([(resource.name(), resource) for resource in resources])
- byuid = dict([(resource.uid(), resource) for resource in resources])
- self.assertEqual(len(resources), 2)
- self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "3.ics",)))
- self.assertEqual(set([resource.uid() for resource in resources]), set(("uid1", "uid3",)))
- self.assertEqual(set([resource.id() for resource in resources]), set((resource_id1, resource_id3,)))
- resource = yield shared.objectResourceWithName("1.ics")
- self.assertTrue(resource is byname["1.ics"])
- resource = yield shared.objectResourceWithName("3.ics")
- self.assertTrue(resource is byname["3.ics"])
- resource = yield shared.objectResourceWithName("Missing.ics")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithUID("uid1")
- self.assertTrue(resource is byuid["uid1"])
- resource = yield shared.objectResourceWithUID("uid3")
- self.assertTrue(resource is byuid["uid3"])
- resource = yield shared.objectResourceWithUID("uid-missing")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithID(resource_id1)
- self.assertTrue(resource is byname["1.ics"])
- resource = yield shared.objectResourceWithID(resource_id3)
- self.assertTrue(resource is byname["3.ics"])
- resource = yield shared.objectResourceWithID(0)
- self.assertTrue(resource is None)
- yield self.otherCommit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- yield object1.remove()
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
- byname = dict([(resource.name(), resource) for resource in resources])
- byuid = dict([(resource.uid(), resource) for resource in resources])
- self.assertEqual(len(resources), 1)
- self.assertEqual(set([resource.name() for resource in resources]), set(("3.ics",)))
- self.assertEqual(set([resource.uid() for resource in resources]), set(("uid3",)))
- self.assertEqual(set([resource.id() for resource in resources]), set((resource_id3,)))
- resource = yield shared.objectResourceWithName("1.ics")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithName("3.ics")
- self.assertTrue(resource is byname["3.ics"])
- resource = yield shared.objectResourceWithName("Missing.ics")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithUID("uid1")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithUID("uid3")
- self.assertTrue(resource is byuid["uid3"])
- resource = yield shared.objectResourceWithUID("uid-missing")
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithID(resource_id1)
- self.assertTrue(resource is None)
- resource = yield shared.objectResourceWithID(resource_id3)
- self.assertTrue(resource is byname["3.ics"])
- resource = yield shared.objectResourceWithID(0)
- self.assertTrue(resource is None)
- yield self.otherCommit()
-
-
- @inlineCallbacks
- def test_objectwith(self):
- """
- Test that action=objectwith works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- resource = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- resource_id = resource.id()
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resource = yield shared.objectResourceWithName("1.ics")
- self.assertTrue(resource is not None)
- self.assertEqual(resource.name(), "1.ics")
- self.assertEqual(resource.uid(), "uid1")
-
- resource = yield shared.objectResourceWithName("2.ics")
- self.assertTrue(resource is None)
-
- yield self.otherCommit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resource = yield shared.objectResourceWithUID("uid1")
- self.assertTrue(resource is not None)
- self.assertEqual(resource.name(), "1.ics")
- self.assertEqual(resource.uid(), "uid1")
-
- resource = yield shared.objectResourceWithUID("uid2")
- self.assertTrue(resource is None)
-
- yield self.otherCommit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resource = yield shared.objectResourceWithID(resource_id)
- self.assertTrue(resource is not None)
- self.assertEqual(resource.name(), "1.ics")
- self.assertEqual(resource.uid(), "uid1")
-
- resource = yield shared.objectResourceWithID(0)
- self.assertTrue(resource is None)
-
- yield self.otherCommit()
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- yield object1.remove()
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resource = yield shared.objectResourceWithName("1.ics")
- self.assertTrue(resource is None)
- yield self.otherCommit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resource = yield shared.objectResourceWithUID("uid1")
- self.assertTrue(resource is None)
- yield self.otherCommit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resource = yield shared.objectResourceWithID(resource_id)
- self.assertTrue(resource is None)
- yield self.otherCommit()
-
-
- @inlineCallbacks
- def test_create(self):
- """
- Test that action=create works.
- """
-
- yield self.createShare("user01", "puser01")
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resource = yield shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- resource_id = resource.id()
- self.assertTrue(resource is not None)
- self.assertEqual(resource.name(), "1.ics")
- self.assertEqual(resource.uid(), "uid1")
- self.assertFalse(resource._componentChanged)
- yield self.otherCommit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- resource = yield shared.objectResourceWithUID("uid1")
- self.assertTrue(resource is not None)
- self.assertEqual(resource.name(), "1.ics")
- self.assertEqual(resource.uid(), "uid1")
- self.assertEqual(resource.id(), resource_id)
- yield self.otherCommit()
-
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- self.assertTrue(object1 is not None)
- self.assertEqual(object1.name(), "1.ics")
- self.assertEqual(object1.uid(), "uid1")
- self.assertEqual(object1.id(), resource_id)
- yield self.commit()
-
-
- @inlineCallbacks
- def test_create_exception(self):
- """
- Test that action=create fails when a duplicate name is used.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- yield self.failUnlessFailure(shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)), ObjectResourceNameAlreadyExistsError)
- yield self.otherAbort()
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
- yield self.failUnlessFailure(shared.createCalendarObjectWithName(".2.ics", Component.fromString(self.caldata2)), ObjectResourceNameNotAllowedError)
- yield self.otherAbort()
-
-
- @inlineCallbacks
- def test_setcomponent(self):
- """
- Test that action=setcomponent works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
- ical = yield shared_object.component()
- self.assertTrue(isinstance(ical, Component))
- self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
- yield self.otherCommit()
-
- shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
- changed = yield shared_object.setComponent(Component.fromString(self.caldata1_changed))
- self.assertFalse(changed)
- ical = yield shared_object.component()
- self.assertTrue(isinstance(ical, Component))
- self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
- yield self.otherCommit()
-
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- ical = yield object1.component()
- self.assertTrue(isinstance(ical, Component))
- self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
- yield self.commit()
-
-
- @inlineCallbacks
- def test_component(self):
- """
- Test that action=component works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
- ical = yield shared_object.component()
- self.assertTrue(isinstance(ical, Component))
- self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
- yield self.otherCommit()
-
-
- @inlineCallbacks
- def test_remove(self):
- """
- Test that action=create works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
- yield shared_object.remove()
- yield self.otherCommit()
-
- shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
- self.assertTrue(shared_object is None)
- yield self.otherCommit()
-
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- self.assertTrue(object1 is None)
- yield self.commit()
-
-
- @inlineCallbacks
- def test_freebusy(self):
- """
- Test that action=component works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- fbstart = "{now:04d}0102T000000Z".format(**self.nowYear)
- fbend = "{now:04d}0103T000000Z".format(**self.nowYear)
-
- shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-
- fbinfo = [[], [], []]
- matchtotal = yield generateFreeBusyInfo(
- shared,
- fbinfo,
- TimeRange(start=fbstart, end=fbend),
- 0,
- excludeuid=None,
- organizer=None,
- organizerPrincipal=None,
- same_calendar_user=False,
- servertoserver=False,
- event_details=False,
- logItems=None
- )
-
- self.assertEqual(matchtotal, 1)
- self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
- self.assertEqual(len(fbinfo[1]), 0)
- self.assertEqual(len(fbinfo[2]), 0)
- yield self.otherCommit()
-
-
- def attachmentToString(self, attachment):
- """
- Convenience to convert an L{IAttachment} to a string.
-
- @param attachment: an L{IAttachment} provider to convert into a string.
-
- @return: a L{Deferred} that fires with the contents of the attachment.
-
- @rtype: L{Deferred} firing C{bytes}
- """
- capture = CaptureProtocol()
- attachment.retrieve(capture)
- return capture.deferred
-
-
- @inlineCallbacks
- def test_add_attachment(self):
- """
- Test that action=add-attachment works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- object1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- resourceID = object1.id()
- yield self.commit()
-
- shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
- attachment, location = yield shared_object.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
- managedID = attachment.managedID()
- from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
- self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
- self.assertTrue("user01/attachments/test" in location)
- yield self.otherCommit()
-
- cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
- self.assertEqual(cobjs, set((resourceID,)))
- attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
- self.assertEqual(attachment.name(), "test.txt")
- data = yield self.attachmentToString(attachment)
- self.assertEqual(data, "Here is some text.")
- yield self.commit()
-
-
- @inlineCallbacks
- def test_update_attachment(self):
- """
- Test that action=update-attachment works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- resourceID = object1.id()
- attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
- managedID = attachment.managedID()
- yield self.commit()
-
- shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
- attachment, location = yield shared_object.updateAttachment(managedID, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some more text."))
- managedID = attachment.managedID()
- from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
- self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
- self.assertTrue("user01/attachments/test" in location)
- yield self.otherCommit()
-
- cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
- self.assertEqual(cobjs, set((resourceID,)))
- attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
- self.assertEqual(attachment.name(), "test.txt")
- data = yield self.attachmentToString(attachment)
- self.assertEqual(data, "Here is some more text.")
- yield self.commit()
-
-
- @inlineCallbacks
- def test_remove_attachment(self):
- """
- Test that action=remove-attachment works.
- """
-
- yield self.createShare("user01", "puser01")
-
- calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
- yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
- yield self.commit()
-
- object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
- resourceID = object1.id()
- attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
- managedID = attachment.managedID()
- yield self.commit()
-
- shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
- yield shared_object.removeAttachment(None, managedID)
- yield self.otherCommit()
-
- cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
- self.assertEqual(cobjs, set())
- attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
- self.assertTrue(attachment is None)
- yield self.commit()
Copied: CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,1083 @@
+##
+# Copyright (c) 2005-2013 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 pycalendar.datetime import DateTime
+from pycalendar.period import Period
+
+from twext.python.clsprop import classproperty
+
+import txweb2.dav.test.util
+from txweb2.http_headers import MimeType
+from txweb2.stream import MemoryStream
+
+from twisted.internet.defer import inlineCallbacks, succeed, returnValue
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import TimeRange
+from twistedcaldav.ical import Component, normalize_iCalStr
+
+from txdav.caldav.datastore.query.filter import Filter
+from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, Server
+from txdav.caldav.datastore.sql import ManagedAttachment
+from txdav.caldav.datastore.test.common import CaptureProtocol
+from txdav.caldav.datastore.test.util import buildCalendarStore, \
+ TestCalendarStoreDirectoryRecord
+from txdav.common.datastore.podding.conduit import PoddingConduit, \
+ FailedCrossPodRequestError
+from txdav.common.datastore.podding.resource import ConduitResource
+from txdav.common.datastore.podding.test.util import MultiStoreConduitTest, \
+ FakeConduitRequest
+from txdav.common.datastore.sql_tables import _BIND_STATUS_ACCEPTED
+from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
+from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError, \
+ ObjectResourceNameNotAllowedError
+from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
+
+
+class TestConduit (CommonCommonTests, txweb2.dav.test.util.TestCase):
+
+ class FakeConduit(object):
+
+ def recv_fake(self, j):
+ return succeed({
+ "result": "ok",
+ "back2u": j["echo"],
+ "more": "bits",
+ })
+
+
+ @inlineCallbacks
+ def setUp(self):
+ yield super(TestConduit, self).setUp()
+ self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
+ self.directory = self._sqlCalendarStore.directoryService()
+
+ for ctr in range(1, 100):
+ self.directory.addRecord(TestCalendarStoreDirectoryRecord(
+ "puser{:02d}".format(ctr),
+ ("puser{:02d}".format(ctr),),
+ "Puser {:02d}".format(ctr),
+ frozenset((
+ "urn:uuid:puser{:02d}".format(ctr),
+ "mailto:puser{:02d}@example.com".format(ctr),
+ )),
+ thisServer=False,
+ ))
+
+ self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
+
+ self.thisServer = Server("A", "http://127.0.0.1", "A", True)
+ Servers.addServer(self.thisServer)
+
+ yield self.populate()
+
+
+ def storeUnderTest(self):
+ """
+ Return a store for testing.
+ """
+ return self._sqlCalendarStore
+
+
+ @inlineCallbacks
+ def populate(self):
+ yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+ self.notifierFactory.reset()
+
+
+ @classproperty(cache=False)
+ def requirements(cls): #@NoSelf
+ return {
+ "user01": {
+ "calendar_1": {
+ },
+ "inbox": {
+ },
+ },
+ "user02": {
+ "calendar_1": {
+ },
+ "inbox": {
+ },
+ },
+ "user03": {
+ "calendar_1": {
+ },
+ "inbox": {
+ },
+ },
+ }
+
+
+ def test_validRequst(self):
+ """
+ Cross-pod request fails when there is no shared secret header present.
+ """
+
+ conduit = PoddingConduit(self.storeUnderTest())
+ r1, r2 = conduit.validRequst("user01", "puser02")
+ self.assertTrue(r1 is not None)
+ self.assertTrue(r2 is not None)
+
+ self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "bogus01", "user02")
+ self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "user01", "bogus02")
+ self.assertRaises(FailedCrossPodRequestError, conduit.validRequst, "user01", "user02")
+
+
+
+class TestConduitToConduit(MultiStoreConduitTest):
+
+ class FakeConduit(PoddingConduit):
+
+ @inlineCallbacks
+ def send_fake(self, txn, ownerUID, shareeUID):
+ _ignore_owner, sharee = self.validRequst(ownerUID, shareeUID)
+ action = {
+ "action": "fake",
+ "echo": "bravo"
+ }
+
+ result = yield self.sendRequest(txn, sharee, action)
+ returnValue(result)
+
+
+ def recv_fake(self, txn, j):
+ return succeed({
+ "result": "ok",
+ "back2u": j["echo"],
+ "more": "bits",
+ })
+
+
+ def makeConduit(self, store):
+ """
+ Use our own variant.
+ """
+ conduit = self.FakeConduit(store)
+ conduit.conduitRequestClass = FakeConduitRequest
+ return conduit
+
+
+ @inlineCallbacks
+ def test_fake_action(self):
+ """
+ Cross-pod request works when conduit does support the action.
+ """
+
+ txn = self.transactionUnderTest()
+ store1 = self.storeUnderTest()
+ response = yield store1.conduit.send_fake(txn, "user01", "puser01")
+ self.assertTrue("result" in response)
+ self.assertEqual(response["result"], "ok")
+ self.assertTrue("back2u" in response)
+ self.assertEqual(response["back2u"], "bravo")
+ self.assertTrue("more" in response)
+ self.assertEqual(response["more"], "bits")
+ yield txn.commit()
+
+ store2 = self.otherStoreUnderTest()
+ txn = store2.newTransaction()
+ response = yield store2.conduit.send_fake(txn, "puser01", "user01")
+ self.assertTrue("result" in response)
+ self.assertEqual(response["result"], "ok")
+ self.assertTrue("back2u" in response)
+ self.assertEqual(response["back2u"], "bravo")
+ self.assertTrue("more" in response)
+ self.assertEqual(response["more"], "bits")
+ yield txn.commit()
+
+
+
+class TestConduitAPI(MultiStoreConduitTest):
+ """
+ Test that the conduit api works.
+ """
+
+ nowYear = {"now": DateTime.getToday().getYear()}
+
+ caldata1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:{now:04d}0102T140000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+ caldata1_changed = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:{now:04d}0102T150000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance changed
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+ caldata2 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid2
+DTSTART:{now:04d}0102T160000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+ caldata3 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid3
+DTSTART:{now:04d}0102T160000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+ @inlineCallbacks
+ def test_basic_share(self):
+ """
+ Test that basic invite/uninvite works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ shared = yield calendar1.shareeView("puser01")
+ self.assertEqual(shared.shareStatus(), _BIND_STATUS_ACCEPTED)
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ self.assertTrue(shared is not None)
+ self.assertTrue(shared.external())
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.uninviteUserFromShare("puser01")
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ self.assertTrue(shared is None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_countobjects(self):
+ """
+ Test that action=countobjects works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ count = yield shared.countObjectResources()
+ self.assertEqual(count, 0)
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ count = yield calendar1.countObjectResources()
+ self.assertEqual(count, 1)
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ count = yield shared.countObjectResources()
+ self.assertEqual(count, 1)
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ count = yield calendar1.countObjectResources()
+ self.assertEqual(count, 0)
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ count = yield shared.countObjectResources()
+ self.assertEqual(count, 0)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_listobjects(self):
+ """
+ Test that action=listobjects works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ objects = yield shared.listObjectResources()
+ self.assertEqual(set(objects), set())
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+ objects = yield calendar1.listObjectResources()
+ self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ objects = yield shared.listObjectResources()
+ self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ objects = yield calendar1.listObjectResources()
+ self.assertEqual(set(objects), set(("2.ics",)))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ objects = yield shared.listObjectResources()
+ self.assertEqual(set(objects), set(("2.ics",)))
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_synctoken(self):
+ """
+ Test that action=synctoken works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_1 = yield calendar1.syncToken()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_1 = yield shared.syncToken()
+ yield self.otherCommit()
+
+ self.assertEqual(token1_1, token2_1)
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_2 = yield calendar1.syncToken()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_2 = yield shared.syncToken()
+ yield self.otherCommit()
+
+ self.assertNotEqual(token1_1, token1_2)
+ self.assertEqual(token1_2, token2_2)
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ count = yield calendar1.countObjectResources()
+ self.assertEqual(count, 0)
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_3 = yield calendar1.syncToken()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_3 = yield shared.syncToken()
+ yield self.otherCommit()
+
+ self.assertNotEqual(token1_1, token1_3)
+ self.assertNotEqual(token1_2, token1_3)
+ self.assertEqual(token1_3, token2_3)
+
+
+ @inlineCallbacks
+ def test_resourcenamessincerevision(self):
+ """
+ Test that action=synctoken works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_1 = yield calendar1.syncToken()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_1 = yield shared.syncToken()
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_2 = yield calendar1.syncToken()
+ names1 = yield calendar1.resourceNamesSinceToken(token1_1)
+ self.assertEqual(names1, ([u"1.ics"], [], [],))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_2 = yield shared.syncToken()
+ names2 = yield shared.resourceNamesSinceToken(token2_1)
+ self.assertEqual(names2, ([u"1.ics"], [], [],))
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ count = yield calendar1.countObjectResources()
+ self.assertEqual(count, 0)
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_3 = yield calendar1.syncToken()
+ names1 = yield calendar1.resourceNamesSinceToken(token1_2)
+ self.assertEqual(names1, ([], [u"1.ics"], [],))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_3 = yield shared.syncToken()
+ names2 = yield shared.resourceNamesSinceToken(token2_2)
+ self.assertEqual(names2, ([], [u"1.ics"], [],))
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ names1 = yield calendar1.resourceNamesSinceToken(token1_3)
+ self.assertEqual(names1, ([], [], [],))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ names2 = yield shared.resourceNamesSinceToken(token2_3)
+ self.assertEqual(names2, ([], [], [],))
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_resourceuidforname(self):
+ """
+ Test that action=resourceuidforname works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ uid = yield calendar1.resourceUIDForName("1.ics")
+ self.assertEqual(uid, "uid1")
+ uid = yield calendar1.resourceUIDForName("2.ics")
+ self.assertTrue(uid is None)
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ uid = yield shared.resourceUIDForName("1.ics")
+ self.assertEqual(uid, "uid1")
+ uid = yield shared.resourceUIDForName("2.ics")
+ self.assertTrue(uid is None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_resourcenameforuid(self):
+ """
+ Test that action=resourcenameforuid works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ name = yield calendar1.resourceNameForUID("uid1")
+ self.assertEqual(name, "1.ics")
+ name = yield calendar1.resourceNameForUID("uid2")
+ self.assertTrue(name is None)
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ name = yield shared.resourceNameForUID("uid1")
+ self.assertEqual(name, "1.ics")
+ name = yield shared.resourceNameForUID("uid2")
+ self.assertTrue(name is None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_search(self):
+ """
+ Test that action=resourcenameforuid works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[caldavxml.ComponentFilter(
+ **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+ )],
+ **{"name": "VCALENDAR"}
+ )
+ )
+ filter = Filter(filter)
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ names = [item[0] for item in (yield calendar1.search(filter))]
+ self.assertEqual(names, ["1.ics", ])
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ names = [item[0] for item in (yield shared.search(filter))]
+ self.assertEqual(names, ["1.ics", ])
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_loadallobjects(self):
+ """
+ Test that action=loadallobjects works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ resource1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ resource_id1 = resource1.id()
+ resource2 = yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+ resource_id2 = resource2.id()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resources = yield shared.objectResources()
+ byname = dict([(resource.name(), resource) for resource in resources])
+ byuid = dict([(resource.uid(), resource) for resource in resources])
+ self.assertEqual(len(resources), 2)
+ self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "2.ics",)))
+ self.assertEqual(set([resource.uid() for resource in resources]), set(("uid1", "uid2",)))
+ self.assertEqual(set([resource.id() for resource in resources]), set((resource_id1, resource_id2,)))
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is byname["1.ics"])
+ resource = yield shared.objectResourceWithName("2.ics")
+ self.assertTrue(resource is byname["2.ics"])
+ resource = yield shared.objectResourceWithName("Missing.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is byuid["uid1"])
+ resource = yield shared.objectResourceWithUID("uid2")
+ self.assertTrue(resource is byuid["uid2"])
+ resource = yield shared.objectResourceWithUID("uid-missing")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id1)
+ self.assertTrue(resource is byname["1.ics"])
+ resource = yield shared.objectResourceWithID(resource_id2)
+ self.assertTrue(resource is byname["2.ics"])
+ resource = yield shared.objectResourceWithID(0)
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resources = yield shared.objectResources()
+ byname = dict([(resource.name(), resource) for resource in resources])
+ byuid = dict([(resource.uid(), resource) for resource in resources])
+ self.assertEqual(len(resources), 1)
+ self.assertEqual(set([resource.name() for resource in resources]), set(("2.ics",)))
+ self.assertEqual(set([resource.uid() for resource in resources]), set(("uid2",)))
+ self.assertEqual(set([resource.id() for resource in resources]), set((resource_id2,)))
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithName("2.ics")
+ self.assertTrue(resource is byname["2.ics"])
+ resource = yield shared.objectResourceWithName("Missing.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid2")
+ self.assertTrue(resource is byuid["uid2"])
+ resource = yield shared.objectResourceWithUID("uid-missing")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id1)
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id2)
+ self.assertTrue(resource is byname["2.ics"])
+ resource = yield shared.objectResourceWithID(0)
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_loadallobjectswithnames(self):
+ """
+ Test that action=loadallobjectswithnames works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ resource1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ resource_id1 = resource1.id()
+ yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+ resource3 = yield calendar1.createCalendarObjectWithName("3.ics", Component.fromString(self.caldata3))
+ resource_id3 = resource3.id()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resources = yield shared.objectResources()
+ self.assertEqual(len(resources), 3)
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
+ byname = dict([(resource.name(), resource) for resource in resources])
+ byuid = dict([(resource.uid(), resource) for resource in resources])
+ self.assertEqual(len(resources), 2)
+ self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "3.ics",)))
+ self.assertEqual(set([resource.uid() for resource in resources]), set(("uid1", "uid3",)))
+ self.assertEqual(set([resource.id() for resource in resources]), set((resource_id1, resource_id3,)))
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is byname["1.ics"])
+ resource = yield shared.objectResourceWithName("3.ics")
+ self.assertTrue(resource is byname["3.ics"])
+ resource = yield shared.objectResourceWithName("Missing.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is byuid["uid1"])
+ resource = yield shared.objectResourceWithUID("uid3")
+ self.assertTrue(resource is byuid["uid3"])
+ resource = yield shared.objectResourceWithUID("uid-missing")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id1)
+ self.assertTrue(resource is byname["1.ics"])
+ resource = yield shared.objectResourceWithID(resource_id3)
+ self.assertTrue(resource is byname["3.ics"])
+ resource = yield shared.objectResourceWithID(0)
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
+ byname = dict([(resource.name(), resource) for resource in resources])
+ byuid = dict([(resource.uid(), resource) for resource in resources])
+ self.assertEqual(len(resources), 1)
+ self.assertEqual(set([resource.name() for resource in resources]), set(("3.ics",)))
+ self.assertEqual(set([resource.uid() for resource in resources]), set(("uid3",)))
+ self.assertEqual(set([resource.id() for resource in resources]), set((resource_id3,)))
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithName("3.ics")
+ self.assertTrue(resource is byname["3.ics"])
+ resource = yield shared.objectResourceWithName("Missing.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid3")
+ self.assertTrue(resource is byuid["uid3"])
+ resource = yield shared.objectResourceWithUID("uid-missing")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id1)
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id3)
+ self.assertTrue(resource is byname["3.ics"])
+ resource = yield shared.objectResourceWithID(0)
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_objectwith(self):
+ """
+ Test that action=objectwith works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ resource = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ resource_id = resource.id()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is not None)
+ self.assertEqual(resource.name(), "1.ics")
+ self.assertEqual(resource.uid(), "uid1")
+
+ resource = yield shared.objectResourceWithName("2.ics")
+ self.assertTrue(resource is None)
+
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is not None)
+ self.assertEqual(resource.name(), "1.ics")
+ self.assertEqual(resource.uid(), "uid1")
+
+ resource = yield shared.objectResourceWithUID("uid2")
+ self.assertTrue(resource is None)
+
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithID(resource_id)
+ self.assertTrue(resource is not None)
+ self.assertEqual(resource.name(), "1.ics")
+ self.assertEqual(resource.uid(), "uid1")
+
+ resource = yield shared.objectResourceWithID(0)
+ self.assertTrue(resource is None)
+
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithID(resource_id)
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_create(self):
+ """
+ Test that action=create works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ resource_id = resource.id()
+ self.assertTrue(resource is not None)
+ self.assertEqual(resource.name(), "1.ics")
+ self.assertEqual(resource.uid(), "uid1")
+ self.assertFalse(resource._componentChanged)
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is not None)
+ self.assertEqual(resource.name(), "1.ics")
+ self.assertEqual(resource.uid(), "uid1")
+ self.assertEqual(resource.id(), resource_id)
+ yield self.otherCommit()
+
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ self.assertTrue(object1 is not None)
+ self.assertEqual(object1.name(), "1.ics")
+ self.assertEqual(object1.uid(), "uid1")
+ self.assertEqual(object1.id(), resource_id)
+ yield self.commit()
+
+
+ @inlineCallbacks
+ def test_create_exception(self):
+ """
+ Test that action=create fails when a duplicate name is used.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ yield self.failUnlessFailure(shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)), ObjectResourceNameAlreadyExistsError)
+ yield self.otherAbort()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ yield self.failUnlessFailure(shared.createCalendarObjectWithName(".2.ics", Component.fromString(self.caldata2)), ObjectResourceNameNotAllowedError)
+ yield self.otherAbort()
+
+
+ @inlineCallbacks
+ def test_setcomponent(self):
+ """
+ Test that action=setcomponent works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ ical = yield shared_object.component()
+ self.assertTrue(isinstance(ical, Component))
+ self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
+ yield self.otherCommit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ changed = yield shared_object.setComponent(Component.fromString(self.caldata1_changed))
+ self.assertFalse(changed)
+ ical = yield shared_object.component()
+ self.assertTrue(isinstance(ical, Component))
+ self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
+ yield self.otherCommit()
+
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ ical = yield object1.component()
+ self.assertTrue(isinstance(ical, Component))
+ self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
+ yield self.commit()
+
+
+ @inlineCallbacks
+ def test_component(self):
+ """
+ Test that action=component works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ ical = yield shared_object.component()
+ self.assertTrue(isinstance(ical, Component))
+ self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_remove(self):
+ """
+ Test that action=create works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ yield shared_object.remove()
+ yield self.otherCommit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ self.assertTrue(shared_object is None)
+ yield self.otherCommit()
+
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ self.assertTrue(object1 is None)
+ yield self.commit()
+
+
+ @inlineCallbacks
+ def test_freebusy(self):
+ """
+ Test that action=component works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ fbstart = "{now:04d}0102T000000Z".format(**self.nowYear)
+ fbend = "{now:04d}0103T000000Z".format(**self.nowYear)
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+
+ fbinfo = [[], [], []]
+ matchtotal = yield generateFreeBusyInfo(
+ shared,
+ fbinfo,
+ TimeRange(start=fbstart, end=fbend),
+ 0,
+ excludeuid=None,
+ organizer=None,
+ organizerPrincipal=None,
+ same_calendar_user=False,
+ servertoserver=False,
+ event_details=False,
+ logItems=None
+ )
+
+ self.assertEqual(matchtotal, 1)
+ self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
+ self.assertEqual(len(fbinfo[1]), 0)
+ self.assertEqual(len(fbinfo[2]), 0)
+ yield self.otherCommit()
+
+
+ def attachmentToString(self, attachment):
+ """
+ Convenience to convert an L{IAttachment} to a string.
+
+ @param attachment: an L{IAttachment} provider to convert into a string.
+
+ @return: a L{Deferred} that fires with the contents of the attachment.
+
+ @rtype: L{Deferred} firing C{bytes}
+ """
+ capture = CaptureProtocol()
+ attachment.retrieve(capture)
+ return capture.deferred
+
+
+ @inlineCallbacks
+ def test_add_attachment(self):
+ """
+ Test that action=add-attachment works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ resourceID = object1.id()
+ yield self.commit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ attachment, location = yield shared_object.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
+ managedID = attachment.managedID()
+ from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
+ self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
+ self.assertTrue("user01/attachments/test" in location)
+ yield self.otherCommit()
+
+ cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
+ self.assertEqual(cobjs, set((resourceID,)))
+ attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
+ self.assertEqual(attachment.name(), "test.txt")
+ data = yield self.attachmentToString(attachment)
+ self.assertEqual(data, "Here is some text.")
+ yield self.commit()
+
+
+ @inlineCallbacks
+ def test_update_attachment(self):
+ """
+ Test that action=update-attachment works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ resourceID = object1.id()
+ attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
+ managedID = attachment.managedID()
+ yield self.commit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ attachment, location = yield shared_object.updateAttachment(managedID, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some more text."))
+ managedID = attachment.managedID()
+ from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
+ self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
+ self.assertTrue("user01/attachments/test" in location)
+ yield self.otherCommit()
+
+ cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
+ self.assertEqual(cobjs, set((resourceID,)))
+ attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
+ self.assertEqual(attachment.name(), "test.txt")
+ data = yield self.attachmentToString(attachment)
+ self.assertEqual(data, "Here is some more text.")
+ yield self.commit()
+
+
+ @inlineCallbacks
+ def test_remove_attachment(self):
+ """
+ Test that action=remove-attachment works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ resourceID = object1.id()
+ attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
+ managedID = attachment.managedID()
+ yield self.commit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ yield shared_object.removeAttachment(None, managedID)
+ yield self.otherCommit()
+
+ cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
+ self.assertEqual(cobjs, set())
+ attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
+ self.assertTrue(attachment is None)
+ yield self.commit()
Deleted: CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,99 +0,0 @@
-##
-# Copyright (c) 2005-2013 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 twisted.internet.defer import inlineCallbacks
-
-from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, \
- Server
-from txdav.caldav.datastore.test.util import buildCalendarStore, \
- TestCalendarStoreDirectoryRecord
-from txdav.common.datastore.podding.resource import ConduitResource
-from txdav.common.datastore.sql_tables import _HOME_STATUS_NORMAL, \
- _HOME_STATUS_EXTERNAL
-from txdav.common.datastore.test.util import CommonCommonTests
-from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
-
-import twext.web2.dav.test.util
-
-
-class ExternalHome(CommonCommonTests, twext.web2.dav.test.util.TestCase):
-
- @inlineCallbacks
- def setUp(self):
- yield super(ExternalHome, self).setUp()
- self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
- self.directory = self._sqlCalendarStore.directoryService()
-
- for ctr in range(1, 100):
- self.directory.addRecord(TestCalendarStoreDirectoryRecord(
- "puser{:02d}".format(ctr),
- ("puser{:02d}".format(ctr),),
- "Puser {:02d}".format(ctr),
- frozenset((
- "urn:uuid:puser{:02d}".format(ctr),
- "mailto:puser{:02d}@example.com".format(ctr),
- )),
- thisServer=False,
- ))
-
- self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
-
- self.thisServer = Server("A", "http://127.0.0.1", "A", True)
- Servers.addServer(self.thisServer)
-
-
- def storeUnderTest(self):
- """
- Return a store for testing.
- """
- return self._sqlCalendarStore
-
-
- @inlineCallbacks
- def test_validNormalHome(self):
- """
- Locally hosted homes are valid.
- """
-
- for i in range(1, 100):
- home = yield self.transactionUnderTest().calendarHomeWithUID("user{:02d}".format(i), create=True)
- self.assertTrue(home is not None)
- self.assertEqual(home._status, _HOME_STATUS_NORMAL)
- calendar = yield home.childWithName("calendar")
- self.assertTrue(calendar is not None)
-
-
- @inlineCallbacks
- def test_validExternalHome(self):
- """
- Externally hosted homes are valid.
- """
-
- for i in range(1, 100):
- home = yield self.transactionUnderTest().calendarHomeWithUID("puser{:02d}".format(i), create=True)
- self.assertTrue(home is not None)
- self.assertEqual(home._status, _HOME_STATUS_EXTERNAL)
- calendar = yield home.childWithName("calendar")
- self.assertTrue(calendar is None)
-
-
- @inlineCallbacks
- def test_invalidHome(self):
- """
- Homes are invalid.
- """
-
- yield self.assertFailure(self.transactionUnderTest().calendarHomeWithUID("buser01", create=True), DirectoryRecordNotFoundError)
Copied: CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,99 @@
+##
+# Copyright (c) 2005-2013 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 twisted.internet.defer import inlineCallbacks
+
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, \
+ Server
+from txdav.caldav.datastore.test.util import buildCalendarStore, \
+ TestCalendarStoreDirectoryRecord
+from txdav.common.datastore.podding.resource import ConduitResource
+from txdav.common.datastore.sql_tables import _HOME_STATUS_NORMAL, \
+ _HOME_STATUS_EXTERNAL
+from txdav.common.datastore.test.util import CommonCommonTests
+from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
+
+import txweb2.dav.test.util
+
+
+class ExternalHome(CommonCommonTests, txweb2.dav.test.util.TestCase):
+
+ @inlineCallbacks
+ def setUp(self):
+ yield super(ExternalHome, self).setUp()
+ self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
+ self.directory = self._sqlCalendarStore.directoryService()
+
+ for ctr in range(1, 100):
+ self.directory.addRecord(TestCalendarStoreDirectoryRecord(
+ "puser{:02d}".format(ctr),
+ ("puser{:02d}".format(ctr),),
+ "Puser {:02d}".format(ctr),
+ frozenset((
+ "urn:uuid:puser{:02d}".format(ctr),
+ "mailto:puser{:02d}@example.com".format(ctr),
+ )),
+ thisServer=False,
+ ))
+
+ self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
+
+ self.thisServer = Server("A", "http://127.0.0.1", "A", True)
+ Servers.addServer(self.thisServer)
+
+
+ def storeUnderTest(self):
+ """
+ Return a store for testing.
+ """
+ return self._sqlCalendarStore
+
+
+ @inlineCallbacks
+ def test_validNormalHome(self):
+ """
+ Locally hosted homes are valid.
+ """
+
+ for i in range(1, 100):
+ home = yield self.transactionUnderTest().calendarHomeWithUID("user{:02d}".format(i), create=True)
+ self.assertTrue(home is not None)
+ self.assertEqual(home._status, _HOME_STATUS_NORMAL)
+ calendar = yield home.childWithName("calendar")
+ self.assertTrue(calendar is not None)
+
+
+ @inlineCallbacks
+ def test_validExternalHome(self):
+ """
+ Externally hosted homes are valid.
+ """
+
+ for i in range(1, 100):
+ home = yield self.transactionUnderTest().calendarHomeWithUID("puser{:02d}".format(i), create=True)
+ self.assertTrue(home is not None)
+ self.assertEqual(home._status, _HOME_STATUS_EXTERNAL)
+ calendar = yield home.childWithName("calendar")
+ self.assertTrue(calendar is None)
+
+
+ @inlineCallbacks
+ def test_invalidHome(self):
+ """
+ Homes are invalid.
+ """
+
+ yield self.assertFailure(self.transactionUnderTest().calendarHomeWithUID("buser01", create=True), DirectoryRecordNotFoundError)
Deleted: CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,275 +0,0 @@
-##
-# Copyright (c) 2005-2013 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 twext.python.clsprop import classproperty
-from twext.web2 import http_headers, responsecode
-import twext.web2.dav.test.util
-from twext.web2.dav.util import allDataFromStream
-from twext.web2.test.test_server import SimpleRequest
-from twisted.internet.defer import inlineCallbacks, succeed
-from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, Server
-from txdav.caldav.datastore.test.util import buildCalendarStore
-from txdav.common.datastore.podding.resource import ConduitResource
-from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
-import json
-from txdav.common.datastore.podding.conduit import PoddingConduit
-
-class ConduitPOST (CommonCommonTests, twext.web2.dav.test.util.TestCase):
-
- class FakeConduit(PoddingConduit):
-
- def recv_fake(self, txn, j):
- return succeed({
- "result": "ok",
- "back2u": j["echo"],
- "more": "bits",
- })
-
-
- @inlineCallbacks
- def setUp(self):
- yield super(ConduitPOST, self).setUp()
- self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
- self.directory = self._sqlCalendarStore.directoryService()
-
- self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
-
- self.thisServer = Server("A", "http://127.0.0.1", "A", True)
- Servers.addServer(self.thisServer)
-
- yield self.populate()
-
-
- def storeUnderTest(self):
- """
- Return a store for testing.
- """
- return self._sqlCalendarStore
-
-
- @inlineCallbacks
- def populate(self):
- yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
- self.notifierFactory.reset()
-
-
- @classproperty(cache=False)
- def requirements(cls): #@NoSelf
- return {
- "user01": {
- "calendar_1": {
- },
- "inbox": {
- },
- },
- "user02": {
- "calendar_1": {
- },
- "inbox": {
- },
- },
- "user03": {
- "calendar_1": {
- },
- "inbox": {
- },
- },
- }
-
-
- @inlineCallbacks
- def test_receive_no_secret(self):
- """
- Cross-pod request fails when there is no shared secret header present.
- """
-
- request = SimpleRequest(
- self.site,
- "POST",
- "/conduit",
- headers=http_headers.Headers(rawHeaders={
- "Content-Type": ("text/plain",)
- }),
- content="""Hello, World!
-""".replace("\n", "\r\n")
- )
-
- response = (yield self.send(request))
- self.assertEqual(response.code, responsecode.FORBIDDEN)
-
-
- @inlineCallbacks
- def test_receive_wrong_mime(self):
- """
- Cross-pod request fails when Content-Type header is wrong.
- """
-
- request = SimpleRequest(
- self.site,
- "POST",
- "/conduit",
- headers=http_headers.Headers(rawHeaders={
- "Content-Type": ("text/plain",),
- self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
- }),
- content="""Hello, World!
-""".replace("\n", "\r\n")
- )
-
- response = (yield self.send(request))
- self.assertEqual(response.code, responsecode.BAD_REQUEST)
-
-
- @inlineCallbacks
- def test_receive_invalid_json(self):
- """
- Cross-pod request fails when request data is not JSON.
- """
-
- request = SimpleRequest(
- self.site,
- "POST",
- "/conduit",
- headers=http_headers.Headers(rawHeaders={
- "Content-Type": ("application/json",),
- self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
- }),
- content="""Hello, World!
-""".replace("\n", "\r\n")
- )
-
- response = (yield self.send(request))
- self.assertEqual(response.code, responsecode.BAD_REQUEST)
-
-
- @inlineCallbacks
- def test_receive_bad_json(self):
- """
- Cross-pod request fails when JSON data does not have an "action".
- """
-
- request = SimpleRequest(
- self.site,
- "POST",
- "/conduit",
- headers=http_headers.Headers(rawHeaders={
- "Content-Type": ("application/json",),
- self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
- }),
- content="""
-{
- "foo":"bar"
-}
-""".replace("\n", "\r\n")
- )
-
- response = (yield self.send(request))
- self.assertEqual(response.code, responsecode.BAD_REQUEST)
-
-
- @inlineCallbacks
- def test_receive_ping(self):
- """
- Cross-pod request works with the "ping" action.
- """
-
- request = SimpleRequest(
- self.site,
- "POST",
- "/conduit",
- headers=http_headers.Headers(rawHeaders={
- "Content-Type": ("application/json",),
- self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
- }),
- content="""
-{
- "action":"ping"
-}
-""".replace("\n", "\r\n")
- )
-
- response = (yield self.send(request))
- self.assertEqual(response.code, responsecode.OK)
- data = (yield allDataFromStream(response.stream))
- j = json.loads(data)
- self.assertTrue("result" in j)
- self.assertEqual(j["result"], "ok")
-
-
- @inlineCallbacks
- def test_receive_fake_conduit_no_action(self):
- """
- Cross-pod request fails when conduit does not support the action.
- """
-
- store = self.storeUnderTest()
- self.patch(store, "conduit", self.FakeConduit(store))
-
- request = SimpleRequest(
- self.site,
- "POST",
- "/conduit",
- headers=http_headers.Headers(rawHeaders={
- "Content-Type": ("application/json",),
- self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
- }),
- content="""
-{
- "action":"bogus",
- "echo":"bravo"
-}
-""".replace("\n", "\r\n")
- )
-
- response = (yield self.send(request))
- self.assertEqual(response.code, responsecode.BAD_REQUEST)
-
-
- @inlineCallbacks
- def test_receive_fake_conduit(self):
- """
- Cross-pod request works when conduit does support the action.
- """
-
- store = self.storeUnderTest()
- self.patch(store, "conduit", self.FakeConduit(store))
-
- request = SimpleRequest(
- self.site,
- "POST",
- "/conduit",
- headers=http_headers.Headers(rawHeaders={
- "Content-Type": ("application/json",),
- self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
- }),
- content="""
-{
- "action":"fake",
- "echo":"bravo"
-}
-""".replace("\n", "\r\n")
- )
-
- response = (yield self.send(request))
- self.assertEqual(response.code, responsecode.OK)
- data = (yield allDataFromStream(response.stream))
- j = json.loads(data)
- self.assertTrue("result" in j)
- self.assertEqual(j["result"], "ok")
- self.assertTrue("back2u" in j)
- self.assertEqual(j["back2u"], "bravo")
- self.assertTrue("more" in j)
- self.assertEqual(j["more"], "bits")
Copied: CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,278 @@
+##
+# Copyright (c) 2005-2013 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 twext.python.clsprop import classproperty
+
+import txweb2.dav.test.util
+from txweb2 import http_headers, responsecode
+from txweb2.dav.util import allDataFromStream
+from txweb2.test.test_server import SimpleRequest
+
+from twisted.internet.defer import inlineCallbacks, succeed
+
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, Server
+from txdav.caldav.datastore.test.util import buildCalendarStore
+from txdav.common.datastore.podding.resource import ConduitResource
+from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
+import json
+from txdav.common.datastore.podding.conduit import PoddingConduit
+
+class ConduitPOST (CommonCommonTests, txweb2.dav.test.util.TestCase):
+
+ class FakeConduit(PoddingConduit):
+
+ def recv_fake(self, txn, j):
+ return succeed({
+ "result": "ok",
+ "back2u": j["echo"],
+ "more": "bits",
+ })
+
+
+ @inlineCallbacks
+ def setUp(self):
+ yield super(ConduitPOST, self).setUp()
+ self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
+ self.directory = self._sqlCalendarStore.directoryService()
+
+ self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
+
+ self.thisServer = Server("A", "http://127.0.0.1", "A", True)
+ Servers.addServer(self.thisServer)
+
+ yield self.populate()
+
+
+ def storeUnderTest(self):
+ """
+ Return a store for testing.
+ """
+ return self._sqlCalendarStore
+
+
+ @inlineCallbacks
+ def populate(self):
+ yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+ self.notifierFactory.reset()
+
+
+ @classproperty(cache=False)
+ def requirements(cls): #@NoSelf
+ return {
+ "user01": {
+ "calendar_1": {
+ },
+ "inbox": {
+ },
+ },
+ "user02": {
+ "calendar_1": {
+ },
+ "inbox": {
+ },
+ },
+ "user03": {
+ "calendar_1": {
+ },
+ "inbox": {
+ },
+ },
+ }
+
+
+ @inlineCallbacks
+ def test_receive_no_secret(self):
+ """
+ Cross-pod request fails when there is no shared secret header present.
+ """
+
+ request = SimpleRequest(
+ self.site,
+ "POST",
+ "/conduit",
+ headers=http_headers.Headers(rawHeaders={
+ "Content-Type": ("text/plain",)
+ }),
+ content="""Hello, World!
+""".replace("\n", "\r\n")
+ )
+
+ response = (yield self.send(request))
+ self.assertEqual(response.code, responsecode.FORBIDDEN)
+
+
+ @inlineCallbacks
+ def test_receive_wrong_mime(self):
+ """
+ Cross-pod request fails when Content-Type header is wrong.
+ """
+
+ request = SimpleRequest(
+ self.site,
+ "POST",
+ "/conduit",
+ headers=http_headers.Headers(rawHeaders={
+ "Content-Type": ("text/plain",),
+ self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+ }),
+ content="""Hello, World!
+""".replace("\n", "\r\n")
+ )
+
+ response = (yield self.send(request))
+ self.assertEqual(response.code, responsecode.BAD_REQUEST)
+
+
+ @inlineCallbacks
+ def test_receive_invalid_json(self):
+ """
+ Cross-pod request fails when request data is not JSON.
+ """
+
+ request = SimpleRequest(
+ self.site,
+ "POST",
+ "/conduit",
+ headers=http_headers.Headers(rawHeaders={
+ "Content-Type": ("application/json",),
+ self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+ }),
+ content="""Hello, World!
+""".replace("\n", "\r\n")
+ )
+
+ response = (yield self.send(request))
+ self.assertEqual(response.code, responsecode.BAD_REQUEST)
+
+
+ @inlineCallbacks
+ def test_receive_bad_json(self):
+ """
+ Cross-pod request fails when JSON data does not have an "action".
+ """
+
+ request = SimpleRequest(
+ self.site,
+ "POST",
+ "/conduit",
+ headers=http_headers.Headers(rawHeaders={
+ "Content-Type": ("application/json",),
+ self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+ }),
+ content="""
+{
+ "foo":"bar"
+}
+""".replace("\n", "\r\n")
+ )
+
+ response = (yield self.send(request))
+ self.assertEqual(response.code, responsecode.BAD_REQUEST)
+
+
+ @inlineCallbacks
+ def test_receive_ping(self):
+ """
+ Cross-pod request works with the "ping" action.
+ """
+
+ request = SimpleRequest(
+ self.site,
+ "POST",
+ "/conduit",
+ headers=http_headers.Headers(rawHeaders={
+ "Content-Type": ("application/json",),
+ self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+ }),
+ content="""
+{
+ "action":"ping"
+}
+""".replace("\n", "\r\n")
+ )
+
+ response = (yield self.send(request))
+ self.assertEqual(response.code, responsecode.OK)
+ data = (yield allDataFromStream(response.stream))
+ j = json.loads(data)
+ self.assertTrue("result" in j)
+ self.assertEqual(j["result"], "ok")
+
+
+ @inlineCallbacks
+ def test_receive_fake_conduit_no_action(self):
+ """
+ Cross-pod request fails when conduit does not support the action.
+ """
+
+ store = self.storeUnderTest()
+ self.patch(store, "conduit", self.FakeConduit(store))
+
+ request = SimpleRequest(
+ self.site,
+ "POST",
+ "/conduit",
+ headers=http_headers.Headers(rawHeaders={
+ "Content-Type": ("application/json",),
+ self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+ }),
+ content="""
+{
+ "action":"bogus",
+ "echo":"bravo"
+}
+""".replace("\n", "\r\n")
+ )
+
+ response = (yield self.send(request))
+ self.assertEqual(response.code, responsecode.BAD_REQUEST)
+
+
+ @inlineCallbacks
+ def test_receive_fake_conduit(self):
+ """
+ Cross-pod request works when conduit does support the action.
+ """
+
+ store = self.storeUnderTest()
+ self.patch(store, "conduit", self.FakeConduit(store))
+
+ request = SimpleRequest(
+ self.site,
+ "POST",
+ "/conduit",
+ headers=http_headers.Headers(rawHeaders={
+ "Content-Type": ("application/json",),
+ self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+ }),
+ content="""
+{
+ "action":"fake",
+ "echo":"bravo"
+}
+""".replace("\n", "\r\n")
+ )
+
+ response = (yield self.send(request))
+ self.assertEqual(response.code, responsecode.OK)
+ data = (yield allDataFromStream(response.stream))
+ j = json.loads(data)
+ self.assertTrue("result" in j)
+ self.assertEqual(j["result"], "ok")
+ self.assertTrue("back2u" in j)
+ self.assertEqual(j["back2u"], "bravo")
+ self.assertTrue("more" in j)
+ self.assertEqual(j["more"], "bits")
Deleted: CalendarServer/trunk/txdav/common/datastore/podding/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/util.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,237 +0,0 @@
-##
-# Copyright (c) 2013 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 twisted.internet.defer import inlineCallbacks, returnValue
-
-from txdav.caldav.datastore.scheduling.ischedule.localservers import Server, \
- Servers
-from txdav.caldav.datastore.test.util import \
- TestCalendarStoreDirectoryRecord, TestCalendarStoreDirectoryService
-from txdav.common.datastore.podding.conduit import PoddingConduit
-from txdav.common.datastore.test.util import CommonCommonTests, SQLStoreBuilder,\
- theStoreBuilder
-
-import twext.web2.dav.test.util
-from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
-from twext.enterprise.ienterprise import AlreadyFinishedError
-import json
-
-class FakeConduitRequest(object):
- """
- A conduit request that sends messages internally rather than using HTTP
- """
-
- storeMap = {}
-
- @classmethod
- def addServerStore(cls, server, store):
- """
- Add a store mapped to a server. These mappings are used to "deliver" conduit
- requests to the appropriate store.
-
- @param uri: the server
- @type uri: L{Server}
- @param store: the store
- @type store: L{ICommonDataStore}
- """
-
- cls.storeMap[server.details()] = store
-
-
- def __init__(self, server, data, stream=None, stream_type=None):
-
- self.server = server
- self.data = json.dumps(data)
- self.stream = stream
- self.streamType = stream_type
-
-
- @inlineCallbacks
- def doRequest(self, txn):
-
- # Generate an HTTP client request
- try:
- response = (yield self._processRequest())
- response = json.loads(response)
- except Exception as e:
- raise ValueError("Failed cross-pod request: {}".format(e))
-
- returnValue(response)
-
-
- @inlineCallbacks
- def _processRequest(self):
- """
- Process the request by sending it to the relevant server.
-
- @return: the HTTP response.
- @rtype: L{Response}
- """
-
- store = self.storeMap[self.server.details()]
- j = json.loads(self.data)
- if self.stream is not None:
- j["stream"] = self.stream
- j["streamType"] = self.streamType
- result = yield store.conduit.processRequest(j)
- result = json.dumps(result)
- returnValue(result)
-
-
-
-class MultiStoreConduitTest(CommonCommonTests, twext.web2.dav.test.util.TestCase):
-
- theStoreBuilder2 = SQLStoreBuilder(secondary=True)
- otherTransaction = None
-
- @inlineCallbacks
- def setUp(self):
- yield super(MultiStoreConduitTest, self).setUp()
-
- server1 = Server("A", "http://127.0.0.1:8008", "A", True)
- Servers.addServer(server1)
-
- server2 = Server("B", "http://127.0.0.1:8108", "B", False)
- Servers.addServer(server2)
-
- self._sqlCalendarStore1 = yield self.makeStore(theStoreBuilder, True, server1, server2)
- self._sqlCalendarStore2 = yield self.makeStore(self.theStoreBuilder2, False, server1, server2)
-
- FakeConduitRequest.addServerStore(server1, self._sqlCalendarStore1)
- FakeConduitRequest.addServerStore(server2, self._sqlCalendarStore2)
-
-
- def storeUnderTest(self):
- """
- Return a store for testing.
- """
- return self._sqlCalendarStore1
-
-
- def otherStoreUnderTest(self):
- """
- Return a store for testing.
- """
- return self._sqlCalendarStore2
-
-
- def newOtherTransaction(self):
- assert self.otherTransaction is None
- store2 = self.otherStoreUnderTest()
- txn = store2.newTransaction()
- @inlineCallbacks
- def maybeCommitThis():
- try:
- yield txn.commit()
- except AlreadyFinishedError:
- pass
- self.addCleanup(maybeCommitThis)
- self.otherTransaction = txn
- return self.otherTransaction
-
-
- def otherTransactionUnderTest(self):
- if self.otherTransaction is None:
- self.newOtherTransaction()
- return self.otherTransaction
-
-
- @inlineCallbacks
- def otherCommit(self):
- assert self.otherTransaction is not None
- yield self.otherTransaction.commit()
- self.otherTransaction = None
-
-
- @inlineCallbacks
- def otherAbort(self):
- assert self.otherTransaction is not None
- yield self.otherTransaction.abort()
- self.otherTransaction = None
-
-
- @inlineCallbacks
- def makeStore(self, builder, internal, server1, server2):
-
- directory = self.makeDirectory(internal, server1, server2)
- store = yield builder.buildStore(self, self.notifierFactory, directory)
- store.queryCacher = None # Cannot use query caching
- store.conduit = self.makeConduit(store)
- returnValue(store)
-
-
- def makeDirectory(self, internal, server1, server2):
-
- directory = TestCalendarStoreDirectoryService()
-
- # User accounts
- for ctr in range(1, 100):
- directory.addRecord(TestCalendarStoreDirectoryRecord(
- "user%02d" % (ctr,),
- ("user%02d" % (ctr,),),
- "User %02d" % (ctr,),
- frozenset((
- "urn:uuid:user%02d" % (ctr,),
- "mailto:user%02d at example.com" % (ctr,),
- )),
- thisServer=internal,
- server=server1
- ))
-
- for ctr in range(1, 100):
- directory.addRecord(TestCalendarStoreDirectoryRecord(
- "puser{:02d}".format(ctr),
- ("puser{:02d}".format(ctr),),
- "Puser {:02d}".format(ctr),
- frozenset((
- "urn:uuid:puser{:02d}".format(ctr),
- "mailto:puser{:02d}@example.com".format(ctr),
- )),
- thisServer=not internal,
- server=server2
- ))
-
- return directory
-
-
- def makeConduit(self, store):
- conduit = PoddingConduit(store)
- conduit.conduitRequestClass = FakeConduitRequest
- return conduit
-
-
- @inlineCallbacks
- def createShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
-
- home = yield self.homeUnderTest(name=ownerGUID, create=True)
- calendar = yield home.calendarWithName(name)
- yield calendar.inviteUserToShare(shareeGUID, _BIND_MODE_WRITE, "shared", shareName="shared-calendar")
- yield self.commit()
-
- home2 = yield self.homeUnderTest(txn=self.newOtherTransaction(), name=shareeGUID)
- yield home2.acceptShare("shared-calendar")
- yield self.otherCommit()
-
- returnValue("shared-calendar")
-
-
- @inlineCallbacks
- def removeShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
-
- home = yield self.homeUnderTest(name=ownerGUID)
- calendar = yield home.calendarWithName(name)
- yield calendar.uninviteUserFromShare(shareeGUID)
- yield self.commit()
Copied: CalendarServer/trunk/txdav/common/datastore/podding/test/util.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/util.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/util.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,239 @@
+##
+# Copyright (c) 2013 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 twisted.internet.defer import inlineCallbacks, returnValue
+
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Server, \
+ Servers
+from txdav.caldav.datastore.test.util import \
+ TestCalendarStoreDirectoryRecord, TestCalendarStoreDirectoryService
+from txdav.common.datastore.podding.conduit import PoddingConduit
+from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
+from txdav.common.datastore.test.util import CommonCommonTests, SQLStoreBuilder,\
+ theStoreBuilder
+
+import txweb2.dav.test.util
+
+from twext.enterprise.ienterprise import AlreadyFinishedError
+
+import json
+
+class FakeConduitRequest(object):
+ """
+ A conduit request that sends messages internally rather than using HTTP
+ """
+
+ storeMap = {}
+
+ @classmethod
+ def addServerStore(cls, server, store):
+ """
+ Add a store mapped to a server. These mappings are used to "deliver" conduit
+ requests to the appropriate store.
+
+ @param uri: the server
+ @type uri: L{Server}
+ @param store: the store
+ @type store: L{ICommonDataStore}
+ """
+
+ cls.storeMap[server.details()] = store
+
+
+ def __init__(self, server, data, stream=None, stream_type=None):
+
+ self.server = server
+ self.data = json.dumps(data)
+ self.stream = stream
+ self.streamType = stream_type
+
+
+ @inlineCallbacks
+ def doRequest(self, txn):
+
+ # Generate an HTTP client request
+ try:
+ response = (yield self._processRequest())
+ response = json.loads(response)
+ except Exception as e:
+ raise ValueError("Failed cross-pod request: {}".format(e))
+
+ returnValue(response)
+
+
+ @inlineCallbacks
+ def _processRequest(self):
+ """
+ Process the request by sending it to the relevant server.
+
+ @return: the HTTP response.
+ @rtype: L{Response}
+ """
+
+ store = self.storeMap[self.server.details()]
+ j = json.loads(self.data)
+ if self.stream is not None:
+ j["stream"] = self.stream
+ j["streamType"] = self.streamType
+ result = yield store.conduit.processRequest(j)
+ result = json.dumps(result)
+ returnValue(result)
+
+
+
+class MultiStoreConduitTest(CommonCommonTests, txweb2.dav.test.util.TestCase):
+
+ theStoreBuilder2 = SQLStoreBuilder(secondary=True)
+ otherTransaction = None
+
+ @inlineCallbacks
+ def setUp(self):
+ yield super(MultiStoreConduitTest, self).setUp()
+
+ server1 = Server("A", "http://127.0.0.1:8008", "A", True)
+ Servers.addServer(server1)
+
+ server2 = Server("B", "http://127.0.0.1:8108", "B", False)
+ Servers.addServer(server2)
+
+ self._sqlCalendarStore1 = yield self.makeStore(theStoreBuilder, True, server1, server2)
+ self._sqlCalendarStore2 = yield self.makeStore(self.theStoreBuilder2, False, server1, server2)
+
+ FakeConduitRequest.addServerStore(server1, self._sqlCalendarStore1)
+ FakeConduitRequest.addServerStore(server2, self._sqlCalendarStore2)
+
+
+ def storeUnderTest(self):
+ """
+ Return a store for testing.
+ """
+ return self._sqlCalendarStore1
+
+
+ def otherStoreUnderTest(self):
+ """
+ Return a store for testing.
+ """
+ return self._sqlCalendarStore2
+
+
+ def newOtherTransaction(self):
+ assert self.otherTransaction is None
+ store2 = self.otherStoreUnderTest()
+ txn = store2.newTransaction()
+ @inlineCallbacks
+ def maybeCommitThis():
+ try:
+ yield txn.commit()
+ except AlreadyFinishedError:
+ pass
+ self.addCleanup(maybeCommitThis)
+ self.otherTransaction = txn
+ return self.otherTransaction
+
+
+ def otherTransactionUnderTest(self):
+ if self.otherTransaction is None:
+ self.newOtherTransaction()
+ return self.otherTransaction
+
+
+ @inlineCallbacks
+ def otherCommit(self):
+ assert self.otherTransaction is not None
+ yield self.otherTransaction.commit()
+ self.otherTransaction = None
+
+
+ @inlineCallbacks
+ def otherAbort(self):
+ assert self.otherTransaction is not None
+ yield self.otherTransaction.abort()
+ self.otherTransaction = None
+
+
+ @inlineCallbacks
+ def makeStore(self, builder, internal, server1, server2):
+
+ directory = self.makeDirectory(internal, server1, server2)
+ store = yield builder.buildStore(self, self.notifierFactory, directory)
+ store.queryCacher = None # Cannot use query caching
+ store.conduit = self.makeConduit(store)
+ returnValue(store)
+
+
+ def makeDirectory(self, internal, server1, server2):
+
+ directory = TestCalendarStoreDirectoryService()
+
+ # User accounts
+ for ctr in range(1, 100):
+ directory.addRecord(TestCalendarStoreDirectoryRecord(
+ "user%02d" % (ctr,),
+ ("user%02d" % (ctr,),),
+ "User %02d" % (ctr,),
+ frozenset((
+ "urn:uuid:user%02d" % (ctr,),
+ "mailto:user%02d at example.com" % (ctr,),
+ )),
+ thisServer=internal,
+ server=server1
+ ))
+
+ for ctr in range(1, 100):
+ directory.addRecord(TestCalendarStoreDirectoryRecord(
+ "puser{:02d}".format(ctr),
+ ("puser{:02d}".format(ctr),),
+ "Puser {:02d}".format(ctr),
+ frozenset((
+ "urn:uuid:puser{:02d}".format(ctr),
+ "mailto:puser{:02d}@example.com".format(ctr),
+ )),
+ thisServer=not internal,
+ server=server2
+ ))
+
+ return directory
+
+
+ def makeConduit(self, store):
+ conduit = PoddingConduit(store)
+ conduit.conduitRequestClass = FakeConduitRequest
+ return conduit
+
+
+ @inlineCallbacks
+ def createShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
+
+ home = yield self.homeUnderTest(name=ownerGUID, create=True)
+ calendar = yield home.calendarWithName(name)
+ yield calendar.inviteUserToShare(shareeGUID, _BIND_MODE_WRITE, "shared", shareName="shared-calendar")
+ yield self.commit()
+
+ home2 = yield self.homeUnderTest(txn=self.newOtherTransaction(), name=shareeGUID)
+ yield home2.acceptShare("shared-calendar")
+ yield self.otherCommit()
+
+ returnValue("shared-calendar")
+
+
+ @inlineCallbacks
+ def removeShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
+
+ home = yield self.homeUnderTest(name=ownerGUID)
+ calendar = yield home.calendarWithName(name)
+ yield calendar.uninviteUserFromShare(shareeGUID)
+ yield self.commit()
Deleted: CalendarServer/trunk/txdav/common/datastore/query/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/__init__.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 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.
-##
Copied: CalendarServer/trunk/txdav/common/datastore/query/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/__init__.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 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.
+##
Deleted: CalendarServer/trunk/txdav/common/datastore/query/expression.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/expression.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/expression.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,382 +0,0 @@
-##
-# Copyright (c) 2006-2013 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.
-##
-
-"""
-Query Expression Elements. These are used to build a 'generic' query
-expression tree that can then be used by different query language
-generators to produce the actual query syntax required (SQL, xpath
-etc).
-"""
-
-__all__ = [
- "allExpression",
- "notExpression",
- "andExpression",
- "orExpression",
- "timerangeExpression",
- "textcompareExpression",
- "containsExpression",
- "notcontainsExpression",
- "isExpression",
- "isnotExpression",
- "startswithExpression",
- "notstartswithExpression",
- "endswithExpression",
- "notendswithExpression",
- "inExpression",
- "notinExpression",
-]
-
-class baseExpression(object):
- """
- The base class for all types of expression.
- """
-
- def __init__(self):
- pass
-
-
- def multi(self):
- """
- Indicate whether this expression is composed of multiple sub-expressions.
-
- @return: C{True} if this expressions contains multiple sub-expressions,
- C{False} otherwise.
- """
-
- return False
-
-
- 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.
- """
-
- def __init__(self):
- pass
-
-
-
-class logicExpression(baseExpression):
- """
- An expression representing a logical operation (boolean).
- """
-
- def __init__(self, expressions):
- self.expressions = expressions
-
-
- def __str__(self):
- """
- Generate a suitable text descriptor of this expression.
-
- @return: a C{str} of the text for this expression.
- """
-
- result = ""
- for e in self.expressions:
- if len(result) != 0:
- result += " " + self.operator() + " "
- result += str(e)
- if len(result):
- result = "(" + result + ")"
- return result
-
-
- def multi(self):
- """
- Indicate whether this expression is composed of multiple expressions.
-
- @return: C{True} if this expressions contains multiple sub-expressions,
- C{False} otherwise.
- """
-
- 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.
- """
-
- def __init__(self, expression):
- super(notExpression, self).__init__([expression])
-
-
- def operator(self):
- return "NOT"
-
-
- def __str__(self):
- result = self.operator() + " " + str(self.expressions[0])
- return result
-
-
- def multi(self):
- """
- Indicate whether this expression is composed of multiple expressions.
-
- @return: C{True} if this expressions contains multiple sub-expressions,
- C{False} otherwise.
- """
-
- return False
-
-
-
-class andExpression(logicExpression):
- """
- Logical AND operation.
- """
-
- def __init__(self, expressions):
- super(andExpression, self).__init__(expressions)
-
-
- def operator(self):
- return "AND"
-
-
- def andWith(self, other):
- self.expressions = tuple(self.expressions) + (other._collapsedExpression(),)
- return self
-
-
-
-class orExpression(logicExpression):
- """
- Logical OR operation.
- """
-
- def __init__(self, expressions):
- super(orExpression, self).__init__(expressions)
-
-
- 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.
- """
-
- def __init__(self, start, end, startfloat, endfloat):
- self.start = start
- self.end = end
- self.startfloat = startfloat
- self.endfloat = endfloat
-
-
- def __str__(self):
- return "timerange(" + str(self.start) + ", " + str(self.end) + ")"
-
-
-
-class textcompareExpression(baseExpression):
- """
- Base class for text comparison expressions.
- """
-
- def __init__(self, field, text, caseless):
- self.field = field
- self.text = text
- self.caseless = caseless
-
-
- def __str__(self):
- return self.operator() + "(" + self.field + ", " + self.text + ", " + str(self.caseless) + ")"
-
-
-
-class containsExpression(textcompareExpression):
- """
- Text CONTAINS (sub-string match) expression.
- """
-
- def __init__(self, field, text, caseless):
- super(containsExpression, self).__init__(field, text, caseless)
-
-
- def operator(self):
- return "contains"
-
-
-
-class notcontainsExpression(textcompareExpression):
- """
- Text NOT CONTAINS (sub-string match) expression.
- """
-
- def __init__(self, field, text, caseless):
- super(notcontainsExpression, self).__init__(field, text, caseless)
-
-
- def operator(self):
- return "does not contain"
-
-
-
-class isExpression(textcompareExpression):
- """
- Text IS (exact string match) expression.
- """
-
- def __init__(self, field, text, caseless):
- super(isExpression, self).__init__(field, text, caseless)
-
-
- def operator(self):
- return "is"
-
-
-
-class isnotExpression(textcompareExpression):
- """
- Text IS NOT (exact string match) expression.
- """
-
- def __init__(self, field, text, caseless):
- super(isnotExpression, self).__init__(field, text, caseless)
-
-
- 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.
- """
-
- def __init__(self, field, text_list, caseless):
- super(inExpression, self).__init__(field, text_list, caseless)
-
-
- def operator(self):
- return "in"
-
-
- def __str__(self):
- return self.operator() + "(" + self.field + ", " + str(self.text) + ", " + str(self.caseless) + ")"
-
-
-
-class notinExpression(textcompareExpression):
- """
- Text NOT IN (exact string match to none of the supplied items) expression.
- """
-
- def __init__(self, field, text, caseless):
- super(notinExpression, self).__init__(field, text, caseless)
-
-
- def operator(self):
- return "not in"
-
-
- def __str__(self):
- return self.operator() + "(" + self.field + ", " + str(self.text) + ", " + str(self.caseless) + ")"
Copied: CalendarServer/trunk/txdav/common/datastore/query/expression.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/expression.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/expression.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/expression.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,382 @@
+##
+# Copyright (c) 2006-2013 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.
+##
+
+"""
+Query Expression Elements. These are used to build a 'generic' query
+expression tree that can then be used by different query language
+generators to produce the actual query syntax required (SQL, xpath
+etc).
+"""
+
+__all__ = [
+ "allExpression",
+ "notExpression",
+ "andExpression",
+ "orExpression",
+ "timerangeExpression",
+ "textcompareExpression",
+ "containsExpression",
+ "notcontainsExpression",
+ "isExpression",
+ "isnotExpression",
+ "startswithExpression",
+ "notstartswithExpression",
+ "endswithExpression",
+ "notendswithExpression",
+ "inExpression",
+ "notinExpression",
+]
+
+class baseExpression(object):
+ """
+ The base class for all types of expression.
+ """
+
+ def __init__(self):
+ pass
+
+
+ def multi(self):
+ """
+ Indicate whether this expression is composed of multiple sub-expressions.
+
+ @return: C{True} if this expressions contains multiple sub-expressions,
+ C{False} otherwise.
+ """
+
+ return False
+
+
+ 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.
+ """
+
+ def __init__(self):
+ pass
+
+
+
+class logicExpression(baseExpression):
+ """
+ An expression representing a logical operation (boolean).
+ """
+
+ def __init__(self, expressions):
+ self.expressions = expressions
+
+
+ def __str__(self):
+ """
+ Generate a suitable text descriptor of this expression.
+
+ @return: a C{str} of the text for this expression.
+ """
+
+ result = ""
+ for e in self.expressions:
+ if len(result) != 0:
+ result += " " + self.operator() + " "
+ result += str(e)
+ if len(result):
+ result = "(" + result + ")"
+ return result
+
+
+ def multi(self):
+ """
+ Indicate whether this expression is composed of multiple expressions.
+
+ @return: C{True} if this expressions contains multiple sub-expressions,
+ C{False} otherwise.
+ """
+
+ 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.
+ """
+
+ def __init__(self, expression):
+ super(notExpression, self).__init__([expression])
+
+
+ def operator(self):
+ return "NOT"
+
+
+ def __str__(self):
+ result = self.operator() + " " + str(self.expressions[0])
+ return result
+
+
+ def multi(self):
+ """
+ Indicate whether this expression is composed of multiple expressions.
+
+ @return: C{True} if this expressions contains multiple sub-expressions,
+ C{False} otherwise.
+ """
+
+ return False
+
+
+
+class andExpression(logicExpression):
+ """
+ Logical AND operation.
+ """
+
+ def __init__(self, expressions):
+ super(andExpression, self).__init__(expressions)
+
+
+ def operator(self):
+ return "AND"
+
+
+ def andWith(self, other):
+ self.expressions = tuple(self.expressions) + (other._collapsedExpression(),)
+ return self
+
+
+
+class orExpression(logicExpression):
+ """
+ Logical OR operation.
+ """
+
+ def __init__(self, expressions):
+ super(orExpression, self).__init__(expressions)
+
+
+ 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.
+ """
+
+ def __init__(self, start, end, startfloat, endfloat):
+ self.start = start
+ self.end = end
+ self.startfloat = startfloat
+ self.endfloat = endfloat
+
+
+ def __str__(self):
+ return "timerange(" + str(self.start) + ", " + str(self.end) + ")"
+
+
+
+class textcompareExpression(baseExpression):
+ """
+ Base class for text comparison expressions.
+ """
+
+ def __init__(self, field, text, caseless):
+ self.field = field
+ self.text = text
+ self.caseless = caseless
+
+
+ def __str__(self):
+ return self.operator() + "(" + self.field + ", " + self.text + ", " + str(self.caseless) + ")"
+
+
+
+class containsExpression(textcompareExpression):
+ """
+ Text CONTAINS (sub-string match) expression.
+ """
+
+ def __init__(self, field, text, caseless):
+ super(containsExpression, self).__init__(field, text, caseless)
+
+
+ def operator(self):
+ return "contains"
+
+
+
+class notcontainsExpression(textcompareExpression):
+ """
+ Text NOT CONTAINS (sub-string match) expression.
+ """
+
+ def __init__(self, field, text, caseless):
+ super(notcontainsExpression, self).__init__(field, text, caseless)
+
+
+ def operator(self):
+ return "does not contain"
+
+
+
+class isExpression(textcompareExpression):
+ """
+ Text IS (exact string match) expression.
+ """
+
+ def __init__(self, field, text, caseless):
+ super(isExpression, self).__init__(field, text, caseless)
+
+
+ def operator(self):
+ return "is"
+
+
+
+class isnotExpression(textcompareExpression):
+ """
+ Text IS NOT (exact string match) expression.
+ """
+
+ def __init__(self, field, text, caseless):
+ super(isnotExpression, self).__init__(field, text, caseless)
+
+
+ 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.
+ """
+
+ def __init__(self, field, text_list, caseless):
+ super(inExpression, self).__init__(field, text_list, caseless)
+
+
+ def operator(self):
+ return "in"
+
+
+ def __str__(self):
+ return self.operator() + "(" + self.field + ", " + str(self.text) + ", " + str(self.caseless) + ")"
+
+
+
+class notinExpression(textcompareExpression):
+ """
+ Text NOT IN (exact string match to none of the supplied items) expression.
+ """
+
+ def __init__(self, field, text, caseless):
+ super(notinExpression, self).__init__(field, text, caseless)
+
+
+ def operator(self):
+ return "not in"
+
+
+ def __str__(self):
+ return self.operator() + "(" + self.field + ", " + str(self.text) + ", " + str(self.caseless) + ")"
Deleted: CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/filegenerator.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,322 +0,0 @@
-##
-# Copyright (c) 2006-2013 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 txdav.common.datastore.query import expression
-
-"""
-SQLLite statement generator from query expressions.
-"""
-
-__all__ = [
- "sqllitegenerator",
-]
-
-import cStringIO as StringIO
-
-class sqllitegenerator(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 = " != "
- STARTSWITHOP = " GLOB "
- NOTSTARTSWITHOP = " NOT GLOB "
- ENDSWITHOP = " GLOB "
- NOTENDSWITHOP = " NOT GLOB "
- INOP = " IN "
- NOTINOP = " NOT IN "
-
- FIELDS = {
- "TYPE": "RESOURCE.TYPE",
- "UID": "RESOURCE.UID",
- }
-
- TIMESPANTEST = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s AND TIMESPAN.END > %s))"
- TIMESPANTEST_NOEND = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.END > %s))"
- TIMESPANTEST_NOSTART = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s))"
- TIMESPANTEST_TAIL_PIECE = " AND TIMESPAN.RESOURCEID == RESOURCE.RESOURCEID"
- TIMESPANTEST_JOIN_ON_PIECE = "TIMESPAN.INSTANCEID == TRANSPARENCY.INSTANCEID AND TRANSPARENCY.PERUSERID == %s"
-
- def __init__(self, expr, calendarid, userid, freebusy=False):
- """
-
- @param expr: the query expression object model
- @type expr: L{Filter}
- @param calendarid: resource ID - not used for file-based per-calendar indexes
- @type calendarid: C{int}
- @param userid: user for whom query is being done - query will be scoped to that user's privileges and their transparency
- @type userid: C{str}
- @param freebusy: whether or not a freebusy query is being done - if it is, additional time range and transparency information is returned
- @type freebusy: C{bool}
- """
- self.expression = expr
- self.calendarid = calendarid
- self.userid = userid if userid else ""
- self.freebusy = freebusy
- self.usedtimespan = False
-
-
- def generate(self):
- """
- Generate the actual SQL 'where ...' expression from the passed in expression tree.
-
- @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
- and the C{list} is the list of argument substitutions to use with the SQL API execute method.
- """
-
- # Init state
- self.sout = StringIO.StringIO()
- self.arguments = []
- self.substitutions = []
- self.usedtimespan = False
-
- # Generate ' where ...' partial statement
- self.generateExpression(self.expression)
-
- # Prefix with ' from ...' partial statement
- select = self.FROM + self.RESOURCEDB
- if self.usedtimespan:
-
- # Free busy needs transparency join
- if self.freebusy:
- self.frontArgument(self.userid)
- select += ", %s LEFT OUTER JOIN %s ON (%s)" % (
- self.TIMESPANDB,
- self.TRANSPARENCYDB,
- self.TIMESPANTEST_JOIN_ON_PIECE
- )
- else:
- select += ", %s" % (
- self.TIMESPANDB,
- )
- select += self.WHERE
- if self.usedtimespan:
- select += "("
- select += self.sout.getvalue()
- if self.usedtimespan:
- if self.calendarid:
- self.setArgument(self.calendarid)
- select += ")%s" % (self.TIMESPANTEST_TAIL_PIECE,)
-
- select = select % tuple(self.substitutions)
-
- return select, self.arguments
-
-
- def generateExpression(self, expr):
- """
- Generate an expression and all it's subexpressions.
-
- @param expr: the L{baseExpression} derived class to write out.
- @return: C{True} if the TIMESPAN table is used, C{False} otherwise.
- """
-
- # Generate based on each type of expression we might encounter
-
- # ALL
- if isinstance(expr, expression.allExpression):
- # Wipe out the ' where ...' clause so everything is matched
- self.sout.truncate(0)
- self.arguments = []
- self.substitutions = []
- self.usedtimespan = False
-
- # NOT
- elif isinstance(expr, expression.notExpression):
- self.sout.write(self.NOTOP)
- self.generateSubExpression(expr.expressions[0])
-
- # AND
- elif isinstance(expr, expression.andExpression):
- first = True
- for e in expr.expressions:
- if first:
- first = False
- else:
- self.sout.write(self.ANDOP)
- self.generateSubExpression(e)
-
- # OR
- elif isinstance(expr, expression.orExpression):
- first = True
- for e in expr.expressions:
- if first:
- first = False
- else:
- self.sout.write(self.OROP)
- self.generateSubExpression(e)
-
- # time-range
- elif isinstance(expr, expression.timerangeExpression):
- if expr.start and expr.end:
- self.setArgument(expr.end)
- self.setArgument(expr.start)
- self.setArgument(expr.endfloat)
- self.setArgument(expr.startfloat)
- test = self.TIMESPANTEST
- elif expr.start and expr.end is None:
- self.setArgument(expr.start)
- self.setArgument(expr.startfloat)
- test = self.TIMESPANTEST_NOEND
- elif not expr.start and expr.end:
- self.setArgument(expr.end)
- self.setArgument(expr.endfloat)
- test = self.TIMESPANTEST_NOSTART
-
- self.sout.write(test)
- self.usedtimespan = True
-
- # CONTAINS
- elif isinstance(expr, expression.containsExpression):
- self.sout.write(expr.field)
- self.sout.write(self.CONTAINSOP)
- self.addArgument(self.containsArgument(expr.text))
-
- # NOT CONTAINS
- elif isinstance(expr, expression.notcontainsExpression):
- self.sout.write(expr.field)
- self.sout.write(self.NOTCONTAINSOP)
- self.addArgument(self.containsArgument(expr.text))
-
- # IS
- elif isinstance(expr, expression.isExpression):
- self.sout.write(expr.field)
- self.sout.write(self.ISOP)
- self.addArgument(expr.text)
-
- # IS NOT
- elif isinstance(expr, expression.isnotExpression):
- self.sout.write(expr.field)
- 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)
- self.sout.write(self.INOP)
- self.sout.write("(")
- for count, item in enumerate(expr.text):
- if count != 0:
- self.sout.write(", ")
- self.addArgument(item)
- self.sout.write(")")
-
- # NOT IN
- elif isinstance(expr, expression.notinExpression):
- self.sout.write(expr.field)
- self.sout.write(self.NOTINOP)
- self.sout.write("(")
- for count, item in enumerate(expr.text):
- if count != 0:
- self.sout.write(", ")
- self.addArgument(item)
- self.sout.write(")")
-
-
- def generateSubExpression(self, expression):
- """
- Generate an SQL expression possibly in parenthesis if its a compound expression.
-
- @param expression: the L{baseExpression} to write out.
- @return: C{True} if the TIMESPAN table is used, C{False} otherwise.
- """
-
- if expression.multi():
- self.sout.write("(")
- self.generateExpression(expression)
- if expression.multi():
- self.sout.write(")")
-
-
- def addArgument(self, arg):
- """
-
- @param arg: the C{str} of the argument to add
- """
-
- # Append argument to the list and add the appropriate substitution string to the output stream.
- self.arguments.append(arg)
- self.substitutions.append(":" + str(len(self.arguments)))
- self.sout.write("%s")
-
-
- def setArgument(self, arg):
- """
-
- @param arg: the C{str} of the argument to add
- @return: C{str} for argument substitution text
- """
-
- # Append argument to the list and add the appropriate substitution string to the output stream.
- self.arguments.append(arg)
- self.substitutions.append(":" + str(len(self.arguments)))
-
-
- def frontArgument(self, arg):
- """
-
- @param arg: the C{str} of the argument to add
- @return: C{str} for argument substitution text
- """
-
- # Append argument to the list and add the appropriate substitution string to the output stream.
- self.arguments.insert(0, arg)
- self.substitutions.append(":" + str(len(self.arguments)))
-
-
- def containsArgument(self, arg):
- return "*%s*" % (arg,)
-
-
- def startswithArgument(self, arg):
- return "%s*" % (arg,)
-
-
- def endswithArgument(self, arg):
- return "*%s" % (arg,)
Copied: CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/filegenerator.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,322 @@
+##
+# Copyright (c) 2006-2013 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 txdav.common.datastore.query import expression
+
+"""
+SQLLite statement generator from query expressions.
+"""
+
+__all__ = [
+ "sqllitegenerator",
+]
+
+import cStringIO as StringIO
+
+class sqllitegenerator(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 = " != "
+ STARTSWITHOP = " GLOB "
+ NOTSTARTSWITHOP = " NOT GLOB "
+ ENDSWITHOP = " GLOB "
+ NOTENDSWITHOP = " NOT GLOB "
+ INOP = " IN "
+ NOTINOP = " NOT IN "
+
+ FIELDS = {
+ "TYPE": "RESOURCE.TYPE",
+ "UID": "RESOURCE.UID",
+ }
+
+ TIMESPANTEST = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s AND TIMESPAN.END > %s))"
+ TIMESPANTEST_NOEND = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.END > %s))"
+ TIMESPANTEST_NOSTART = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s))"
+ TIMESPANTEST_TAIL_PIECE = " AND TIMESPAN.RESOURCEID == RESOURCE.RESOURCEID"
+ TIMESPANTEST_JOIN_ON_PIECE = "TIMESPAN.INSTANCEID == TRANSPARENCY.INSTANCEID AND TRANSPARENCY.PERUSERID == %s"
+
+ def __init__(self, expr, calendarid, userid, freebusy=False):
+ """
+
+ @param expr: the query expression object model
+ @type expr: L{Filter}
+ @param calendarid: resource ID - not used for file-based per-calendar indexes
+ @type calendarid: C{int}
+ @param userid: user for whom query is being done - query will be scoped to that user's privileges and their transparency
+ @type userid: C{str}
+ @param freebusy: whether or not a freebusy query is being done - if it is, additional time range and transparency information is returned
+ @type freebusy: C{bool}
+ """
+ self.expression = expr
+ self.calendarid = calendarid
+ self.userid = userid if userid else ""
+ self.freebusy = freebusy
+ self.usedtimespan = False
+
+
+ def generate(self):
+ """
+ Generate the actual SQL 'where ...' expression from the passed in expression tree.
+
+ @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+ and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+ """
+
+ # Init state
+ self.sout = StringIO.StringIO()
+ self.arguments = []
+ self.substitutions = []
+ self.usedtimespan = False
+
+ # Generate ' where ...' partial statement
+ self.generateExpression(self.expression)
+
+ # Prefix with ' from ...' partial statement
+ select = self.FROM + self.RESOURCEDB
+ if self.usedtimespan:
+
+ # Free busy needs transparency join
+ if self.freebusy:
+ self.frontArgument(self.userid)
+ select += ", %s LEFT OUTER JOIN %s ON (%s)" % (
+ self.TIMESPANDB,
+ self.TRANSPARENCYDB,
+ self.TIMESPANTEST_JOIN_ON_PIECE
+ )
+ else:
+ select += ", %s" % (
+ self.TIMESPANDB,
+ )
+ select += self.WHERE
+ if self.usedtimespan:
+ select += "("
+ select += self.sout.getvalue()
+ if self.usedtimespan:
+ if self.calendarid:
+ self.setArgument(self.calendarid)
+ select += ")%s" % (self.TIMESPANTEST_TAIL_PIECE,)
+
+ select = select % tuple(self.substitutions)
+
+ return select, self.arguments
+
+
+ def generateExpression(self, expr):
+ """
+ Generate an expression and all it's subexpressions.
+
+ @param expr: the L{baseExpression} derived class to write out.
+ @return: C{True} if the TIMESPAN table is used, C{False} otherwise.
+ """
+
+ # Generate based on each type of expression we might encounter
+
+ # ALL
+ if isinstance(expr, expression.allExpression):
+ # Wipe out the ' where ...' clause so everything is matched
+ self.sout.truncate(0)
+ self.arguments = []
+ self.substitutions = []
+ self.usedtimespan = False
+
+ # NOT
+ elif isinstance(expr, expression.notExpression):
+ self.sout.write(self.NOTOP)
+ self.generateSubExpression(expr.expressions[0])
+
+ # AND
+ elif isinstance(expr, expression.andExpression):
+ first = True
+ for e in expr.expressions:
+ if first:
+ first = False
+ else:
+ self.sout.write(self.ANDOP)
+ self.generateSubExpression(e)
+
+ # OR
+ elif isinstance(expr, expression.orExpression):
+ first = True
+ for e in expr.expressions:
+ if first:
+ first = False
+ else:
+ self.sout.write(self.OROP)
+ self.generateSubExpression(e)
+
+ # time-range
+ elif isinstance(expr, expression.timerangeExpression):
+ if expr.start and expr.end:
+ self.setArgument(expr.end)
+ self.setArgument(expr.start)
+ self.setArgument(expr.endfloat)
+ self.setArgument(expr.startfloat)
+ test = self.TIMESPANTEST
+ elif expr.start and expr.end is None:
+ self.setArgument(expr.start)
+ self.setArgument(expr.startfloat)
+ test = self.TIMESPANTEST_NOEND
+ elif not expr.start and expr.end:
+ self.setArgument(expr.end)
+ self.setArgument(expr.endfloat)
+ test = self.TIMESPANTEST_NOSTART
+
+ self.sout.write(test)
+ self.usedtimespan = True
+
+ # CONTAINS
+ elif isinstance(expr, expression.containsExpression):
+ self.sout.write(expr.field)
+ self.sout.write(self.CONTAINSOP)
+ self.addArgument(self.containsArgument(expr.text))
+
+ # NOT CONTAINS
+ elif isinstance(expr, expression.notcontainsExpression):
+ self.sout.write(expr.field)
+ self.sout.write(self.NOTCONTAINSOP)
+ self.addArgument(self.containsArgument(expr.text))
+
+ # IS
+ elif isinstance(expr, expression.isExpression):
+ self.sout.write(expr.field)
+ self.sout.write(self.ISOP)
+ self.addArgument(expr.text)
+
+ # IS NOT
+ elif isinstance(expr, expression.isnotExpression):
+ self.sout.write(expr.field)
+ 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)
+ self.sout.write(self.INOP)
+ self.sout.write("(")
+ for count, item in enumerate(expr.text):
+ if count != 0:
+ self.sout.write(", ")
+ self.addArgument(item)
+ self.sout.write(")")
+
+ # NOT IN
+ elif isinstance(expr, expression.notinExpression):
+ self.sout.write(expr.field)
+ self.sout.write(self.NOTINOP)
+ self.sout.write("(")
+ for count, item in enumerate(expr.text):
+ if count != 0:
+ self.sout.write(", ")
+ self.addArgument(item)
+ self.sout.write(")")
+
+
+ def generateSubExpression(self, expression):
+ """
+ Generate an SQL expression possibly in parenthesis if its a compound expression.
+
+ @param expression: the L{baseExpression} to write out.
+ @return: C{True} if the TIMESPAN table is used, C{False} otherwise.
+ """
+
+ if expression.multi():
+ self.sout.write("(")
+ self.generateExpression(expression)
+ if expression.multi():
+ self.sout.write(")")
+
+
+ def addArgument(self, arg):
+ """
+
+ @param arg: the C{str} of the argument to add
+ """
+
+ # Append argument to the list and add the appropriate substitution string to the output stream.
+ self.arguments.append(arg)
+ self.substitutions.append(":" + str(len(self.arguments)))
+ self.sout.write("%s")
+
+
+ def setArgument(self, arg):
+ """
+
+ @param arg: the C{str} of the argument to add
+ @return: C{str} for argument substitution text
+ """
+
+ # Append argument to the list and add the appropriate substitution string to the output stream.
+ self.arguments.append(arg)
+ self.substitutions.append(":" + str(len(self.arguments)))
+
+
+ def frontArgument(self, arg):
+ """
+
+ @param arg: the C{str} of the argument to add
+ @return: C{str} for argument substitution text
+ """
+
+ # Append argument to the list and add the appropriate substitution string to the output stream.
+ self.arguments.insert(0, arg)
+ self.substitutions.append(":" + str(len(self.arguments)))
+
+
+ def containsArgument(self, arg):
+ return "*%s*" % (arg,)
+
+
+ def startswithArgument(self, arg):
+ return "%s*" % (arg,)
+
+
+ def endswithArgument(self, arg):
+ return "*%s" % (arg,)
Deleted: CalendarServer/trunk/txdav/common/datastore/query/generator.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/generator.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/generator.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,164 +0,0 @@
-##
-# Copyright (c) 2006-2013 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 twext.enterprise.dal.syntax import Select, Parameter, Not
-from txdav.common.datastore.query import expression
-
-"""
-SQL statement generator from query expressions.
-"""
-
-__all__ = [
- "SQLQueryGenerator",
-]
-
-class SQLQueryGenerator(object):
-
- def __init__(self, expr, collection, whereid):
- """
-
- @param expr: the query expression object model
- @type expr: L{expression}
- @param collection: the resource targeted by the query
- @type collection: L{CommonHomeChild}
- """
- self.expression = expr
- self.collection = collection
- self.whereid = whereid
-
-
- def generate(self):
- """
- Generate the actual SQL statement from the passed in expression tree.
-
- @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
- and the C{list} is the list of argument substitutions to use with the SQL API execute method.
- """
-
- # Init state
- self.arguments = {}
- self.argcount = 0
- obj = self.collection._objectSchema
-
- columns = [obj.RESOURCE_NAME, obj.UID]
-
- # For SQL data DB we need to restrict the query to just the targeted collection resource-id if provided
- if self.whereid:
- # AND the whole thing
- test = expression.isExpression(obj.PARENT_RESOURCE_ID, self.whereid, True)
- self.expression = test if isinstance(self.expression, expression.allExpression) else test.andWith(self.expression)
-
- # Generate ' where ...' partial statement
- where = self.generateExpression(self.expression)
-
- select = Select(
- columns,
- From=obj,
- Where=where,
- Distinct=True,
- )
-
- return select, self.arguments
-
-
- def generateExpression(self, expr):
- """
- Generate an expression and all it's subexpressions.
-
- @param expr: the L{baseExpression} derived class to write out.
- """
-
- # Generate based on each type of expression we might encounter
- partial = None
-
- # ALL
- if isinstance(expr, expression.allExpression):
- # Everything is matched
- partial = None
- self.arguments = {}
-
- # NOT
- elif isinstance(expr, expression.notExpression):
- partial = Not(self.generateExpression(expr.expressions[0]))
-
- # AND
- elif isinstance(expr, expression.andExpression):
- for e in expr.expressions:
- next = self.generateExpression(e)
- partial = partial.And(next) if partial is not None else next
-
- # OR
- elif isinstance(expr, expression.orExpression):
- for e in expr.expressions:
- next = self.generateExpression(e)
- partial = partial.Or(next) if partial is not None else next
-
- # CONTAINS
- elif isinstance(expr, expression.containsExpression):
- partial = expr.field.Contains(expr.text)
-
- # NOT CONTAINS
- elif isinstance(expr, expression.notcontainsExpression):
- partial = expr.field.NotContains(expr.text)
-
- # IS
- elif isinstance(expr, expression.isExpression):
- partial = expr.field == expr.text
-
- # IS NOT
- elif isinstance(expr, expression.isnotExpression):
- partial = expr.field != expr.text
-
- # STARTSWITH
- elif isinstance(expr, expression.startswithExpression):
- partial = expr.field.StartsWith(expr.text)
-
- # NOT STARTSWITH
- elif isinstance(expr, expression.notstartswithExpression):
- partial = expr.field.NotStartsWith(expr.text)
-
- # ENDSWITH
- elif isinstance(expr, expression.endswithExpression):
- partial = expr.field.EndsWith(expr.text)
-
- # NOT ENDSWITH
- elif isinstance(expr, expression.notendswithExpression):
- partial = expr.field.NotEndsWith(expr.text)
-
- # IN
- elif isinstance(expr, expression.inExpression):
- argname = self.addArgument(expr.text)
- partial = expr.field.In(Parameter(argname, len(expr.text)))
-
- # NOT IN
- elif isinstance(expr, expression.notinExpression):
- argname = self.addArgument(expr.text)
- partial = expr.field.NotIn(Parameter(argname, len(expr.text)))
-
- return partial
-
-
- def addArgument(self, arg):
- """
-
- @param arg: the C{str} of the argument to add
- """
-
- # Append argument to the list and add the appropriate substitution string to the output stream.
- self.argcount += 1
- argname = "arg{}".format(self.argcount)
- self.arguments[argname] = arg
- return argname
Copied: CalendarServer/trunk/txdav/common/datastore/query/generator.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/generator.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/generator.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/generator.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,164 @@
+##
+# Copyright (c) 2006-2013 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 twext.enterprise.dal.syntax import Select, Parameter, Not
+from txdav.common.datastore.query import expression
+
+"""
+SQL statement generator from query expressions.
+"""
+
+__all__ = [
+ "SQLQueryGenerator",
+]
+
+class SQLQueryGenerator(object):
+
+ def __init__(self, expr, collection, whereid):
+ """
+
+ @param expr: the query expression object model
+ @type expr: L{expression}
+ @param collection: the resource targeted by the query
+ @type collection: L{CommonHomeChild}
+ """
+ self.expression = expr
+ self.collection = collection
+ self.whereid = whereid
+
+
+ def generate(self):
+ """
+ Generate the actual SQL statement from the passed in expression tree.
+
+ @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+ and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+ """
+
+ # Init state
+ self.arguments = {}
+ self.argcount = 0
+ obj = self.collection._objectSchema
+
+ columns = [obj.RESOURCE_NAME, obj.UID]
+
+ # For SQL data DB we need to restrict the query to just the targeted collection resource-id if provided
+ if self.whereid:
+ # AND the whole thing
+ test = expression.isExpression(obj.PARENT_RESOURCE_ID, self.whereid, True)
+ self.expression = test if isinstance(self.expression, expression.allExpression) else test.andWith(self.expression)
+
+ # Generate ' where ...' partial statement
+ where = self.generateExpression(self.expression)
+
+ select = Select(
+ columns,
+ From=obj,
+ Where=where,
+ Distinct=True,
+ )
+
+ return select, self.arguments
+
+
+ def generateExpression(self, expr):
+ """
+ Generate an expression and all it's subexpressions.
+
+ @param expr: the L{baseExpression} derived class to write out.
+ """
+
+ # Generate based on each type of expression we might encounter
+ partial = None
+
+ # ALL
+ if isinstance(expr, expression.allExpression):
+ # Everything is matched
+ partial = None
+ self.arguments = {}
+
+ # NOT
+ elif isinstance(expr, expression.notExpression):
+ partial = Not(self.generateExpression(expr.expressions[0]))
+
+ # AND
+ elif isinstance(expr, expression.andExpression):
+ for e in expr.expressions:
+ next = self.generateExpression(e)
+ partial = partial.And(next) if partial is not None else next
+
+ # OR
+ elif isinstance(expr, expression.orExpression):
+ for e in expr.expressions:
+ next = self.generateExpression(e)
+ partial = partial.Or(next) if partial is not None else next
+
+ # CONTAINS
+ elif isinstance(expr, expression.containsExpression):
+ partial = expr.field.Contains(expr.text)
+
+ # NOT CONTAINS
+ elif isinstance(expr, expression.notcontainsExpression):
+ partial = expr.field.NotContains(expr.text)
+
+ # IS
+ elif isinstance(expr, expression.isExpression):
+ partial = expr.field == expr.text
+
+ # IS NOT
+ elif isinstance(expr, expression.isnotExpression):
+ partial = expr.field != expr.text
+
+ # STARTSWITH
+ elif isinstance(expr, expression.startswithExpression):
+ partial = expr.field.StartsWith(expr.text)
+
+ # NOT STARTSWITH
+ elif isinstance(expr, expression.notstartswithExpression):
+ partial = expr.field.NotStartsWith(expr.text)
+
+ # ENDSWITH
+ elif isinstance(expr, expression.endswithExpression):
+ partial = expr.field.EndsWith(expr.text)
+
+ # NOT ENDSWITH
+ elif isinstance(expr, expression.notendswithExpression):
+ partial = expr.field.NotEndsWith(expr.text)
+
+ # IN
+ elif isinstance(expr, expression.inExpression):
+ argname = self.addArgument(expr.text)
+ partial = expr.field.In(Parameter(argname, len(expr.text)))
+
+ # NOT IN
+ elif isinstance(expr, expression.notinExpression):
+ argname = self.addArgument(expr.text)
+ partial = expr.field.NotIn(Parameter(argname, len(expr.text)))
+
+ return partial
+
+
+ def addArgument(self, arg):
+ """
+
+ @param arg: the C{str} of the argument to add
+ """
+
+ # Append argument to the list and add the appropriate substitution string to the output stream.
+ self.argcount += 1
+ argname = "arg{}".format(self.argcount)
+ self.arguments[argname] = arg
+ return argname
Deleted: CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/__init__.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 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.
-##
Copied: CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 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.
+##
Deleted: CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/test_expression.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,167 +0,0 @@
-##
-# Copyright (c) 2011-2013 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 txdav.common.datastore.query import expression
-from twisted.trial.unittest import TestCase
-
-class Tests(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,))
Copied: CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/test_expression.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,167 @@
+##
+# Copyright (c) 2011-2013 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 txdav.common.datastore.query import expression
+from twisted.trial.unittest import TestCase
+
+class Tests(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,))
Deleted: CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/test_generator.py 2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,127 +0,0 @@
-##
-# Copyright (c) 2012-2013 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 twext.enterprise.dal.syntax import SQLFragment, Parameter
-from txdav.common.datastore.query.generator import SQLQueryGenerator
-from txdav.common.datastore.query import expression
-
-"""
-Tests for L{txdav.common.datastore.sql}.
-"""
-
-from twisted.trial.unittest import TestCase
-
-from txdav.common.datastore.sql_tables import schema
-
-class SQLQueryGeneratorTests(TestCase):
- """
- Tests for shared functionality in L{txdav.common.datastore.sql}.
- """
-
- class FakeHomeChild(object):
- _objectSchema = schema.CALENDAR_OBJECT
-
- def id(self):
- return 1234
-
-
- def test_all_query(self):
-
- expr = expression.allExpression()
- resource = self.FakeHomeChild()
- select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
- self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ?", [1234]))
- self.assertEqual(args, {})
-
-
- def test_uid_query(self):
-
- resource = self.FakeHomeChild()
- obj = resource._objectSchema
- expr = expression.isExpression(obj.UID, 5678, False)
- select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
- self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_UID = ?", [1234, 5678]))
- self.assertEqual(args, {})
-
-
- def test_or_query(self):
-
- resource = self.FakeHomeChild()
- obj = resource._objectSchema
- expr = expression.orExpression((
- expression.isExpression(obj.UID, 5678, False),
- expression.isnotExpression(obj.RESOURCE_NAME, "foobar.ics", False),
- ))
- select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
- self.assertEqual(
- select.toSQL(),
- SQLFragment(
- "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and (ICALENDAR_UID = ? or RESOURCE_NAME != ?)",
- [1234, 5678, "foobar.ics"]
- )
- )
- self.assertEqual(args, {})
-
-
- def test_and_query(self):
-
- resource = self.FakeHomeChild()
- obj = resource._objectSchema
- expr = expression.andExpression((
- expression.isExpression(obj.UID, 5678, False),
- expression.isnotExpression(obj.RESOURCE_NAME, "foobar.ics", False),
- ))
- select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
- self.assertEqual(
- select.toSQL(),
- SQLFragment(
- "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_UID = ? and RESOURCE_NAME != ?",
- [1234, 5678, "foobar.ics"]
- )
- )
- self.assertEqual(args, {})
-
-
- def test_not_query(self):
-
- resource = self.FakeHomeChild()
- obj = resource._objectSchema
- expr = expression.notExpression(expression.isExpression(obj.UID, 5678, False))
- select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
- self.assertEqual(
- select.toSQL(),
- SQLFragment(
- "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and not ICALENDAR_UID = ?",
- [1234, 5678]
- )
- )
- self.assertEqual(args, {})
-
-
- def test_in_query(self):
-
- resource = self.FakeHomeChild()
- obj = resource._objectSchema
- expr = expression.inExpression(obj.RESOURCE_NAME, ["1.ics", "2.ics", "3.ics"], False)
- select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
- self.assertEqual(
- select.toSQL(),
- SQLFragment(
- "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and RESOURCE_NAME in (?, ?, ?)",
- [1234, Parameter('arg1', 3)]
- )
- )
- self.assertEqual(args, {"arg1": ["1.ics", "2.ics", "3.ics"]})
Copied: CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/test_generator.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,127 @@
+##
+# Copyright (c) 2012-2013 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 twext.enterprise.dal.syntax import SQLFragment, Parameter
+from txdav.common.datastore.query.generator import SQLQueryGenerator
+from txdav.common.datastore.query import expression
+
+"""
+Tests for L{txdav.common.datastore.sql}.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from txdav.common.datastore.sql_tables import schema
+
+class SQLQueryGeneratorTests(TestCase):
+ """
+ Tests for shared functionality in L{txdav.common.datastore.sql}.
+ """
+
+ class FakeHomeChild(object):
+ _objectSchema = schema.CALENDAR_OBJECT
+
+ def id(self):
+ return 1234
+
+
+ def test_all_query(self):
+
+ expr = expression.allExpression()
+ resource = self.FakeHomeChild()
+ select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+ self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ?", [1234]))
+ self.assertEqual(args, {})
+
+
+ def test_uid_query(self):
+
+ resource = self.FakeHomeChild()
+ obj = resource._objectSchema
+ expr = expression.isExpression(obj.UID, 5678, False)
+ select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+ self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_UID = ?", [1234, 5678]))
+ self.assertEqual(args, {})
+
+
+ def test_or_query(self):
+
+ resource = self.FakeHomeChild()
+ obj = resource._objectSchema
+ expr = expression.orExpression((
+ expression.isExpression(obj.UID, 5678, False),
+ expression.isnotExpression(obj.RESOURCE_NAME, "foobar.ics", False),
+ ))
+ select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+ self.assertEqual(
+ select.toSQL(),
+ SQLFragment(
+ "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and (ICALENDAR_UID = ? or RESOURCE_NAME != ?)",
+ [1234, 5678, "foobar.ics"]
+ )
+ )
+ self.assertEqual(args, {})
+
+
+ def test_and_query(self):
+
+ resource = self.FakeHomeChild()
+ obj = resource._objectSchema
+ expr = expression.andExpression((
+ expression.isExpression(obj.UID, 5678, False),
+ expression.isnotExpression(obj.RESOURCE_NAME, "foobar.ics", False),
+ ))
+ select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+ self.assertEqual(
+ select.toSQL(),
+ SQLFragment(
+ "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_UID = ? and RESOURCE_NAME != ?",
+ [1234, 5678, "foobar.ics"]
+ )
+ )
+ self.assertEqual(args, {})
+
+
+ def test_not_query(self):
+
+ resource = self.FakeHomeChild()
+ obj = resource._objectSchema
+ expr = expression.notExpression(expression.isExpression(obj.UID, 5678, False))
+ select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+ self.assertEqual(
+ select.toSQL(),
+ SQLFragment(
+ "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and not ICALENDAR_UID = ?",
+ [1234, 5678]
+ )
+ )
+ self.assertEqual(args, {})
+
+
+ def test_in_query(self):
+
+ resource = self.FakeHomeChild()
+ obj = resource._objectSchema
+ expr = expression.inExpression(obj.RESOURCE_NAME, ["1.ics", "2.ics", "3.ics"], False)
+ select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+ self.assertEqual(
+ select.toSQL(),
+ SQLFragment(
+ "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and RESOURCE_NAME in (?, ?, ?)",
+ [1234, Parameter('arg1', 3)]
+ )
+ )
+ self.assertEqual(args, {"arg1": ["1.ics", "2.ics", "3.ics"]})
Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -14,9 +14,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
-from collections import namedtuple
-from txdav.xml import element
-from txdav.base.propertystore.base import PropertyName
"""
SQL data store.
@@ -57,32 +54,40 @@
from txdav.base.datastore.util import QueryCacher
from txdav.base.datastore.util import normalizeUUIDOrNot
+from txdav.base.propertystore.base import PropertyName
from txdav.base.propertystore.none import PropertyStore as NonePropertyStore
from txdav.base.propertystore.sql import PropertyStore
from txdav.caldav.icalendarstore import ICalendarTransaction, ICalendarStore
from txdav.carddav.iaddressbookstore import IAddressBookTransaction
from txdav.common.datastore.common import HomeChildBase
+from txdav.common.datastore.podding.conduit import PoddingConduit
from txdav.common.datastore.sql_tables import _BIND_MODE_OWN, \
_BIND_STATUS_ACCEPTED, _BIND_STATUS_DECLINED, _BIND_STATUS_INVALID, \
_BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_DELETED, \
- _BIND_MODE_INDIRECT
+ _BIND_MODE_INDIRECT, _HOME_STATUS_NORMAL, _HOME_STATUS_EXTERNAL
from txdav.common.datastore.sql_tables import schema, splitSQLString
-from txdav.common.icommondatastore import ConcurrentModification
+from txdav.common.icommondatastore import ConcurrentModification, \
+ RecordNotAllowedError, ExternalShareFailed, ShareNotAllowed, \
+ IndexedSearchException
from txdav.common.icommondatastore import HomeChildNameNotAllowedError, \
HomeChildNameAlreadyExistsError, NoSuchHomeChildError, \
ObjectResourceNameNotAllowedError, ObjectResourceNameAlreadyExistsError, \
NoSuchObjectResourceError, AllRetriesFailed, InvalidSubscriptionValues, \
InvalidIMIPTokenValues, TooManyObjectResourcesError, \
SyncTokenValidException
-from txdav.common.idirectoryservice import IStoreDirectoryService
+from txdav.common.idirectoryservice import IStoreDirectoryService, \
+ DirectoryRecordNotFoundError
from txdav.common.inotifications import INotificationCollection, \
INotificationObject
from txdav.idav import ChangeCategory
+from txdav.xml import element
from uuid import uuid4, UUID
from zope.interface import implements, directlyProvides
+from collections import namedtuple
+import itertools
import json
import sys
import time
@@ -189,6 +194,8 @@
else:
self.queryCacher = None
+ self.conduit = PoddingConduit(self)
+
# Always import these here to trigger proper "registration" of the calendar and address book
# home classes
__import__("txdav.caldav.datastore.sql")
@@ -210,16 +217,18 @@
@inlineCallbacks
- def _withEachHomeDo(self, homeTable, homeFromTxn, action, batchSize): #@UnusedVariable
+ def _withEachHomeDo(self, homeTable, homeFromTxn, action, batchSize, processExternal=False):
"""
Implementation of L{ICalendarStore.withEachCalendarHomeDo} and
L{IAddressbookStore.withEachAddressbookHomeDo}.
"""
txn = yield self.newTransaction()
try:
- allUIDs = yield (Select([homeTable.OWNER_UID], From=homeTable)
- .on(txn))
+ allUIDs = yield (Select([homeTable.OWNER_UID], From=homeTable).on(txn))
for [uid] in allUIDs:
+ home = yield homeFromTxn(txn, uid)
+ if not processExternal and home.external():
+ continue
yield action(txn, (yield homeFromTxn(txn, uid)))
except:
a, b, c = sys.exc_info()
@@ -229,25 +238,25 @@
yield txn.commit()
- def withEachCalendarHomeDo(self, action, batchSize=None):
+ def withEachCalendarHomeDo(self, action, batchSize=None, processExternal=False):
"""
Implementation of L{ICalendarStore.withEachCalendarHomeDo}.
"""
return self._withEachHomeDo(
schema.CALENDAR_HOME,
lambda txn, uid: txn.calendarHomeWithUID(uid),
- action, batchSize
+ action, batchSize, processExternal
)
- def withEachAddressbookHomeDo(self, action, batchSize=None):
+ def withEachAddressbookHomeDo(self, action, batchSize=None, processExternal=False):
"""
Implementation of L{IAddressbookStore.withEachAddressbookHomeDo}.
"""
return self._withEachHomeDo(
schema.ADDRESSBOOK_HOME,
lambda txn, uid: txn.addressbookHomeWithUID(uid),
- action, batchSize
+ action, batchSize, processExternal
)
@@ -473,7 +482,7 @@
self._label = label
self._migrating = migrating
self._primaryHomeType = None
- self._disableCache = disableCache
+ self._disableCache = disableCache or not store.queryCachingEnabled()
if disableCache:
self._queryCacher = None
else:
@@ -1467,10 +1476,108 @@
if shareeView is not None:
yield shareeView.declineShare()
- returnValue(shareeView)
+ returnValue(shareeView is not None)
+ #
+ # External (cross-pod) sharing - entry point is the sharee's home collection.
+ #
+ @inlineCallbacks
+ def processExternalInvite(self, ownerUID, ownerRID, ownerName, shareUID, bindMode, summary, copy_invite_properties, supported_components=None):
+ """
+ External invite received.
+ """
+ # Get the owner home - create external one if not present
+ ownerHome = yield self._txn.homeWithUID(self._homeType, ownerUID, create=True)
+ if ownerHome is None or not ownerHome.external():
+ raise ExternalShareFailed("Invalid owner UID: {}".format(ownerUID))
+
+ # Try to find owner calendar via its external id
+ ownerView = yield ownerHome.childWithExternalID(ownerRID)
+ if ownerView is None:
+ try:
+ ownerView = yield ownerHome.createChildWithName(ownerName, externalID=ownerRID)
+ except HomeChildNameAlreadyExistsError:
+ # This is odd - it means we possibly have a left over sharer collection which the sharer likely removed
+ # and re-created with the same name but now it has a different externalID and is not found by the initial
+ # query. What we do is check to see whether any shares still reference the old ID - if they do we are hosed.
+ # If not, we can remove the old item and create a new one.
+ oldOwnerView = yield ownerHome.childWithName(ownerName)
+ invites = yield oldOwnerView.sharingInvites()
+ if len(invites) != 0:
+ log.error("External invite collection name is present with a different externalID and still has shares")
+ raise
+ log.error("External invite collection name is present with a different externalID - trying to fix")
+ yield ownerHome.removeExternalChild(oldOwnerView)
+ ownerView = yield ownerHome.createChildWithName(ownerName, externalID=ownerRID)
+
+ if supported_components is not None and hasattr(ownerView, "setSupportedComponents"):
+ yield ownerView.setSupportedComponents(supported_components)
+
+ # Now carry out the share operation
+ if bindMode == _BIND_MODE_DIRECT:
+ shareeView = yield ownerView.directShareWithUser(self.uid(), shareName=shareUID)
+ else:
+ shareeView = yield ownerView.inviteUserToShare(self.uid(), bindMode, summary, shareName=shareUID)
+
+ shareeView.setInviteCopyProperties(copy_invite_properties)
+
+
+ @inlineCallbacks
+ def processExternalUninvite(self, ownerUID, ownerRID, shareUID):
+ """
+ External invite received.
+ """
+
+ # Get the owner home
+ ownerHome = yield self._txn.homeWithUID(self._homeType, ownerUID)
+ if ownerHome is None or not ownerHome.external():
+ raise ExternalShareFailed("Invalid owner UID: {}".format(ownerUID))
+
+ # Try to find owner calendar via its external id
+ ownerView = yield ownerHome.childWithExternalID(ownerRID)
+ if ownerView is None:
+ raise ExternalShareFailed("Invalid share ID: {}".format(shareUID))
+
+ # Now carry out the share operation
+ yield ownerView.uninviteUserFromShare(self.uid())
+
+ # See if there are any references to the external share - if not remove it
+ invites = yield ownerView.sharingInvites()
+ if len(invites) == 0:
+ yield ownerHome.removeExternalChild(ownerView)
+
+
+ @inlineCallbacks
+ def processExternalReply(self, ownerUID, shareeUID, shareUID, bindStatus, summary=None):
+ """
+ External invite received.
+ """
+
+ # Make sure the shareeUID and shareUID match
+
+ # Get the owner home - create external one if not present
+ shareeHome = yield self._txn.homeWithUID(self._homeType, shareeUID)
+ if shareeHome is None or not shareeHome.external():
+ raise ExternalShareFailed("Invalid sharee UID: {}".format(shareeUID))
+
+ # Try to find owner calendar via its external id
+ shareeView = yield shareeHome.anyObjectWithShareUID(shareUID)
+ if shareeView is None:
+ raise ExternalShareFailed("Invalid share UID: {}".format(shareUID))
+
+ # Now carry out the share operation
+ if bindStatus == _BIND_STATUS_ACCEPTED:
+ yield shareeHome.acceptShare(shareUID, summary)
+ elif bindStatus == _BIND_STATUS_DECLINED:
+ if shareeView.direct():
+ yield shareeView.deleteShare()
+ else:
+ yield shareeHome.declineShare(shareUID)
+
+
+
class CommonHome(SharingHomeMixIn):
log = Logger()
@@ -1478,6 +1585,7 @@
_homeType = None
_homeTable = None
_homeMetaDataTable = None
+ _externalClass = None
_childClass = None
_childTable = None
_notifierPrefix = None
@@ -1487,10 +1595,30 @@
_cacher = None # Initialize in derived classes
+ @classmethod
+ @inlineCallbacks
+ def makeClass(cls, transaction, ownerUID, no_cache=False):
+ """
+ Build the actual home class taking into account the possibility that we might need to
+ switch in the external version of the class.
+
+ @param transaction: transaction
+ @type transaction: L{CommonStoreTransaction}
+ @param ownerUID: owner UID of home to load
+ @type ownerUID: C{str}
+ @param no_cache: should cached query be used
+ @type no_cache: C{bool}
+ """
+ home = cls(transaction, ownerUID)
+ actualHome = yield home.initFromStore(no_cache)
+ returnValue(actualHome)
+
+
def __init__(self, transaction, ownerUID):
self._txn = transaction
self._ownerUID = ownerUID
self._resourceID = None
+ self._status = _HOME_STATUS_NORMAL
self._dataVersion = None
self._childrenLoaded = False
self._children = {}
@@ -1554,6 +1682,7 @@
return (
cls._homeSchema.RESOURCE_ID,
cls._homeSchema.OWNER_UID,
+ cls._homeSchema.STATUS,
)
@@ -1568,6 +1697,7 @@
return (
"_resourceID",
"_ownerUID",
+ "_status",
)
@@ -1612,39 +1742,57 @@
"""
result = yield self._cacher.get(self._ownerUID)
if result is None:
- result = yield self._homeColumnsFromOwnerQuery.on(
- self._txn, ownerUID=self._ownerUID)
- if result and not no_cache:
- yield self._cacher.set(self._ownerUID, result)
+ result = yield self._homeColumnsFromOwnerQuery.on(self._txn, ownerUID=self._ownerUID)
+ if result:
+ result = result[0]
+ if not no_cache:
+ yield self._cacher.set(self._ownerUID, result)
if result:
- for attr, value in zip(self.homeAttributes(), result[0]):
+ for attr, value in zip(self.homeAttributes(), result):
setattr(self, attr, value)
- queryCacher = self._txn._queryCacher
- if queryCacher:
- # Get cached copy
- cacheKey = queryCacher.keyForHomeMetaData(self._resourceID)
- data = yield queryCacher.get(cacheKey)
+ # STOP! If the status is external we need to convert this object to a CommonHomeExternal class which will
+ # have the right behavior for non-hosted external users.
+ if self._status == _HOME_STATUS_EXTERNAL:
+ actualHome = self._externalClass(self._txn, self._ownerUID, self._resourceID)
else:
- data = None
- if data is None:
- # Don't have a cached copy
- data = (yield self._metaDataQuery.on(
- self._txn, resourceID=self._resourceID))[0]
- if queryCacher:
- # Cache the data
- yield queryCacher.setAfterCommit(self._txn, cacheKey, data)
+ actualHome = self
+ yield actualHome.initMetaDataFromStore()
+ yield actualHome._loadPropertyStore()
- for attr, value in zip(self.metadataAttributes(), data):
- setattr(self, attr, value)
+ for factory_type, factory in self._txn._notifierFactories.items():
+ actualHome.addNotifier(factory_type, factory.newNotifier(actualHome))
- yield self._loadPropertyStore()
- returnValue(self)
+ returnValue(actualHome)
else:
returnValue(None)
+ @inlineCallbacks
+ def initMetaDataFromStore(self):
+ """
+ Load up the metadata and property store
+ """
+
+ queryCacher = self._txn._queryCacher
+ if queryCacher:
+ # Get cached copy
+ cacheKey = queryCacher.keyForHomeMetaData(self._resourceID)
+ data = yield queryCacher.get(cacheKey)
+ else:
+ data = None
+ if data is None:
+ # Don't have a cached copy
+ data = (yield self._metaDataQuery.on(self._txn, resourceID=self._resourceID))[0]
+ if queryCacher:
+ # Cache the data
+ yield queryCacher.setAfterCommit(self._txn, cacheKey, data)
+
+ for attr, value in zip(self.metadataAttributes(), data):
+ setattr(self, attr, value)
+
+
@classmethod
@inlineCallbacks
def listHomes(cls, txn):
@@ -1664,16 +1812,20 @@
@classmethod
@inlineCallbacks
def homeWithUID(cls, txn, uid, create=False):
- homeObject = cls(txn, uid)
- for factory_type, factory in txn._notifierFactories.items():
- homeObject.addNotifier(factory_type, factory.newNotifier(homeObject))
- homeObject = (yield homeObject.initFromStore())
+ homeObject = yield cls.makeClass(txn, uid)
if homeObject is not None:
returnValue(homeObject)
else:
if not create:
returnValue(None)
+ # Determine if the user is local or external
+ record = txn.directoryService().recordWithUID(uid)
+ if record is None:
+ raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(uid))
+
+ state = _HOME_STATUS_NORMAL if record.thisServer() else _HOME_STATUS_EXTERNAL
+
# Use savepoint so we can do a partial rollback if there is a race condition
# where this row has already been inserted
savepoint = SavepointAction("homeWithUID")
@@ -1685,19 +1837,17 @@
resourceid = (yield Insert(
{
cls._homeSchema.OWNER_UID: uid,
+ cls._homeSchema.STATUS: state,
cls._homeSchema.DATAVERSION: cls._dataVersionValue,
},
- Return=cls._homeSchema.RESOURCE_ID).on(txn))[0][0]
- yield Insert(
- {cls._homeMetaDataSchema.RESOURCE_ID: resourceid}).on(txn)
+ Return=cls._homeSchema.RESOURCE_ID
+ ).on(txn))[0][0]
+ yield Insert({cls._homeMetaDataSchema.RESOURCE_ID: resourceid}).on(txn)
except Exception: # FIXME: Really want to trap the pg.DatabaseError but in a non-DB specific manner
yield savepoint.rollback(txn)
# Retry the query - row may exist now, if not re-raise
- homeObject = cls(txn, uid)
- for factory_type, factory in txn._notifierFactories.items():
- homeObject.addNotifier(factory_type, factory.newNotifier(homeObject))
- homeObject = (yield homeObject.initFromStore())
+ homeObject = yield cls.makeClass(txn, uid)
if homeObject:
returnValue(homeObject)
else:
@@ -1708,10 +1858,7 @@
# Note that we must not cache the owner_uid->resource_id
# mapping in _cacher when creating as we don't want that to appear
# until AFTER the commit
- home = cls(txn, uid)
- for factory_type, factory in txn._notifierFactories.items():
- home.addNotifier(factory_type, factory.newNotifier(home))
- home = (yield home.initFromStore(no_cache=True))
+ home = yield cls.makeClass(txn, uid, no_cache=True)
yield home.createdHome()
returnValue(home)
@@ -1727,7 +1874,7 @@
def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+ return "<%s: %s, %s>" % (self.__class__.__name__, self._resourceID, self._ownerUID)
def id(self):
@@ -1749,6 +1896,15 @@
return self._ownerUID
+ def external(self):
+ """
+ Is this an external home.
+
+ @return: a string.
+ """
+ return False
+
+
def transaction(self):
return self._txn
@@ -1866,6 +2022,17 @@
return self._childClass.objectWithID(self, resourceID)
+ def childWithExternalID(self, externalID):
+ """
+ Retrieve the child with the given C{externalID} contained in this
+ home.
+
+ @param name: a string.
+ @return: an L{ICalendar} or C{None} if no such child exists.
+ """
+ return self._childClass.objectWithExternalID(self, externalID)
+
+
def allChildWithID(self, resourceID):
"""
Retrieve the child with the given C{resourceID} contained in this
@@ -1878,12 +2045,11 @@
@inlineCallbacks
- def createChildWithName(self, name):
+ def createChildWithName(self, name, externalID=None):
if name.startswith("."):
raise HomeChildNameNotAllowedError(name)
- yield self._childClass.create(self, name)
- child = (yield self.childWithName(name))
+ child = yield self._childClass.create(self, name, externalID=externalID)
returnValue(child)
@@ -2025,6 +2191,10 @@
record a revision for the sharee home and sharee collection name with the "deleted" flag set. That way
the shared collection can be reported as removed.
+ For external shared collections we need to report them as invalid as we cannot aggregate the sync token
+ for this home with the sync token from the external share which is under the control of the other pod.
+ Reporting it as invalid means that clients should do requests directly on the share itself to sync it.
+
@param revision: the sync revision to compare to
@type revision: C{str}
@param depth: depth for determine what changed
@@ -2043,6 +2213,7 @@
changed = set()
deleted = set()
+ invalid = set()
deleted_collections = set()
for path, name, wasdeleted in results:
if wasdeleted:
@@ -2071,13 +2242,17 @@
# TODO: think about whether this can be done in one query rather than looping over each share
for share in (yield self.children()):
if not share.owned():
- sharedChanged, sharedDeleted = yield share.sharedChildResourceNamesSinceRevision(revision, depth)
+ sharedChanged, sharedDeleted, sharedInvalid = yield share.sharedChildResourceNamesSinceRevision(revision, depth)
changed |= sharedChanged
+ changed -= sharedInvalid
deleted |= sharedDeleted
+ deleted -= sharedInvalid
+ invalid |= sharedInvalid
changed = sorted(changed)
deleted = sorted(deleted)
- returnValue((changed, deleted))
+ invalid = sorted(invalid)
+ returnValue((changed, deleted, invalid,))
@inlineCallbacks
@@ -2400,7 +2575,7 @@
def revisionFromToken(self, token):
if token is None:
return 0
- elif isinstance(token, str):
+ elif isinstance(token, str) or isinstance(token, unicode):
_ignore_uuid, revision = token.split("_", 1)
return int(revision)
else:
@@ -2466,6 +2641,7 @@
changed = []
deleted = []
+ invalid = []
for name, wasdeleted in results:
if name:
if wasdeleted:
@@ -2474,7 +2650,7 @@
else:
changed.append(name)
- returnValue((changed, deleted))
+ returnValue((changed, deleted, invalid))
@classproperty
@@ -2759,6 +2935,7 @@
return Insert({
bind.HOME_RESOURCE_ID: Parameter("homeID"),
bind.RESOURCE_ID: Parameter("resourceID"),
+ bind.EXTERNAL_ID: Parameter("externalID"),
bind.RESOURCE_NAME: Parameter("name"),
bind.BIND_MODE: Parameter("mode"),
bind.BIND_STATUS: Parameter("bindStatus"),
@@ -2840,11 +3017,21 @@
"""
bind = cls._bindSchema
return cls._bindFor((bind.RESOURCE_ID == Parameter("resourceID"))
- .And(bind.HOME_RESOURCE_ID == Parameter("homeID"))
- )
+ .And(bind.HOME_RESOURCE_ID == Parameter("homeID")))
@classproperty
+ def _bindForExternalIDAndHomeID(cls): #@NoSelf
+ """
+ DAL query that looks up home bind rows by home child
+ resource ID and home resource ID.
+ """
+ bind = cls._bindSchema
+ return cls._bindFor((bind.EXTERNAL_ID == Parameter("externalID"))
+ .And(bind.HOME_RESOURCE_ID == Parameter("homeID")))
+
+
+ @classproperty
def _bindForNameAndHomeID(cls): #@NoSelf
"""
DAL query that looks up any bind rows by home child
@@ -2852,15 +3039,14 @@
"""
bind = cls._bindSchema
return cls._bindFor((bind.RESOURCE_NAME == Parameter("name"))
- .And(bind.HOME_RESOURCE_ID == Parameter("homeID"))
- )
+ .And(bind.HOME_RESOURCE_ID == Parameter("homeID")))
#
# Higher level API
#
@inlineCallbacks
- def inviteUserToShare(self, shareeUID, mode, summary):
+ def inviteUserToShare(self, shareeUID, mode, summary, shareName=None):
"""
Invite a user to share this collection - either create the share if it does not exist, or
update the existing share with new values. Make sure a notification is sent as well.
@@ -2879,15 +3065,19 @@
status = _BIND_STATUS_INVITED if shareeView.shareStatus() in (_BIND_STATUS_DECLINED, _BIND_STATUS_INVALID) else None
yield self.updateShare(shareeView, mode=mode, status=status, summary=summary)
else:
- shareeView = yield self.createShare(shareeUID=shareeUID, mode=mode, summary=summary)
+ shareeView = yield self.createShare(shareeUID=shareeUID, mode=mode, summary=summary, shareName=shareName)
- # Send invite notification
- yield self._sendInviteNotification(shareeView)
+ # Check for external
+ if shareeView.viewerHome().external():
+ yield self._sendExternalInvite(shareeView)
+ else:
+ # Send invite notification
+ yield self._sendInviteNotification(shareeView)
returnValue(shareeView)
@inlineCallbacks
- def directShareWithUser(self, shareeUID):
+ def directShareWithUser(self, shareeUID, shareName=None):
"""
Create a direct share with the specified user. Note it is currently up to the app layer
to enforce access control - this is not ideal as we really should have control of that in
@@ -2902,8 +3092,13 @@
# Ignore if it already exists
shareeView = yield self.shareeView(shareeUID)
if shareeView is None:
- shareeView = yield self.createShare(shareeUID=shareeUID, mode=_BIND_MODE_DIRECT)
+ shareeView = yield self.createShare(shareeUID=shareeUID, mode=_BIND_MODE_DIRECT, shareName=shareName)
yield shareeView.newShare()
+
+ # Check for external
+ if shareeView.viewerHome().external():
+ yield self._sendExternalInvite(shareeView)
+
returnValue(shareeView)
@@ -2919,13 +3114,16 @@
shareeView = yield self.shareeView(shareeUID)
if shareeView is not None:
- # If current user state is accepted then we send an invite with the new state, otherwise
- # we cancel any existing invites for the user
- if not shareeView.direct():
- if shareeView.shareStatus() != _BIND_STATUS_ACCEPTED:
- yield self._removeInviteNotification(shareeView)
- else:
- yield self._sendInviteNotification(shareeView, notificationState=_BIND_STATUS_DELETED)
+ if shareeView.viewerHome().external():
+ yield self._sendExternalUninvite(shareeView)
+ else:
+ # If current user state is accepted then we send an invite with the new state, otherwise
+ # we cancel any existing invites for the user
+ if not shareeView.direct():
+ if shareeView.shareStatus() != _BIND_STATUS_ACCEPTED:
+ yield self._removeInviteNotification(shareeView)
+ else:
+ yield self._sendInviteNotification(shareeView, notificationState=_BIND_STATUS_DELETED)
# Remove the bind
yield self.removeShare(shareeView)
@@ -2938,10 +3136,13 @@
"""
if not self.direct() and self.shareStatus() != _BIND_STATUS_ACCEPTED:
+ if self.external():
+ yield self._replyExternalInvite(_BIND_STATUS_ACCEPTED, summary)
ownerView = yield self.ownerView()
yield ownerView.updateShare(self, status=_BIND_STATUS_ACCEPTED)
yield self.newShare(displayname=summary)
- yield self._sendReplyNotification(ownerView, summary)
+ if not ownerView.external():
+ yield self._sendReplyNotification(ownerView, summary)
@inlineCallbacks
@@ -2951,24 +3152,43 @@
"""
if not self.direct() and self.shareStatus() != _BIND_STATUS_DECLINED:
+ if self.external():
+ yield self._replyExternalInvite(_BIND_STATUS_DECLINED)
ownerView = yield self.ownerView()
yield ownerView.updateShare(self, status=_BIND_STATUS_DECLINED)
- yield self._sendReplyNotification(ownerView)
+ if not ownerView.external():
+ yield self._sendReplyNotification(ownerView)
@inlineCallbacks
def deleteShare(self):
"""
- This share is being deleted - either decline or remove (for direct shares).
+ This share is being deleted (by the sharee) - either decline or remove (for direct shares).
"""
ownerView = yield self.ownerView()
if self.direct():
yield ownerView.removeShare(self)
+ if ownerView.external():
+ yield self._replyExternalInvite(_BIND_STATUS_DECLINED)
else:
yield self.declineShare()
+ @inlineCallbacks
+ def ownerDeleteShare(self):
+ """
+ This share is being deleted (by the owner) - either decline or remove (for direct shares).
+ """
+
+ # Change status on store object
+ yield self.setShared(False)
+
+ # Remove all sharees (direct and invited)
+ for invitation in (yield self.sharingInvites()):
+ yield self.uninviteUserFromShare(invitation.shareeUID)
+
+
def newShare(self, displayname=None):
"""
Override in derived classes to do any specific operations needed when a share
@@ -3068,10 +3288,57 @@
#
- # Lower level API
+ # External/cross-pod API
#
+ @inlineCallbacks
+ def _sendExternalInvite(self, shareeView):
+ yield self._txn.store().conduit.send_shareinvite(
+ self._txn,
+ shareeView.ownerHome()._homeType,
+ shareeView.ownerHome().uid(),
+ self.id(),
+ self.shareName(),
+ shareeView.viewerHome().uid(),
+ shareeView.shareUID(),
+ shareeView.shareMode(),
+ shareeView.shareMessage(),
+ self.getInviteCopyProperties(),
+ supported_components=self.getSupportedComponents() if hasattr(self, "getSupportedComponents") else None,
+ )
+
+
@inlineCallbacks
+ def _sendExternalUninvite(self, shareeView):
+
+ yield self._txn.store().conduit.send_shareuninvite(
+ self._txn,
+ shareeView.ownerHome()._homeType,
+ shareeView.ownerHome().uid(),
+ self.id(),
+ shareeView.viewerHome().uid(),
+ shareeView.shareUID(),
+ )
+
+
+ @inlineCallbacks
+ def _replyExternalInvite(self, status, summary=None):
+
+ yield self._txn.store().conduit.send_sharereply(
+ self._txn,
+ self.viewerHome()._homeType,
+ self.ownerHome().uid(),
+ self.viewerHome().uid(),
+ self.shareUID(),
+ status,
+ summary,
+ )
+
+
+ #
+ # Lower level API
+ #
+ @inlineCallbacks
def ownerView(self):
"""
Return the owner resource counterpart of this shared resource.
@@ -3094,7 +3361,7 @@
@inlineCallbacks
- def shareWith(self, shareeHome, mode, status=None, summary=None):
+ def shareWith(self, shareeHome, mode, status=None, summary=None, shareName=None):
"""
Share this (owned) L{CommonHomeChild} with another home.
@@ -3122,11 +3389,12 @@
@inlineCallbacks
def doInsert(subt):
- newName = self.newShareName()
+ newName = shareName if shareName is not None else self.newShareName()
yield self._bindInsertQuery.on(
subt,
homeID=shareeHome._resourceID,
resourceID=self._resourceID,
+ externalID=self._externalID,
name=newName,
mode=mode,
bindStatus=status,
@@ -3160,7 +3428,7 @@
@inlineCallbacks
- def createShare(self, shareeUID, mode, summary=None):
+ def createShare(self, shareeUID, mode, summary=None, shareName=None):
"""
Create a new shared resource. If the mode is direct, the share is created in accepted state,
otherwise the share is created in invited state.
@@ -3172,6 +3440,7 @@
mode=mode,
status=_BIND_STATUS_INVITED if mode != _BIND_MODE_DIRECT else _BIND_STATUS_ACCEPTED,
summary=summary,
+ shareName=shareName,
)
shareeView = yield self.shareeView(shareeUID)
returnValue(shareeView)
@@ -3232,12 +3501,7 @@
if summary is not None:
shareeView._bindMessage = columnMap[bind.MESSAGE]
- queryCacher = self._txn._queryCacher
- if queryCacher:
- cacheKey = queryCacher.keyForObjectWithName(shareeView._home._resourceID, shareeView._name)
- yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
- cacheKey = queryCacher.keyForObjectWithResourceID(shareeView._home._resourceID, shareeView._resourceID)
- yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
+ yield shareeView.invalidateQueryCache()
# Must send notification to ensure cache invalidation occurs
yield self.notifyPropertyChanged()
@@ -3291,12 +3555,7 @@
homeID=shareeHome._resourceID,
)
- queryCacher = self._txn._queryCacher
- if queryCacher:
- cacheKey = queryCacher.keyForObjectWithName(shareeHome._resourceID, shareeView._name)
- yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
- cacheKey = queryCacher.keyForObjectWithResourceID(shareeHome._resourceID, shareeView._resourceID)
- yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
+ yield shareeView.invalidateQueryCache()
@inlineCallbacks
@@ -3496,6 +3755,24 @@
return self._bindMessage
+ def getInviteCopyProperties(self):
+ """
+ Get a dictionary of property name/values (as strings) for properties that are shadowable and
+ need to be copied to a sharee's collection when an external (cross-pod) share is created.
+ Sub-classes should override to expose the properties they care about.
+ """
+ return {}
+
+
+ def setInviteCopyProperties(self, props):
+ """
+ Copy a set of shadowable properties (as name/value strings) onto this shared resource when
+ a cross-pod invite is processed. Sub-classes should override to expose the properties they
+ care about.
+ """
+ pass
+
+
@classmethod
def metadataColumns(cls):
"""
@@ -3540,13 +3817,14 @@
cls._bindSchema.BIND_MODE,
cls._bindSchema.HOME_RESOURCE_ID,
cls._bindSchema.RESOURCE_ID,
+ cls._bindSchema.EXTERNAL_ID,
cls._bindSchema.RESOURCE_NAME,
cls._bindSchema.BIND_STATUS,
cls._bindSchema.BIND_REVISION,
cls._bindSchema.MESSAGE
)
- bindColumnCount = 7
+ bindColumnCount = 8
@classmethod
def additionalBindColumns(cls):
@@ -3606,6 +3884,7 @@
yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForHomeChildMetaData(self._resourceID))
yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForObjectWithName(self._home._resourceID, self._name))
yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForObjectWithResourceID(self._home._resourceID, self._resourceID))
+ yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForObjectWithExternalID(self._home._resourceID, self._externalID))
@@ -3621,6 +3900,7 @@
"_resourceID",
)
+ _externalClass = None
_objectResourceClass = None
_bindSchema = None
@@ -3631,11 +3911,148 @@
_objectSchema = None
- def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None):
+ @classmethod
+ @inlineCallbacks
+ def makeClass(cls, home, bindData, additionalBindData, metadataData, propstore=None, ownerHome=None):
+ """
+ Given the various database rows, build the actual class.
+ @param home: the parent home object
+ @type home: L{CommonHome}
+ @param bindData: the standard set of bind columns
+ @type bindData: C{list}
+ @param additionalBindData: additional bind data specific to sub-classes
+ @type additionalBindData: C{list}
+ @param metadataData: metadata data
+ @type metadataData: C{list}
+ @param propstore: a property store to use, or C{None} to load it automatically
+ @type propstore: L{PropertyStore}
+ @param ownerHome: the home of the owner, or C{None} to figure it out automatically
+ @type ownerHome: L{CommonHome}
+
+ @return: the constructed child class
+ @rtype: L{CommonHomeChild}
+ """
+
+ bindMode, _ignore_homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = bindData
+
+ if ownerHome is None:
+ if bindMode == _BIND_MODE_OWN:
+ ownerHome = home
+ ownerName = name
+ else:
+ ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
+ else:
+ ownerName = None
+
+ c = cls._externalClass if ownerHome.external() else cls
+ child = c(
+ home=home,
+ name=name,
+ resourceID=resourceID,
+ mode=bindMode,
+ status=bindStatus,
+ revision=bindRevision,
+ message=bindMessage,
+ ownerHome=ownerHome,
+ ownerName=ownerName,
+ externalID=externalID,
+ )
+
+ if additionalBindData:
+ for attr, value in zip(child.additionalBindAttributes(), additionalBindData):
+ setattr(child, attr, value)
+
+ if metadataData:
+ for attr, value in zip(child.metadataAttributes(), metadataData):
+ setattr(child, attr, value)
+
+ # We have to re-adjust the property store object to account for possible shared
+ # collections as previously we loaded them all as if they were owned
+ if propstore and bindMode != _BIND_MODE_OWN:
+ propstore._setDefaultUserUID(ownerHome.uid())
+ yield child._loadPropertyStore(propstore)
+
+ returnValue(child)
+
+
+ @classmethod
+ @inlineCallbacks
+ def _getDBData(cls, home, name, resourceID, externalID):
+ """
+ Given a set of identifying information, load the data rows for the object. Only one of
+ L{name}, L{resourceID} or L{externalID} is specified - others are C{None}.
+
+ @param home: the parent home object
+ @type home: L{CommonHome}
+ @param name: the resource name
+ @type name: C{str}
+ @param resourceID: the resource ID
+ @type resourceID: C{int}
+ @param externalID: the resource ID of the external (cross-pod) referenced item
+ @type externalID: C{int}
+ """
+
+ # Get the bind row data
+ row = None
+ queryCacher = home._txn._queryCacher
+
+ if queryCacher:
+ # Retrieve data from cache
+ if name:
+ cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
+ elif resourceID:
+ cacheKey = queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID)
+ elif externalID:
+ cacheKey = queryCacher.keyForObjectWithExternalID(home._resourceID, externalID)
+ row = yield queryCacher.get(cacheKey)
+
+ if row is None:
+ # No cached copy
+ if name:
+ rows = yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)
+ elif resourceID:
+ rows = yield cls._bindForResourceIDAndHomeID.on(home._txn, resourceID=resourceID, homeID=home._resourceID)
+ elif externalID:
+ rows = yield cls._bindForExternalIDAndHomeID.on(home._txn, externalID=externalID, homeID=home._resourceID)
+ row = rows[0] if rows else None
+
+ if not row:
+ returnValue(None)
+
+ if queryCacher:
+ # Cache the result
+ queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithName(home._resourceID, name), row)
+ queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID), row)
+ queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithExternalID(home._resourceID, externalID), row)
+
+ bindData = row[:cls.bindColumnCount]
+ additionalBindData = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+ resourceID = bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)]
+
+ # Get the matching metadata data
+ metadataData = None
+ if queryCacher:
+ # Retrieve from cache
+ cacheKey = queryCacher.keyForHomeChildMetaData(resourceID)
+ metadataData = yield queryCacher.get(cacheKey)
+
+ if metadataData is None:
+ # No cached copy
+ metadataData = (yield cls._metadataByIDQuery.on(home._txn, resourceID=resourceID))[0]
+ if queryCacher:
+ # Cache the results
+ yield queryCacher.setAfterCommit(home._txn, cacheKey, metadataData)
+
+ returnValue((bindData, additionalBindData, metadataData,))
+
+
+ def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, externalID=None):
+
self._home = home
self._name = name
self._resourceID = resourceID
+ self._externalID = externalID
self._bindMode = mode
self._bindStatus = status
self._bindRevision = revision
@@ -3657,9 +4074,7 @@
else:
self._notifiers = None
- self._index = None # Derived classes need to set this
-
def memoMe(self, key, memo): #@UnusedVariable
"""
Add this object to the memo dictionary in whatever fashion is appropriate.
@@ -3685,7 +4100,7 @@
rows = yield cls._acceptedBindForHomeID.on(
home._txn, homeID=home._resourceID
)
- names = [row[3] for row in rows]
+ names = [row[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] for row in rows]
returnValue(names)
@@ -3718,119 +4133,66 @@
# Create the actual objects merging in properties
for dataRow in dataRows:
- bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
- additionalBind = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
- metadata = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+ bindData = dataRow[:cls.bindColumnCount] #@UnusedVariable
+ resourceID = bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)]
+ additionalBindData = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+ metadataData = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+ propstore = propertyStores.get(resourceID, None)
- if bindMode == _BIND_MODE_OWN:
- ownerHome = home
- ownerName = bindName
- else:
- #TODO: get all ownerHomeIDs at once
- ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
-
- child = cls(
- home=home,
- name=bindName,
- resourceID=resourceID,
- mode=bindMode,
- status=bindStatus,
- revision=bindRevision,
- message=bindMessage,
- ownerHome=ownerHome,
- ownerName=ownerName,
- )
- for attr, value in zip(cls.additionalBindAttributes(), additionalBind):
- setattr(child, attr, value)
- for attr, value in zip(cls.metadataAttributes(), metadata):
- setattr(child, attr, value)
+ child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, propstore)
child._syncTokenRevision = revisions[resourceID]
- propstore = propertyStores.get(resourceID, None)
- # We have to re-adjust the property store object to account for possible shared
- # collections as previously we loaded them all as if they were owned
- if propstore and bindMode != _BIND_MODE_OWN:
- propstore._setDefaultUserUID(ownerHome.uid())
- yield child._loadPropertyStore(propstore)
results.append(child)
+
returnValue(results)
@classmethod
def objectWithName(cls, home, name, accepted=True):
- return cls._objectWithNameOrID(home, name=name, accepted=accepted)
+ return cls.objectWith(home, name=name, accepted=accepted)
@classmethod
def objectWithID(cls, home, resourceID, accepted=True):
- return cls._objectWithNameOrID(home, resourceID=resourceID, accepted=accepted)
+ return cls.objectWith(home, resourceID=resourceID, accepted=accepted)
@classmethod
+ def objectWithExternalID(cls, home, externalID, accepted=True):
+ return cls.objectWith(home, externalID=externalID, accepted=accepted)
+
+
+ @classmethod
@inlineCallbacks
- def _objectWithNameOrID(cls, home, name=None, resourceID=None, accepted=True):
- # replaces objectWithName()
+ def objectWith(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
"""
- Retrieve the child with the given C{name} or C{resourceID} contained in the given
- C{home}.
+ Create the object using one of the specified arguments as the key to load it. One
+ and only one of the keyword arguments must be set.
- @param home: a L{CommonHome}.
+ @param parent: parent collection
+ @type parent: L{CommonHomeChild}
+ @param name: name of the resource, or C{None}
+ @type name: C{str}
+ @param uid: resource data UID, or C{None}
+ @type uid: C{str}
+ @param resourceID: resource id
+ @type resourceID: C{int}
+ @param accepted: if C{True} only load owned or accepted share items
+ @type accepted: C{bool}
- @param name: a string; the name of the L{CommonHomeChild} to retrieve.
-
- @return: an L{CommonHomeChild} or C{None} if no such child
- exists.
+ @return: the new object or C{None} if not found
+ @rtype: C{CommonHomeChild}
"""
- rows = None
- queryCacher = home._txn._queryCacher
- if queryCacher:
- # Retrieve data from cache
- if name:
- cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
- else:
- cacheKey = queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID)
- rows = yield queryCacher.get(cacheKey)
-
- if rows is None:
- # No cached copy
- if name:
- rows = yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)
- else:
- rows = yield cls._bindForResourceIDAndHomeID.on(home._txn, resourceID=resourceID, homeID=home._resourceID)
-
- if not rows:
+ dbData = yield cls._getDBData(home, name, resourceID, externalID)
+ if dbData is None:
returnValue(None)
+ bindData, additionalBindData, metadataData = dbData
- row = rows[0]
- bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
-
- if queryCacher:
- # Cache the result
- queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithName(home._resourceID, name), rows)
- queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID), rows)
-
+ bindStatus = bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)]
if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
returnValue(None)
- additionalBind = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
- if bindMode == _BIND_MODE_OWN:
- ownerHome = home
- ownerName = name
- else:
- ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
-
- child = cls(
- home=home,
- name=name,
- resourceID=resourceID,
- mode=bindMode,
- status=bindStatus,
- revision=bindRevision,
- message=bindMessage,
- ownerHome=ownerHome,
- ownerName=ownerName
- )
- yield child.initFromStore(additionalBind)
+ child = yield cls.makeClass(home, bindData, additionalBindData, metadataData)
returnValue(child)
@@ -3858,35 +4220,28 @@
@classmethod
@inlineCallbacks
- def create(cls, home, name):
+ def create(cls, home, name, externalID=None):
- if (yield cls._bindForNameAndHomeID.on(home._txn,
- name=name, homeID=home._resourceID)):
+ if (yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)):
raise HomeChildNameAlreadyExistsError(name)
if name.startswith("."):
raise HomeChildNameNotAllowedError(name)
# Create this object
- resourceID = (
- yield cls._insertHomeChild.on(home._txn))[0][0]
+ resourceID = (yield cls._insertHomeChild.on(home._txn))[0][0]
# Initialize this object
- _created, _modified = (
- yield cls._insertHomeChildMetaData.on(home._txn,
- resourceID=resourceID))[0]
+ _created, _modified = (yield cls._insertHomeChildMetaData.on(home._txn, resourceID=resourceID))[0]
# Bind table needs entry
yield cls._bindInsertQuery.on(
- home._txn, homeID=home._resourceID, resourceID=resourceID,
+ home._txn, homeID=home._resourceID, resourceID=resourceID, externalID=externalID,
name=name, mode=_BIND_MODE_OWN, bindStatus=_BIND_STATUS_ACCEPTED,
message=None,
)
# Initialize other state
- child = cls(home, name, resourceID, _BIND_MODE_OWN, _BIND_STATUS_ACCEPTED)
- child._created = _created
- child._modified = _modified
- yield child._loadPropertyStore()
+ child = yield cls.objectWithID(home, resourceID)
yield child._initSyncToken()
@@ -3907,44 +4262,32 @@
Where=child.RESOURCE_ID == Parameter("resourceID"))
- @inlineCallbacks
- def initFromStore(self, additionalBind=None):
+ def id(self):
"""
- Initialise this object from the store, based on its already-populated
- resource ID. We read in and cache all the extra metadata from the DB to
- avoid having to do DB queries for those individually later.
+ Retrieve the store identifier for this collection.
+
+ @return: store identifier.
+ @rtype: C{int}
"""
- queryCacher = self._txn._queryCacher
- if queryCacher:
- # Retrieve from cache
- cacheKey = queryCacher.keyForHomeChildMetaData(self._resourceID)
- dataRows = yield queryCacher.get(cacheKey)
- else:
- dataRows = None
- if dataRows is None:
- # No cached copy
- dataRows = (yield self._metadataByIDQuery.on(self._txn, resourceID=self._resourceID))[0]
- if queryCacher:
- # Cache the results
- yield queryCacher.setAfterCommit(self._txn, cacheKey, dataRows)
+ return self._resourceID
- if additionalBind:
- for attr, value in zip(self.additionalBindAttributes(), additionalBind):
- setattr(self, attr, value)
- for attr, value in zip(self.metadataAttributes(), dataRows):
- setattr(self, attr, value)
- yield self._loadPropertyStore()
+ def external_id(self):
+ """
+ Retrieve the external store identifier for this collection.
+ @return: a string.
+ """
+ return self._externalID
- def id(self):
+
+ def external(self):
"""
- Retrieve the store identifier for this collection.
+ Is this an external home.
- @return: store identifier.
- @rtype: C{int}
+ @return: a string.
"""
- return self._resourceID
+ return self.ownerHome().external()
@property
@@ -3956,10 +4299,6 @@
return self._txn.store().directoryService()
- def retrieveOldIndex(self):
- return self._index
-
-
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
@@ -3994,14 +4333,13 @@
@return: a L{Deferred} which fires when the modification is complete.
"""
+
+ if self.isShared() or self.external():
+ raise ShareNotAllowed("Cannot rename a shared collection")
+
oldName = self._name
- queryCacher = self._home._txn._queryCacher
- if queryCacher:
- cacheKey = queryCacher.keyForObjectWithName(self._home._resourceID, oldName)
- yield queryCacher.invalidateAfterCommit(self._home._txn, cacheKey)
- cacheKey = queryCacher.keyForObjectWithResourceID(self._home._resourceID, self._resourceID)
- yield queryCacher.invalidateAfterCommit(self._home._txn, cacheKey)
+ yield self.invalidateQueryCache()
yield self._renameQuery.on(self._txn, name=name,
resourceID=self._resourceID,
@@ -4028,19 +4366,16 @@
@inlineCallbacks
def remove(self):
+ # Stop sharing first
+ yield self.ownerDeleteShare()
+
# Do before setting _resourceID making changes
yield self.notifyPropertyChanged()
- queryCacher = self._home._txn._queryCacher
- if queryCacher:
- cacheKey = queryCacher.keyForObjectWithName(self._home._resourceID, self._name)
- yield queryCacher.invalidateAfterCommit(self._home._txn, cacheKey)
- cacheKey = queryCacher.keyForObjectWithResourceID(self._home._resourceID, self._resourceID)
- yield queryCacher.invalidateAfterCommit(self._home._txn, cacheKey)
+ yield self.invalidateQueryCache()
yield self._deletedSyncToken()
- yield self._deleteQuery.on(self._txn, NoSuchHomeChildError,
- resourceID=self._resourceID)
+ yield self._deleteQuery.on(self._txn, NoSuchHomeChildError, resourceID=self._resourceID)
yield self.properties()._removeResource()
# Set to non-existent state
@@ -4092,6 +4427,7 @@
for result in results:
self._objects[result.name()] = result
self._objects[result.uid()] = result
+ self._objects[result.id()] = result
self._objectNames = sorted([result.name() for result in results])
returnValue(results)
@@ -4105,6 +4441,7 @@
for result in results:
self._objects[result.name()] = result
self._objects[result.uid()] = result
+ self._objects[result.id()] = result
self._objectNames = sorted([result.name() for result in results])
returnValue(results)
@@ -4141,8 +4478,7 @@
@inlineCallbacks
def countObjectResources(self):
if self._objectNames is None:
- rows = yield self._objectCountQuery.on(
- self._txn, resourceID=self._resourceID)
+ rows = yield self._objectCountQuery.on(self._txn, resourceID=self._resourceID)
returnValue(rows[0][0])
returnValue(len(self._objectNames))
@@ -4174,18 +4510,13 @@
We create the empty object first then have it initialize itself from the
store.
"""
- if resourceID:
- objectResource = (
- yield self._objectResourceClass.objectWithID(self, resourceID)
- )
- else:
- objectResource = (
- yield self._objectResourceClass.objectWithName(self, name, uid)
- )
+ objectResource = (
+ yield self._objectResourceClass.objectWith(self, name=name, uid=uid, resourceID=resourceID)
+ )
if objectResource:
self._objects[objectResource.name()] = objectResource
self._objects[objectResource.uid()] = objectResource
- self._objects[objectResource._resourceID] = objectResource
+ self._objects[objectResource.id()] = objectResource
else:
if resourceID:
self._objects[resourceID] = None
@@ -4232,7 +4563,7 @@
obj = cls._objectSchema
return Select(
[obj.UID], From=obj,
- Where=(obj.UID == Parameter("name")
+ Where=(obj.RESOURCE_NAME == Parameter("name")
).And(obj.PARENT_RESOURCE_ID == Parameter("resourceID")))
@@ -4275,6 +4606,7 @@
)
self._objects[objectResource.name()] = objectResource
self._objects[objectResource.uid()] = objectResource
+ self._objects[objectResource.id()] = objectResource
# Note: create triggers a notification when the component is set, so we
# don't need to call notify() here like we do for object removal.
@@ -4285,6 +4617,7 @@
def removedObjectResource(self, child):
self._objects.pop(child.name(), None)
self._objects.pop(child.uid(), None)
+ self._objects.pop(child.id(), None)
if self._objectNames and child.name() in self._objectNames:
self._objectNames.remove(child.name())
yield self._deleteRevision(child.name())
@@ -4317,10 +4650,9 @@
@inlineCallbacks
- def moveObjectResource(self, child, newparent, newname=None):
+ def _validObjectResource(self, child, newparent, newname=None):
"""
- Move a child of this collection into another collection without actually removing/re-inserting the data.
- Make sure sync and cache details for both collections are updated.
+ Check that the move operation is valid
TODO: check that the resource name does not exist in the new parent, or that the UID
does not exist there too.
@@ -4334,7 +4666,6 @@
"""
name = child.name()
- uid = child.uid()
if newname is None:
newname = name
@@ -4353,10 +4684,34 @@
if child_count >= config.MaxResourcesPerCollection:
raise TooManyObjectResourcesError()
+ returnValue(newname)
+
+
+ @inlineCallbacks
+ def moveObjectResource(self, child, newparent, newname=None):
+ """
+ Move a child of this collection into another collection without actually removing/re-inserting the data.
+ Make sure sync and cache details for both collections are updated.
+
+ TODO: check that the resource name does not exist in the new parent, or that the UID
+ does not exist there too.
+
+ @param child: the child resource to move
+ @type child: L{CommonObjectResource}
+ @param newparent: the parent to move to
+ @type newparent: L{CommonHomeChild}
+ @param newname: new name to use in new parent
+ @type newname: C{str} or C{None} for existing name
+ """
+
+ name = child.name()
+ newname = yield self._validObjectResource(child, newparent, newname)
+ uid = child.uid()
+
# Clean this collections cache and signal sync change
self._objects.pop(name, None)
self._objects.pop(uid, None)
- self._objects.pop(child._resourceID, None)
+ self._objects.pop(child.id(), None)
yield self._deleteRevision(name)
yield self.notifyChanged()
@@ -4387,11 +4742,68 @@
# Signal sync change on new collection
newparent._objects.pop(name, None)
newparent._objects.pop(uid, None)
- newparent._objects.pop(child._resourceID, None)
+ newparent._objects.pop(child.id(), None)
yield newparent._insertRevision(newname)
yield newparent.notifyChanged()
+ @inlineCallbacks
+ def moveObjectResourceCreateDelete(self, child, newparent, newname=None):
+ """
+ Move a child of this collection into another collection by doing a create/delete.
+
+ TODO: check that the resource name does not exist in the new parent, or that the UID
+ does not exist there too.
+
+ @param child: the child resource to move
+ @type child: L{CommonObjectResource}
+ @param newparent: the parent to move to
+ @type newparent: L{CommonHomeChild}
+ @param newname: new name to use in new parent
+ @type newname: C{str} or C{None} for existing name
+ """
+
+ name = child.name()
+ newname = yield self._validObjectResource(child, newparent, newname)
+
+ # Do a move as a create/delete
+ component = yield child.component()
+ yield newparent.moveObjectResourceHere(name, component)
+ yield self.moveObjectResourceAway(child.id(), child)
+
+
+ @inlineCallbacks
+ def moveObjectResourceHere(self, name, component):
+ """
+ Create a new child in this collection as part of a move operation. This needs to be split out because
+ behavior differs for sub-classes and cross-pod operations.
+
+ @param name: new name to use in new parent
+ @type name: C{str} or C{None} for existing name
+ @param component: data for new resource
+ @type component: L{Component}
+ """
+
+ yield self.createObjectResourceWithName(name, component)
+
+
+ @inlineCallbacks
+ def moveObjectResourceAway(self, rid, child=None):
+ """
+ Remove the child as the result of a move operation. This needs to be split out because
+ behavior differs for sub-classes and cross-pod operations.
+
+ @param rid: the child resource-id to move
+ @type rid: C{int}
+ @param child: the child resource to move - might be C{None} for cross-pod
+ @type child: L{CommonObjectResource}
+ """
+
+ if child is None:
+ child = yield self.objectResourceWithID(rid)
+ yield child.remove()
+
+
def objectResourcesHaveProperties(self):
return False
@@ -4407,12 +4819,26 @@
@type revision: C{int}
"""
- if revision != 0 and revision < self._bindRevision:
+ if revision != 0 and revision < self._bindRevision and not self.external():
raise SyncTokenValidException
-
return super(CommonHomeChild, self).resourceNamesSinceRevision(revision)
+ def search(self, filter):
+ """
+ Do a query of the contents of this collection.
+
+ @param filter: the query filter to use
+ @type filter: L{Filter}
+
+ @return: the names of the matching resources
+ @rtype: C{list}
+ """
+
+ # This implementation raises - sub-classes override to do the actual query
+ raise IndexedSearchException()
+
+
@inlineCallbacks
def sharedChildResourceNamesSinceRevision(self, revision, depth):
"""
@@ -4436,47 +4862,56 @@
"""
assert not self.owned()
- if revision != 0 and revision < self._bindRevision:
- if depth != '1':
- raise SyncTokenValidException
- else:
- revision = 0
-
changed = set()
deleted = set()
- rev = self._revisionsSchema
- results = [
- (
- self.name(),
- name if name else "",
- wasdeleted
- )
- for name, wasdeleted in
- (yield Select([rev.RESOURCE_NAME, rev.DELETED],
- From=rev,
- Where=(rev.REVISION > revision).And(
- rev.RESOURCE_ID == self._resourceID)).on(self._txn))
- if name
- ]
+ invalid = set()
+ if self.external():
+ if depth == "1":
+ pass
+ else:
+ name = self.name() + "/"
+ invalid.add(name)
+ else:
+ if revision != 0 and revision < self._bindRevision:
+ if depth != "1":
+ raise SyncTokenValidException
+ else:
+ revision = 0
- for path, name, wasdeleted in results:
- if wasdeleted:
- if revision:
- if depth == "1":
- changed.add("%s/" % (path,))
- else:
- deleted.add("%s/%s" % (path, name,))
+ rev = self._revisionsSchema
+ results = [
+ (
+ self.name(),
+ name if name else "",
+ wasdeleted
+ )
+ for name, wasdeleted in
+ (yield Select([rev.RESOURCE_NAME, rev.DELETED],
+ From=rev,
+ Where=(rev.REVISION > revision).And(
+ rev.RESOURCE_ID == self._resourceID)).on(self._txn))
+ if name
+ ]
- # Always report collection as changed
- changed.add("%s/" % (path,))
+ for path, name, wasdeleted in results:
+ if wasdeleted:
+ if revision:
+ if depth == "1":
+ changed.add("%s/" % (path,))
+ else:
+ deleted.add("%s/%s" % (path, name,))
- # Resource changed - for depth "infinity" report resource as changed
- if depth != "1":
- changed.add("%s/%s" % (path, name,))
+ # Always report collection as changed
+ changed.add("%s/" % (path,))
- returnValue((changed, deleted))
+ if name:
+ # Resource changed - for depth "infinity" report resource as changed
+ if depth != "1":
+ changed.add("%s/%s" % (path, name,))
+ returnValue((changed, deleted, invalid,))
+
@inlineCallbacks
def _loadPropertyStore(self, props=None):
if props is None:
@@ -4656,10 +5091,85 @@
"_parentCollection",
)
+ _externalClass = None
_objectSchema = None
+ _componentClass = None
BATCH_LOAD_SIZE = 50
+
+ @classmethod
+ @inlineCallbacks
+ def makeClass(cls, parent, objectData, propstore=None):
+ """
+ Given the various database rows, build the actual class.
+
+ @param parent: the parent collection object
+ @type parent: L{CommonHomeChild}
+ @param objectData: the standard set of object columns
+ @type objectData: C{list}
+ @param propstore: a property store to use, or C{None} to load it automatically
+ @type propstore: L{PropertyStore}
+
+ @return: the constructed child class
+ @rtype: L{CommonHomeChild}
+ """
+
+ c = cls._externalClass if parent.external() else cls
+ child = c(
+ parent,
+ objectData[cls._allColumns().index(cls._objectSchema.RESOURCE_NAME)],
+ objectData[cls._allColumns().index(cls._objectSchema.UID)],
+ )
+
+ for attr, value in zip(child._rowAttributes(), objectData):
+ setattr(child, attr, value)
+
+ yield child._loadPropertyStore(propstore)
+
+ returnValue(child)
+
+
+ @classmethod
+ @inlineCallbacks
+ def _getDBData(cls, parent, name, uid, resourceID):
+ """
+ Given a set of identifying information, load the data rows for the object. Only one of
+ L{name}, L{uid} or L{resourceID} is specified - others are C{None}.
+
+ @param parent: the parent collection object
+ @type parent: L{CommonHomeChild}
+ @param name: the resource name
+ @type name: C{str}
+ @param uid: the UID of the data
+ @type uid: C{str}
+ @param resourceID: the resource ID
+ @type resourceID: C{int}
+ """
+
+ rows = None
+ if name:
+ rows = yield cls._allColumnsWithParentAndName.on(
+ parent._txn,
+ name=name,
+ parentID=parent._resourceID
+ )
+ elif uid:
+ rows = yield cls._allColumnsWithParentAndUID.on(
+ parent._txn,
+ uid=uid,
+ parentID=parent._resourceID
+ )
+ elif resourceID:
+ rows = yield cls._allColumnsWithParentAndID.on(
+ parent._txn,
+ resourceID=resourceID,
+ parentID=parent._resourceID
+ )
+
+ returnValue(rows[0] if rows else None)
+
+
def __init__(self, parent, name, uid, resourceID=None, options=None): #@UnusedVariable
self._parentCollection = parent
self._resourceID = resourceID
@@ -4669,7 +5179,8 @@
self._size = None
self._created = None
self._modified = None
- self._notificationData = None
+ self._textData = None
+ self._cachedComponent = None
self._locked = False
@@ -4677,7 +5188,7 @@
@classproperty
def _allColumnsWithParentQuery(cls): #@NoSelf
obj = cls._objectSchema
- return Select(cls._allColumns, From=obj,
+ return Select(cls._allColumns(), From=obj,
Where=obj.PARENT_RESOURCE_ID == Parameter("parentID"))
@@ -4718,11 +5229,10 @@
# Create the actual objects merging in properties
for row in dataRows:
- child = cls(parent, "", None)
- child._initFromRow(tuple(row))
- yield child._loadPropertyStore(
- props=propertyStores.get(child._resourceID, None)
- )
+ resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+ propstore = propertyStores.get(resourceID, None)
+
+ child = yield cls.makeClass(parent, row, propstore=propstore)
results.append(child)
returnValue(results)
@@ -4732,7 +5242,9 @@
@inlineCallbacks
def loadAllObjectsWithNames(cls, parent, names):
"""
- Load all child objects with the specified names, doing so in batches.
+ Load all child objects with the specified names, doing so in batches (because we need to match
+ using SQL "resource_name in (...)" where there might be a character length limit on the number
+ of items in the set).
"""
names = tuple(names)
results = []
@@ -4747,7 +5259,7 @@
@classmethod
def _allColumnsWithParentAndNamesQuery(cls, names):
obj = cls._objectSchema
- return Select(cls._allColumns, From=obj,
+ return Select(cls._allColumns(), From=obj,
Where=(obj.PARENT_RESOURCE_ID == Parameter("parentID")).And(
obj.RESOURCE_NAME.In(Parameter("names", len(names)))))
@@ -4771,7 +5283,7 @@
# Optimize case of single name to load
if len(names) == 1:
- obj = yield cls.objectWithName(parent, names[0], None)
+ obj = yield cls.objectWithName(parent, names[0])
returnValue([obj] if obj else [])
results = []
@@ -4792,30 +5304,61 @@
# Create the actual objects merging in properties
for row in dataRows:
- child = cls(parent, "", None)
- child._initFromRow(tuple(row))
- yield child._loadPropertyStore(
- props=propertyStores.get(child._resourceID, None)
- )
+ resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+ propstore = propertyStores.get(resourceID, None)
+
+ child = yield cls.makeClass(parent, row, propstore=propstore)
results.append(child)
returnValue(results)
@classmethod
- def objectWithName(cls, parent, name, uid):
- objectResource = cls(parent, name, uid, None)
- return objectResource.initFromStore()
+ def objectWithName(cls, parent, name):
+ return cls.objectWith(parent, name=name)
@classmethod
+ def objectWithUID(cls, parent, uid):
+ return cls.objectWith(parent, uid=uid)
+
+
+ @classmethod
def objectWithID(cls, parent, resourceID):
- objectResource = cls(parent, None, None, resourceID)
- return objectResource.initFromStore()
+ return cls.objectWith(parent, resourceID=resourceID)
@classmethod
@inlineCallbacks
+ def objectWith(cls, parent, name=None, uid=None, resourceID=None):
+ """
+ Create the object using one of the specified arguments as the key to load it. One
+ and only one of the keyword arguments must be set.
+
+ @param parent: parent collection
+ @type parent: L{CommonHomeChild}
+ @param name: name of the resource, or C{None}
+ @type name: C{str}
+ @param uid: resource data UID, or C{None}
+ @type uid: C{str}
+ @param resourceID: resource id
+ @type resourceID: C{int}
+
+ @return: the new object or C{None} if not found
+ @rtype: C{CommonObjectResource}
+ """
+
+ row = yield cls._getDBData(parent, name, uid, resourceID)
+
+ if row:
+ child = yield cls.makeClass(parent, row)
+ returnValue(child)
+ else:
+ returnValue(None)
+
+
+ @classmethod
+ @inlineCallbacks
def create(cls, parent, name, component, options=None):
child = (yield parent.objectResourceWithName(name))
@@ -4825,13 +5368,13 @@
if name.startswith("."):
raise ObjectResourceNameNotAllowedError(name)
- objectResource = cls(parent, name, None, None, options=options)
+ c = cls._externalClass if parent.external() else cls
+ objectResource = c(parent, name, None, None, options=options)
yield objectResource.setComponent(component, inserting=True)
yield objectResource._loadPropertyStore(created=True)
# Note: setComponent triggers a notification, so we don't need to
# call notify( ) here like we do for object removal.
-
returnValue(objectResource)
@@ -4842,7 +5385,7 @@
parameter and a given instance column matches a given parameter name.
"""
return Select(
- cls._allColumns, From=cls._objectSchema,
+ cls._allColumns(), From=cls._objectSchema,
Where=(column == Parameter(paramName)).And(
cls._objectSchema.PARENT_RESOURCE_ID == Parameter("parentID"))
)
@@ -4863,38 +5406,7 @@
return cls._allColumnsWithParentAnd(cls._objectSchema.RESOURCE_ID, "resourceID")
- @inlineCallbacks
- def initFromStore(self):
- """
- Initialise this object from the store. We read in and cache all the
- extra metadata from the DB to avoid having to do DB queries for those
- individually later. Either the name or uid is present, so we have to
- tweak the query accordingly.
-
- @return: L{self} if object exists in the DB, else C{None}
- """
-
- if self._name:
- rows = yield self._allColumnsWithParentAndName.on(
- self._txn, name=self._name,
- parentID=self._parentCollection._resourceID)
- elif self._uid:
- rows = yield self._allColumnsWithParentAndUID.on(
- self._txn, uid=self._uid,
- parentID=self._parentCollection._resourceID)
- elif self._resourceID:
- rows = yield self._allColumnsWithParentAndID.on(
- self._txn, resourceID=self._resourceID,
- parentID=self._parentCollection._resourceID)
- if rows:
- self._initFromRow(tuple(rows[0]))
- yield self._loadPropertyStore()
- returnValue(self)
- else:
- returnValue(None)
-
-
- @classproperty
+ @classmethod
def _allColumns(cls): #@NoSelf
"""
Full set of columns in the object table that need to be loaded to
@@ -4912,21 +5424,52 @@
]
- def _initFromRow(self, row):
+ @classmethod
+ def _rowAttributes(cls): #@NoSelf
+ return (
+ "_resourceID",
+ "_name",
+ "_uid",
+ "_md5",
+ "_size",
+ "_created",
+ "_modified",
+ )
+
+
+ @classmethod
+ def _otherSerializedAttributes(cls): #@NoSelf
+ return (
+ "_componentChanged",
+ )
+
+
+ def externalize(self):
"""
- Given a select result using the columns from L{_allColumns}, initialize
- the object resource state.
+ Create a dictionary mapping key attributes so this object can be sent over a cross-pod call
+ and reconstituted at the other end. Note that the other end may have a different schema so
+ the attributes may not match exactly and will need to be processed accordingly.
"""
- (self._resourceID,
- self._name,
- self._uid,
- self._md5,
- self._size,
- self._created,
- self._modified,) = tuple(row)
+ return dict([(attr[1:], getattr(self, attr, None)) for attr in itertools.chain(self._rowAttributes(), self._otherSerializedAttributes())])
+ @classmethod
@inlineCallbacks
+ def internalize(cls, parent, mapping):
+ """
+ Given a mapping generated by L{externalize}, convert the values into an array of database
+ like items that conforms to the ordering of L{_allColumns} so it can be fed into L{makeClass}.
+ Note that there may be a schema mismatch with the external data, so treat missing items as
+ C{None} and ignore extra items.
+ """
+
+ child = yield cls.makeClass(parent, [mapping.get(row[1:]) for row in cls._rowAttributes()])
+ for attr in cls._otherSerializedAttributes():
+ setattr(child, attr, mapping.get(attr[1:]))
+ returnValue(child)
+
+
+ @inlineCallbacks
def _loadPropertyStore(self, props=None, created=False):
if props is None:
if self._parentCollection.objectResourcesHaveProperties():
@@ -5036,7 +5579,7 @@
self._locked = True
- def setComponent(self, component, inserting=False, options=None):
+ def setComponent(self, component, inserting=False):
raise NotImplementedError
@@ -5069,9 +5612,15 @@
"""
yield self.moveValidation(destination, name)
- yield self._parentCollection.moveObjectResource(self, destination, name)
+ # If possible we do a "fast" move by simply fixing up the database information directly rather than
+ # re-writing any data. That is only possible when the source and destination are on this pod.
+ if not self._parentCollection.external() and not destination.external():
+ yield self._parentCollection.moveObjectResource(self, destination, name)
+ else:
+ yield self._parentCollection.moveObjectResourceCreateDelete(self, destination, name)
+
def moveValidation(self, destination, name):
raise NotImplementedError
@@ -5092,7 +5641,8 @@
self._size = None
self._created = None
self._modified = None
- self._notificationData = None
+ self._textData = None
+ self._cachedComponent = None
def removeNotifyCategory(self):
@@ -5148,19 +5698,19 @@
@inlineCallbacks
def _text(self):
- if self._notificationData is None:
+ if self._textData is None:
texts = (
yield self._textByIDQuery.on(self._txn,
resourceID=self._resourceID)
)
if texts:
text = texts[0][0]
- self._notificationData = text
+ self._textData = text
returnValue(text)
else:
raise ConcurrentModification()
else:
- returnValue(self._notificationData)
+ returnValue(self._textData)
@@ -5224,6 +5774,15 @@
resourceID = rows[0][0]
created = False
elif create:
+ # Determine if the user is local or external
+ record = txn.directoryService().recordWithUID(uid)
+ if record is None:
+ raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(uid))
+
+ state = _HOME_STATUS_NORMAL if record.thisServer() else _HOME_STATUS_EXTERNAL
+ if state == _HOME_STATUS_EXTERNAL:
+ raise RecordNotAllowedError("Cannot store notifications for external user: {}".format(uid))
+
# Use savepoint so we can do a partial rollback if there is a race
# condition where this row has already been inserted
savepoint = SavepointAction("notificationsWithUID")
Copied: CalendarServer/trunk/txdav/common/datastore/sql_external.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_external.py (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_external.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,470 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 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.
+##
+"""
+SQL data store.
+"""
+
+from twext.internet.decorate import memoizedKey
+from twext.python.log import Logger
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
+from txdav.base.propertystore.sql import PropertyStore
+from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
+ CommonObjectResource
+from txdav.common.datastore.sql_tables import _HOME_STATUS_EXTERNAL
+from txdav.common.icommondatastore import NonExistentExternalShare, \
+ ExternalShareFailed
+
+
+log = Logger()
+
+class CommonHomeExternal(CommonHome):
+ """
+ A CommonHome for a user not hosted on this system, but on another pod. This is needed to provide a
+ "reference" to the external user so we can share with them. Actual operations to list child resources, etc
+ are all stubbed out since no data for the user is actually hosted in this store.
+ """
+
+ def __init__(self, transaction, ownerUID, resourceID):
+ super(CommonHomeExternal, self).__init__(transaction, ownerUID)
+ self._resourceID = resourceID
+ self._status = _HOME_STATUS_EXTERNAL
+
+
+ def initFromStore(self, no_cache=False):
+ """
+ Never called - this should be done by CommonHome.initFromStore only.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def external(self):
+ """
+ Is this an external home.
+
+ @return: a string.
+ """
+ return True
+
+
+ def children(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def loadChildren(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def listChildren(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def objectWithShareUID(self, shareUID):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def invitedObjectWithShareUID(self, shareUID):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ @memoizedKey("name", "_children")
+ @inlineCallbacks
+ def createChildWithName(self, name, externalID=None):
+ """
+ No real children - only external ones.
+ """
+ if externalID is None:
+ raise AssertionError("CommonHomeExternal: not supported")
+ child = yield super(CommonHomeExternal, self).createChildWithName(name, externalID)
+ returnValue(child)
+
+
+ def removeChildWithName(self, name):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ @inlineCallbacks
+ def removeExternalChild(self, child):
+ """
+ Remove an external child. Check that it is invalid or unused before calling this because if there
+ are valid references to it, removing will break things.
+ """
+ if child._externalID is None:
+ raise AssertionError("CommonHomeExternal: not supported")
+ yield super(CommonHomeExternal, self).removeChildWithName(child.name())
+
+
+ def syncToken(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def resourceNamesSinceRevision(self, revision, depth):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ @inlineCallbacks
+ def _loadPropertyStore(self):
+ """
+ No property store - stub to a NonePropertyStore.
+ """
+ props = yield PropertyStore.load(
+ self.uid(),
+ self.uid(),
+ self._txn,
+ self._resourceID,
+ notifyCallback=self.notifyChanged
+ )
+ self._propertyStore = props
+
+
+ def properties(self):
+ return self._propertyStore
+
+
+ def objectResourcesWithUID(self, uid, ignore_children=[], allowShared=True):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def objectResourceWithID(self, rid):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def notifyChanged(self):
+ """
+ Notifications are not handled for external homes - make this a no-op.
+ """
+ return succeed(None)
+
+
+ def bumpModified(self):
+ """
+ No changes recorded for external homes - make this a no-op.
+ """
+ return succeed(None)
+
+
+ def removeUnacceptedShares(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+# def ownerHomeAndChildNameForChildID(self, resourceID):
+# """
+# No children.
+# """
+# raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class CommonHomeChildExternal(CommonHomeChild):
+ """
+ A CommonHomeChild for a collection not hosted on this system, but on another pod. This will forward
+ specific apis to the other pod using cross-pod requests.
+ """
+
+ def external(self):
+ """
+ Is this an external home.
+
+ @return: a string.
+ """
+ return True
+
+
+ def fixNonExistentExternalShare(self):
+ """
+ An external request has returned and indicates the external share no longer exists. That
+ means this shared resource is an "orphan" and needs to be remove (uninvited) to clean things up.
+ """
+ log.error("Non-existent share detected and removed for {share}", share=self)
+ ownerView = yield self.ownerView()
+ yield ownerView.removeShare(self)
+
+
+ @inlineCallbacks
+ def remove(self):
+ """
+ External shares are never removed directly - instead they must be "uninvited". However,
+ the owner's external calendar can be removed.
+ """
+ if self.owned():
+ yield super(CommonHomeChildExternal, self).remove()
+ else:
+ raise AssertionError("CommonHomeChildExternal: not supported")
+
+
+ @inlineCallbacks
+ def listObjectResources(self):
+ if self._objectNames is None:
+ try:
+ self._objectNames = yield self._txn.store().conduit.send_listobjects(self)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+
+ returnValue(self._objectNames)
+
+
+ @inlineCallbacks
+ def countObjectResources(self):
+ if self._objectNames is None:
+ try:
+ count = yield self._txn.store().conduit.send_countobjects(self)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+ returnValue(count)
+ returnValue(len(self._objectNames))
+
+
+ @inlineCallbacks
+ def resourceNameForUID(self, uid):
+ try:
+ resource = self._objects[uid]
+ returnValue(resource.name() if resource else None)
+ except KeyError:
+ pass
+
+ try:
+ name = yield self._txn.store().conduit.send_resourcenameforuid(self, uid)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+
+ if name:
+ returnValue(name)
+ else:
+ self._objects[uid] = None
+ returnValue(None)
+
+
+ @inlineCallbacks
+ def resourceUIDForName(self, name):
+ try:
+ resource = self._objects[name]
+ returnValue(resource.uid() if resource else None)
+ except KeyError:
+ pass
+
+ try:
+ uid = yield self._txn.store().conduit.send_resourceuidforname(self, name)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+
+ if uid:
+ returnValue(uid)
+ else:
+ self._objects[name] = None
+ returnValue(None)
+
+
+ @inlineCallbacks
+ def moveObjectResource(self, child, newparent, newname=None):
+ """
+ The base class does an optimization to avoid removing/re-creating
+ the actual object resource data. That might not always be possible
+ with external shares if the shared resource is moved to a collection
+ that is not shared or shared by someone else on a different (third)
+ pod. The best bet here is to treat the move as a delete/create.
+ """
+ raise NotImplementedError("TODO: external resource")
+
+
+ @inlineCallbacks
+ def moveObjectResourceHere(self, name, component):
+ """
+ Create a new child in this collection as part of a move operation. This needs to be split out because
+ behavior differs for sub-classes and cross-pod operations.
+
+ @param name: new name to use in new parent
+ @type name: C{str} or C{None} for existing name
+ @param component: data for new resource
+ @type component: L{Component}
+ """
+
+ try:
+ result = yield self._txn.store().conduit.send_movehere(self, name, str(component))
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+ returnValue(result)
+
+
+ @inlineCallbacks
+ def moveObjectResourceAway(self, rid, child=None):
+ """
+ Remove the child as the result of a move operation. This needs to be split out because
+ behavior differs for sub-classes and cross-pod operations.
+
+ @param rid: the child resource-id to move
+ @type rid: C{int}
+ @param child: the child resource to move - might be C{None} for cross-pod
+ @type child: L{CommonObjectResource}
+ """
+
+ try:
+ result = yield self._txn.store().conduit.send_moveaway(self, rid)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+ returnValue(result)
+
+
+ @inlineCallbacks
+ def syncToken(self):
+ if self._syncTokenRevision is None:
+ try:
+ token = yield self._txn.store().conduit.send_synctoken(self)
+ self._syncTokenRevision = self.revisionFromToken(token)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+ returnValue(("%s_%s" % (self._externalID, self._syncTokenRevision,)))
+
+
+ @inlineCallbacks
+ def resourceNamesSinceRevision(self, revision):
+ try:
+ names = yield self._txn.store().conduit.send_resourcenamessincerevision(self, revision)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+
+ returnValue(names)
+
+
+ @inlineCallbacks
+ def search(self, filter, **kwargs):
+ try:
+ results = yield self._txn.store().conduit.send_search(self, filter.serialize(), **kwargs)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+
+ returnValue(results)
+
+
+
+class CommonObjectResourceExternal(CommonObjectResource):
+ """
+ A CommonObjectResource for a resource not hosted on this system, but on another pod. This will forward
+ specific apis to the other pod using cross-pod requests.
+ """
+
+ @classmethod
+ @inlineCallbacks
+ def loadAllObjects(cls, parent):
+ mapping_list = yield parent._txn.store().conduit.send_loadallobjects(parent, None)
+
+ results = []
+ if mapping_list:
+ for mapping in mapping_list:
+ child = yield cls.internalize(parent, mapping)
+ results.append(child)
+ returnValue(results)
+
+
+ @classmethod
+ @inlineCallbacks
+ def loadAllObjectsWithNames(cls, parent, names):
+ mapping_list = yield parent._txn.store().conduit.send_loadallobjectswithnames(parent, None, names)
+
+ results = []
+ if mapping_list:
+ for mapping in mapping_list:
+ child = yield cls.internalize(parent, mapping)
+ results.append(child)
+ returnValue(results)
+
+
+ @classmethod
+ @inlineCallbacks
+ def objectWith(cls, parent, name=None, uid=None, resourceID=None):
+ mapping = yield parent._txn.store().conduit.send_objectwith(parent, None, name, uid, resourceID)
+
+ if mapping:
+ child = yield cls.internalize(parent, mapping)
+ returnValue(child)
+ else:
+ returnValue(None)
+
+
+ @classmethod
+ @inlineCallbacks
+ def create(cls, parent, name, component, options=None):
+ mapping = yield parent._txn.store().conduit.send_create(parent, None, name, str(component), options=options)
+
+ if mapping:
+ child = yield cls.internalize(parent, mapping)
+ returnValue(child)
+ else:
+ returnValue(None)
+
+
+ @inlineCallbacks
+ def setComponent(self, component, **kwargs):
+ self._componentChanged = yield self._txn.store().conduit.send_setcomponent(self.parentCollection(), self, str(component), **kwargs)
+ self._cachedComponent = None
+ returnValue(self._componentChanged)
+
+
+ @inlineCallbacks
+ def component(self):
+ if self._cachedComponent is None:
+ text = yield self._txn.store().conduit.send_component(self.parentCollection(), self)
+ self._cachedComponent = self._componentClass.fromString(text)
+
+ returnValue(self._cachedComponent)
+
+
+ @inlineCallbacks
+ def remove(self):
+ yield self._txn.store().conduit.send_remove(self.parentCollection(), self)
Deleted: CalendarServer/trunk/txdav/common/datastore/sql_legacy.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_legacy.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/sql_legacy.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,886 +0,0 @@
-# -*- test-case-name: twistedcaldav.test.test_sharing,twistedcaldav.test.test_calendarquery -*-
-##
-# Copyright (c) 2010-2013 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.
-##
-
-
-"""
-PostgreSQL data store.
-"""
-
-import StringIO
-
-
-from twisted.python import hashlib
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue
-
-from twistedcaldav.config import config
-from twistedcaldav.dateops import normalizeForIndex, pyCalendarTodatetime
-from twistedcaldav.memcachepool import CachePoolUserMixIn
-from twistedcaldav.query import \
- calendarqueryfilter, calendarquery, addressbookquery, expression, \
- addressbookqueryfilter
-from twistedcaldav.query.sqlgenerator import sqlgenerator
-
-from txdav.caldav.icalendarstore import TimeRangeLowerLimit, TimeRangeUpperLimit
-from txdav.common.icommondatastore import IndexedSearchException, \
- ReservationError, NoSuchObjectResourceError
-
-from txdav.common.datastore.sql_tables import schema
-from twext.enterprise.dal.syntax import Parameter, Select
-from twext.python.clsprop import classproperty
-from twext.python.log import Logger
-
-from pycalendar.datetime import DateTime
-from pycalendar.duration import Duration
-
-log = Logger()
-
-indexfbtype_to_icalfbtype = {
- 0: '?',
- 1: 'F',
- 2: 'B',
- 3: 'U',
- 4: 'T',
-}
-
-class MemcachedUIDReserver(CachePoolUserMixIn):
- log = Logger()
-
- def __init__(self, index, cachePool=None):
- self.index = index
- self._cachePool = cachePool
-
-
- def _key(self, uid):
- return 'reservation:%s' % (
- hashlib.md5('%s:%s' % (uid,
- self.index.resource._resourceID)).hexdigest())
-
-
- def reserveUID(self, uid):
- self.log.debug("Reserving UID %r @ %r" % (
- uid,
- self.index.resource))
-
- def _handleFalse(result):
- if result is False:
- raise ReservationError(
- "UID %s already reserved for calendar collection %s."
- % (uid, self.index.resource._name)
- )
-
- d = self.getCachePool().add(self._key(uid),
- 'reserved',
- expireTime=config.UIDReservationTimeOut)
- d.addCallback(_handleFalse)
- return d
-
-
- def unreserveUID(self, uid):
- self.log.debug("Unreserving UID %r @ %r" % (
- uid,
- self.index.resource))
-
- def _handleFalse(result):
- if result is False:
- raise ReservationError(
- "UID %s is not reserved for calendar collection %s."
- % (uid, self.index.resource._resourceID)
- )
-
- d = self.getCachePool().delete(self._key(uid))
- d.addCallback(_handleFalse)
- return d
-
-
- def isReservedUID(self, uid):
- self.log.debug("Is reserved UID %r @ %r" % (
- uid,
- self.index.resource))
-
- def _checkValue((flags, value)):
- if value is None:
- return False
- else:
- return True
-
- d = self.getCachePool().get(self._key(uid))
- d.addCallback(_checkValue)
- return d
-
-
-
-class DummyUIDReserver(object):
- log = Logger()
-
- def __init__(self, index):
- self.index = index
- self.reservations = set()
-
-
- def _key(self, uid):
- return 'reservation:%s' % (
- hashlib.md5('%s:%s' % (uid,
- self.index.resource._resourceID)).hexdigest())
-
-
- def reserveUID(self, uid):
- self.log.debug("Reserving UID %r @ %r" % (
- uid,
- self.index.resource))
-
- key = self._key(uid)
- if key in self.reservations:
- raise ReservationError(
- "UID %s already reserved for calendar collection %s."
- % (uid, self.index.resource._name)
- )
- self.reservations.add(key)
- return succeed(None)
-
-
- def unreserveUID(self, uid):
- self.log.debug("Unreserving UID %r @ %r" % (
- uid,
- self.index.resource))
-
- key = self._key(uid)
- if key in self.reservations:
- self.reservations.remove(key)
- return succeed(None)
-
-
- def isReservedUID(self, uid):
- self.log.debug("Is reserved UID %r @ %r" % (
- uid,
- self.index.resource))
- key = self._key(uid)
- return succeed(key in self.reservations)
-
-
-
-class RealSQLBehaviorMixin(object):
- """
- Class attributes for 'real' SQL behavior; avoid idiosyncracies of SQLite,
- use standard SQL constructions, and depend on the full schema in
- sql_schema/current.sql rather than the partial one in twistedcaldav which depends
- on the placement of the database in the filesystem for some information.
- """
-
- ISOP = " = "
- 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",
- }
- RESOURCEDB = "CALENDAR_OBJECT"
- TIMESPANDB = "TIME_RANGE"
-
- TIMESPANTEST = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s))"
- TIMESPANTEST_NOEND = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.END_DATE > %s))"
- TIMESPANTEST_NOSTART = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s))"
- TIMESPANTEST_TAIL_PIECE = " AND TIME_RANGE.CALENDAR_OBJECT_RESOURCE_ID = CALENDAR_OBJECT.RESOURCE_ID AND TIME_RANGE.CALENDAR_RESOURCE_ID = %s"
- TIMESPANTEST_JOIN_ON_PIECE = "TIME_RANGE.INSTANCE_ID = TRANSPARENCY.TIME_RANGE_INSTANCE_ID AND TRANSPARENCY.USER_ID = %s"
-
- def generate(self):
- """
- Generate the actual SQL 'where ...' expression from the passed in
- expression tree.
-
- @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
- partial SQL statement, and the C{list} is the list of argument
- substitutions to use with the SQL API execute method.
- """
-
- # Init state
- self.sout = StringIO.StringIO()
- self.arguments = []
- 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:
- # timerange expression forces a join on calendarid
- pass
- else:
- # AND the whole thing with calendarid
- 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.generateExpression(self.expression)
-
- # Prefix with ' from ...' partial statement
- select = self.FROM + self.RESOURCEDB
- if self.usedtimespan:
-
- # Free busy needs transparency join
- if self.freebusy:
- self.frontArgument(self.userid)
- select += ", %s LEFT OUTER JOIN %s ON (%s)" % (
- self.TIMESPANDB,
- self.TRANSPARENCYDB,
- self.TIMESPANTEST_JOIN_ON_PIECE
- )
- else:
- select += ", %s" % (
- self.TIMESPANDB,
- )
- select += self.WHERE
- if self.usedtimespan:
- select += "("
- select += self.sout.getvalue()
- if self.usedtimespan:
- if self.calendarid:
- self.setArgument(self.calendarid)
- select += ")%s" % (self.TIMESPANTEST_TAIL_PIECE,)
-
- select = select % tuple(self.substitutions)
-
- return select, self.arguments
-
-
-
-class FormatParamStyleMixin(object):
- """
- Mixin for overriding methods on sqlgenerator that generate arguments
- according to format/pyformat rules rather than the base class's 'numeric'
- rules.
- """
-
- def addArgument(self, arg):
- self.arguments.append(arg)
- self.substitutions.append("%s")
- self.sout.write("%s")
-
-
- def setArgument(self, arg):
- self.arguments.append(arg)
- self.substitutions.append("%s")
-
-
- def frontArgument(self, arg):
- self.arguments.insert(0, arg)
- self.substitutions.insert(0, "%s")
-
-
-
-class postgresqlgenerator(FormatParamStyleMixin, CalDAVSQLBehaviorMixin,
- sqlgenerator):
- """
- Query generator for PostgreSQL indexed searches.
- """
-
-
-
-def fixbools(sqltext):
- return sqltext.replace("TRUE", "1").replace("FALSE", "0")
-
-
-
-class oraclesqlgenerator(CalDAVSQLBehaviorMixin, sqlgenerator):
- """
- Query generator for Oracle indexed searches.
- """
- TIMESPANTEST = fixbools(CalDAVSQLBehaviorMixin.TIMESPANTEST)
- TIMESPANTEST_NOEND = fixbools(CalDAVSQLBehaviorMixin.TIMESPANTEST_NOEND)
- TIMESPANTEST_NOSTART = fixbools(CalDAVSQLBehaviorMixin.TIMESPANTEST_NOSTART)
- TIMESPANTEST_TAIL_PIECE = fixbools(
- CalDAVSQLBehaviorMixin.TIMESPANTEST_TAIL_PIECE)
- TIMESPANTEST_JOIN_ON_PIECE = fixbools(
- CalDAVSQLBehaviorMixin.TIMESPANTEST_JOIN_ON_PIECE)
-
-
-
-class LegacyIndexHelper(object):
- log = Logger()
-
- @inlineCallbacks
- def isAllowedUID(self, uid, *names):
- """
- Checks to see whether to allow an operation which would add the
- specified UID to the index. Specifically, the operation may not
- violate the constraint that UIDs must be unique.
- @param uid: the UID to check
- @param names: the names of resources being replaced or deleted by the
- operation; UIDs associated with these resources are not checked.
- @return: True if the UID is not in the index and is not reserved,
- False otherwise.
- """
- rname = yield self.resourceNameForUID(uid)
- returnValue(rname is None or rname in names)
-
-
- def reserveUID(self, uid):
- return self.reserver.reserveUID(uid)
-
-
- def unreserveUID(self, uid):
- return self.reserver.unreserveUID(uid)
-
-
- def isReservedUID(self, uid):
- return self.reserver.isReservedUID(uid)
-
-
-
-class PostgresLegacyIndexEmulator(LegacyIndexHelper):
- """
- Emulator for L{twistedcaldv.index.Index} and
- L{twistedcaldv.index.IndexSchedule}.
- """
-
- def __init__(self, calendar):
- self.resource = self.calendar = calendar
- if (
- hasattr(config, "Memcached") and
- config.Memcached.Pools.Default.ClientEnabled
- ):
- self.reserver = MemcachedUIDReserver(self)
- else:
- # This is only used with unit tests
- self.reserver = DummyUIDReserver(self)
-
- _objectSchema = schema.CALENDAR_OBJECT
-
- @property
- def _txn(self):
- return self.calendar._txn
-
-
- @inlineCallbacks
- def isAllowedUID(self, uid, *names):
- """
- Checks to see whether to allow an operation which would add the
- specified UID to the index. Specifically, the operation may not
- violate the constraint that UIDs must be unique.
- @param uid: the UID to check
- @param names: the names of resources being replaced or deleted by the
- operation; UIDs associated with these resources are not checked.
- @return: True if the UID is not in the index and is not reserved,
- False otherwise.
- """
- rname = yield self.resourceNameForUID(uid)
- returnValue(rname is None or rname in names)
-
-
- @inlineCallbacks
- def resourceUIDForName(self, name):
- uid = yield self.calendar.resourceUIDForName(name)
- returnValue(uid)
-
-
- @inlineCallbacks
- def resourceNameForUID(self, uid):
- name = yield self.calendar.resourceNameForUID(uid)
- returnValue(name)
-
-
- @classproperty
- def _notExpandedWithinQuery(cls): #@NoSelf
- """
- DAL query to satisfy L{PostgresLegacyIndexEmulator.notExpandedBeyond}.
- """
- co = schema.CALENDAR_OBJECT
- return Select(
- [co.RESOURCE_NAME],
- From=co,
- Where=((co.RECURRANCE_MIN > Parameter("minDate"))
- .Or(co.RECURRANCE_MAX < Parameter("maxDate")))
- .And(co.CALENDAR_RESOURCE_ID == Parameter("resourceID"))
- )
-
-
- @inlineCallbacks
- def notExpandedWithin(self, minDate, maxDate):
- """
- Gives all resources which have not been expanded beyond a given date
- in the database. (Unused; see above L{postgresqlgenerator}.
- """
- returnValue([row[0] for row in (
- yield self._notExpandedWithinQuery.on(
- self._txn,
- minDate=pyCalendarTodatetime(normalizeForIndex(minDate)) if minDate is not None else None,
- maxDate=pyCalendarTodatetime(normalizeForIndex(maxDate)),
- resourceID=self.calendar._resourceID))]
- )
-
-
- @inlineCallbacks
- def reExpandResource(self, name, expand_start, expand_end):
- """
- Given a resource name, remove it from the database and re-add it
- with a longer expansion.
- """
- obj = yield self.calendar.calendarObjectWithName(name)
-
- # Use a new transaction to do this update quickly without locking the row for too long. However, the original
- # transaction may have the row locked, so use wait=False and if that fails, fall back to using the original txn.
-
- newTxn = obj.transaction().store().newTransaction()
- try:
- yield obj.lock(wait=False, txn=newTxn)
- except NoSuchObjectResourceError:
- yield newTxn.commit()
- returnValue(None)
- except:
- yield newTxn.abort()
- newTxn = None
-
- # Now do the re-expand using the appropriate transaction
- try:
- doExpand = False
- if newTxn is None:
- doExpand = True
- else:
- # We repeat this check because the resource may have been re-expanded by someone else
- rmin, rmax = (yield obj.recurrenceMinMax(txn=newTxn))
-
- # If the resource is not fully expanded, see if within the required range or not.
- # Note that expand_start could be None if no lower limit is applied, but expand_end will
- # never be None
- if rmax is not None and rmax < expand_end:
- doExpand = True
- if rmin is not None and expand_start is not None and rmin > expand_start:
- doExpand = True
-
- if doExpand:
- yield obj.updateDatabase(
- (yield obj.component()),
- expand_until=expand_end,
- reCreate=True,
- txn=newTxn,
- )
- finally:
- if newTxn is not None:
- yield newTxn.commit()
-
-
- @inlineCallbacks
- def testAndUpdateIndex(self, minDate, maxDate):
- # Find out if the index is expanded far enough
- names = yield self.notExpandedWithin(minDate, maxDate)
-
- # Actually expand recurrence max
- for name in names:
- self.log.info("Search falls outside range of index for %s %s to %s" %
- (name, minDate, maxDate))
- yield self.reExpandResource(name, minDate, maxDate)
-
-
- @inlineCallbacks
- def indexedSearch(self, filter, useruid='', fbtype=False):
- """
- Finds resources matching the given qualifiers.
-
- @param filter: the L{Filter} for the calendar-query to execute.
-
- @return: a L{Deferred} which fires with an iterable of tuples for each
- resource matching the given C{qualifiers}. The tuples are C{(name,
- uid, type)}, where C{name} is the resource name, C{uid} is the
- resource UID, and C{type} is the resource iCalendar component type.
- """
- # Detect which style of parameter-generation we're using. Naming is a
- # little off here, because the reason we're using the numeric one is
- # that it happens to be used by the oracle binding that we're using,
- # whereas the postgres binding happens to use the 'pyformat' (e.g. %s)
- # parameter style.
- if self.calendar._txn.paramstyle == 'numeric':
- generator = oraclesqlgenerator
- else:
- generator = postgresqlgenerator
- # Make sure we have a proper Filter element and get the partial SQL
- # statement to use.
- if isinstance(filter, calendarqueryfilter.Filter):
- qualifiers = calendarquery.sqlcalendarquery(
- filter, self.calendar._resourceID, useruid, fbtype,
- generator=generator
- )
- if qualifiers is not None:
-
- today = DateTime.getToday()
-
- # Determine how far we need to extend the current expansion of
- # events. If we have an open-ended time-range we will expand
- # one year past the start. That should catch bounded
- # recurrences - unbounded will have been indexed with an
- # "infinite" value always included.
- maxDate, isStartDate = filter.getmaxtimerange()
- if maxDate:
- maxDate = maxDate.duplicate()
- maxDate.offsetDay(1)
- maxDate.setDateOnly(True)
- upperLimit = today + Duration(days=config.FreeBusyIndexExpandMaxDays)
- if maxDate > upperLimit:
- raise TimeRangeUpperLimit(upperLimit)
- if isStartDate:
- maxDate += Duration(days=365)
-
- # Determine if the start date is too early for the restricted range we
- # are applying. If it is today or later we don't need to worry about truncation
- # in the past.
- minDate, _ignore_isEndDate = filter.getmintimerange()
- if minDate >= today:
- minDate = None
- if minDate is not None and config.FreeBusyIndexLowerLimitDays:
- truncateLowerLimit = today - Duration(days=config.FreeBusyIndexLowerLimitDays)
- if minDate < truncateLowerLimit:
- raise TimeRangeLowerLimit(truncateLowerLimit)
-
- if maxDate is not None or minDate is not None:
- yield self.testAndUpdateIndex(minDate, maxDate)
-
- else:
- # We cannot handle this filter in an indexed search
- raise IndexedSearchException()
- else:
- qualifiers = None
-
- # Perform the search
- if qualifiers is None:
- rowiter = yield self.bruteForceSearch()
- else:
- if fbtype:
- # For a free-busy time-range query we return all instances
- rowiter = yield self._txn.execSQL(
- """
- select DISTINCT
- CALENDAR_OBJECT.RESOURCE_NAME,
- CALENDAR_OBJECT.ICALENDAR_UID,
- CALENDAR_OBJECT.ICALENDAR_TYPE,
- CALENDAR_OBJECT.ORGANIZER,
- TIME_RANGE.FLOATING, TIME_RANGE.START_DATE,
- TIME_RANGE.END_DATE, TIME_RANGE.FBTYPE,
- TIME_RANGE.TRANSPARENT, TRANSPARENCY.TRANSPARENT
- """ +
- qualifiers[0],
- qualifiers[1]
- )
- else:
- rowiter = yield self._txn.execSQL(
- """
- select
- DISTINCT CALENDAR_OBJECT.RESOURCE_NAME,
- CALENDAR_OBJECT.ICALENDAR_UID,
- CALENDAR_OBJECT.ICALENDAR_TYPE
- """ +
- qualifiers[0],
- qualifiers[1]
- )
-
- # Check result for missing resources
-
- results = []
- for row in rowiter:
- if fbtype:
- row = list(row)
- row[4] = 'Y' if row[4] else 'N'
- row[7] = indexfbtype_to_icalfbtype[row[7]]
- if row[9] is not None:
- row[8] = row[9]
- row[8] = 'T' if row[8] else 'F'
- del row[9]
- results.append(row)
- returnValue(results)
-
-
- @classproperty
- def _bruteForceQuery(cls): #@NoSelf
- """
- DAL query for all C{CALENDAR_OBJECT} rows in the calendar represented by
- this index.
- """
- obj = cls._objectSchema
- return Select(
- [obj.RESOURCE_NAME, obj.ICALENDAR_UID, obj.ICALENDAR_TYPE],
- From=obj, Where=obj.PARENT_RESOURCE_ID == Parameter("resourceID")
- )
-
-
- def bruteForceSearch(self):
- return self._bruteForceQuery.on(
- self._txn, resourceID=self.resource._resourceID)
-
-
- @inlineCallbacks
- def resourcesExist(self, names):
- returnValue(list(set(names).intersection(
- set((yield self.calendar.listCalendarObjects())))))
-
-
- @classproperty
- def _resourceExistsQuery(cls): #@NoSelf
- """
- DAL query to determine whether a calendar object exists in the
- collection represented by this index.
- """
- obj = cls._objectSchema
- return Select(
- [obj.RESOURCE_NAME], From=obj,
- Where=(obj.RESOURCE_NAME == Parameter("name"))
- .And(obj.PARENT_RESOURCE_ID == Parameter("resourceID"))
- )
-
-
- @inlineCallbacks
- def resourceExists(self, name):
- returnValue((bool(
- (yield self._resourceExistsQuery.on(
- self._txn, name=name, resourceID=self.resource._resourceID))
- )))
-
-
-
-class PostgresLegacyInboxIndexEmulator(PostgresLegacyIndexEmulator):
- """
- UIDs need not be unique in the 'inbox' calendar, so override those
- behaviors intended to ensure that.
- """
-
- def isAllowedUID(self):
- return succeed(True)
-
-
- def reserveUID(self, uid):
- return succeed(None)
-
-
- def unreserveUID(self, uid):
- return succeed(None)
-
-
- def isReservedUID(self, uid):
- return succeed(False)
-
-
-
-# CARDDAV
-
-class CardDAVSQLBehaviorMixin(RealSQLBehaviorMixin):
- """
- Query generator for CardDAV indexed searches.
- """
-
- FIELDS = {
- "UID": "ADDRESSBOOK_OBJECT.VCARD_UID",
- }
- RESOURCEDB = "ADDRESSBOOK_OBJECT"
-
- def generate(self):
- """
- Generate the actual SQL 'where ...' expression from the passed in
- expression tree.
-
- @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
- partial SQL statement, and the C{list} is the list of argument
- substitutions to use with the SQL API execute method.
- """
-
- # Init state
- self.sout = StringIO.StringIO()
- 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_HOME_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)
-
- # Prefix with ' from ...' partial statement
- select = self.FROM + self.RESOURCEDB
- select += self.sout.getvalue()
-
- select = select % tuple(self.substitutions)
-
- return select, self.arguments
-
-
-
-class postgresqladbkgenerator(FormatParamStyleMixin, CardDAVSQLBehaviorMixin, sqlgenerator):
- """
- 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
- L{twistedcaldv.index.IndexSchedule}.
- """
-
- _objectSchema = schema.ADDRESSBOOK_OBJECT
-
- def __init__(self, addressbook):
- self.resource = self.addressbook = addressbook
- if (
- hasattr(config, "Memcached") and
- config.Memcached.Pools.Default.ClientEnabled
- ):
- self.reserver = MemcachedUIDReserver(self)
- else:
- # This is only used with unit tests
- self.reserver = DummyUIDReserver(self)
-
-
- @property
- def _txn(self):
- return self.addressbook._txn
-
-
- @inlineCallbacks
- def resourceUIDForName(self, name):
- obj = yield self.addressbook.addressbookObjectWithName(name)
- if obj is None:
- returnValue(None)
- returnValue(obj.uid())
-
-
- @inlineCallbacks
- def resourceNameForUID(self, uid):
- obj = yield self.addressbook.addressbookObjectWithUID(uid)
- if obj is None:
- returnValue(None)
- returnValue(obj.name())
-
-
- def searchValid(self, filter):
- if isinstance(filter, addressbookqueryfilter.Filter):
- qualifiers = addressbookquery.sqladdressbookquery(filter)
- else:
- qualifiers = None
-
- return qualifiers is not None
-
-
- @inlineCallbacks
- def search(self, filter):
- """
- Finds resources matching the given qualifiers.
- @param filter: the L{Filter} for the addressbook-query to execute.
- @return: an iterable of tuples for each resource matching the
- given C{qualifiers}. The tuples are C{(name, uid, type)}, where
- C{name} is the resource name, C{uid} is the resource UID, and
- C{type} is the resource iCalendar component type.x
- """
- if self.addressbook._txn.paramstyle == 'numeric':
- generator = oraclesqladbkgenerator
- else:
- generator = postgresqladbkgenerator
- # Make sure we have a proper Filter element and get the partial SQL statement to use.
- if isinstance(filter, addressbookqueryfilter.Filter):
- qualifiers = addressbookquery.sqladdressbookquery(
- filter, self.addressbook._resourceID, generator=generator)
- else:
- qualifiers = None
- if qualifiers is not None:
- rowiter = yield self._txn.execSQL(
- "select DISTINCT ADDRESSBOOK_OBJECT.RESOURCE_NAME, ADDRESSBOOK_OBJECT.VCARD_UID" +
- qualifiers[0],
- qualifiers[1]
- )
- else:
- rowiter = yield Select(
- [self._objectSchema.RESOURCE_NAME,
- self._objectSchema.VCARD_UID],
- From=self._objectSchema,
- Where=self._objectSchema.ADDRESSBOOK_HOME_RESOURCE_ID ==
- self.addressbook._resourceID
- ).on(self.addressbook._txn)
-
- returnValue(list(rowiter))
-
-
- def indexedSearch(self, filter, useruid='', fbtype=False):
- """
- Always raise L{IndexedSearchException}, since these indexes are not
- fully implemented yet.
- """
- raise IndexedSearchException()
-
-
- @inlineCallbacks
- def resourcesExist(self, names):
- returnValue(list(set(names).intersection(
- set((yield self.addressbook.listAddressBookObjects())))))
Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql 2014-01-02 17:19:18 UTC (rev 12211)
@@ -18,9 +18,17 @@
create table CALENDAR_HOME (
"RESOURCE_ID" integer primary key,
"OWNER_UID" nvarchar2(255) unique,
+ "STATUS" integer default 0 not null,
"DATAVERSION" integer default 0 not null
);
+create table HOME_STATUS (
+ "ID" integer primary key,
+ "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into HOME_STATUS (DESCRIPTION, ID) values ('normal', 0);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('external', 1);
create table CALENDAR (
"RESOURCE_ID" integer primary key
);
@@ -50,6 +58,7 @@
create table NOTIFICATION_HOME (
"RESOURCE_ID" integer primary key,
"OWNER_UID" nvarchar2(255) unique,
+ "STATUS" integer default 0 not null,
"DATAVERSION" integer default 0 not null
);
@@ -210,6 +219,7 @@
"RESOURCE_ID" integer primary key,
"ADDRESSBOOK_PROPERTY_STORE_ID" integer not null,
"OWNER_UID" nvarchar2(255) unique,
+ "STATUS" integer default 0 not null,
"DATAVERSION" integer default 0 not null
);
@@ -373,7 +383,7 @@
"VALUE" nvarchar2(255)
);
-insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '30');
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '31');
insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '5');
insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');
Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql 2014-01-02 17:19:18 UTC (rev 12211)
@@ -53,10 +53,22 @@
create table CALENDAR_HOME (
RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
- OWNER_UID varchar(255) not null unique, -- implicit index
+ OWNER_UID varchar(255) not null unique, -- implicit index
+ STATUS integer default 0 not null, -- enum HOME_STATUS
DATAVERSION integer default 0 not null
);
+-- Enumeration of statuses
+
+create table HOME_STATUS (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into HOME_STATUS values (0, 'normal' );
+insert into HOME_STATUS values (1, 'external');
+
+
--------------
-- Calendar --
--------------
@@ -65,6 +77,7 @@
RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
);
+
----------------------------
-- Calendar Home Metadata --
----------------------------
@@ -91,6 +104,7 @@
create index CALENDAR_HOME_METADATA_DEFAULT_POLLS on
CALENDAR_HOME_METADATA(DEFAULT_POLLS);
+
-----------------------
-- Calendar Metadata --
-----------------------
@@ -110,6 +124,7 @@
create table NOTIFICATION_HOME (
RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
OWNER_UID varchar(255) not null unique, -- implicit index
+ STATUS integer default 0 not null, -- enum HOME_STATUS
DATAVERSION integer default 0 not null
);
@@ -139,6 +154,7 @@
create table CALENDAR_BIND (
CALENDAR_HOME_RESOURCE_ID integer not null references CALENDAR_HOME,
CALENDAR_RESOURCE_ID integer not null references CALENDAR on delete cascade,
+ EXTERNAL_ID integer default null,
CALENDAR_RESOURCE_NAME varchar(255) not null,
BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
@@ -380,6 +396,7 @@
RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
ADDRESSBOOK_PROPERTY_STORE_ID integer default nextval('RESOURCE_ID_SEQ') not null, -- implicit index
OWNER_UID varchar(255) not null unique, -- implicit index
+ STATUS integer default 0 not null, -- enum HOME_STATUS
DATAVERSION integer default 0 not null
);
@@ -405,6 +422,7 @@
create table SHARED_ADDRESSBOOK_BIND (
ADDRESSBOOK_HOME_RESOURCE_ID integer not null references ADDRESSBOOK_HOME,
OWNER_HOME_RESOURCE_ID integer not null references ADDRESSBOOK_HOME on delete cascade,
+ EXTERNAL_ID integer default null,
ADDRESSBOOK_RESOURCE_NAME varchar(255) not null,
BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
@@ -503,6 +521,7 @@
create table SHARED_GROUP_BIND (
ADDRESSBOOK_HOME_RESOURCE_ID integer not null references ADDRESSBOOK_HOME,
GROUP_RESOURCE_ID integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+ EXTERNAL_ID integer default null,
GROUP_ADDRESSBOOK_NAME varchar(255) not null,
BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
@@ -711,7 +730,7 @@
VALUE varchar(255)
);
-insert into CALENDARSERVER values ('VERSION', '30');
+insert into CALENDARSERVER values ('VERSION', '31');
insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '5');
insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');
Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v30.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v30.sql (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v30.sql 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,508 @@
+create sequence RESOURCE_ID_SEQ;
+create sequence INSTANCE_ID_SEQ;
+create sequence ATTACHMENT_ID_SEQ;
+create sequence REVISION_SEQ;
+create sequence WORKITEM_SEQ;
+create table NODE_INFO (
+ "HOSTNAME" nvarchar2(255),
+ "PID" integer not null,
+ "PORT" integer not null,
+ "TIME" timestamp default CURRENT_TIMESTAMP at time zone 'UTC' not null,
+ primary key("HOSTNAME", "PORT")
+);
+
+create table NAMED_LOCK (
+ "LOCK_NAME" nvarchar2(255) primary key
+);
+
+create table CALENDAR_HOME (
+ "RESOURCE_ID" integer primary key,
+ "OWNER_UID" nvarchar2(255) unique,
+ "DATAVERSION" integer default 0 not null
+);
+
+create table CALENDAR (
+ "RESOURCE_ID" integer primary key
+);
+
+create table CALENDAR_HOME_METADATA (
+ "RESOURCE_ID" integer primary key references CALENDAR_HOME on delete cascade,
+ "QUOTA_USED_BYTES" integer default 0 not null,
+ "DEFAULT_EVENTS" integer default null references CALENDAR on delete set null,
+ "DEFAULT_TASKS" integer default null references CALENDAR on delete set null,
+ "DEFAULT_POLLS" integer default null references CALENDAR on delete set null,
+ "ALARM_VEVENT_TIMED" nclob default null,
+ "ALARM_VEVENT_ALLDAY" nclob default null,
+ "ALARM_VTODO_TIMED" nclob default null,
+ "ALARM_VTODO_ALLDAY" nclob default null,
+ "AVAILABILITY" nclob default null,
+ "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table CALENDAR_METADATA (
+ "RESOURCE_ID" integer primary key references CALENDAR on delete cascade,
+ "SUPPORTED_COMPONENTS" nvarchar2(255) default null,
+ "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table NOTIFICATION_HOME (
+ "RESOURCE_ID" integer primary key,
+ "OWNER_UID" nvarchar2(255) unique,
+ "DATAVERSION" integer default 0 not null
+);
+
+create table NOTIFICATION (
+ "RESOURCE_ID" integer primary key,
+ "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME,
+ "NOTIFICATION_UID" nvarchar2(255),
+ "NOTIFICATION_TYPE" nvarchar2(255),
+ "NOTIFICATION_DATA" nclob,
+ "MD5" nchar(32),
+ "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ unique("NOTIFICATION_UID", "NOTIFICATION_HOME_RESOURCE_ID")
+);
+
+create table CALENDAR_BIND (
+ "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+ "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+ "CALENDAR_RESOURCE_NAME" nvarchar2(255),
+ "BIND_MODE" integer not null,
+ "BIND_STATUS" integer not null,
+ "BIND_REVISION" integer default 0 not null,
+ "MESSAGE" nclob,
+ "TRANSP" integer default 0 not null,
+ "ALARM_VEVENT_TIMED" nclob default null,
+ "ALARM_VEVENT_ALLDAY" nclob default null,
+ "ALARM_VTODO_TIMED" nclob default null,
+ "ALARM_VTODO_ALLDAY" nclob default null,
+ "TIMEZONE" nclob default null,
+ primary key("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_ID"),
+ unique("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_NAME")
+);
+
+create table CALENDAR_BIND_MODE (
+ "ID" integer primary key,
+ "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('own', 0);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('write', 2);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('direct', 3);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('indirect', 4);
+create table CALENDAR_BIND_STATUS (
+ "ID" integer primary key,
+ "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invited', 0);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('accepted', 1);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('declined', 2);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invalid', 3);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('deleted', 4);
+create table CALENDAR_TRANSP (
+ "ID" integer primary key,
+ "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('opaque', 0);
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('transparent', 1);
+create table CALENDAR_OBJECT (
+ "RESOURCE_ID" integer primary key,
+ "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+ "RESOURCE_NAME" nvarchar2(255),
+ "ICALENDAR_TEXT" nclob,
+ "ICALENDAR_UID" nvarchar2(255),
+ "ICALENDAR_TYPE" nvarchar2(255),
+ "ATTACHMENTS_MODE" integer default 0 not null,
+ "DROPBOX_ID" nvarchar2(255),
+ "ORGANIZER" nvarchar2(255),
+ "RECURRANCE_MIN" date,
+ "RECURRANCE_MAX" date,
+ "ACCESS" integer default 0 not null,
+ "SCHEDULE_OBJECT" integer default 0,
+ "SCHEDULE_TAG" nvarchar2(36) default null,
+ "SCHEDULE_ETAGS" nclob default null,
+ "PRIVATE_COMMENTS" integer default 0 not null,
+ "MD5" nchar(32),
+ "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ unique("CALENDAR_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MO (
+ "ID" integer primary key,
+ "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('none', 0);
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('write', 2);
+create table CALENDAR_ACCESS_TYPE (
+ "ID" integer primary key,
+ "DESCRIPTION" nvarchar2(32) unique
+);
+
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('', 0);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('public', 1);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('private', 2);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('confidential', 3);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('restricted', 4);
+create table TIME_RANGE (
+ "INSTANCE_ID" integer primary key,
+ "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+ "CALENDAR_OBJECT_RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+ "FLOATING" integer not null,
+ "START_DATE" timestamp not null,
+ "END_DATE" timestamp not null,
+ "FBTYPE" integer not null,
+ "TRANSPARENT" integer not null
+);
+
+create table FREE_BUSY_TYPE (
+ "ID" integer primary key,
+ "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('unknown', 0);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('free', 1);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy', 2);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-unavailable', 3);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-tentative', 4);
+create table TRANSPARENCY (
+ "TIME_RANGE_INSTANCE_ID" integer not null references TIME_RANGE on delete cascade,
+ "USER_ID" nvarchar2(255),
+ "TRANSPARENT" integer not null
+);
+
+create table ATTACHMENT (
+ "ATTACHMENT_ID" integer primary key,
+ "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+ "DROPBOX_ID" nvarchar2(255),
+ "CONTENT_TYPE" nvarchar2(255),
+ "SIZE" integer not null,
+ "MD5" nchar(32),
+ "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "PATH" nvarchar2(1024)
+);
+
+create table ATTACHMENT_CALENDAR_OBJECT (
+ "ATTACHMENT_ID" integer not null references ATTACHMENT on delete cascade,
+ "MANAGED_ID" nvarchar2(255),
+ "CALENDAR_OBJECT_RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+ primary key("ATTACHMENT_ID", "CALENDAR_OBJECT_RESOURCE_ID"),
+ unique("MANAGED_ID", "CALENDAR_OBJECT_RESOURCE_ID")
+);
+
+create table RESOURCE_PROPERTY (
+ "RESOURCE_ID" integer not null,
+ "NAME" nvarchar2(255),
+ "VALUE" nclob,
+ "VIEWER_UID" nvarchar2(255),
+ primary key("RESOURCE_ID", "NAME", "VIEWER_UID")
+);
+
+create table ADDRESSBOOK_HOME (
+ "RESOURCE_ID" integer primary key,
+ "ADDRESSBOOK_PROPERTY_STORE_ID" integer not null,
+ "OWNER_UID" nvarchar2(255) unique,
+ "DATAVERSION" integer default 0 not null
+);
+
+create table ADDRESSBOOK_HOME_METADATA (
+ "RESOURCE_ID" integer primary key references ADDRESSBOOK_HOME on delete cascade,
+ "QUOTA_USED_BYTES" integer default 0 not null,
+ "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table SHARED_ADDRESSBOOK_BIND (
+ "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+ "OWNER_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+ "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
+ "BIND_MODE" integer not null,
+ "BIND_STATUS" integer not null,
+ "BIND_REVISION" integer default 0 not null,
+ "MESSAGE" nclob,
+ primary key("ADDRESSBOOK_HOME_RESOURCE_ID", "OWNER_HOME_RESOURCE_ID"),
+ unique("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_NAME")
+);
+
+create table ADDRESSBOOK_OBJECT (
+ "RESOURCE_ID" integer primary key,
+ "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+ "RESOURCE_NAME" nvarchar2(255),
+ "VCARD_TEXT" nclob,
+ "VCARD_UID" nvarchar2(255),
+ "KIND" integer not null,
+ "MD5" nchar(32),
+ "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ unique("ADDRESSBOOK_HOME_RESOURCE_ID", "RESOURCE_NAME"),
+ unique("ADDRESSBOOK_HOME_RESOURCE_ID", "VCARD_UID")
+);
+
+create table ADDRESSBOOK_OBJECT_KIND (
+ "ID" integer primary key,
+ "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('person', 0);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('group', 1);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('resource', 2);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('location', 3);
+create table ABO_MEMBERS (
+ "GROUP_ID" integer not null,
+ "ADDRESSBOOK_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+ "MEMBER_ID" integer not null,
+ "REVISION" integer not null,
+ "REMOVED" integer default 0 not null,
+ primary key("GROUP_ID", "MEMBER_ID", "REVISION")
+);
+
+create table ABO_FOREIGN_MEMBERS (
+ "GROUP_ID" integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+ "ADDRESSBOOK_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+ "MEMBER_ADDRESS" nvarchar2(255),
+ primary key("GROUP_ID", "MEMBER_ADDRESS")
+);
+
+create table SHARED_GROUP_BIND (
+ "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+ "GROUP_RESOURCE_ID" integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+ "GROUP_ADDRESSBOOK_NAME" nvarchar2(255),
+ "BIND_MODE" integer not null,
+ "BIND_STATUS" integer not null,
+ "BIND_REVISION" integer default 0 not null,
+ "MESSAGE" nclob,
+ primary key("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_RESOURCE_ID"),
+ unique("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_ADDRESSBOOK_NAME")
+);
+
+create table CALENDAR_OBJECT_REVISIONS (
+ "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+ "CALENDAR_RESOURCE_ID" integer references CALENDAR,
+ "CALENDAR_NAME" nvarchar2(255) default null,
+ "RESOURCE_NAME" nvarchar2(255),
+ "REVISION" integer not null,
+ "DELETED" integer not null
+);
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+ "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+ "OWNER_HOME_RESOURCE_ID" integer references ADDRESSBOOK_HOME,
+ "ADDRESSBOOK_NAME" nvarchar2(255) default null,
+ "OBJECT_RESOURCE_ID" integer default 0,
+ "RESOURCE_NAME" nvarchar2(255),
+ "REVISION" integer not null,
+ "DELETED" integer not null
+);
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+ "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME on delete cascade,
+ "RESOURCE_NAME" nvarchar2(255),
+ "REVISION" integer not null,
+ "DELETED" integer not null,
+ unique("NOTIFICATION_HOME_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table APN_SUBSCRIPTIONS (
+ "TOKEN" nvarchar2(255),
+ "RESOURCE_KEY" nvarchar2(255),
+ "MODIFIED" integer not null,
+ "SUBSCRIBER_GUID" nvarchar2(255),
+ "USER_AGENT" nvarchar2(255) default null,
+ "IP_ADDR" nvarchar2(255) default null,
+ primary key("TOKEN", "RESOURCE_KEY")
+);
+
+create table IMIP_TOKENS (
+ "TOKEN" nvarchar2(255),
+ "ORGANIZER" nvarchar2(255),
+ "ATTENDEE" nvarchar2(255),
+ "ICALUID" nvarchar2(255),
+ "ACCESSED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ primary key("ORGANIZER", "ATTENDEE", "ICALUID")
+);
+
+create table IMIP_INVITATION_WORK (
+ "WORK_ID" integer primary key not null,
+ "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "FROM_ADDR" nvarchar2(255),
+ "TO_ADDR" nvarchar2(255),
+ "ICALENDAR_TEXT" nclob
+);
+
+create table IMIP_POLLING_WORK (
+ "WORK_ID" integer primary key not null,
+ "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table IMIP_REPLY_WORK (
+ "WORK_ID" integer primary key not null,
+ "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "ORGANIZER" nvarchar2(255),
+ "ATTENDEE" nvarchar2(255),
+ "ICALENDAR_TEXT" nclob
+);
+
+create table PUSH_NOTIFICATION_WORK (
+ "WORK_ID" integer primary key not null,
+ "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "PUSH_ID" nvarchar2(255),
+ "PRIORITY" integer not null
+);
+
+create table GROUP_CACHER_POLLING_WORK (
+ "WORK_ID" integer primary key not null,
+ "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+ "WORK_ID" integer primary key not null,
+ "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+ "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
+);
+
+create table CALENDARSERVER (
+ "NAME" nvarchar2(255) primary key,
+ "VALUE" nvarchar2(255)
+);
+
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '30');
+insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '5');
+insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');
+create index CALENDAR_HOME_METADAT_3cb9049e on CALENDAR_HOME_METADATA (
+ DEFAULT_EVENTS
+);
+
+create index CALENDAR_HOME_METADAT_d55e5548 on CALENDAR_HOME_METADATA (
+ DEFAULT_TASKS
+);
+
+create index CALENDAR_HOME_METADAT_910264ce on CALENDAR_HOME_METADATA (
+ DEFAULT_POLLS
+);
+
+create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (
+ NOTIFICATION_HOME_RESOURCE_ID
+);
+
+create index CALENDAR_BIND_RESOURC_e57964d4 on CALENDAR_BIND (
+ CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_CALEN_a9a453a9 on CALENDAR_OBJECT (
+ CALENDAR_RESOURCE_ID,
+ ICALENDAR_UID
+);
+
+create index CALENDAR_OBJECT_CALEN_96e83b73 on CALENDAR_OBJECT (
+ CALENDAR_RESOURCE_ID,
+ RECURRANCE_MAX
+);
+
+create index CALENDAR_OBJECT_ICALE_82e731d5 on CALENDAR_OBJECT (
+ ICALENDAR_UID
+);
+
+create index CALENDAR_OBJECT_DROPB_de041d80 on CALENDAR_OBJECT (
+ DROPBOX_ID
+);
+
+create index TIME_RANGE_CALENDAR_R_beb6e7eb on TIME_RANGE (
+ CALENDAR_RESOURCE_ID
+);
+
+create index TIME_RANGE_CALENDAR_O_acf37bd1 on TIME_RANGE (
+ CALENDAR_OBJECT_RESOURCE_ID
+);
+
+create index TRANSPARENCY_TIME_RAN_5f34467f on TRANSPARENCY (
+ TIME_RANGE_INSTANCE_ID
+);
+
+create index ATTACHMENT_CALENDAR_H_0078845c on ATTACHMENT (
+ CALENDAR_HOME_RESOURCE_ID
+);
+
+create index ATTACHMENT_CALENDAR_O_81508484 on ATTACHMENT_CALENDAR_OBJECT (
+ CALENDAR_OBJECT_RESOURCE_ID
+);
+
+create index SHARED_ADDRESSBOOK_BI_e9a2e6d4 on SHARED_ADDRESSBOOK_BIND (
+ OWNER_HOME_RESOURCE_ID
+);
+
+create index ABO_MEMBERS_ADDRESSBO_4effa879 on ABO_MEMBERS (
+ ADDRESSBOOK_ID
+);
+
+create index ABO_MEMBERS_MEMBER_ID_8d66adcf on ABO_MEMBERS (
+ MEMBER_ID
+);
+
+create index ABO_FOREIGN_MEMBERS_A_1fd2c5e9 on ABO_FOREIGN_MEMBERS (
+ ADDRESSBOOK_ID
+);
+
+create index SHARED_GROUP_BIND_RES_cf52f95d on SHARED_GROUP_BIND (
+ GROUP_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_3a3956c4 on CALENDAR_OBJECT_REVISIONS (
+ CALENDAR_HOME_RESOURCE_ID,
+ CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_6d9d929c on CALENDAR_OBJECT_REVISIONS (
+ CALENDAR_RESOURCE_ID,
+ RESOURCE_NAME,
+ DELETED,
+ REVISION
+);
+
+create index CALENDAR_OBJECT_REVIS_265c8acf on CALENDAR_OBJECT_REVISIONS (
+ CALENDAR_RESOURCE_ID,
+ REVISION
+);
+
+create index ADDRESSBOOK_OBJECT_RE_2bfcf757 on ADDRESSBOOK_OBJECT_REVISIONS (
+ ADDRESSBOOK_HOME_RESOURCE_ID,
+ OWNER_HOME_RESOURCE_ID
+);
+
+create index ADDRESSBOOK_OBJECT_RE_00fe8288 on ADDRESSBOOK_OBJECT_REVISIONS (
+ OWNER_HOME_RESOURCE_ID,
+ RESOURCE_NAME,
+ DELETED,
+ REVISION
+);
+
+create index ADDRESSBOOK_OBJECT_RE_45004780 on ADDRESSBOOK_OBJECT_REVISIONS (
+ OWNER_HOME_RESOURCE_ID,
+ REVISION
+);
+
+create index NOTIFICATION_OBJECT_R_036a9cee on NOTIFICATION_OBJECT_REVISIONS (
+ NOTIFICATION_HOME_RESOURCE_ID,
+ REVISION
+);
+
+create index APN_SUBSCRIPTIONS_RES_9610d78e on APN_SUBSCRIPTIONS (
+ RESOURCE_KEY
+);
+
+create index IMIP_TOKENS_TOKEN_e94b918f on IMIP_TOKENS (
+ TOKEN
+);
+
+create index CALENDAR_OBJECT_SPLIT_af71dcda on CALENDAR_OBJECT_SPLITTER_WORK (
+ RESOURCE_ID
+);
+
Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v30.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v30.sql (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v30.sql 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,717 @@
+-- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+
+----
+-- Copyright (c) 2010-2013 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.
+----
+
+
+-----------------
+-- Resource ID --
+-----------------
+
+create sequence RESOURCE_ID_SEQ;
+
+
+-------------------------
+-- Cluster Bookkeeping --
+-------------------------
+
+-- Information about a process connected to this database.
+
+-- Note that this must match the node info schema in twext.enterprise.queue.
+create table NODE_INFO (
+ HOSTNAME varchar(255) not null,
+ PID integer not null,
+ PORT integer not null,
+ TIME timestamp not null default timezone('UTC', CURRENT_TIMESTAMP),
+
+ primary key (HOSTNAME, PORT)
+);
+
+-- Unique named locks. This table should always be empty, but rows are
+-- temporarily created in order to prevent undesirable concurrency.
+create table NAMED_LOCK (
+ LOCK_NAME varchar(255) primary key
+);
+
+
+-------------------
+-- Calendar Home --
+-------------------
+
+create table CALENDAR_HOME (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+ OWNER_UID varchar(255) not null unique, -- implicit index
+ DATAVERSION integer default 0 not null
+);
+
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+----------------------------
+-- Calendar Home Metadata --
+----------------------------
+
+create table CALENDAR_HOME_METADATA (
+ RESOURCE_ID integer primary key references CALENDAR_HOME on delete cascade, -- implicit index
+ QUOTA_USED_BYTES integer default 0 not null,
+ DEFAULT_EVENTS integer default null references CALENDAR on delete set null,
+ DEFAULT_TASKS integer default null references CALENDAR on delete set null,
+ DEFAULT_POLLS integer default null references CALENDAR on delete set null,
+ ALARM_VEVENT_TIMED text default null,
+ ALARM_VEVENT_ALLDAY text default null,
+ ALARM_VTODO_TIMED text default null,
+ ALARM_VTODO_ALLDAY text default null,
+ AVAILABILITY text default null,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+create index CALENDAR_HOME_METADATA_DEFAULT_EVENTS on
+ CALENDAR_HOME_METADATA(DEFAULT_EVENTS);
+create index CALENDAR_HOME_METADATA_DEFAULT_TASKS on
+ CALENDAR_HOME_METADATA(DEFAULT_TASKS);
+create index CALENDAR_HOME_METADATA_DEFAULT_POLLS on
+ CALENDAR_HOME_METADATA(DEFAULT_POLLS);
+
+-----------------------
+-- Calendar Metadata --
+-----------------------
+
+create table CALENDAR_METADATA (
+ RESOURCE_ID integer primary key references CALENDAR on delete cascade, -- implicit index
+ SUPPORTED_COMPONENTS varchar(255) default null,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+ OWNER_UID varchar(255) not null unique, -- implicit index
+ DATAVERSION integer default 0 not null
+);
+
+create table NOTIFICATION (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+ NOTIFICATION_HOME_RESOURCE_ID integer not null references NOTIFICATION_HOME,
+ NOTIFICATION_UID varchar(255) not null,
+ NOTIFICATION_TYPE varchar(255) not null,
+ NOTIFICATION_DATA text not null,
+ MD5 char(32) not null,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+ unique(NOTIFICATION_UID, NOTIFICATION_HOME_RESOURCE_ID) -- implicit index
+);
+
+create index NOTIFICATION_NOTIFICATION_HOME_RESOURCE_ID on
+ NOTIFICATION(NOTIFICATION_HOME_RESOURCE_ID);
+
+
+-------------------
+-- Calendar Bind --
+-------------------
+
+-- Joins CALENDAR_HOME and CALENDAR
+
+create table CALENDAR_BIND (
+ CALENDAR_HOME_RESOURCE_ID integer not null references CALENDAR_HOME,
+ CALENDAR_RESOURCE_ID integer not null references CALENDAR on delete cascade,
+ CALENDAR_RESOURCE_NAME varchar(255) not null,
+ BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
+ BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
+ BIND_REVISION integer default 0 not null,
+ MESSAGE text,
+ TRANSP integer default 0 not null, -- enum CALENDAR_TRANSP
+ ALARM_VEVENT_TIMED text default null,
+ ALARM_VEVENT_ALLDAY text default null,
+ ALARM_VTODO_TIMED text default null,
+ ALARM_VTODO_ALLDAY text default null,
+ TIMEZONE text default null,
+
+ primary key(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID), -- implicit index
+ unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME) -- implicit index
+);
+
+create index CALENDAR_BIND_RESOURCE_ID on
+ CALENDAR_BIND(CALENDAR_RESOURCE_ID);
+
+-- Enumeration of calendar bind modes
+
+create table CALENDAR_BIND_MODE (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_MODE values (0, 'own' );
+insert into CALENDAR_BIND_MODE values (1, 'read' );
+insert into CALENDAR_BIND_MODE values (2, 'write');
+insert into CALENDAR_BIND_MODE values (3, 'direct');
+insert into CALENDAR_BIND_MODE values (4, 'indirect');
+
+-- Enumeration of statuses
+
+create table CALENDAR_BIND_STATUS (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_STATUS values (0, 'invited' );
+insert into CALENDAR_BIND_STATUS values (1, 'accepted');
+insert into CALENDAR_BIND_STATUS values (2, 'declined');
+insert into CALENDAR_BIND_STATUS values (3, 'invalid');
+insert into CALENDAR_BIND_STATUS values (4, 'deleted');
+
+
+-- Enumeration of transparency
+
+create table CALENDAR_TRANSP (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_TRANSP values (0, 'opaque' );
+insert into CALENDAR_TRANSP values (1, 'transparent');
+
+
+---------------------
+-- Calendar Object --
+---------------------
+
+create table CALENDAR_OBJECT (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+ CALENDAR_RESOURCE_ID integer not null references CALENDAR on delete cascade,
+ RESOURCE_NAME varchar(255) not null,
+ ICALENDAR_TEXT text not null,
+ ICALENDAR_UID varchar(255) not null,
+ ICALENDAR_TYPE varchar(255) not null,
+ ATTACHMENTS_MODE integer default 0 not null, -- enum CALENDAR_OBJECT_ATTACHMENTS_MODE
+ DROPBOX_ID varchar(255),
+ ORGANIZER varchar(255),
+ RECURRANCE_MIN date, -- minimum date that recurrences have been expanded to.
+ RECURRANCE_MAX date, -- maximum date that recurrences have been expanded to.
+ ACCESS integer default 0 not null,
+ SCHEDULE_OBJECT boolean default false,
+ SCHEDULE_TAG varchar(36) default null,
+ SCHEDULE_ETAGS text default null,
+ PRIVATE_COMMENTS boolean default false not null,
+ MD5 char(32) not null,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+ unique (CALENDAR_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+
+ -- since the 'inbox' is a 'calendar resource' for the purpose of storing
+ -- calendar objects, this constraint has to be selectively enforced by the
+ -- application layer.
+
+ -- unique(CALENDAR_RESOURCE_ID, ICALENDAR_UID)
+);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_AND_ICALENDAR_UID on
+ CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_RECURRANCE_MAX on
+ CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, RECURRANCE_MAX);
+
+create index CALENDAR_OBJECT_ICALENDAR_UID on
+ CALENDAR_OBJECT(ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_DROPBOX_ID on
+ CALENDAR_OBJECT(DROPBOX_ID);
+
+-- Enumeration of attachment modes
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MODE (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (0, 'none' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (1, 'read' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (2, 'write');
+
+
+-- Enumeration of calendar access types
+
+create table CALENDAR_ACCESS_TYPE (
+ ID integer primary key,
+ DESCRIPTION varchar(32) not null unique
+);
+
+insert into CALENDAR_ACCESS_TYPE values (0, '' );
+insert into CALENDAR_ACCESS_TYPE values (1, 'public' );
+insert into CALENDAR_ACCESS_TYPE values (2, 'private' );
+insert into CALENDAR_ACCESS_TYPE values (3, 'confidential' );
+insert into CALENDAR_ACCESS_TYPE values (4, 'restricted' );
+
+
+-----------------
+-- Instance ID --
+-----------------
+
+create sequence INSTANCE_ID_SEQ;
+
+
+----------------
+-- Time Range --
+----------------
+
+create table TIME_RANGE (
+ INSTANCE_ID integer primary key default nextval('INSTANCE_ID_SEQ'), -- implicit index
+ CALENDAR_RESOURCE_ID integer not null references CALENDAR on delete cascade,
+ CALENDAR_OBJECT_RESOURCE_ID integer not null references CALENDAR_OBJECT on delete cascade,
+ FLOATING boolean not null,
+ START_DATE timestamp not null,
+ END_DATE timestamp not null,
+ FBTYPE integer not null,
+ TRANSPARENT boolean not null
+);
+
+create index TIME_RANGE_CALENDAR_RESOURCE_ID on
+ TIME_RANGE(CALENDAR_RESOURCE_ID);
+create index TIME_RANGE_CALENDAR_OBJECT_RESOURCE_ID on
+ TIME_RANGE(CALENDAR_OBJECT_RESOURCE_ID);
+
+
+-- Enumeration of free/busy types
+
+create table FREE_BUSY_TYPE (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into FREE_BUSY_TYPE values (0, 'unknown' );
+insert into FREE_BUSY_TYPE values (1, 'free' );
+insert into FREE_BUSY_TYPE values (2, 'busy' );
+insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
+insert into FREE_BUSY_TYPE values (4, 'busy-tentative' );
+
+
+------------------
+-- Transparency --
+------------------
+
+create table TRANSPARENCY (
+ TIME_RANGE_INSTANCE_ID integer not null references TIME_RANGE on delete cascade,
+ USER_ID varchar(255) not null,
+ TRANSPARENT boolean not null
+);
+
+create index TRANSPARENCY_TIME_RANGE_INSTANCE_ID on
+ TRANSPARENCY(TIME_RANGE_INSTANCE_ID);
+
+
+----------------
+-- Attachment --
+----------------
+
+create sequence ATTACHMENT_ID_SEQ;
+
+create table ATTACHMENT (
+ ATTACHMENT_ID integer primary key default nextval('ATTACHMENT_ID_SEQ'), -- implicit index
+ CALENDAR_HOME_RESOURCE_ID integer not null references CALENDAR_HOME,
+ DROPBOX_ID varchar(255),
+ CONTENT_TYPE varchar(255) not null,
+ SIZE integer not null,
+ MD5 char(32) not null,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ PATH varchar(1024) not null
+);
+
+create index ATTACHMENT_CALENDAR_HOME_RESOURCE_ID on
+ ATTACHMENT(CALENDAR_HOME_RESOURCE_ID);
+
+-- Many-to-many relationship between attachments and calendar objects
+create table ATTACHMENT_CALENDAR_OBJECT (
+ ATTACHMENT_ID integer not null references ATTACHMENT on delete cascade,
+ MANAGED_ID varchar(255) not null,
+ CALENDAR_OBJECT_RESOURCE_ID integer not null references CALENDAR_OBJECT on delete cascade,
+
+ primary key (ATTACHMENT_ID, CALENDAR_OBJECT_RESOURCE_ID), -- implicit index
+ unique (MANAGED_ID, CALENDAR_OBJECT_RESOURCE_ID) --implicit index
+);
+
+create index ATTACHMENT_CALENDAR_OBJECT_CALENDAR_OBJECT_RESOURCE_ID on
+ ATTACHMENT_CALENDAR_OBJECT(CALENDAR_OBJECT_RESOURCE_ID);
+
+-----------------------
+-- Resource Property --
+-----------------------
+
+create table RESOURCE_PROPERTY (
+ RESOURCE_ID integer not null, -- foreign key: *.RESOURCE_ID
+ NAME varchar(255) not null,
+ VALUE text not null, -- FIXME: xml?
+ VIEWER_UID varchar(255),
+
+ primary key (RESOURCE_ID, NAME, VIEWER_UID) -- implicit index
+);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+ ADDRESSBOOK_PROPERTY_STORE_ID integer default nextval('RESOURCE_ID_SEQ') not null, -- implicit index
+ OWNER_UID varchar(255) not null unique, -- implicit index
+ DATAVERSION integer default 0 not null
+);
+
+
+-------------------------------
+-- AddressBook Home Metadata --
+-------------------------------
+
+create table ADDRESSBOOK_HOME_METADATA (
+ RESOURCE_ID integer primary key references ADDRESSBOOK_HOME on delete cascade, -- implicit index
+ QUOTA_USED_BYTES integer default 0 not null,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+-----------------------------
+-- Shared AddressBook Bind --
+-----------------------------
+
+-- Joins sharee ADDRESSBOOK_HOME and owner ADDRESSBOOK_HOME
+
+create table SHARED_ADDRESSBOOK_BIND (
+ ADDRESSBOOK_HOME_RESOURCE_ID integer not null references ADDRESSBOOK_HOME,
+ OWNER_HOME_RESOURCE_ID integer not null references ADDRESSBOOK_HOME on delete cascade,
+ ADDRESSBOOK_RESOURCE_NAME varchar(255) not null,
+ BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
+ BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
+ BIND_REVISION integer default 0 not null,
+ MESSAGE text, -- FIXME: xml?
+
+ primary key (ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID), -- implicit index
+ unique (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME) -- implicit index
+);
+
+create index SHARED_ADDRESSBOOK_BIND_RESOURCE_ID on
+ SHARED_ADDRESSBOOK_BIND(OWNER_HOME_RESOURCE_ID);
+
+
+------------------------
+-- AddressBook Object --
+------------------------
+
+create table ADDRESSBOOK_OBJECT (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+ ADDRESSBOOK_HOME_RESOURCE_ID integer not null references ADDRESSBOOK_HOME on delete cascade,
+ RESOURCE_NAME varchar(255) not null,
+ VCARD_TEXT text not null,
+ VCARD_UID varchar(255) not null,
+ KIND integer not null, -- enum ADDRESSBOOK_OBJECT_KIND
+ MD5 char(32) not null,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+ unique (ADDRESSBOOK_HOME_RESOURCE_ID, RESOURCE_NAME), -- implicit index
+ unique (ADDRESSBOOK_HOME_RESOURCE_ID, VCARD_UID) -- implicit index
+);
+
+
+-----------------------------
+-- AddressBook Object kind --
+-----------------------------
+
+create table ADDRESSBOOK_OBJECT_KIND (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND values (0, 'person');
+insert into ADDRESSBOOK_OBJECT_KIND values (1, 'group' );
+insert into ADDRESSBOOK_OBJECT_KIND values (2, 'resource');
+insert into ADDRESSBOOK_OBJECT_KIND values (3, 'location');
+
+
+----------------------------------
+-- Revisions, forward reference --
+----------------------------------
+
+create sequence REVISION_SEQ;
+
+---------------------------------
+-- Address Book Object Members --
+---------------------------------
+
+create table ABO_MEMBERS (
+ GROUP_ID integer not null, -- references ADDRESSBOOK_OBJECT on delete cascade, -- AddressBook Object's (kind=='group') RESOURCE_ID
+ ADDRESSBOOK_ID integer not null references ADDRESSBOOK_HOME on delete cascade,
+ MEMBER_ID integer not null, -- references ADDRESSBOOK_OBJECT, -- member AddressBook Object's RESOURCE_ID
+ REVISION integer default nextval('REVISION_SEQ') not null,
+ REMOVED boolean default false not null,
+
+ primary key (GROUP_ID, MEMBER_ID, REVISION) -- implicit index
+);
+
+create index ABO_MEMBERS_ADDRESSBOOK_ID on
+ ABO_MEMBERS(ADDRESSBOOK_ID);
+create index ABO_MEMBERS_MEMBER_ID on
+ ABO_MEMBERS(MEMBER_ID);
+
+------------------------------------------
+-- Address Book Object Foreign Members --
+------------------------------------------
+
+create table ABO_FOREIGN_MEMBERS (
+ GROUP_ID integer not null references ADDRESSBOOK_OBJECT on delete cascade, -- AddressBook Object's (kind=='group') RESOURCE_ID
+ ADDRESSBOOK_ID integer not null references ADDRESSBOOK_HOME on delete cascade,
+ MEMBER_ADDRESS varchar(255) not null, -- member AddressBook Object's 'calendar' address
+
+ primary key (GROUP_ID, MEMBER_ADDRESS) -- implicit index
+);
+
+create index ABO_FOREIGN_MEMBERS_ADDRESSBOOK_ID on
+ ABO_FOREIGN_MEMBERS(ADDRESSBOOK_ID);
+
+-----------------------
+-- Shared Group Bind --
+-----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK_OBJECT (kind == group)
+
+create table SHARED_GROUP_BIND (
+ ADDRESSBOOK_HOME_RESOURCE_ID integer not null references ADDRESSBOOK_HOME,
+ GROUP_RESOURCE_ID integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+ GROUP_ADDRESSBOOK_NAME varchar(255) not null,
+ BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
+ BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
+ BIND_REVISION integer default 0 not null,
+ MESSAGE text, -- FIXME: xml?
+
+ primary key (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_RESOURCE_ID), -- implicit index
+ unique (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_ADDRESSBOOK_NAME) -- implicit index
+);
+
+create index SHARED_GROUP_BIND_RESOURCE_ID on
+ SHARED_GROUP_BIND(GROUP_RESOURCE_ID);
+
+
+---------------
+-- Revisions --
+---------------
+
+-- create sequence REVISION_SEQ;
+
+
+-------------------------------
+-- Calendar Object Revisions --
+-------------------------------
+
+create table CALENDAR_OBJECT_REVISIONS (
+ CALENDAR_HOME_RESOURCE_ID integer not null references CALENDAR_HOME,
+ CALENDAR_RESOURCE_ID integer references CALENDAR,
+ CALENDAR_NAME varchar(255) default null,
+ RESOURCE_NAME varchar(255),
+ REVISION integer default nextval('REVISION_SEQ') not null,
+ DELETED boolean not null
+);
+
+create index CALENDAR_OBJECT_REVISIONS_HOME_RESOURCE_ID_CALENDAR_RESOURCE_ID
+ on CALENDAR_OBJECT_REVISIONS(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+ on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+ on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, REVISION);
+
+
+----------------------------------
+-- AddressBook Object Revisions --
+----------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+ ADDRESSBOOK_HOME_RESOURCE_ID integer not null references ADDRESSBOOK_HOME,
+ OWNER_HOME_RESOURCE_ID integer references ADDRESSBOOK_HOME,
+ ADDRESSBOOK_NAME varchar(255) default null,
+ OBJECT_RESOURCE_ID integer default 0,
+ RESOURCE_NAME varchar(255),
+ REVISION integer default nextval('REVISION_SEQ') not null,
+ DELETED boolean not null
+);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_HOME_RESOURCE_ID_OWNER_HOME_RESOURCE_ID
+ on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+ on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_REVISION
+ on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, REVISION);
+
+
+-----------------------------------
+-- Notification Object Revisions --
+-----------------------------------
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+ NOTIFICATION_HOME_RESOURCE_ID integer not null references NOTIFICATION_HOME on delete cascade,
+ RESOURCE_NAME varchar(255),
+ REVISION integer default nextval('REVISION_SEQ') not null,
+ DELETED boolean not null,
+
+ unique(NOTIFICATION_HOME_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+);
+
+create index NOTIFICATION_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+ on NOTIFICATION_OBJECT_REVISIONS(NOTIFICATION_HOME_RESOURCE_ID, REVISION);
+
+
+-------------------------------------------
+-- Apple Push Notification Subscriptions --
+-------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+ TOKEN varchar(255) not null,
+ RESOURCE_KEY varchar(255) not null,
+ MODIFIED integer not null,
+ SUBSCRIBER_GUID varchar(255) not null,
+ USER_AGENT varchar(255) default null,
+ IP_ADDR varchar(255) default null,
+
+ primary key (TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+ on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+
+-----------------
+-- IMIP Tokens --
+-----------------
+
+create table IMIP_TOKENS (
+ TOKEN varchar(255) not null,
+ ORGANIZER varchar(255) not null,
+ ATTENDEE varchar(255) not null,
+ ICALUID varchar(255) not null,
+ ACCESSED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+ primary key (ORGANIZER, ATTENDEE, ICALUID) -- implicit index
+);
+
+create index IMIP_TOKENS_TOKEN
+ on IMIP_TOKENS(TOKEN);
+
+
+----------------
+-- Work Items --
+----------------
+
+create sequence WORKITEM_SEQ;
+
+
+---------------------------
+-- IMIP Inivitation Work --
+---------------------------
+
+create table IMIP_INVITATION_WORK (
+ WORK_ID integer primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+ NOT_BEFORE timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ FROM_ADDR varchar(255) not null,
+ TO_ADDR varchar(255) not null,
+ ICALENDAR_TEXT text not null
+);
+
+
+-----------------------
+-- IMIP Polling Work --
+-----------------------
+
+create table IMIP_POLLING_WORK (
+ WORK_ID integer primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+ NOT_BEFORE timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+---------------------
+-- IMIP Reply Work --
+---------------------
+
+create table IMIP_REPLY_WORK (
+ WORK_ID integer primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+ NOT_BEFORE timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ ORGANIZER varchar(255) not null,
+ ATTENDEE varchar(255) not null,
+ ICALENDAR_TEXT text not null
+);
+
+
+------------------------
+-- Push Notifications --
+------------------------
+
+create table PUSH_NOTIFICATION_WORK (
+ WORK_ID integer primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+ NOT_BEFORE timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ PUSH_ID varchar(255) not null,
+ PRIORITY integer not null -- 1:low 5:medium 10:high
+);
+
+-----------------
+-- GroupCacher --
+-----------------
+
+create table GROUP_CACHER_POLLING_WORK (
+ WORK_ID integer primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+ NOT_BEFORE timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+--------------------------
+-- Object Splitter Work --
+--------------------------
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+ WORK_ID integer primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+ NOT_BEFORE timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ RESOURCE_ID integer not null references CALENDAR_OBJECT on delete cascade
+);
+
+create index CALENDAR_OBJECT_SPLITTER_WORK_RESOURCE_ID on
+ CALENDAR_OBJECT_SPLITTER_WORK(RESOURCE_ID);
+
+--------------------
+-- Schema Version --
+--------------------
+
+create table CALENDARSERVER (
+ NAME varchar(255) primary key, -- implicit index
+ VALUE varchar(255)
+);
+
+insert into CALENDARSERVER values ('VERSION', '30');
+insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '5');
+insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');
Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_30_to_31.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_30_to_31.sql (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_30_to_31.sql 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,53 @@
+----
+-- Copyright (c) 2012-2013 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.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 30 to 31 --
+---------------------------------------------------
+
+-- Home related updates
+
+alter table CALENDAR_HOME
+ add ("STATUS" integer default 0 not null);
+
+alter table NOTIFICATION_HOME
+ add ("STATUS" integer default 0 not null);
+
+alter table ADDRESSBOOK_HOME
+ add ("STATUS" integer default 0 not null);
+
+create table HOME_STATUS (
+ "ID" integer primary key,
+ "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into HOME_STATUS (DESCRIPTION, ID) values ('normal', 0);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('external', 1);
+
+-- Bind changes
+alter table CALENDAR_BIND
+ add ("EXTERNAL_ID" integer default null);
+
+alter table SHARED_ADDRESSBOOK_BIND
+ add ("EXTERNAL_ID" integer default null);
+
+alter table SHARED_GROUP_BIND
+ add ("EXTERNAL_ID" integer default null);
+
+
+-- Now update the version
+-- No data upgrades
+update CALENDARSERVER set VALUE = '31' where NAME = 'VERSION';
Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_30_to_31.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_30_to_31.sql (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_30_to_31.sql 2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,55 @@
+----
+-- Copyright (c) 2012-2013 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.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 30 to 31 --
+---------------------------------------------------
+
+-- Home related updates
+
+alter table CALENDAR_HOME
+ add column STATUS integer default 0 not null;
+
+alter table NOTIFICATION_HOME
+ add column STATUS integer default 0 not null;
+
+alter table ADDRESSBOOK_HOME
+ add column STATUS integer default 0 not null;
+
+-- Enumeration of statuses
+
+create table HOME_STATUS (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into HOME_STATUS values (0, 'normal' );
+insert into HOME_STATUS values (1, 'external');
+
+-- Bind changes
+alter table CALENDAR_BIND
+ add column EXTERNAL_ID integer default null;
+
+alter table SHARED_ADDRESSBOOK_BIND
+ add column EXTERNAL_ID integer default null;
+
+alter table SHARED_GROUP_BIND
+ add column EXTERNAL_ID integer default null;
+
+
+-- Now update the version
+-- No data upgrades
+update CALENDARSERVER set VALUE = '31' where NAME = 'VERSION';
Modified: CalendarServer/trunk/txdav/common/datastore/sql_tables.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_tables.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/sql_tables.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -133,6 +133,15 @@
# Various constants
+_homeStatus = _schemaConstants(
+ schema.HOME_STATUS.DESCRIPTION,
+ schema.HOME_STATUS.ID
+)
+
+
+_HOME_STATUS_NORMAL = _homeStatus('normal')
+_HOME_STATUS_EXTERNAL = _homeStatus('external')
+
_bindStatus = _schemaConstants(
schema.CALENDAR_BIND_STATUS.DESCRIPTION,
schema.CALENDAR_BIND_STATUS.ID
Modified: CalendarServer/trunk/txdav/common/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/test/test_sql.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/test/test_sql.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -19,21 +19,28 @@
"""
from twext.enterprise.dal.syntax import Select
-from txdav.xml import element as davxml
+from twext.enterprise.dal.syntax import Insert
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.task import Clock
from twisted.trial.unittest import TestCase
from twisted.internet.defer import Deferred
+from txdav.caldav.datastore.test.util import buildDirectoryRecord
from txdav.common.datastore.sql import log, CommonStoreTransactionMonitor, \
CommonHome, CommonHomeChild, ECALENDARTYPE
from txdav.common.datastore.sql_tables import schema
from txdav.common.datastore.test.util import CommonCommonTests, buildStore
from txdav.common.icommondatastore import AllRetriesFailed
-from twext.enterprise.dal.syntax import Insert
from txdav.common.datastore.sql import fixUUIDNormalization
+from txdav.xml import element as davxml
+from uuid import UUID
+
+exampleUID = UUID("a" * 32)
+denormalizedUID = str(exampleUID)
+normalizedUID = denormalizedUID.upper()
+
class CommonSQLStoreTests(CommonCommonTests, TestCase):
"""
Tests for shared functionality in L{txdav.common.datastore.sql}.
@@ -46,6 +53,9 @@
"""
yield super(CommonSQLStoreTests, self).setUp()
self._sqlStore = yield buildStore(self, self.notifierFactory)
+ self._sqlStore.directoryService().addRecord(buildDirectoryRecord(denormalizedUID))
+ self._sqlStore.directoryService().addRecord(buildDirectoryRecord(normalizedUID))
+ self._sqlStore.directoryService().addRecord(buildDirectoryRecord("uid"))
def storeUnderTest(self):
@@ -308,31 +318,31 @@
token = yield homeChild.syncToken()
yield homeChild._changeRevision("insert", "C")
changed = yield homeChild.resourceNamesSinceToken(token)
- self.assertEqual(changed, (["C"], [],))
+ self.assertEqual(changed, (["C"], [], [],))
# update test
token = yield homeChild.syncToken()
yield homeChild._changeRevision("update", "C")
changed = yield homeChild.resourceNamesSinceToken(token)
- self.assertEqual(changed, (["C"], [],))
+ self.assertEqual(changed, (["C"], [], [],))
# delete test
token = yield homeChild.syncToken()
yield homeChild._changeRevision("delete", "C")
changed = yield homeChild.resourceNamesSinceToken(token)
- self.assertEqual(changed, ([], ["C"],))
+ self.assertEqual(changed, ([], ["C"], [],))
# missing update test
token = yield homeChild.syncToken()
yield homeChild._changeRevision("update", "D")
changed = yield homeChild.resourceNamesSinceToken(token)
- self.assertEqual(changed, (["D"], [],))
+ self.assertEqual(changed, (["D"], [], [],))
# missing delete test
token = yield homeChild.syncToken()
yield homeChild._changeRevision("delete", "E")
changed = yield homeChild.resourceNamesSinceToken(token)
- self.assertEqual(changed, ([], [],))
+ self.assertEqual(changed, ([], [], [],))
yield txn.abort()
@@ -421,10 +431,3 @@
yield fixUUIDNormalization(self.storeUnderTest())
self.assertEqual((yield self.allHomeUIDs(schema.ADDRESSBOOK_HOME)),
[[normalizedUID]])
-
-
-
-from uuid import UUID
-exampleUID = UUID("a" * 32)
-denormalizedUID = str(exampleUID)
-normalizedUID = denormalizedUID.upper()
Modified: CalendarServer/trunk/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/test/util.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/test/util.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -112,12 +112,14 @@
def recordWithUID(self, uid):
return self.records.get(uid)
+
def recordWithGUID(self, guid):
for record in self.records.itervalues():
if record.guid == guid:
return record
return None
+
def addRecord(self, record):
self.records[record.uid] = record
@@ -127,32 +129,70 @@
implements(IStoreDirectoryRecord)
- def __init__(self, uid, shortNames, fullName, extras={}):
+ def __init__(self, uid, shortNames, fullName, thisServer=True, server=None, extras={}):
self.uid = uid
self.guid = uid
self.shortNames = shortNames
self.fullName = fullName
self.displayName = self.fullName if self.fullName else self.shortNames[0]
+ self._thisServer = thisServer
+ self._server = server
self.extras = extras
+ def thisServer(self):
+ return self._thisServer
+
+ def server(self):
+ return self._server
+
+
+
+def buildDirectory(homes=None):
+
+ directory = TestStoreDirectoryService()
+
+ # User accounts
+ for ctr in range(1, 100):
+ directory.addRecord(TestStoreDirectoryRecord(
+ "user%02d" % (ctr,),
+ ("user%02d" % (ctr,),),
+ "User %02d" % (ctr,),
+ ))
+
+ homes = set(homes) if homes is not None else set()
+ for uid in homes:
+ directory.addRecord(buildDirectoryRecord(uid))
+
+ return directory
+
+
+
+def buildDirectoryRecord(uid):
+ return TestStoreDirectoryRecord(
+ uid,
+ (uid,),
+ uid.capitalize(),
+ )
+
+
+
class SQLStoreBuilder(object):
"""
Test-fixture-builder which can construct a PostgresStore.
"""
- sharedService = None
- currentTestID = None
+ def __init__(self, secondary=False):
+ self.sharedService = None
+ self.currentTestID = None
+ self.sharedDBPath = "_test_sql_db" + str(os.getpid()) + ("-2" if secondary else "")
- SHARED_DB_PATH = "_test_sql_db" + str(os.getpid())
-
- @classmethod
- def createService(cls, serviceFactory):
+ def createService(self, serviceFactory):
"""
Create a L{PostgresService} to use for building a store.
"""
- dbRoot = CachingFilePath(cls.SHARED_DB_PATH)
+ dbRoot = CachingFilePath(self.sharedDBPath)
return PostgresService(
dbRoot, serviceFactory, current_sql_schema, resetSchema=True,
databaseName="caldav",
@@ -168,17 +208,15 @@
)
- @classmethod
- def childStore(cls):
+ def childStore(self):
"""
Create a store suitable for use in a child process, that is hooked up
to the store that a parent test process is managing.
"""
disableMemcacheForTest(TestCase())
staticQuota = 3000
- attachmentRoot = (CachingFilePath(cls.SHARED_DB_PATH)
- .child("attachments"))
- stubsvc = cls.createService(lambda cf: Service())
+ attachmentRoot = (CachingFilePath(self.sharedDBPath).child("attachments"))
+ stubsvc = self.createService(lambda cf: Service())
cp = ConnectionPool(stubsvc.produceConnection, maxConnections=1)
# Attach the service to the running reactor.
@@ -194,17 +232,17 @@
return cds
- def buildStore(self, testCase, notifierFactory, directoryService=None):
+ def buildStore(self, testCase, notifierFactory, directoryService=None, homes=None):
"""
Do the necessary work to build a store for a particular test case.
@return: a L{Deferred} which fires with an L{IDataStore}.
"""
disableMemcacheForTest(testCase)
- dbRoot = CachingFilePath(self.SHARED_DB_PATH)
+ dbRoot = CachingFilePath(self.sharedDBPath)
attachmentRoot = dbRoot.child("attachments")
if directoryService is None:
- directoryService = TestStoreDirectoryService()
+ directoryService = buildDirectory(homes=homes)
if self.sharedService is None:
ready = Deferred()
def getReady(connectionFactory, storageService):
@@ -251,8 +289,7 @@
attachmentRoot.createDirectory()
currentTestID = testCase.id()
- cp = ConnectionPool(self.sharedService.produceConnection,
- maxConnections=5)
+ cp = ConnectionPool(self.sharedService.produceConnection, maxConnections=5)
quota = deriveQuota(testCase)
store = CommonDataStore(
cp.connection,
@@ -314,6 +351,7 @@
theStoreBuilder = SQLStoreBuilder()
buildStore = theStoreBuilder.buildStore
+cleanStore = theStoreBuilder.cleanStore
_notSet = object()
@@ -685,13 +723,13 @@
@inlineCallbacks
- def homeUnderTest(self, txn=None, name="home1"):
+ def homeUnderTest(self, txn=None, name="home1", create=False):
"""
Get the calendar home detailed by C{requirements['home1']}.
"""
if txn is None:
txn = self.transactionUnderTest()
- returnValue((yield txn.calendarHomeWithUID(name)))
+ returnValue((yield txn.calendarHomeWithUID(name, create=create)))
@inlineCallbacks
Modified: CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/util.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/util.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/util.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -19,7 +19,7 @@
from twisted.internet.defer import inlineCallbacks, returnValue
from txdav.base.propertystore.base import PropertyName
from txdav.base.propertystore.sql import PropertyStore
-from txdav.common.datastore.sql_tables import schema
+from txdav.common.datastore.sql_tables import schema, _HOME_STATUS_EXTERNAL
from twisted.python.failure import Failure
log = Logger()
@@ -134,7 +134,7 @@
@inlineCallbacks
-def doToEachHomeNotAtVersion(store, homeSchema, version, doIt, logStr, filterOwnerUID=None):
+def doToEachHomeNotAtVersion(store, homeSchema, version, doIt, logStr, filterOwnerUID=None, processExternal=False):
"""
Do something to each home whose version column indicates it is older
than the specified version. Do this in batches as there may be a lot of work to do. Also,
@@ -161,7 +161,7 @@
txn = store.newTransaction("updateDataVersion")
try:
rows = yield Select(
- [homeSchema.RESOURCE_ID, homeSchema.OWNER_UID, ],
+ [homeSchema.RESOURCE_ID, homeSchema.OWNER_UID, homeSchema.STATUS, ],
From=homeSchema,
Where=where,
OrderBy=homeSchema.OWNER_UID,
@@ -173,9 +173,10 @@
logUpgradeStatus("End {}".format(logStr), count, total)
returnValue(None)
- # Apply to the home
- homeResourceID, _ignore_owner_uid = rows[0]
- yield doIt(txn, homeResourceID)
+ # Apply to the home if not external
+ homeResourceID, _ignore_owner_uid, homeStatus = rows[0]
+ if homeStatus != _HOME_STATUS_EXTERNAL or processExternal:
+ yield doIt(txn, homeResourceID)
# Update the home to the current version
yield Update(
Modified: CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -143,7 +143,17 @@
self.filesPath, {"push": StubNotifierFactory()}, TestStoreDirectoryService(), True, True
)
self.sqlStore = yield theStoreBuilder.buildStore(
- self, StubNotifierFactory()
+ self,
+ StubNotifierFactory(),
+ homes=(
+ "home1",
+ "home2",
+ "home3",
+ "home_defaults",
+ "home_no_splits",
+ "home_splits",
+ "home_splits_shared",
+ )
)
self.upgrader = UpgradeToDatabaseStep(self.fileStore, self.sqlStore)
Modified: CalendarServer/trunk/txdav/common/icommondatastore.py
===================================================================
--- CalendarServer/trunk/txdav/common/icommondatastore.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/icommondatastore.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -51,6 +51,13 @@
+class RecordNotAllowedError(CommonStoreError):
+ """
+ User not allowed.
+ """
+
+
+
class NameNotAllowedError(CommonStoreError):
"""
Attempt to create an object with a name that is not allowed.
@@ -205,6 +212,29 @@
"""
+
+class ShareNotAllowed(CommonStoreError):
+ """
+ An operation on a shared resource is not allowed.
+ """
+
+
+
+class ExternalShareFailed(CommonStoreError):
+ """
+ An external sharee operation failed.
+ """
+
+
+
+class NonExistentExternalShare(CommonStoreError):
+ """
+ An external sharee operation failed because the share does not exist on the
+ other pod. The caller of the external request receiving this exception should
+ remove the local external share to "heal" this mismatch.
+ """
+
+
# Indexing / sync tokens
Modified: CalendarServer/trunk/txdav/common/idirectoryservice.py
===================================================================
--- CalendarServer/trunk/txdav/common/idirectoryservice.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/idirectoryservice.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -26,6 +26,20 @@
"IStoreDirectoryRecord",
]
+class IStoreDirectoryError(Exception):
+ """
+ Base class for directory related errors.
+ """
+
+
+
+class DirectoryRecordNotFoundError(Exception):
+ """
+ Directory record not found.
+ """
+
+
+
class IStoreDirectoryService(Interface):
"""
Directory Service for looking up users.
@@ -48,6 +62,7 @@
"""
+
class IStoreDirectoryRecord(Interface):
"""
Directory record object
@@ -62,3 +77,27 @@
fullName = Attribute("Full name for the entity associated with the record: C{str}")
displayName = Attribute("Display name for entity associated with the record: C{str}")
+
+ def serverURI(): #@NoSelf
+ """
+ Return the URI for the record's server "pod".
+
+ @return: a URI.
+ @rtype: C{str}
+ """
+
+ def server(): #@NoSelf
+ """
+ Return the L{txdav.caldav.datastore.scheduling.localservers.Server} for the record's server "pod".
+
+ @return: a pod server record.
+ @rtype: L{txdav.caldav.datastore.scheduling.localservers.Server}
+ """
+
+ def thisServer(): #@NoSelf
+ """
+ Indicates whether the record is hosted on this server "pod".
+
+ @return: C{True} if hosted by this service.
+ @rtype: C{bool}
+ """
Modified: CalendarServer/trunk/txweb2/http.py
===================================================================
--- CalendarServer/trunk/txweb2/http.py 2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txweb2/http.py 2014-01-02 17:19:18 UTC (rev 12211)
@@ -558,7 +558,7 @@
"""
def __init__(self, code, jobj):
"""
- @param xml_responses: an iterable of davxml.Response objects.
+ @param jobj: a Python object that can be serialized to JSON.
"""
Response.__init__(self, code, stream=json.dumps(jobj))
self.headers.setHeader("content-type", http_headers.MimeType("application", "json"))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/4bb7e9a5/attachment.html>
More information about the calendarserver-changes
mailing list