[CalendarServer-changes] [5595] CalendarServer/branches/new-store
source_changes at macosforge.org
source_changes at macosforge.org
Wed May 12 22:04:16 PDT 2010
Revision: 5595
http://trac.macosforge.org/projects/calendarserver/changeset/5595
Author: glyph at apple.com
Date: 2010-05-12 22:04:13 -0700 (Wed, 12 May 2010)
Log Message:
-----------
Merge forward from 'transations' branch.
Modified Paths:
--------------
CalendarServer/branches/new-store/conf/auth/accounts-test.xml
CalendarServer/branches/new-store/conf/auth/augments-test.xml
CalendarServer/branches/new-store/test
CalendarServer/branches/new-store/twext/python/datetime.py
CalendarServer/branches/new-store/twext/python/filepath.py
CalendarServer/branches/new-store/twext/python/test/test_datetime.py
CalendarServer/branches/new-store/twext/web2/dav/element/base.py
CalendarServer/branches/new-store/twistedcaldav/directory/test/test_calendar.py
CalendarServer/branches/new-store/twistedcaldav/directory/test/test_guidchange.py
CalendarServer/branches/new-store/twistedcaldav/directory/test/test_proxyprincipalmembers.py
CalendarServer/branches/new-store/twistedcaldav/directory/test/test_xmlfile.py
CalendarServer/branches/new-store/twistedcaldav/resource.py
CalendarServer/branches/new-store/twistedcaldav/static.py
CalendarServer/branches/new-store/twistedcaldav/test/test_calendarquery.py
CalendarServer/branches/new-store/twistedcaldav/test/test_collectioncontents.py
CalendarServer/branches/new-store/twistedcaldav/test/test_index.py
CalendarServer/branches/new-store/twistedcaldav/test/test_mkcalendar.py
CalendarServer/branches/new-store/twistedcaldav/test/test_multiget.py
CalendarServer/branches/new-store/twistedcaldav/test/test_props.py
CalendarServer/branches/new-store/twistedcaldav/test/util.py
CalendarServer/branches/new-store/txcaldav/calendarstore/file.py
CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py
CalendarServer/branches/new-store/txcaldav/icalendarstore.py
CalendarServer/branches/new-store/txdav/idav.py
CalendarServer/branches/new-store/txdav/propertystore/base.py
CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py
CalendarServer/branches/new-store/txdav/propertystore/xattr.py
Added Paths:
-----------
CalendarServer/branches/new-store/twistedcaldav/test/test_wrapping.py
Property Changed:
----------------
CalendarServer/branches/new-store/
Property changes on: CalendarServer/branches/new-store
___________________________________________________________________
Modified: svn:mergeinfo
- /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/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/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
Modified: CalendarServer/branches/new-store/conf/auth/accounts-test.xml
===================================================================
--- CalendarServer/branches/new-store/conf/auth/accounts-test.xml 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/conf/auth/accounts-test.xml 2010-05-13 05:04:13 UTC (rev 5595)
@@ -35,6 +35,51 @@
<first-name>Apprentice</first-name>
<last-name>Super User</last-name>
</user>
+ <user>
+ <uid>wsanchez</uid>
+ <guid>wsanchez</guid>
+ <email-address>wsanchez at apple.com</email-address>
+ <password>demo</password>
+ <name>Wilfredo Sanchez Vega</name>
+ <first-name>Wilfredo</first-name>
+ <last-name>Sanchez Vega</last-name>
+ </user>
+ <user>
+ <uid>cdaboo</uid>
+ <guid>cdaboo</guid>
+ <email-address>cdaboo at apple.com</email-address>
+ <password>demo</password>
+ <name>Cyrus Daboo</name>
+ <first-name>Cyrus</first-name>
+ <last-name>Daboo</last-name>
+ </user>
+ <user>
+ <uid>sagen</uid>
+ <guid>sagen</guid>
+ <email-address>sagen at apple.com</email-address>
+ <password>demo</password>
+ <name>Morgen Sagen</name>
+ <first-name>Morgen</first-name>
+ <last-name>Sagen</last-name>
+ </user>
+ <user>
+ <uid>dre</uid>
+ <guid>andre</guid>
+ <email-address>dre at apple.com</email-address>
+ <password>demo</password>
+ <name>Andre LaBranche</name>
+ <first-name>Andre</first-name>
+ <last-name>LaBranche</last-name>
+ </user>
+ <user>
+ <uid>glyph</uid>
+ <guid>glyph</guid>
+ <email-address>glyph at apple.com</email-address>
+ <password>demo</password>
+ <name>Glyph Lefkowitz</name>
+ <first-name>Glyph</first-name>
+ <last-name>Lefkowitz</last-name>
+ </user>
<user repeat="99">
<uid>user%02d</uid>
<uid>User %02d</uid>
@@ -102,4 +147,58 @@
<member type="users">user01</member>
</members>
</group>
+ <location>
+ <uid>mercury</uid>
+ <guid>mercury</guid>
+ <password>demo</password>
+ <name>Mecury Conference Room, Building 1, 2nd Floor</name>
+ </location>
+ <location>
+ <uid>venus</uid>
+ <guid>venus</guid>
+ <password>demo</password>
+ <name>Venus Conference Room, Building 1, 2nd Floor</name>
+ </location>
+ <location>
+ <uid>Earth</uid>
+ <guid>Earth</guid>
+ <password>demo</password>
+ <name>Earth Conference Room, Building 1, 1st Floor</name>
+ </location>
+ <location>
+ <uid>mars</uid>
+ <guid>mars</guid>
+ <password>demo</password>
+ <name>Mars Conference Room, Building 1, 1st Floor</name>
+ </location>
+ <location>
+ <uid>jupiter</uid>
+ <guid>jupiter</guid>
+ <password>demo</password>
+ <name>Jupiter Conference Room, Building 2, 1st Floor</name>
+ </location>
+ <location>
+ <uid>neptune</uid>
+ <guid>neptune</guid>
+ <password>demo</password>
+ <name>Neptune Conference Room, Building 2, 1st Floor</name>
+ </location>
+ <location>
+ <uid>pluto</uid>
+ <guid>pluto</guid>
+ <password>demo</password>
+ <name>Pluto Conference Room, Building 2, 1st Floor</name>
+ </location>
+ <location>
+ <uid>saturn</uid>
+ <guid>saturn</guid>
+ <password>demo</password>
+ <name>Saturn Conference Room, Building 2, 1st Floor</name>
+ </location>
+ <location>
+ <uid>uranus</uid>
+ <guid>uranus</guid>
+ <password>demo</password>
+ <name>Uranus Conference Room, Building 3, 1st Floor</name>
+ </location>
</accounts>
Modified: CalendarServer/branches/new-store/conf/auth/augments-test.xml
===================================================================
--- CalendarServer/branches/new-store/conf/auth/augments-test.xml 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/conf/auth/augments-test.xml 2010-05-13 05:04:13 UTC (rev 5595)
@@ -25,6 +25,38 @@
<enable-calendar>true</enable-calendar>
<enable-addressbook>true</enable-addressbook>
</record>
+
+ <record>
+ <uid>wsanchez</uid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <enable-addressbook>true</enable-addressbook>
+ </record>
+ <record>
+ <uid>glyph</uid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <enable-addressbook>true</enable-addressbook>
+ </record>
+ <record>
+ <uid>sagen</uid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <enable-addressbook>true</enable-addressbook>
+ </record>
+ <record>
+ <uid>dre</uid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <enable-addressbook>true</enable-addressbook>
+ </record>
+ <record>
+ <uid>cdaboo</uid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <enable-addressbook>true</enable-addressbook>
+ </record>
+
<record repeat="10">
<uid>location%02d</uid>
<enable>true</enable>
Modified: CalendarServer/branches/new-store/test
===================================================================
--- CalendarServer/branches/new-store/test 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/test 2010-05-13 05:04:13 UTC (rev 5595)
@@ -21,8 +21,13 @@
wd="$(cd "$(dirname "$0")" && pwd -L)";
-. support/py.sh;
+DAVD=cal;
+. support/build.sh;
+
+do_setup="false";
+do_get="false";
+
random="--random=$(date "+%s")";
no_colour="";
until_fail="";
@@ -62,14 +67,13 @@
done;
shift $((${OPTIND} - 1));
-twisted="$(cd "${wd}/.." && pwd -L)/Twisted";
+export PYTHONPATH="${wd}";
+dependencies;
-export PYTHONPATH="$("${wd}/run" -p)";
-
if [ $# -gt 0 ]; then
test_modules="$@";
else
test_modules="calendarserver twistedcaldav twext txdav txcaldav txcarddav ${m_twisted}";
fi;
-cd "${wd}" && "${python}" "${twisted}/bin/trial" --rterrors ${random} ${until_fail} ${no_colour} ${coverage} ${test_modules};
+cd "${wd}" && "trial" --rterrors ${random} ${until_fail} ${no_colour} ${coverage} ${test_modules};
Modified: CalendarServer/branches/new-store/twext/python/datetime.py
===================================================================
--- CalendarServer/branches/new-store/twext/python/datetime.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twext/python/datetime.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -136,6 +136,14 @@
def dateOrDatetime(self):
return self._dateOrDatetime
+ def timetuple(self):
+ #
+ # This is required to make comparison with datetimes work. See:
+ # http://bugs.python.org/issue8005
+ # http://docs.python.org/release/2.6.5/library/datetime.html#datetime.date.day
+ #
+ return self._dateOrDatetime.timetuple()
+
def iCalendarString(self):
if self._isDatetime:
return dateTimeToString(self._dateOrDatetime)
Modified: CalendarServer/branches/new-store/twext/python/filepath.py
===================================================================
--- CalendarServer/branches/new-store/twext/python/filepath.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twext/python/filepath.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -31,14 +31,14 @@
from types import FunctionType, MethodType
from errno import EINVAL
-from twisted.python.filepath import FilePath
+from twisted.python.filepath import FilePath as _FilePath
from stat import S_ISDIR
-class CachingFilePath(FilePath, object):
+class CachingFilePath(_FilePath, object):
"""
- A descendent of L{twisted.python.filepath.FilePath} which implements a more
- aggressive caching policy.
+ A descendent of L{_FilePath} which implements a more aggressive caching
+ policy.
"""
_listdir = _listdir # integration points for tests
@@ -56,11 +56,11 @@
@property
def siblingExtensionSearch(self):
"""
- Dynamically create a version of L{FilePath.siblingExtensionSearch} that
+ Dynamically create a version of L{_FilePath.siblingExtensionSearch} that
uses a pluggable 'listdir' implementation.
"""
return MethodType(FunctionType(
- FilePath.siblingExtensionSearch.im_func.func_code,
+ _FilePath.siblingExtensionSearch.im_func.func_code,
{'listdir': self._retryListdir,
'basename': _basename,
'dirname': _dirname,
@@ -121,20 +121,19 @@
def moveTo(self, destination, followLinks=True):
"""
- Override L{FilePath.moveTo}, updating extended cache information if
+ Override L{_FilePath.moveTo}, updating extended cache information if
necessary.
"""
- try:
- return super(CachingFilePath, self).moveTo(destination, followLinks)
- except OSError:
- raise
- else:
- self.changed()
+ result = super(CachingFilePath, self).moveTo(destination, followLinks)
+ self.changed()
+ # Work with vanilla FilePath destinations to pacify the tests.
+ if hasattr(destination, "changed"):
+ destination.changed()
def remove(self):
"""
- Override L{FilePath.remove}, updating extended cache information if
+ Override L{_FilePath.remove}, updating extended cache information if
necessary.
"""
try:
Modified: CalendarServer/branches/new-store/twext/python/test/test_datetime.py
===================================================================
--- CalendarServer/branches/new-store/twext/python/test/test_datetime.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twext/python/test/test_datetime.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -46,6 +46,10 @@
class DatetimeTests(TestCase):
+ @testUnimplemented
+ def test_timetuple(self):
+ raise NotImplementedError()
+
@timezones
def test_date_date(self, tz):
d = date.today()
@@ -101,7 +105,7 @@
#
self.assertTrue (first == base(first) )
- #self.assertTrue (base(first) == first ) # Bug in datetime
+ self.assertTrue (base(first) == first ) # Bug in datetime
self.assertTrue (first == base(first) )
self.assertTrue (first != base(second))
self.assertTrue (base(first) != second )
@@ -109,36 +113,36 @@
self.assertTrue (first < second )
self.assertTrue (second < third )
self.assertTrue (first < base(second))
- #self.assertTrue (base(second) < third ) # Bug in datetime
+ self.assertTrue (base(second) < third ) # Bug in datetime
self.assertTrue (first < second )
self.assertTrue (second < third )
- #self.assertTrue (base(first) < second )
+ self.assertTrue (base(first) < second )
self.assertTrue (second < base(third) ) # Bug in datetime
self.assertTrue (first <= second )
self.assertTrue (second <= third )
self.assertTrue (first <= base(second))
- #self.assertTrue (base(second) <= third ) # Bug in datetime
+ self.assertTrue (base(second) <= third ) # Bug in datetime
self.assertTrue (first <= base(second))
- #self.assertTrue (base(second) <= third ) # Bug in datetime
+ self.assertTrue (base(second) <= third ) # Bug in datetime
self.assertTrue (first <= second )
self.assertTrue (second <= third )
- #self.assertTrue (base(first) <= second ) # Bug in datetime
+ self.assertTrue (base(first) <= second ) # Bug in datetime
self.assertTrue (second <= base(third) )
self.assertFalse(first > second )
self.assertFalse(second > third )
self.assertFalse(first > base(second))
- #self.assertFalse(base(second) > third ) # Bug in datetime
+ self.assertFalse(base(second) > third ) # Bug in datetime
self.assertFalse(first > second )
self.assertFalse(second > third )
- #self.assertFalse(base(first) > second ) # Bug in datetime
+ self.assertFalse(base(first) > second ) # Bug in datetime
self.assertFalse(second > base(third) )
self.assertFalse(first >= second )
self.assertFalse(second >= third )
self.assertFalse(first >= base(second))
- #self.assertFalse(base(second) >= third ) # Bug in datetime
+ self.assertFalse(base(second) >= third ) # Bug in datetime
self.assertFalse(first >= second )
self.assertFalse(second >= third )
- #self.assertFalse(base(first) >= second ) # Bug in datetime
+ self.assertFalse(base(first) >= second ) # Bug in datetime
self.assertFalse(second >= base(third) )
def test_date_iCalendarString(self):
@@ -231,7 +235,7 @@
@testUnimplemented
def test_compare(self):
- raise NotImplemented
+ raise NotImplementedError()
@featureUnimplemented
def test_overlapsWith(self):
Modified: CalendarServer/branches/new-store/twext/web2/dav/element/base.py
===================================================================
--- CalendarServer/branches/new-store/twext/web2/dav/element/base.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twext/web2/dav/element/base.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -75,11 +75,11 @@
protected = False # See RFC 3253 section 1.4.1
unregistered = False # Subclass of factory; doesn't register
- def qname(self):
- return (self.namespace, self.name)
+ def qname(cls):
+ return (cls.namespace, cls.name)
- def sname(self):
- return "{%s}%s" % (self.namespace, self.name)
+ def sname(cls):
+ return "{%s}%s" % (cls.namespace, cls.name)
qname = classmethod(qname)
sname = classmethod(sname)
Modified: CalendarServer/branches/new-store/twistedcaldav/directory/test/test_calendar.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/directory/test/test_calendar.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/directory/test/test_calendar.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -14,57 +14,25 @@
# limitations under the License.
##
-import os
-
from twisted.internet.defer import inlineCallbacks
from twext.web2.dav import davxml
from twext.web2.test.test_server import SimpleRequest
from twistedcaldav import caldavxml
-from twistedcaldav.directory import augment
-from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
-from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.static import CalendarHomeProvisioningFile
-import twistedcaldav.test.util
-from twistedcaldav.config import config
+from twistedcaldav.test.util import TestCase
-class ProvisionedCalendars (twistedcaldav.test.util.TestCase):
+class ProvisionedCalendars (TestCase):
"""
Directory service provisioned principals.
"""
def setUp(self):
super(ProvisionedCalendars, self).setUp()
-
- # Setup the initial directory
- self.xmlFile = os.path.join(config.DataRoot, "accounts.xml")
- fd = open(self.xmlFile, "w")
- fd.write(open(xmlFile.path, "r").read())
- fd.close()
- self.directoryService = XMLDirectoryService({'xmlFile' : "accounts.xml"})
- augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
-
- # Set up a principals hierarchy for each service we're testing with
- name = "principals"
- url = "/" + name + "/"
- provisioningResource = DirectoryPrincipalProvisioningResource(url, self.directoryService)
-
- self.site.resource.putChild("principals", provisioningResource)
-
+ self.createStockDirectoryService()
self.setupCalendars()
- self.site.resource.setAccessControlList(davxml.ACL())
- def setupCalendars(self):
- calendarCollection = CalendarHomeProvisioningFile(
- os.path.join(self.docroot, "calendars"),
- self.directoryService,
- "/calendars/"
- )
- self.site.resource.putChild("calendars", calendarCollection)
-
def test_NonExistentCalendarHome(self):
def _response(resource):
Modified: CalendarServer/branches/new-store/twistedcaldav/directory/test/test_guidchange.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/directory/test/test_guidchange.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/directory/test/test_guidchange.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -13,62 +13,34 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
-from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
-from twistedcaldav.static import CalendarHomeProvisioningFile
from twistedcaldav.directory.directory import DirectoryService
-import os
-
from twext.web2.dav import davxml
from twext.web2.dav.resource import AccessDeniedError
from twext.web2.test.test_server import SimpleRequest
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
+from twistedcaldav.test.util import TestCase, xmlFile
-import twistedcaldav.test.util
-from twistedcaldav.directory import augment
-
-class ProvisionedPrincipals (twistedcaldav.test.util.TestCase):
+class ProvisionedPrincipals(TestCase):
"""
Directory service provisioned principals.
"""
def setUp(self):
super(ProvisionedPrincipals, self).setUp()
-
- # Setup the initial directory
- self.xmlFile = os.path.abspath(self.mktemp())
- fd = open(self.xmlFile, "w")
- fd.write(open(xmlFile.path, "r").read())
- fd.close()
- self.directoryService = XMLDirectoryService({'xmlFile' : self.xmlFile})
- augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
-
- # Set up a principals hierarchy for each service we're testing with
- name = "principals"
- url = "/" + name + "/"
- provisioningResource = DirectoryPrincipalProvisioningResource(url, self.directoryService)
-
- self.site.resource.putChild("principals", provisioningResource)
-
+ # Setup the initial directory
+ self.createStockDirectoryService()
self.setupCalendars()
self.site.resource.setAccessControlList(davxml.ACL())
- def setupCalendars(self):
- calendarCollection = CalendarHomeProvisioningFile(
- os.path.join(self.docroot, "calendars"),
- self.directoryService,
- "/calendars/"
- )
- self.site.resource.putChild("calendars", calendarCollection)
def resetCalendars(self):
del self.site.resource.putChildren["calendars"]
self.setupCalendars()
+
def test_guidchange(self):
"""
DirectoryPrincipalResource.proxies()
@@ -80,10 +52,9 @@
def privs1(result):
# Change GUID in record
- fd = open(self.xmlFile, "w")
- fd.write(open(xmlFile.path, "r").read().replace(oldUID, newUID))
- fd.close()
- fd = None
+ self.xmlFile.setContent(
+ xmlFile.getContent().replace(oldUID, newUID)
+ )
# Force re-read of records (not sure why _fileInfo has to be wiped here...)
self.directoryService._fileInfo = (0, 0)
Modified: CalendarServer/branches/new-store/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/directory/test/test_proxyprincipalmembers.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/directory/test/test_proxyprincipalmembers.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -19,8 +19,7 @@
from twext.web2.dav import davxml
from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile,\
- proxiesFile
+from twistedcaldav.test.util import xmlFile, augmentsFile, proxiesFile
from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
from twistedcaldav.directory.xmlfile import XMLDirectoryService
@@ -34,7 +33,7 @@
"""
Directory service provisioned principals.
"""
-
+
@inlineCallbacks
def setUp(self):
super(ProxyPrincipals, self).setUp()
Modified: CalendarServer/branches/new-store/twistedcaldav/directory/test/test_xmlfile.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/directory/test/test_xmlfile.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/directory/test/test_xmlfile.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -14,20 +14,14 @@
# limitations under the License.
##
-import os
-
from twext.python.filepath import CachingFilePath as FilePath
-from twistedcaldav.test.util import TestCase
from twistedcaldav.directory import augment
from twistedcaldav.directory.directory import DirectoryService
import twistedcaldav.directory.test.util
from twistedcaldav.directory.xmlfile import XMLDirectoryService
+from twistedcaldav.test.util import TestCase, xmlFile, augmentsFile
-xmlFile = FilePath(os.path.join(os.path.dirname(__file__), "accounts.xml"))
-augmentsFile = FilePath(os.path.join(os.path.dirname(__file__), "augments.xml"))
-proxiesFile = FilePath(os.path.join(os.path.dirname(__file__), "proxies.xml"))
-
# FIXME: Add tests for GUID hooey, once we figure out what that means here
class XMLFileBase(object):
@@ -104,7 +98,8 @@
"""
def service(self):
directory = XMLDirectoryService({'xmlFile' : self.xmlFile()}, alwaysStat=True)
- augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(self.augmentsFile().path,))
+ self.patch(augment, "AugmentService",
+ augment.AugmentXMLDB(xmlFiles=(self.augmentsFile().path,)))
return directory
def test_changedXML(self):
@@ -304,5 +299,10 @@
))
def test_recordTypesSubset(self):
- directory = XMLDirectoryService({'xmlFile' : self.xmlFile(), 'recordTypes' : (DirectoryService.recordType_users, DirectoryService.recordType_groups)}, alwaysStat=True)
+ directory = XMLDirectoryService(
+ {'xmlFile' : self.xmlFile(),
+ 'recordTypes' : (DirectoryService.recordType_users,
+ DirectoryService.recordType_groups)},
+ alwaysStat=True
+ )
self.assertEquals(set(("users", "groups")), set(directory.recordTypes()))
Modified: CalendarServer/branches/new-store/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/resource.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/resource.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -39,7 +39,7 @@
from twext.web2.dav.http import ErrorResponse
from twisted.internet import reactor
-from twisted.internet.defer import Deferred, succeed
+from twisted.internet.defer import Deferred, succeed, maybeDeferred
from twisted.internet.defer import inlineCallbacks, returnValue
from twext.web2 import responsecode
from twext.web2.dav import davxml
@@ -154,6 +154,54 @@
return super(CalDAVResource, self).render(request)
+ _associatedTransaction = None
+
+ def associateWithTransaction(self, transaction):
+ """
+ Associate this resource with a L{txcaldav.idav.ITransaction}; when this
+ resource (or any of its children) are rendered successfully, commit the
+ transaction. Otherwise, abort the transaction.
+
+ @param transaction: the transaction to associate this resource and its
+ children with.
+
+ @type transaction: L{txcaldav.idav.ITransaction}
+ """
+ # FIXME: needs to reject association with transaction if it's already
+ # got one (resources associated with a transaction are not reusable)
+ self._associatedTransaction = transaction
+
+
+ def propagateTransaction(self, otherResource):
+ """
+ Propagate the transaction associated with this resource to another
+ resource (which should ostensibly be a child resource).
+
+ @param otherResource: Another L{CalDAVResource}, usually one being
+ constructed as a child of this one.
+
+ @type otherResource: L{CalDAVResource} (or a subclass thereof)
+ """
+ otherResource.associateWithTransaction(self._associatedTransaction)
+
+
+ def renderHTTP(self, request):
+ """
+ Override C{renderHTTP} to commit the transaction when the resource is
+ successfully rendered.
+
+ @param request: the request to generate a response for.
+ @type request: L{twext.web2.iweb.IRequest}
+ """
+ d = maybeDeferred(super(CalDAVResource, self).renderHTTP, request)
+ def succeeded(result):
+ if self._associatedTransaction is not None:
+ self._associatedTransaction.commit()
+ return result
+ # FIXME: needs a failure handler
+ return d.addCallback(succeeded)
+
+
##
# WebDAV
##
Modified: CalendarServer/branches/new-store/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/static.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/static.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -1,3 +1,4 @@
+# -*- test-case-name: twistedcaldav.test -*-
##
# Copyright (c) 2005-2010 Apple Inc. All rights reserved.
#
@@ -108,6 +109,9 @@
from twistedcaldav.notifications import NotificationCollectionResource,\
NotificationResource
+from txcaldav.calendarstore.file import CalendarStore
+from txdav.propertystore.base import PropertyName
+
log = Logger()
class ReadOnlyResourceMixIn(object):
@@ -124,6 +128,70 @@
(caldav_namespace, "calendar-collection-location-ok")
)
+
+class _NewStorePropertiesWrapper(object):
+ """
+ Wrap a new-style property store (a L{txdav.idav.IPropertyStore}) in the old-
+ style interface for compatibility with existing code.
+ """
+
+ def __init__(self, newPropertyStore):
+ """
+ Initialize an old-style property store from a new one.
+
+ @param newPropertyStore: the new-style property store.
+ @type newPropertyStore: L{txdav.idav.IPropertyStore}
+ """
+ self._newPropertyStore = newPropertyStore
+
+ @classmethod
+ def _convertKey(cls, qname):
+ namespace, name = qname
+ return PropertyName(namespace, name)
+
+
+ # FIXME 'uid' here should be verifying something.
+ def get(self, qname, uid=None):
+ """
+
+ """
+ try:
+ return self._newPropertyStore[self._convertKey(qname)]
+ except KeyError:
+ raise HTTPError(StatusResponse(
+ responsecode.NOT_FOUND,
+ "No such property: {%s}%s" % qname))
+
+
+ def set(self, property, uid=None):
+ """
+
+ """
+ self._newPropertyStore[self._convertKey(property.qname())] = property
+
+
+ def delete(self, qname, uid=None):
+ """
+
+ """
+ del self._newPropertyStore[self._convertKey(qname)]
+
+
+ def contains(self, qname, uid=None, cache=True):
+ """
+
+ """
+ return (self._convertKey(qname) in self._newPropertyStore)
+
+
+ def list(self, uid=None, filterByUID=True, cache=True):
+ """
+
+ """
+ return [(pname.namespace, pname.name) for pname in
+ self._newPropertyStore.keys()]
+
+
class CalDAVFile (LinkFollowerMixIn, CalDAVResource, DAVFile):
"""
CalDAV-accessible L{DAVFile} resource.
@@ -186,12 +254,15 @@
def deadProperties(self, caching=True):
if not hasattr(self, "_dead_properties"):
+ # FIXME: this code should actually be dead, as the property store
+ # should be initialized as part of the traversal process.
+
# Get the property store from super
deadProperties = super(CalDAVFile, self).deadProperties()
if caching:
# Wrap the property store in a memory store
- deadProperties = CachingPropertyStore(deadProperties)
+ deadProperties = CachingPropertyStore(deadProperties)
self._dead_properties = deadProperties
@@ -202,11 +273,19 @@
##
def createCalendar(self, request):
- #
- # request object is required because we need to validate against parent
- # resources, and we need the request in order to locate the parents.
- #
+ """
+ External API for creating a calendar. Verify that the parent is a
+ collection, exists, is I{not} a calendar collection; that this resource
+ does not yet exist, then create it.
+ @param request: the request used to look up parent resources to
+ validate.
+
+ @type request: L{twext.web2.iweb.IRequest}
+
+ @return: a deferred that fires when a calendar collection has been
+ created in this resource.
+ """
if self.fp.exists():
log.err("Attempt to create collection where file exists: %s" % (self.fp.path,))
raise HTTPError(StatusResponse(responsecode.NOT_ALLOWED, "File exists"))
@@ -234,29 +313,37 @@
parent.addCallback(_defer)
return parent
+
def createCalendarCollection(self):
- #
- # Create the collection once we know it is safe to do so
- #
- def onCalendarCollection(status):
- if status != responsecode.CREATED:
- raise HTTPError(status)
+ """
+ Internal API for creating a calendar collection.
- # Initialize CTag on the calendar collection
- d1 = self.bumpSyncToken()
+ This will immediately create the collection without performing any
+ verification. For the normal API, see L{CalDAVFile.createCalendar}.
- # Calendar is initially transparent to freebusy
- self.writeDeadProperty(caldavxml.ScheduleCalendarTransp(caldavxml.Transparent()))
+ @return: a L{Deferred} which fires when the underlying collection has
+ actually been created.
+ """
+ # d = self.createSpecialCollection(davxml.ResourceType.calendar)
+ d = succeed(responsecode.CREATED)
+ calendarName = self.fp.basename()
+ self._newStoreParentHome.createCalendarWithName(calendarName)
+ self._newStoreCalendar = self._newStoreParentHome.calendarWithName(
+ calendarName
+ )
+ self._dead_properties = _NewStorePropertiesWrapper(
+ self._newStoreCalendar.properties()
+ )
+ return d
- # Create the index so its ready when the first PUTs come in
- d1.addCallback(lambda _: self.index().create())
- d1.addCallback(lambda _: status)
- return d1
- d = self.createSpecialCollection(davxml.ResourceType.calendar)
- d.addCallback(onCalendarCollection)
- return d
+ def isCollection(self):
+ if getattr(self, "_newStoreCalendar", None) is not None:
+ # FIXME: this should really be represented by a separate class
+ return True
+ return super(CalDAVFile, self).isCollection()
+
def createSpecialCollection(self, resourceType=None):
#
# Create the collection once we know it is safe to do so
@@ -620,10 +707,21 @@
similar = super(CalDAVFile, self).createSimilarFile(path)
if isCalendarCollectionResource(self):
+ similar._newStoreObject = \
+ self._newStoreCalendar.calendarObjectWithName(
+ similar.fp.basename()
+ )
+ if similar._newStoreObject is not None:
+ # FIXME: what about creation in http_PUT?
+ similar._dead_properties = _NewStorePropertiesWrapper(
+ similar._newStoreObject.properties()
+ )
+ # FIXME: tests should fail without this:
+ # self.propagateTransaction(similar)
# Short-circuit stat with information we know to be true at this point
if isinstance(path, FilePath) and hasattr(self, "knownChildren"):
- if os.path.basename(path.path) in self.knownChildren:
+ if path.basename() in self.knownChildren:
path.existsCached = True
path.isDirCached = False
@@ -822,28 +920,45 @@
super(AutoProvisioningFileMixIn, self)._initTypeAndEncoding()
-class CalendarHomeProvisioningFile (AutoProvisioningFileMixIn, DirectoryCalendarHomeProvisioningResource, DAVFile):
+class CalendarHomeProvisioningFile(AutoProvisioningFileMixIn,
+ DirectoryCalendarHomeProvisioningResource,
+ DAVFile):
"""
Resource which provisions calendar home collections as needed.
"""
+
def __init__(self, path, directory, url):
"""
- @param path: the path to the file which will back the resource.
+ Initialize this L{CalendarHomeProvisioningFile}.
+
+ @param path: the path to the filesystem directory which will back the
+ resource.
+
+ @type path: L{FilePath}
+
@param directory: an L{IDirectoryService} to provision calendars from.
- @param url: the canonical URL for the resource.
+
+ @param url: the canonical URL for this L{CalendarHomeProvisioningFile}
+ resource.
"""
DAVFile.__init__(self, path)
DirectoryCalendarHomeProvisioningResource.__init__(self, directory, url)
+ storagePath = self.fp.child(uidsResourceName)
+ self._newStore = CalendarStore(storagePath)
+
def provisionChild(self, name):
if name == uidsResourceName:
return CalendarHomeUIDProvisioningFile(self.fp.child(name).path, self)
return CalendarHomeTypeProvisioningFile(self.fp.child(name).path, self, name)
+
def createSimilarFile(self, path):
raise HTTPError(responsecode.NOT_FOUND)
+
+
class CalendarHomeTypeProvisioningFile (AutoProvisioningFileMixIn, DirectoryCalendarHomeTypeProvisioningResource, DAVFile):
def __init__(self, path, parent, recordType):
"""
@@ -854,6 +969,8 @@
DAVFile.__init__(self, path)
DirectoryCalendarHomeTypeProvisioningResource.__init__(self, parent, recordType)
+
+
class CalendarHomeUIDProvisioningFile (AutoProvisioningFileMixIn, DirectoryCalendarHomeUIDProvisioningResource, DAVFile):
def __init__(self, path, parent, homeResourceClass=None):
"""
@@ -950,7 +1067,8 @@
def url(self):
return joinURL(self.parent.url(), self.record.uid)
-class CalendarHomeFile (AutoProvisioningFileMixIn, SharedHomeMixin, DirectoryCalendarHomeResource, CalDAVFile):
+class CalendarHomeFile(AutoProvisioningFileMixIn, SharedHomeMixin,
+ DirectoryCalendarHomeResource, CalDAVFile):
"""
Calendar home collection resource.
"""
@@ -969,7 +1087,13 @@
self.clientNotifier = ClientNotifier(self)
CalDAVFile.__init__(self, path)
DirectoryCalendarHomeResource.__init__(self, parent, record)
+ txn = self.parent.parent._newStore.newTransaction()
+ self._newStoreCalendarHome = (
+ txn.calendarHomeWithUID(self.record.uid, create=True)
+ )
+ self.associateWithTransaction(txn)
+
def provision(self):
result = super(CalendarHomeFile, self).provision()
if config.Sharing.Enabled:
@@ -1010,8 +1134,21 @@
if self.comparePath(path):
return self
else:
- similar = CalDAVFile(path, principalCollections=self.principalCollections())
+ similar = CalDAVFile(
+ path, principalCollections=self.principalCollections()
+ )
similar.clientNotifier = self.clientNotifier
+ similar._newStoreParentHome = self._newStoreCalendarHome
+ similar._newStoreCalendar = (
+ self._newStoreCalendarHome.calendarWithName(
+ similar.fp.basename()
+ )
+ )
+ if similar._newStoreCalendar is not None:
+ similar._dead_properties = _NewStorePropertiesWrapper(
+ similar._newStoreCalendar.properties()
+ )
+ self.propagateTransaction(similar)
return similar
def getChild(self, name):
Modified: CalendarServer/branches/new-store/twistedcaldav/test/test_calendarquery.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/test/test_calendarquery.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/test/test_calendarquery.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -26,13 +26,13 @@
from twext.web2.dav.util import davXMLFromStream
from twext.web2.test.test_server import SimpleRequest
-import twistedcaldav.test.util
from twistedcaldav import caldavxml
from twistedcaldav import ical
from twistedcaldav.index import db_basename
from twistedcaldav.query import queryfilter
+from twistedcaldav.test.util import HomeTestCase
-class CalendarQuery (twistedcaldav.test.util.TestCase):
+class CalendarQuery (HomeTestCase):
"""
calendar-query REPORT
"""
Modified: CalendarServer/branches/new-store/twistedcaldav/test/test_collectioncontents.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/test/test_collectioncontents.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/test/test_collectioncontents.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -14,9 +14,8 @@
# limitations under the License.
##
-import os
-
from twisted.internet.defer import DeferredList
+from twext.python.filepath import CachingFilePath as FilePath
from twext.web2 import responsecode
from twext.web2.iweb import IResponse
from twext.web2.stream import MemoryStream, FileStream
@@ -27,41 +26,48 @@
from twistedcaldav.memcachelock import MemcacheLock
from twistedcaldav.memcacher import Memcacher
from twistedcaldav.method.put_common import StoreCalendarObjectResource
-import twistedcaldav.test.util
-class CollectionContents (twistedcaldav.test.util.TestCase):
+
+from twistedcaldav.test.util import HomeTestCase
+
+class CollectionContents(HomeTestCase):
"""
PUT request
"""
- data_dir = os.path.join(os.path.dirname(__file__), "data")
+ dataPath = FilePath(__file__).sibling("data")
+
def setUp(self):
-
# Need to fake out memcache
def _getFakeMemcacheProtocol(self):
-
result = super(MemcacheLock, self)._getMemcacheProtocol()
if isinstance(result, Memcacher.nullCacher):
result = self._memcacheProtocol = Memcacher.memoryCacher()
-
return result
-
- MemcacheLock._getMemcacheProtocol = _getFakeMemcacheProtocol
+ self.patch(MemcacheLock, "_getMemcacheProtocol",
+ _getFakeMemcacheProtocol)
+
# Need to not do implicit behavior during these tests
def _fakeDoImplicitScheduling(self):
return False, False, False
-
- StoreCalendarObjectResource.doImplicitScheduling = _fakeDoImplicitScheduling
+ self.patch(StoreCalendarObjectResource , "doImplicitScheduling",
+ _fakeDoImplicitScheduling)
+
+ # Tests in this suite assume that the root resource is a calendar home.
+ # FIXME: there should be a centralized way of saying 'make this look
+ # like a calendar home'
super(CollectionContents, self).setUp()
+
def test_collection_in_calendar(self):
"""
Make (regular) collection in calendar
"""
calendar_path, calendar_uri = self.mkdtemp("collection_in_calendar")
- os.rmdir(calendar_path)
+ calPath = FilePath(calendar_path)
+ calPath.remove()
def mkcalendar_cb(response):
response = IResponse(response)
@@ -75,7 +81,7 @@
if response.code != responsecode.FORBIDDEN:
self.fail("Incorrect response to nested MKCOL: %s" % (response.code,))
- nested_uri = os.path.join(calendar_uri, "nested")
+ nested_uri = "/".join([calendar_uri, "nested"])
request = SimpleRequest(self.site, "MKCOL", nested_uri)
self.send(request, mkcol_cb)
@@ -95,27 +101,37 @@
finally:
dst_file.close()
+ def openHolidays(self):
+ """
+ Open the 'Holidays.ics' calendar.
+
+ @return: an open file pointing at the start of Holidays.ics
+
+ @rtype: C{file}
+ """
+ f = self.dataPath.child("Holidays.ics").open()
+ self.addCleanup(f.close)
+ return f
+
+
def test_monolithic_ical(self):
"""
Monolithic iCalendar file in calendar collection
"""
# FIXME: Should FileStream be OK here?
- dst_file = file(os.path.join(self.data_dir, "Holidays.ics"))
- try:
- stream = FileStream(dst_file)
- return self._test_file_in_calendar("monolithic iCalendar file in calendar", (stream, responsecode.FORBIDDEN))
- finally:
- dst_file.close()
+ dst_file = self.openHolidays()
+ stream = FileStream(dst_file)
+ return self._test_file_in_calendar("monolithic iCalendar file in calendar", (stream, responsecode.FORBIDDEN))
+
def test_single_events(self):
"""
Single events in calendar collection
"""
work = []
- stream = file(os.path.join(self.data_dir, "Holidays.ics"))
- try: calendar = Component.fromStream(stream)
- finally: stream.close()
+ stream = self.openHolidays()
+ calendar = Component.fromStream(stream)
for subcomponent in calendar.subcomponents():
if subcomponent.name() == "VEVENT":
@@ -126,20 +142,24 @@
return self._test_file_in_calendar("single event in calendar", *work)
+
def test_duplicate_uids(self):
"""
Mutiple resources with the same UID.
"""
- stream = file(os.path.join(self.data_dir, "Holidays", "C318AA54-1ED0-11D9-A5E0-000A958A3252.ics"))
+ stream = self.dataPath.child(
+ "Holidays").child(
+ "C318AA54-1ED0-11D9-A5E0-000A958A3252.ics").open()
try: calendar = str(Component.fromStream(stream))
finally: stream.close()
return self._test_file_in_calendar(
"mutiple resources with the same UID",
- (MemoryStream(calendar), responsecode.CREATED ),
+ (MemoryStream(calendar), responsecode.CREATED),
(MemoryStream(calendar), responsecode.FORBIDDEN),
)
+
def _test_file_in_calendar(self, what, *work):
"""
Creates a calendar collection, then PUTs a resource into that collection
@@ -147,7 +167,8 @@
PUT request matches the given response_code.
"""
calendar_path, calendar_uri = self.mkdtemp("calendar")
- os.rmdir(calendar_path)
+ calPath = FilePath(calendar_path)
+ calPath.remove()
def mkcalendar_cb(response):
response = IResponse(response)
@@ -155,7 +176,7 @@
if response.code != responsecode.CREATED:
self.fail("MKCALENDAR failed: %s" % (response.code,))
- if not os.path.isdir(calendar_path):
+ if not calPath.isdir():
self.fail("MKCALENDAR did not create a collection")
ds = []
@@ -164,12 +185,11 @@
for stream, response_code in work:
def put_cb(response, stream=stream, response_code=response_code):
response = IResponse(response)
-
+
if response.code != response_code:
self.fail("Incorrect response to %s: %s (!= %s)" % (what, response.code, response_code))
- dst_uri = os.path.join(calendar_uri, "dst%d.ics" % (c,))
-
+ dst_uri = "/".join([calendar_uri, "dst%d.ics" % (c,)])
request = SimpleRequest(self.site, "PUT", dst_uri)
request.headers.setHeader("if-none-match", "*")
request.headers.setHeader("content-type", MimeType("text", "calendar"))
@@ -187,11 +207,9 @@
"""
Make sure database files are not listed as children.
"""
- colpath = self.site.resource.fp.path
- fd = open(os.path.join(colpath, "._bogus"), "w")
- fd.close()
- fd = open(os.path.join(colpath, "bogus"), "w")
- fd.close()
+ colpath = self.site.resource.fp
+ colpath.child("._bogus").touch()
+ colpath.child("bogus").touch()
children = self.site.resource.listChildren()
self.assertTrue("bogus" in children)
self.assertFalse("._bogus" in children)
@@ -201,7 +219,8 @@
Make (regular) collection in calendar
"""
calendar_path, calendar_uri = self.mkdtemp("dot_file_in_calendar")
- os.rmdir(calendar_path)
+ calPath = FilePath(calendar_path)
+ calPath.remove()
def mkcalendar_cb(response):
response = IResponse(response)
@@ -215,11 +234,14 @@
if response.code != responsecode.FORBIDDEN:
self.fail("Incorrect response to dot file PUT: %s" % (response.code,))
- stream = file(os.path.join(self.data_dir, "Holidays", "C318AA54-1ED0-11D9-A5E0-000A958A3252.ics"))
+ stream = self.dataPath.child(
+ "Holidays").child(
+ "C318AA54-1ED0-11D9-A5E0-000A958A3252.ics"
+ ).open()
try: calendar = str(Component.fromStream(stream))
finally: stream.close()
- event_uri = os.path.join(calendar_uri, ".event.ics")
+ event_uri = "/".join([calendar_uri, ".event.ics"])
request = SimpleRequest(self.site, "PUT", event_uri)
request.headers.setHeader("content-type", MimeType("text", "calendar"))
Modified: CalendarServer/branches/new-store/twistedcaldav/test/test_index.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/test/test_index.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/test/test_index.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -28,12 +28,35 @@
from twistedcaldav.query import queryfilter
from twistedcaldav.test.util import InMemoryMemcacheProtocol
import twistedcaldav.test.util
-from vobject.icalendar import utc
-import sqlite3
import datetime
import os
+
+class MinimalResourceReplacement(object):
+ """
+ Provide the minimal set of attributes and methods from CalDAVFile required
+ by L{Index}.
+ """
+
+ def __init__(self, filePath):
+ self.fp = filePath
+
+
+ def isCalendarCollection(self):
+ return True
+
+
+ def getChild(self, name):
+ # FIXME: this should really return something with a child method
+ return self.fp.child(name)
+
+
+ def initSyncToken(self):
+ pass
+
+
+
class SQLIndexTests (twistedcaldav.test.util.TestCase):
"""
Test abstract SQL DB class
@@ -42,7 +65,10 @@
def setUp(self):
super(SQLIndexTests, self).setUp()
self.site.resource.isCalendarCollection = lambda: True
- self.db = Index(self.site.resource)
+ self.indexDirPath = self.site.resource.fp
+ # FIXME: since this resource lies about isCalendarCollection, it doesn't
+ # have all the associated backend machinery to actually get children.
+ self.db = Index(MinimalResourceReplacement(self.indexDirPath))
def test_reserve_uid_ok(self):
@@ -235,7 +261,7 @@
revision += 1
calendar = Component.fromString(calendar_txt)
if ok:
- f = open(os.path.join(self.site.resource.fp.path, name), "w")
+ f = open(os.path.join(self.indexDirPath.path, name), "w")
f.write(calendar_txt)
del f
@@ -409,7 +435,7 @@
revision += 1
calendar = Component.fromString(calendar_txt)
- f = open(os.path.join(self.site.resource.fp.path, name), "w")
+ f = open(os.path.join(self.indexDirPath.path, name), "w")
f.write(calendar_txt)
del f
@@ -808,7 +834,7 @@
revision += 1
calendar = Component.fromString(calendar_txt)
- f = open(os.path.join(self.site.resource.fp.path, name), "w")
+ f = open(os.path.join(self.indexDirPath.path, name), "w")
f.write(calendar_txt)
del f
Modified: CalendarServer/branches/new-store/twistedcaldav/test/test_mkcalendar.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/test/test_mkcalendar.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/test/test_mkcalendar.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -25,11 +25,11 @@
from twext.web2.dav.fileop import rmdir
from twext.web2.test.test_server import SimpleRequest
-import twistedcaldav.test.util
from twistedcaldav import caldavxml
from twistedcaldav.static import CalDAVFile
+from twistedcaldav.test.util import HomeTestCase
-class MKCALENDAR (twistedcaldav.test.util.TestCase):
+class MKCALENDAR (HomeTestCase):
"""
MKCALENDAR request
"""
Modified: CalendarServer/branches/new-store/twistedcaldav/test/test_multiget.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/test/test_multiget.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/test/test_multiget.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -26,12 +26,12 @@
from twext.web2.dav.util import davXMLFromStream
from twext.web2.test.test_server import SimpleRequest
-import twistedcaldav.test.util
from twistedcaldav import caldavxml
from twistedcaldav import ical
from twistedcaldav.index import db_basename
+from twistedcaldav.test.util import HomeTestCase
-class CalendarMultiget (twistedcaldav.test.util.TestCase):
+class CalendarMultiget (HomeTestCase):
"""
calendar-multiget REPORT
"""
Modified: CalendarServer/branches/new-store/twistedcaldav/test/test_props.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/test/test_props.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/test/test_props.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -25,9 +25,9 @@
from twext.web2.test.test_server import SimpleRequest
from twistedcaldav import caldavxml
-import twistedcaldav.test.util
+from twistedcaldav.test.util import HomeTestCase
-class Properties (twistedcaldav.test.util.TestCase):
+class Properties(HomeTestCase):
"""
CalDAV properties
"""
Copied: CalendarServer/branches/new-store/twistedcaldav/test/test_wrapping.py (from rev 5593, CalendarServer/branches/users/wsanchez/transations/twistedcaldav/test/test_wrapping.py)
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/test/test_wrapping.py (rev 0)
+++ CalendarServer/branches/new-store/twistedcaldav/test/test_wrapping.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -0,0 +1,172 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+"""
+Tests for the interaction between model-level and protocol-level logic.
+"""
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from twistedcaldav.directory.calendar import uidsResourceName
+from twistedcaldav.ical import Component as VComponent
+
+from twistedcaldav.test.util import TestCase
+
+from txcaldav.calendarstore.file import CalendarStore, CalendarHome
+from txcaldav.calendarstore.test.test_file import event4_text
+
+
+
+class WrappingTests(TestCase):
+ """
+ Tests for L{twistedcaldav.static.CalDAVFile} creating the appropriate type
+ of txcaldav.calendarstore.file underlying object when it can determine what
+ type it really represents.
+ """
+
+ def setUp(self):
+ # Mostly copied from
+ # twistedcaldav.directory.test.test_calendar.ProvisionedCalendars; this
+ # should probably be refactored, perhaps even to call (some part of) the
+ # actual root-resource construction logic?
+ super(WrappingTests, self).setUp()
+
+ # Setup the initial directory
+ self.createStockDirectoryService()
+ self.setupCalendars()
+
+
+ def populateOneObject(self, objectName, objectText):
+ """
+ Populate one calendar object in the test user's calendar.
+
+ @param objectName: The name of a calendar object.
+ @type objectName: str
+ @param objectText: Some iCalendar text to populate it with.
+ @type objectText: str
+ """
+ record = self.directoryService.recordWithShortName("users", "wsanchez")
+ uid = record.uid
+ # XXX there should be a more test-friendly way to ensure the directory
+ # actually exists
+ try:
+ self.calendarCollection._newStore._path.createDirectory()
+ except:
+ pass
+ txn = self.calendarCollection._newStore.newTransaction()
+ home = txn.calendarHomeWithUID(uid, True)
+ cal = home.calendarWithName("calendar")
+ if cal is None:
+ home.createCalendarWithName("calendar")
+ cal = home.calendarWithName("calendar")
+ cal.createCalendarObjectWithName(objectName, VComponent.fromString(objectText))
+ txn.commit()
+
+
+ @inlineCallbacks
+ def getResource(self, path):
+ """
+ Retrieve a resource from the site.
+
+ @param path: the path from the root of the site (not starting with a
+ slash)
+
+ @type path: C{str}
+ """
+ segments = path.split("/")
+ resource = self.site.resource
+ while segments:
+ resource, segments = yield resource.locateChild(None, segments)
+ returnValue(resource)
+
+
+ def test_createStore(self):
+ """
+ Creating a CalendarHomeProvisioningFile will create a paired
+ CalendarStore.
+ """
+ self.assertIsInstance(self.calendarCollection._newStore, CalendarStore)
+ self.assertEquals(self.calendarCollection._newStore._path,
+ self.calendarCollection.fp.child(uidsResourceName))
+
+
+ @inlineCallbacks
+ def test_lookupCalendarHome(self):
+ """
+ When a L{CalDAVFile} representing an existing calendar home is looked up
+ in a CalendarHomeFile, it will create a corresponding L{CalendarHome}
+ via C{newTransaction().calendarHomeWithUID}.
+ """
+ calDavFile = yield self.getResource("calendars/users/wsanchez/")
+ self.assertEquals(calDavFile.fp, calDavFile._newStoreCalendarHome._path)
+ self.assertIsInstance(calDavFile._newStoreCalendarHome, CalendarHome)
+
+
+ @inlineCallbacks
+ def test_lookupExistingCalendar(self):
+ """
+ When a L{CalDAVFile} representing an existing calendar collection is
+ looked up in a L{CalendarHomeFile} representing a calendar home, it will
+ create a corresponding L{Calendar} via C{CalendarHome.calendarWithName}.
+ """
+ calDavFile = yield self.getResource("calendars/users/wsanchez/calendar")
+ self.assertEquals(calDavFile.fp, calDavFile._newStoreCalendar._path)
+
+
+ @inlineCallbacks
+ def test_lookupNewCalendar(self):
+ """
+ When a L{CalDAVFile} which represents a not-yet-created calendar
+ collection is looked up in a L{CalendarHomeFile} representing a calendar
+ home, it will initially have a new storage backend set to C{None}, but
+ when the calendar is created via a protocol action, the backend will be
+ initialized to match.
+ """
+ calDavFile = yield self.getResource("calendars/users/wsanchez/frobozz")
+ self.assertIdentical(calDavFile._newStoreCalendar, None)
+ calDavFile.createCalendarCollection()
+ self.assertEquals(calDavFile.fp, calDavFile._newStoreCalendar._path)
+
+
+ @inlineCallbacks
+ def test_lookupSpecial(self):
+ """
+ When a L{CalDAVFile} I{not} representing a calendar collection - one of
+ the special collections, like the dropbox or freebusy URLs - is looked
+ up in a L{CalendarHomeFile} representing a calendar home, it will I{not}
+ create a corresponding L{Calendar} via C{CalendarHome.calendarWithName}.
+ """
+ for specialName in ['dropbox', 'freebusy', 'notifications']:
+ calDavFile = yield self.getResource(
+ "calendars/users/wsanchez/%s" % (specialName,)
+ )
+ self.assertIdentical(
+ getattr(calDavFile, "_newStoreCalendar", None), None
+ )
+
+
+ @inlineCallbacks
+ def test_lookupCalendarObject(self):
+ """
+ When a L{CalDAVFile} representing an existing calendar object is looked
+ up on a L{CalDAVFile} representing a calendar collection, a parallel
+ L{CalendarObject} will be created (with a matching FilePath).
+ """
+ self.populateOneObject("1.ics", event4_text)
+ calDavFileCalendar = yield self.getResource(
+ "calendars/users/wsanchez/calendar/1.ics"
+ )
+ self.assertEquals(calDavFileCalendar._newStoreObject._path,
+ calDavFileCalendar.fp)
Modified: CalendarServer/branches/new-store/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/test/util.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/twistedcaldav/test/util.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -15,6 +15,7 @@
##
from __future__ import with_statement
+from twext.web2.dav import davxml
__all__ = [
"featureUnimplemented",
@@ -33,12 +34,17 @@
from twisted.internet.protocol import ProcessProtocol
from twext.python.memcacheclient import ClientFactory
+from twext.python.filepath import CachingFilePath as FilePath
import twext.web2.dav.test.util
from twext.web2.http import HTTPError, StatusResponse
from twistedcaldav import memcacher
from twistedcaldav.config import config
-from twistedcaldav.static import CalDAVFile
+from twistedcaldav.static import CalDAVFile, CalendarHomeProvisioningFile
+from twistedcaldav.directory.xmlfile import XMLDirectoryService
+from twistedcaldav.directory import augment
+from twistedcaldav.directory.principal import (
+ DirectoryPrincipalProvisioningResource)
DelayedCall.debug = True
@@ -50,9 +56,56 @@
testUnimplemented = lambda f: _todo(f, "Test unimplemented")
todo = lambda why: lambda f: _todo(f, why)
+dirTest = FilePath(__file__).parent().sibling("directory").child("test")
+
+xmlFile = dirTest.child("accounts.xml")
+augmentsFile = dirTest.child("augments.xml")
+proxiesFile = dirTest.child("proxies.xml")
+
class TestCase(twext.web2.dav.test.util.TestCase):
resource_class = CalDAVFile
+ def createStockDirectoryService(self):
+ """
+ Create a stock C{directoryService} attribute and assign it.
+ """
+ self.xmlFile = FilePath(config.DataRoot).child("accounts.xml")
+ self.xmlFile.setContent(xmlFile.getContent())
+
+ # *temporarily* set up an augment service so this directory service will
+ # work.
+ self.patch(augment, "AugmentService", augment.AugmentXMLDB(
+ xmlFiles=(augmentsFile.path,)
+ )
+ )
+
+ self.directoryService = XMLDirectoryService(
+ {'xmlFile' : "accounts.xml"}
+ )
+
+ # FIXME: see FIXME in DirectoryPrincipalProvisioningResource.__init__;
+ # this performs a necessary modification to the directory service
+ # object for it to be fully functional.
+ self.principalsResource = DirectoryPrincipalProvisioningResource(
+ "/principals/", self.directoryService
+ )
+
+
+ def setupCalendars(self):
+ """
+ Set up the resource at /calendars (a L{CalendarHomeProvisioningFile}),
+ and assign it as C{self.calendarCollection}.
+ """
+ path = self.site.resource.fp.child("calendars")
+ path.createDirectory()
+ self.calendarCollection = CalendarHomeProvisioningFile(
+ path,
+ self.directoryService,
+ "/calendars/"
+ )
+ self.site.resource.putChild("calendars", self.calendarCollection)
+
+
def setUp(self):
super(TestCase, self).setUp()
@@ -210,6 +263,65 @@
return verifyChildren(root, structure)
+
+class HomeTestCase(TestCase):
+ """
+ Utility class for tests which wish to interact with a calendar home rather
+ than a top-level resource hierarchy.
+ """
+
+ def setUp(self):
+ """
+ Replace self.site.resource with an appropriately provisioned
+ CalendarHomeFile, and replace self.docroot with a path pointing at that
+ file.
+ """
+ super(HomeTestCase, self).setUp()
+
+ fp = FilePath(self.mktemp())
+
+ self.createStockDirectoryService()
+
+ self.homeProvisioner = CalendarHomeProvisioningFile(
+ fp, self.directoryService, "/"
+ )
+ self._refreshRoot()
+
+
+ def _refreshRoot(self):
+ """
+ Refresh the user resource positioned at the root of this site, to give
+ it a new transaction.
+ """
+ users = self.homeProvisioner.getChild("users")
+ user = users.getChild("wsanchez")
+
+ # Force the request to succeed regardless of the implementation of
+ # accessControlList.
+ user.accessControlList = lambda request, *a, **k: succeed(
+ self.grantInherit(davxml.All())
+ )
+
+ # Fix the site to point directly at the user's calendar home so that we
+ # can focus on testing just that rather than hierarchy traversal..
+ self.site.resource = user
+
+ # Fix the docroot so that 'mkdtemp' will create directories in the right
+ # place (beneath the calendar).
+ self.docroot = user.fp.path
+
+
+ def send(self, request, callback):
+ """
+ Override C{send} in order to refresh the 'user' resource each time, to
+ get a new transaction to associate with the calendar home.
+ """
+ self._refreshRoot()
+ return super(HomeTestCase, self).send(request, callback)
+
+
+
+
class InMemoryPropertyStore(object):
def __init__(self):
class _FauxPath(object):
Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/file.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/file.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -1,3 +1,4 @@
+# -*- test-case-name: txcaldav.calendarstore.test.test_file -*-
##
# Copyright (c) 2010 Apple Inc. All rights reserved.
#
@@ -13,6 +14,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
+from twext.web2.dav.element.rfc2518 import ResourceType
+from txdav.propertystore.base import PropertyName
+from twistedcaldav.caldavxml import ScheduleCalendarTransp, Transparent
"""
File calendar store.
@@ -29,7 +33,6 @@
from zope.interface import implements
-from twext.python.filepath import CachingFilePath as FilePath
from twisted.internet.defer import inlineCallbacks
from twext.python.log import LoggingMixIn
@@ -38,6 +41,7 @@
from txdav.propertystore.xattr import PropertyStore
+from txcaldav.icalendarstore import ICalendarStoreTransaction
from txcaldav.icalendarstore import ICalendarStore, ICalendarHome
from txcaldav.icalendarstore import ICalendar, ICalendarObject
from txcaldav.icalendarstore import CalendarNameNotAllowedError
@@ -53,71 +57,217 @@
from twistedcaldav.index import Index as OldIndex
from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
+def _isValidName(name):
+ """
+ Determine if the given string is a valid name. i.e. does it conflict with
+ any of the other entities which may be on the filesystem?
+ @param name: a name which might be given to a calendar.
+ """
+ return not name.startswith(".")
+
+
+_unset = object()
+
+class _cached(object):
+ """
+ This object is a decorator for a 0-argument method which should be called
+ only once, and its result cached so that future invocations just return the
+ same result without calling the underlying method again.
+
+ @ivar thunk: the function to call to generate a cached value.
+ """
+
+ def __init__(self, thunk):
+ self.thunk = thunk
+
+
+ def __get__(self, oself, owner):
+ def inner():
+ cacheKey = "_" + self.thunk.__name__ + "_cached"
+ cached = getattr(oself, cacheKey, _unset)
+ if cached is _unset:
+ value = self.thunk(oself)
+ setattr(oself, cacheKey, value)
+ return value
+ else:
+ return cached
+ return inner
+
+
+
class CalendarStore(LoggingMixIn):
+ """
+ An implementation of L{ICalendarObject} backed by a
+ L{twext.python.filepath.CachingFilePath}.
+
+ @ivar _path: A L{CachingFilePath} referencing a directory on disk that
+ stores all calendar data for a group of uids.
+ """
implements(ICalendarStore)
- calendarHomeClass = property(lambda _: CalendarHome)
-
def __init__(self, path):
"""
- @param path: a L{FilePath}
+ Create a calendar store.
+
+ @param path: a L{FilePath} pointing at a directory on disk.
"""
- assert isinstance(path, FilePath)
+ self._path = path
- self.path = path
-
- if not path.isdir():
+# if not path.isdir():
# FIXME: Add CalendarStoreNotFoundError?
- raise NotFoundError("No such calendar store")
+# raise NotFoundError("No such calendar store")
def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self.path.path)
+ return "<%s: %s>" % (self.__class__.__name__, self._path.path)
+ def newTransaction(self):
+ """
+ Create a new filesystem-based transaction.
+
+ @see Transaction
+ """
+ return Transaction(self)
+
+
+
+class Transaction(LoggingMixIn):
+ """
+ In-memory implementation of
+
+ Note that this provides basic 'undo' support, but not truly transactional
+ operations.
+ """
+
+ implements(ICalendarStoreTransaction)
+
+ def __init__(self, calendarStore):
+ """
+ Initialize a transaction; do not call this directly, instead call
+ L{CalendarStore.newTransaction}.
+
+ @param calendarStore: The store that created this transaction.
+
+ @type calendarStore: L{CalendarStore}
+ """
+ self.calendarStore = calendarStore
+ self.aborted = False
+ self.committed = False
+ self._operations = []
+ self._calendarHomes = {}
+
+
+ def addOperation(self, operation):
+ self._operations.append(operation)
+
+ def abort(self):
+ if self.aborted:
+ raise RuntimeError("already aborted")
+ if self.committed:
+ raise RuntimeError("already committed")
+ self.aborted = True
+
+ def commit(self):
+ if self.aborted:
+ raise RuntimeError("already aborted")
+ if self.committed:
+ raise RuntimeError("already committed")
+
+ self.committed = True
+ undos = []
+
+ for operation in self._operations:
+ try:
+ undo = operation()
+ if undo is not None:
+ undos.append(undo)
+ except Exception, e:
+ for undo in undos:
+ try:
+ undo()
+ except Exception, e:
+ self.log_error("Exception while undoing transaction: %s" % (e,))
+ raise
+
def calendarHomeWithUID(self, uid, create=False):
+ if (uid, self) in self._calendarHomes:
+ return self._calendarHomes[(uid, self)]
+
if uid.startswith("."):
return None
assert len(uid) >= 4
- childPath = self.path.child(uid[0:2]).child(uid[2:4]).child(uid)
+ childPath1 = self.calendarStore._path.child(uid[0:2])
+ childPath2 = childPath1.child(uid[2:4])
+ childPath3 = childPath2.child(uid)
- if not childPath.isdir():
- if create:
- childPath.makedirs()
- else:
- return None
+ def do():
+ def createDirectory(path):
+ try:
+ path.createDirectory()
+ except (IOError, OSError), e:
+ if e.errno != errno.EEXIST:
+ # Ignore, in case someone else created the
+ # directory while we were trying to as well.
+ raise
- return CalendarHome(childPath, self)
+ if not childPath3.isdir():
+ if not childPath2.isdir():
+ if not childPath1.isdir():
+ createDirectory(childPath1)
+ createDirectory(childPath2)
+ createDirectory(childPath3)
+ if create:
+ self.addOperation(do)
+ elif not childPath3.isdir():
+ return None
+ calendarHome = CalendarHome(childPath3, self.calendarStore, self)
+ self._calendarHomes[(uid, self)] = calendarHome
+ return calendarHome
+
+
+
class CalendarHome(LoggingMixIn):
implements(ICalendarHome)
- calendarClass = property(lambda _: Calendar)
-
- def __init__(self, path, calendarStore):
- self.path = path
+ def __init__(self, path, calendarStore, transaction):
+ self._path = path
self.calendarStore = calendarStore
+ self._transaction = transaction
+ self._newCalendars = {}
+ self._removedCalendars = set()
def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self.path)
+ return "<%s: %s>" % (self.__class__.__name__, self._path)
def uid(self):
- return self.path.basename()
+ return self._path.basename()
+
+
+ def _updateSyncToken(self, reset=False):
+ "Stub for updating sync token."
def calendars(self):
- return (
+ return set(self._newCalendars.itervalues()) | set(
self.calendarWithName(name)
- for name in self.path.listdir()
+ for name in self._path.listdir()
if not name.startswith(".")
)
def calendarWithName(self, name):
+ calendar = self._newCalendars.get(name)
+ if calendar is not None:
+ return calendar
+ if name in self._removedCalendars:
+ return None
+
if name.startswith("."):
return None
- childPath = self.path.child(name)
+ childPath = self._path.child(name)
if childPath.isdir():
return Calendar(childPath, self)
else:
@@ -127,101 +277,189 @@
if name.startswith("."):
raise CalendarNameNotAllowedError(name)
- childPath = self.path.child(name)
+ childPath = self._path.child(name)
- try:
- childPath.createDirectory()
- except (IOError, OSError), e:
- if e.errno == errno.EEXIST:
- raise CalendarAlreadyExistsError(name)
- raise
+ if name not in self._removedCalendars and childPath.isdir():
+ raise CalendarAlreadyExistsError(name)
+ c = self._newCalendars[name] = Calendar(childPath, self)
+ def do():
+ try:
+ childPath.createDirectory()
+ # FIXME: direct tests, undo for index creation
+ Index(c)._oldIndex.create()
+
+ # Return undo
+ return lambda: childPath.remove()
+ except (IOError, OSError), e:
+ if e.errno == errno.EEXIST and childPath.isdir():
+ raise CalendarAlreadyExistsError(name)
+ raise
+
+ self._transaction.addOperation(do)
+ props = c.properties()
+ PN = PropertyName.fromString
+ CalendarType = ResourceType.calendar #@UndefinedVariable
+ props[PN(ResourceType.sname())] = CalendarType
+
+ # Calendars are initially transparent to freebusy. FIXME: freebusy
+ # needs more structured support than this.
+ props[PN(ScheduleCalendarTransp.sname())] = Transparent()
+ # FIXME: there's no need for 'flush' to be a public method of the
+ # property store any more. It should just be transactional like
+ # everything else; the API here would better be expressed as
+ # c.properties().participateInTxn(txn)
+ self._transaction.addOperation(c.properties().flush)
+ # FIXME: return c # maybe ?
+
def removeCalendarWithName(self, name):
- if name.startswith("."):
+ if name.startswith(".") or name in self._removedCalendars:
raise NoSuchCalendarError(name)
- childPath = self.path.child(name)
- try:
- childPath.remove()
- except (IOError, OSError), e:
- if e.errno == errno.ENOENT:
- raise NoSuchCalendarError(name)
- raise
+ self._removedCalendars.add(name)
+ childPath = self._path.child(name)
+ if name not in self._newCalendars and not childPath.isdir():
+ raise NoSuchCalendarError(name)
+ def do(transaction=self._transaction):
+ for i in xrange(1000):
+ trash = childPath.sibling("._del_%s_%d" % (childPath.basename(), i))
+ if not trash.exists():
+ break
+ else:
+ raise InternalDataStoreError("Unable to create trash target for calendar at %s" % (childPath,))
+
+ try:
+ childPath.moveTo(trash)
+ except (IOError, OSError), e:
+ if e.errno == errno.ENOENT:
+ raise NoSuchCalendarError(name)
+ raise
+
+ def cleanup():
+ try:
+ trash.remove()
+ except Exception, e:
+ self.log_error("Unable to delete trashed calendar at %s: %s" % (trash.fp, e))
+
+ transaction.addOperation(cleanup)
+
+ def undo():
+ trash.moveTo(childPath)
+
+ return undo
+
+
def properties(self):
- if not hasattr(self, "_properties"):
- self._properties = PropertyStore(self.path)
- return self._properties
+ # FIXME: needs tests for actual functionality
+ # FIXME: needs to be cached
+ return PropertyStore(self._path)
+
class Calendar(LoggingMixIn):
+ """
+ File-based implementation of L{ICalendar}.
+ """
implements(ICalendar)
- calendarObjectClass = property(lambda _: CalendarObject)
-
def __init__(self, path, calendarHome):
- self.path = path
+ self._path = path
self.calendarHome = calendarHome
+ self._transaction = calendarHome._transaction
+ self._newCalendarObjects = {}
+ self._cachedCalendarObjects = {}
+ self._removedCalendarObjects = set()
+
def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self.path.path)
+ return "<%s: %s>" % (self.__class__.__name__, self._path.path)
- def index(self):
- if not hasattr(self, "_index"):
- self._index = Index(self)
- return self._index
def name(self):
- return self.path.basename()
+ return self._path.basename()
+
def ownerCalendarHome(self):
return self.calendarHome
- def _calendarObjects_index(self):
- return self.index().calendarObjects()
- def _calendarObjects_listdir(self):
+# def _calendarObjects_index(self):
+# return self.index().calendarObjects()
+#
+# def _calendarObjects_listdir(self):
+# return (
+# self.calendarObjectWithName(name)
+# for name in self.path.listdir()
+# if not name.startswith(".")
+# )
+
+ def calendarObjects(self):
return (
self.calendarObjectWithName(name)
- for name in self.path.listdir()
- if not name.startswith(".")
+ for name in (
+ set(self._newCalendarObjects.iterkeys()) |
+ set(name for name in self._path.listdir() if not name.startswith("."))
+ )
)
- calendarObjects = _calendarObjects_index
def calendarObjectWithName(self, name):
- childPath = self.path.child(name)
- if childPath.isfile():
- return CalendarObject(childPath, self)
+ if name in self._removedCalendarObjects:
+ return None
+ if name in self._newCalendarObjects:
+ return self._newCalendarObjects[name]
+ if name in self._cachedCalendarObjects:
+ return self._cachedCalendarObjects[name]
+
+
+ calendarObjectPath = self._path.child(name)
+ if calendarObjectPath.isfile():
+ obj = CalendarObject(calendarObjectPath, self)
+ self._cachedCalendarObjects[name] = obj
+ return obj
else:
return None
+
def calendarObjectWithUID(self, uid):
- raise NotImplementedError()
+ # FIXME: This _really_ needs to be inspecting an index, not parsing
+ # every resource.
+ for calendarObjectPath in self._path.children():
+ if not _isValidName(calendarObjectPath.basename()):
+ continue
+ obj = CalendarObject(calendarObjectPath, self)
+ if obj.component().resourceUID() == uid:
+ return obj
+
def createCalendarObjectWithName(self, name, component):
if name.startswith("."):
raise CalendarObjectNameNotAllowedError(name)
- childPath = self.path.child(name)
- if childPath.exists():
+ calendarObjectPath = self._path.child(name)
+ if calendarObjectPath.exists():
raise CalendarObjectNameAlreadyExistsError(name)
- calendarObject = CalendarObject(childPath, self)
+ calendarObject = CalendarObject(calendarObjectPath, self)
calendarObject.setComponent(component)
+ self._cachedCalendarObjects[name] = calendarObject
+
def removeCalendarObjectWithName(self, name):
if name.startswith("."):
raise NoSuchCalendarObjectError(name)
- childPath = self.path.child(name)
- if childPath.isfile():
- childPath.remove()
+ calendarObjectPath = self._path.child(name)
+ if calendarObjectPath.isfile():
+ self._removedCalendarObjects.add(name)
+ # FIXME: test for undo
+ calendarObjectPath.remove()
else:
raise NoSuchCalendarObjectError(name)
def removeCalendarObjectWithUID(self, uid):
- raise NotImplementedError()
+ self.removeCalendarObjectWithName(self.calendarObjectWithUID(uid)._path.basename())
def syncToken(self):
raise NotImplementedError()
@@ -257,28 +495,40 @@
def calendarObjectsSinceToken(self, token):
raise NotImplementedError()
+
+ @_cached
def properties(self):
- if not hasattr(self, "_properties"):
- self._properties = PropertyStore(self.path)
- return self._properties
+ # FIXME: needs direct tests - only covered by calendar store tests
+ # FIXME: transactions
+ return PropertyStore(self._path)
+
class CalendarObject(LoggingMixIn):
+ """
+ @ivar path: The path of the .ics file on disk
+
+ @type path: L{FilePath}
+ """
implements(ICalendarObject)
def __init__(self, path, calendar):
- self.path = path
+ self._path = path
self.calendar = calendar
+ self._component = None
+
def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self.path.path)
+ return "<%s: %s>" % (self.__class__.__name__, self._path.path)
+
def name(self):
- return self.path.basename()
+ return self._path.basename()
+
def setComponent(self, component):
if not isinstance(component, VComponent):
- raise TypeError(VComponent)
+ raise TypeError(type(component))
try:
if component.resourceUID() != self.uid():
@@ -296,67 +546,71 @@
raise InvalidCalendarComponentError(e)
self._component = component
- if hasattr(self, "_text"):
- del self._text
+ # FIXME: needs to clear text cache
- fh = self.path.open("w")
- try:
- fh.write(str(component))
- finally:
- fh.close()
+ def do():
+ backup = None
+ if self._path.exists():
+ backup = self._path.temporarySibling()
+ self._path.moveTo(backup)
+ fh = self._path.open("w")
+ try:
+ # FIXME: concurrency problem; if this write is interrupted
+ # halfway through, the underlying file will be corrupt.
+ fh.write(str(component))
+ finally:
+ fh.close()
+ def undo():
+ if backup:
+ backup.moveTo(self._path)
+ else:
+ self._path.remove()
+ return undo
+ self.calendar._transaction.addOperation(do)
+
def component(self):
- if not hasattr(self, "_component"):
- text = self.iCalendarText()
+ if self._component is not None:
+ return self._component
+ text = self.iCalendarText()
- try:
- component = VComponent.fromString(text)
- except InvalidICalendarDataError, e:
- raise InternalDataStoreError(
- "File corruption detected (%s) in file: %s"
- % (e, self.path.path)
- )
+ try:
+ component = VComponent.fromString(text)
+ except InvalidICalendarDataError, e:
+ raise InternalDataStoreError(
+ "File corruption detected (%s) in file: %s"
+ % (e, self._path.path)
+ )
+ return component
- del self._text
- self._component = component
- return self._component
-
def iCalendarText(self):
- #
- # Note I'm making an assumption here that caching both is
- # redundant, so we're caching the text if it's asked for and
- # we don't have the component cached, then tossing it and
- # relying on the component if we have that cached. -wsv
- #
- if not hasattr(self, "_text"):
- if hasattr(self, "_component"):
- return str(self._component)
+ if self._component is not None:
+ return str(self._component)
+ try:
+ fh = self._path.open()
+ except IOError, e:
+ if e[0] == errno.ENOENT:
+ raise NoSuchCalendarObjectError(self)
+ else:
+ raise
- try:
- fh = self.path.open()
- except IOError, e:
- if e[0] == errno.ENOENT:
- raise NoSuchCalendarObjectError(self)
+ try:
+ text = fh.read()
+ finally:
+ fh.close()
- try:
- text = fh.read()
- finally:
- fh.close()
+ if not (
+ text.startswith("BEGIN:VCALENDAR\r\n") or
+ text.endswith("\r\nEND:VCALENDAR\r\n")
+ ):
+ raise InternalDataStoreError(
+ "File corruption detected (improper start) in file: %s"
+ % (self._path.path,)
+ )
+ return text
- if not (
- text.startswith("BEGIN:VCALENDAR\r\n") or
- text.endswith("\r\nEND:VCALENDAR\r\n")
- ):
- raise InternalDataStoreError(
- "File corruption detected (improper start) in file: %s"
- % (self.path.path,)
- )
- self._text = text
-
- return self._text
-
def uid(self):
if not hasattr(self, "_uid"):
self._uid = self.component().resourceUID()
@@ -372,10 +626,11 @@
def properties(self):
if not hasattr(self, "_properties"):
- self._properties = PropertyStore(self.path)
+ self._properties = PropertyStore(self._path)
return self._properties
+
class Index (object):
#
# OK, here's where we get ugly.
@@ -387,7 +642,7 @@
"""
def __init__(self, calendar):
self.calendar = calendar
- self.fp = self.calendar.path
+ self.fp = self.calendar._path
def isCalendarCollection(self):
return True
@@ -407,9 +662,15 @@
return None
def bumpSyncToken(self, reset=False):
+ # FIXME: needs direct tests
return self.calendar._updateSyncToken(reset)
+ def initSyncToken(self):
+ # FIXME: needs direct tests
+ self.bumpSyncToken(True)
+
+
def __init__(self, calendar):
self.calendar = calendar
self._oldIndex = OldIndex(Index.StubResource(calendar))
Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -25,7 +25,10 @@
from twext.python.vcomponent import VComponent
+from twext.web2.dav import davxml
+
from txdav.idav import IPropertyStore
+from txdav.propertystore.base import PropertyName
from txcaldav.icalendarstore import ICalendarStore, ICalendarHome
from txcaldav.icalendarstore import ICalendar, ICalendarObject
@@ -126,16 +129,20 @@
)
+
def _todo(f, why):
f.todo = why
return f
+
+
featureUnimplemented = lambda f: _todo(f, "Feature unimplemented")
testUnimplemented = lambda f: _todo(f, "Test unimplemented")
todo = lambda why: lambda f: _todo(f, why)
class PropertiesTestMixin(object):
+
def test_properties(self):
properties = self.home1.properties()
@@ -145,6 +152,7 @@
)
+
def setUpCalendarStore(test):
test.root = FilePath(test.mktemp())
test.root.createDirectory()
@@ -152,24 +160,36 @@
calendarPath = test.root.child("store")
storePath.copyTo(calendarPath)
+
test.calendarStore = CalendarStore(calendarPath)
+ test.txn = test.calendarStore.newTransaction()
assert test.calendarStore is not None, "No calendar store?"
+
+
def setUpHome1(test):
setUpCalendarStore(test)
- test.home1 = test.calendarStore.calendarHomeWithUID("home1")
+ test.home1 = test.txn.calendarHomeWithUID("home1")
assert test.home1 is not None, "No calendar home?"
+
+
def setUpCalendar1(test):
setUpHome1(test)
test.calendar1 = test.home1.calendarWithName("calendar_1")
assert test.calendar1 is not None, "No calendar?"
+
class CalendarStoreTest(unittest.TestCase):
+ """
+ Test cases for L{CalendarStore}.
+ """
+
def setUp(self):
setUpCalendarStore(self)
+
def test_interface(self):
"""
Interface is completed and conforming.
@@ -179,67 +199,79 @@
except BrokenMethodImplementation, e:
self.fail(e)
+
def test_init(self):
"""
Ivars are correctly initialized.
"""
self.failUnless(
- isinstance(self.calendarStore.path, FilePath),
- self.calendarStore.path
+ isinstance(self.calendarStore._path, FilePath),
+ self.calendarStore._path
)
+
def test_calendarHomeWithUID_exists(self):
"""
Find an existing calendar home by UID.
"""
- calendarHome = self.calendarStore.calendarHomeWithUID("home1")
+ calendarHome = self.calendarStore.newTransaction().calendarHomeWithUID("home1")
self.failUnless(isinstance(calendarHome, CalendarHome))
+
def test_calendarHomeWithUID_absent(self):
"""
Missing calendar home.
"""
self.assertEquals(
- self.calendarStore.calendarHomeWithUID("xyzzy"),
+ self.calendarStore.newTransaction().calendarHomeWithUID("xyzzy"),
None
)
+
def test_calendarHomeWithUID_create(self):
"""
Create missing calendar home.
"""
- calendarHome = self.calendarStore.calendarHomeWithUID(
+ txn = self.calendarStore.newTransaction()
+ calendarHome = txn.calendarHomeWithUID(
"xyzzy",
create=True
)
self.failUnless(isinstance(calendarHome, CalendarHome))
- self.failUnless(calendarHome.path.isdir())
+ self.failIf(calendarHome._path.isdir())
+ txn.commit()
+ self.failUnless(calendarHome._path.isdir())
+
def test_calendarHomeWithUID_create_exists(self):
"""
Create missing calendar home.
"""
- calendarHome = self.calendarStore.calendarHomeWithUID("home1")
+ calendarHome = self.calendarStore.newTransaction().calendarHomeWithUID("home1")
self.failUnless(isinstance(calendarHome, CalendarHome))
+
def test_calendarHomeWithUID_dot(self):
"""
Filenames starting with "." are reserved by this
implementation, so no UIDs may start with ".".
"""
self.assertEquals(
- self.calendarStore.calendarHomeWithUID("xyzzy"),
+ self.calendarStore.newTransaction().calendarHomeWithUID("xyzzy"),
None
)
+
class CalendarHomeTest(unittest.TestCase, PropertiesTestMixin):
+
def setUp(self):
setUpHome1(self)
+
def test_interface(self):
"""
Interface is completed and conforming.
@@ -249,31 +281,34 @@
except BrokenMethodImplementation, e:
self.fail(e)
+
def test_init(self):
"""
Ivars are correctly initialized.
"""
self.failUnless(
- isinstance(self.home1.path, FilePath),
- self.home1.path
+ isinstance(self.home1._path, FilePath),
+ self.home1._path
)
self.assertEquals(
self.home1.calendarStore,
self.calendarStore
)
+
def test_uid(self):
"""
UID is correct.
"""
self.assertEquals(self.home1.uid(), "home1")
+
def test_calendars(self):
"""
Find all of the calendars.
"""
# Add a dot directory to make sure we don't find it
- self.home1.path.child(".foo").createDirectory()
+ self.home1._path.child(".foo").createDirectory()
calendars = tuple(self.home1.calendars())
@@ -285,6 +320,7 @@
home1_calendarNames
)
+
def test_calendarWithName_exists(self):
"""
Find existing calendar by name.
@@ -294,21 +330,24 @@
self.failUnless(isinstance(calendar, Calendar), calendar)
self.assertEquals(calendar.name(), name)
+
def test_calendarWithName_absent(self):
"""
Missing calendar.
"""
self.assertEquals(self.home1.calendarWithName("xyzzy"), None)
+
def test_calendarWithName_dot(self):
"""
Filenames starting with "." are reserved by this
implementation, so no calendar names may start with ".".
"""
name = ".foo"
- self.home1.path.child(name).createDirectory()
+ self.home1._path.child(name).createDirectory()
self.assertEquals(self.home1.calendarWithName(name), None)
+
def test_createCalendarWithName_absent(self):
"""
Create a new calendar.
@@ -317,7 +356,22 @@
assert self.home1.calendarWithName(name) is None
self.home1.createCalendarWithName(name)
self.failUnless(self.home1.calendarWithName(name) is not None)
+ def checkProperties():
+ calendarProperties = self.home1.calendarWithName(name).properties()
+ self.assertEquals(
+ calendarProperties[
+ PropertyName.fromString(davxml.ResourceType.sname())
+ ],
+ davxml.ResourceType.calendar)
+ checkProperties()
+ self.txn.commit()
+ self.home1 = self.calendarStore.newTransaction().calendarHomeWithUID(
+ "home1")
+ # Sanity check: are the properties actually persisted?
+ # FIXME: no independent testing of this right now
+ checkProperties()
+
def test_createCalendarWithName_exists(self):
"""
Attempt to create an existing calendar should raise.
@@ -328,6 +382,7 @@
self.home1.createCalendarWithName, name
)
+
def test_createCalendarWithName_dot(self):
"""
Filenames starting with "." are reserved by this
@@ -338,15 +393,18 @@
self.home1.createCalendarWithName, ".foo"
)
+
def test_removeCalendarWithName_exists(self):
"""
Remove an existing calendar.
"""
for name in home1_calendarNames:
- assert self.home1.calendarWithName(name) is not None
+ self.assertNotIdentical(self.home1.calendarWithName(name),
+ None)
self.home1.removeCalendarWithName(name)
self.assertEquals(self.home1.calendarWithName(name), None)
+
def test_removeCalendarWithName_absent(self):
"""
Attempt to remove an non-existing calendar should raise.
@@ -356,22 +414,27 @@
self.home1.removeCalendarWithName, "xyzzy"
)
+
def test_removeCalendarWithName_dot(self):
"""
Filenames starting with "." are reserved by this
implementation, so no calendar names may start with ".".
"""
name = ".foo"
- self.home1.path.child(name).createDirectory()
+ self.home1._path.child(name).createDirectory()
self.assertRaises(
NoSuchCalendarError,
self.home1.removeCalendarWithName, name
)
+
+
class CalendarTest(unittest.TestCase, PropertiesTestMixin):
+
def setUp(self):
setUpCalendar1(self)
+
def test_interface(self):
"""
Interface is completed and conforming.
@@ -381,12 +444,13 @@
except BrokenMethodImplementation, e:
self.fail(e)
+
def test_init(self):
"""
Ivars are correctly initialized.
"""
self.failUnless(
- isinstance(self.calendar1.path, FilePath),
+ isinstance(self.calendar1._path, FilePath),
self.calendar1
)
self.failUnless(
@@ -394,12 +458,14 @@
self.calendar1.calendarHome
)
+
def test_name(self):
"""
Name is correct.
"""
self.assertEquals(self.calendar1.name(), "calendar_1")
+
def test_ownerCalendarHome(self):
"""
Owner is correct.
@@ -410,9 +476,10 @@
self.home1.uid()
)
+
def _test_calendarObjects(self, which):
# Add a dot file to make sure we don't find it
- self.home1.path.child(".foo").createDirectory()
+ self.home1._path.child(".foo").createDirectory()
methodName = "_calendarObjects_%s" % (which,)
method = getattr(self.calendar1, methodName)
@@ -429,6 +496,7 @@
calendar1_objectNames
)
+ @featureUnimplemented
def test_calendarObjects_listdir(self):
"""
Find all of the calendar objects using the listdir
@@ -436,7 +504,8 @@
"""
return self._test_calendarObjects("listdir")
- @todo("Index is missing 1.ics?")
+
+ @featureUnimplemented
def test_calendarObjects_index(self):
"""
Find all of the calendar objects using the index
@@ -444,6 +513,7 @@
"""
return self._test_calendarObjects("index")
+
def test_calendarObjectWithName_exists(self):
"""
Find existing calendar object by name.
@@ -456,12 +526,14 @@
)
self.assertEquals(calendarObject.name(), name)
+
def test_calendarObjectWithName_absent(self):
"""
Missing calendar object.
"""
self.assertEquals(self.calendar1.calendarObjectWithName("xyzzy"), None)
+
def test_calendarObjectWithName_dot(self):
"""
Filenames starting with "." are reserved by this
@@ -469,9 +541,10 @@
".".
"""
name = ".foo.ics"
- self.home1.path.child(name).touch()
+ self.home1._path.child(name).touch()
self.assertEquals(self.calendar1.calendarObjectWithName(name), None)
+
@featureUnimplemented
def test_calendarObjectWithUID_exists(self):
"""
@@ -487,25 +560,27 @@
self.calendar1.calendarObjectWithName("1.ics").component()
)
- @featureUnimplemented
+
def test_calendarObjectWithUID_absent(self):
"""
Missing calendar object.
"""
self.assertEquals(self.calendar1.calendarObjectWithUID("xyzzy"), None)
+
def test_createCalendarObjectWithName_absent(self):
"""
Create a new calendar object.
"""
name = "4.ics"
- assert self.calendar1.calendarObjectWithName(name) is None
+ self.assertIdentical(self.calendar1.calendarObjectWithName(name), None)
component = VComponent.fromString(event4_text)
self.calendar1.createCalendarObjectWithName(name, component)
calendarObject = self.calendar1.calendarObjectWithName(name)
self.assertEquals(calendarObject.component(), component)
+
def test_createCalendarObjectWithName_exists(self):
"""
Attempt to create an existing calendar object should raise.
@@ -516,6 +591,7 @@
"1.ics", VComponent.fromString(event4_text)
)
+
def test_createCalendarObjectWithName_dot(self):
"""
Filenames starting with "." are reserved by this
@@ -528,6 +604,7 @@
".foo", VComponent.fromString(event4_text)
)
+
@featureUnimplemented
def test_createCalendarObjectWithName_uidconflict(self):
"""
@@ -543,6 +620,7 @@
name, component
)
+
def test_createCalendarObjectWithName_invalid(self):
"""
Attempt to create a calendar object with a invalid iCalendar text
@@ -554,18 +632,21 @@
"new", VComponent.fromString(event4notCalDAV_text)
)
+
def test_removeCalendarObjectWithName_exists(self):
"""
Remove an existing calendar object.
"""
for name in calendar1_objectNames:
- assert self.calendar1.calendarObjectWithName(name) is not None
+ self.assertNotIdentical(
+ self.calendar1.calendarObjectWithName(name), None
+ )
self.calendar1.removeCalendarObjectWithName(name)
- self.assertEquals(
- self.calendar1.calendarObjectWithName(name),
- None
+ self.assertIdentical(
+ self.calendar1.calendarObjectWithName(name), None
)
+
def test_removeCalendarObjectWithName_absent(self):
"""
Attempt to remove an non-existing calendar object should raise.
@@ -575,6 +656,7 @@
self.calendar1.removeCalendarObjectWithName, "xyzzy"
)
+
def test_removeCalendarObjectWithName_dot(self):
"""
Filenames starting with "." are reserved by this
@@ -582,20 +664,21 @@
".".
"""
name = ".foo"
- self.calendar1.path.child(name).touch()
+ self.calendar1._path.child(name).touch()
self.assertRaises(
NoSuchCalendarObjectError,
self.calendar1.removeCalendarObjectWithName, name
)
- @featureUnimplemented
+
def test_removeCalendarObjectWithUID_exists(self):
"""
Remove an existing calendar object.
"""
for name in calendar1_objectNames:
- uid = name.rstrip(".ics")
- assert self.calendar1.calendarObjectWithUID(uid) is not None
+ uid = (u'uid' + name.rstrip(".ics"))
+ self.assertNotIdentical(self.calendar1.calendarObjectWithUID(uid),
+ None)
self.calendar1.removeCalendarObjectWithUID(uid)
self.assertEquals(
self.calendar1.calendarObjectWithUID(uid),
@@ -606,6 +689,88 @@
None
)
+
+ def _refresh(self):
+ """
+ Re-read the (committed) home1 and calendar1 objects in a new
+ transaction.
+ """
+ self.txn = self.calendarStore.newTransaction()
+ self.home1 = self.txn.calendarHomeWithUID("home1")
+ self.calendar1 = self.home1.calendarWithName("calendar_1")
+
+
+ def test_undoCreateCalendarObject(self):
+ """
+ If a calendar object is created as part of a transaction, it will be
+ removed if that transaction has to be aborted.
+ """
+ # Make sure that the calendar home is actually committed; rolling back
+ # calendar home creation will remove the whole directory.
+ self.txn.commit()
+ self._refresh()
+ self.calendar1.createCalendarObjectWithName(
+ "sample.ics",
+ VComponent.fromString(event4_text)
+ )
+ self._refresh()
+ self.assertIdentical(
+ self.calendar1.calendarObjectWithName("sample.ics"),
+ None
+ )
+
+
+ def doThenUndo(self):
+ """
+ Commit the current transaction, but add an operation that will cause it
+ to fail at the end. Finally, refresh all attributes with a new
+ transaction so that further oparations can be performed in a valid
+ context.
+ """
+ def fail():
+ raise RuntimeError("oops")
+ self.txn.addOperation(fail)
+ self.assertRaises(RuntimeError, self.txn.commit)
+ self._refresh()
+
+
+ def test_undoModifyCalendarObject(self):
+ """
+ If an existing calendar object is modified as part of a transaction, it
+ should be restored to its previous status if the transaction aborts.
+ """
+ originalComponent = self.calendar1.calendarObjectWithName(
+ "1.ics").component()
+ self.calendar1.calendarObjectWithName("1.ics").setComponent(
+ VComponent.fromString(event1modified_text)
+ )
+ # Sanity check.
+ self.assertEquals(
+ self.calendar1.calendarObjectWithName("1.ics").component(),
+ VComponent.fromString(event1modified_text)
+ )
+ self.doThenUndo()
+ self.assertEquals(
+ self.calendar1.calendarObjectWithName("1.ics").component(),
+ originalComponent
+ )
+
+
+ def test_modifyCalendarObjectCaches(self):
+ """
+ Modifying a calendar object should cache the modified component in
+ memory, to avoid unnecessary parsing round-trips.
+ """
+ modifiedComponent = VComponent.fromString(event1modified_text)
+ self.calendar1.calendarObjectWithName("1.ics").setComponent(
+ modifiedComponent
+ )
+ self.assertIdentical(
+ modifiedComponent,
+ self.calendar1.calendarObjectWithName("1.ics").component()
+ )
+
+
@featureUnimplemented
def test_removeCalendarObjectWithUID_absent(self):
"""
@@ -616,6 +781,7 @@
self.calendar1.removeCalendarObjectWithUID, "xyzzy"
)
+
@testUnimplemented
def test_syncToken(self):
"""
@@ -623,6 +789,7 @@
"""
raise NotImplementedError()
+
@testUnimplemented
def test_calendarObjectsInTimeRange(self):
"""
@@ -630,6 +797,7 @@
"""
raise NotImplementedError()
+
@testUnimplemented
def test_calendarObjectsSinceToken(self):
"""
@@ -639,11 +807,13 @@
raise NotImplementedError()
+
class CalendarObjectTest(unittest.TestCase, PropertiesTestMixin):
def setUp(self):
setUpCalendar1(self)
self.object1 = self.calendar1.calendarObjectWithName("1.ics")
+
def test_interface(self):
"""
Interface is completed and conforming.
@@ -653,34 +823,38 @@
except BrokenMethodImplementation, e:
self.fail(e)
+
def test_init(self):
"""
Ivars are correctly initialized.
"""
self.failUnless(
- isinstance(self.object1.path, FilePath),
- self.object1.path
+ isinstance(self.object1._path, FilePath),
+ self.object1._path
)
self.failUnless(
isinstance(self.object1.calendar, Calendar),
self.object1.calendar
)
+
def test_name(self):
"""
Name is correct.
"""
self.assertEquals(self.object1.name(), "1.ics")
+
def test_setComponent(self):
"""
- Rewrite component.
+ L{CalendarObject.setComponent} changes the result of
+ L{CalendarObject.component} within the same transaction.
"""
component = VComponent.fromString(event1modified_text)
calendarObject = self.calendar1.calendarObjectWithName("1.ics")
- oldComponent = calendarObject.component() # Trigger caching
- assert component != oldComponent
+ oldComponent = calendarObject.component()
+ self.assertNotEqual(component, oldComponent)
calendarObject.setComponent(component)
self.assertEquals(calendarObject.component(), component)
@@ -688,6 +862,7 @@
calendarObject = self.calendar1.calendarObjectWithName("1.ics")
self.assertEquals(calendarObject.component(), component)
+
def test_setComponent_uidchanged(self):
component = VComponent.fromString(event4_text)
@@ -697,6 +872,7 @@
calendarObject.setComponent, component
)
+
def test_setComponent_invalid(self):
calendarObject = self.calendar1.calendarObjectWithName("1.ics")
self.assertRaises(
@@ -705,6 +881,7 @@
VComponent.fromString(event4notCalDAV_text)
)
+
def test_component(self):
"""
Component is correct.
@@ -720,6 +897,7 @@
self.assertEquals(component.mainType(), "VEVENT")
self.assertEquals(component.resourceUID(), "uid1")
+
def text_iCalendarText(self):
"""
iCalendar text is correct.
@@ -730,18 +908,21 @@
self.failUnless("\r\nUID:uid-1\r\n" in text)
self.failUnless(text.endswith("\r\nEND:VCALENDAR\r\n"))
+
def test_uid(self):
"""
UID is correct.
"""
self.assertEquals(self.object1.uid(), "uid1")
+
def test_componentType(self):
"""
Component type is correct.
"""
self.assertEquals(self.object1.componentType(), "VEVENT")
+
def test_organizer(self):
"""
Organizer is correct.
Modified: CalendarServer/branches/new-store/txcaldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/icalendarstore.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/txcaldav/icalendarstore.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -1,3 +1,4 @@
+# -*- test-case-name: txcaldav.calendarstore -*-
##
# Copyright (c) 2010 Apple Inc. All rights reserved.
#
@@ -18,10 +19,10 @@
Calendar store interfaces
"""
-# FIXME: Still to do:
-# - Where to defer?
-# - commit() and abort()
+from zope.interface import Interface
+from txdav.idav import ITransaction
+
__all__ = [
# Exceptions
"CalendarStoreError",
@@ -45,13 +46,17 @@
"ICalendarObject",
]
-from zope.interface import Interface #, Attribute
-from datetime import datetime, date, tzinfo
-from twext.python.vcomponent import VComponent
-from txdav.idav import IPropertyStore
+# The following imports are used by the L{} links below, but shouldn't actually
+# be imported.as they're not really needed.
+# from datetime import datetime, date, tzinfo
+# from twext.python.vcomponent import VComponent
+
+# from txdav.idav import IPropertyStore
+# from txdav.idav import ITransaction
+
#
# Exceptions
#
@@ -130,6 +135,15 @@
"""
Calendar store
"""
+ def newTransaction():
+ """
+ Create a new transaction.
+ """
+
+class ICalendarStoreTransaction(ITransaction):
+ """
+ Calendar store transaction
+ """
def calendarHomeWithUID(uid, create=False):
"""
Retrieve the calendar home for the principal with the given
@@ -143,6 +157,21 @@
"""
+ def abort():
+ """
+ Mark this transaction as invalid, reversing all of its effects.
+ """
+ # FIXME: probably needs to be deferred.
+
+
+ def commit():
+ """
+ Persist the effects of this transaction.
+ """
+ # FIXME: probably needs to be deferred.
+
+
+
class ICalendarHome(Interface):
"""
Calendar home
@@ -152,14 +181,6 @@
includes both calendars owned by the principal as well as
calendars that have been shared with and accepts by the principal.
"""
- # FIXME: We need a principal interface somewhere, possibly part of
- # an idirectory rework. IDirectoryRecord may be close...
- #def owner():
- # """
- # Retrieve the owner principal for this calendar home.
- # @return: a ???
- # """
-
def uid():
"""
Retrieve the unique identifier for this calendar home.
@@ -339,6 +360,7 @@
A calendar object decribes an event, to-do, or other iCalendar
object.
"""
+
def setComponent(component):
"""
Rewrite this calendar object to match the given C{component}.
Modified: CalendarServer/branches/new-store/txdav/idav.py
===================================================================
--- CalendarServer/branches/new-store/txdav/idav.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/txdav/idav.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -84,3 +84,25 @@
"""
Abort any pending changes.
"""
+
+
+class ITransaction(Interface):
+ """
+ Transaction that can be aborted and either succeeds or fails in
+ its entirety.
+ """
+ def abort():
+ """
+ Abort this transaction.
+ """
+
+ def commit():
+ """
+ Perform this transaction.
+ """
+
+
+class AbortedTransactionError(RuntimeError):
+ """
+ This transaction has aborted. Go away.
+ """
Modified: CalendarServer/branches/new-store/txdav/propertystore/base.py
===================================================================
--- CalendarServer/branches/new-store/txdav/propertystore/base.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/txdav/propertystore/base.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -49,9 +49,31 @@
self.namespace = namespace
self.name = name
+
+ def _cmpval(self):
+ """
+ Return a value to use for hashing and comparisons.
+ """
+ return (self.namespace, self.name)
+
+
+ # FIXME: need direct tests for presence-in-dictionary
def __hash__(self):
- return hash((self.namespace, self.name))
+ return hash(self._cmpval())
+
+ def __eq__(self, other):
+ if not isinstance(other, PropertyName):
+ return NotImplemented
+ return self._cmpval() == other._cmpval()
+
+
+ def __ne__(self, other):
+ if not isinstance(other, PropertyName):
+ return NotImplemented
+ return self._cmpval() != other._cmpval()
+
+
def __repr__(self):
return "<%s: %s>" % (
self.__class__.__name__,
Modified: CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py
===================================================================
--- CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -18,7 +18,7 @@
Property store tests.
"""
-from twext.python.filepath import FilePath
+from twext.python.filepath import CachingFilePath as FilePath
from txdav.propertystore.base import PropertyName
from txdav.propertystore.test.base import propertyName
Modified: CalendarServer/branches/new-store/txdav/propertystore/xattr.py
===================================================================
--- CalendarServer/branches/new-store/txdav/propertystore/xattr.py 2010-05-13 04:55:07 UTC (rev 5594)
+++ CalendarServer/branches/new-store/txdav/propertystore/xattr.py 2010-05-13 05:04:13 UTC (rev 5595)
@@ -83,6 +83,12 @@
)
def __init__(self, path):
+ """
+ Initialize a L{PropertyStore}.
+
+ @param path: the path to set extended attributes on.
+ @type path: L{CachingFilePath}
+ """
self.path = path
self.attrs = xattr(path.path)
self.removed = set()
@@ -117,7 +123,7 @@
try:
data = self.attrs[self._encodeKey(key)]
except IOError, e:
- if e.errno in _ERRNO_NO_ATTR:
+ if e.errno in [_ERRNO_NO_ATTR]:
raise KeyError(key)
raise PropertyStoreError(e)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100512/dcef0b00/attachment-0001.html>
More information about the calendarserver-changes
mailing list