[Zope3-checkins] SVN: Zope3/trunk/src/zope/app/dav/ Merged isarsprint-dav-work changes r28063:28187 into the trunk; final work on WebDAV properties making registered interfaces supported now too.

Martijn Pieters mj at zopatista.com
Thu Oct 14 10:27:04 EDT 2004


Log message for revision 28188:
  Merged isarsprint-dav-work changes r28063:28187 into the trunk; final work on WebDAV properties making registered interfaces supported now too.

Changed:
  U   Zope3/trunk/src/zope/app/dav/ftests/test_proppatch.py
  U   Zope3/trunk/src/zope/app/dav/interfaces.py
  U   Zope3/trunk/src/zope/app/dav/opaquenamespaces.py
  U   Zope3/trunk/src/zope/app/dav/propfind.py
  U   Zope3/trunk/src/zope/app/dav/proppatch.py
  U   Zope3/trunk/src/zope/app/dav/tests/test_proppatch.py
  U   Zope3/trunk/src/zope/app/dav/widget.py

-=-
Modified: Zope3/trunk/src/zope/app/dav/ftests/test_proppatch.py
===================================================================
--- Zope3/trunk/src/zope/app/dav/ftests/test_proppatch.py	2004-10-14 14:22:55 UTC (rev 28187)
+++ Zope3/trunk/src/zope/app/dav/ftests/test_proppatch.py	2004-10-14 14:27:03 UTC (rev 28188)
@@ -65,6 +65,31 @@
         self._assertOPropsEqual(pt, 
             {u'uri://foo': {u'bar': '<bar>spam</bar>'}})
         
+    def test_remove_dctitle(self):
+        self.addPage('/pt', u'<span />')
+        pt = traverse(self.getRootFolder(), '/pt')
+        adapted = IZopeDublinCore(pt)
+        adapted.title = u'Test'
+        transaction.commit()
+        # DC Title is a required field with no default, so a 409 is expected
+        expect = self._makePropstat(('http://purl.org/dc/1.1',),
+                                    '<title xmlns="a0"/>', 409)
+        self.verifyPropOK(path='/pt', 
+            namespaces=(('DC', 'http://purl.org/dc/1.1'),),
+            rm=('<DC:title/>',), expect=expect)
+        
+    def test_set_dctitle(self):
+        self.addPage('/pt', u'<span />')
+        pt = traverse(self.getRootFolder(), '/pt')
+        adapted = IZopeDublinCore(pt)
+        transaction.commit()
+        expect = self._makePropstat(('http://purl.org/dc/1.1',),
+                                    '<title xmlns="a0"/>')
+        self.verifyPropOK(path='/pt', 
+            namespaces=(('DC', 'http://purl.org/dc/1.1'),),
+            set=('<DC:title>Test Title</DC:title>',), expect=expect)
+        self.assertEqual(IZopeDublinCore(pt).title, u'Test Title')
+        
     def _assertOPropsEqual(self, obj, expect):
         oprops = IDAVOpaqueNamespaces(obj)
         namespacesA = list(oprops.keys())

Modified: Zope3/trunk/src/zope/app/dav/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/app/dav/interfaces.py	2004-10-14 14:22:55 UTC (rev 28187)
+++ Zope3/trunk/src/zope/app/dav/interfaces.py	2004-10-14 14:27:03 UTC (rev 28188)
@@ -19,7 +19,7 @@
 
 from zope.interface import Interface
 from zope.schema import Text
-from zope.app.form.interfaces import IWidget
+from zope.app.form.interfaces import IInputWidget
 
 
 class IDAVNamespace(Interface):
@@ -40,9 +40,11 @@
                                 resources.  If present, it contains a
                                 timestamp of the moment when the
                                 resource was created (i.e., the moment
-                                it had non- null state).''')
+                                it had non- null state).''',
 
+                            readonly=True)
 
+
 class IDAVDisplayName(Interface):
 
     displayname = Text(title=u'''Provides a name for the resource that\
@@ -102,8 +104,10 @@
                                 The getcontentlength property MUST be
                                 defined on any DAV compliant resource
                                 that returns the Content-Length header
-                                in response to a GET.''')
+                                in response to a GET.''',
 
+                            readonly=True)
+
     getcontenttype = Text(title=u'''Contains the Content-Type header\
                                 returned by a GET without accept\
                                 headers''',
@@ -120,8 +124,10 @@
                    description=u'''\
                                 The getetag property MUST be defined
                                 on any DAV compliant resource that
-                                returns the Etag header.''')
+                                returns the Etag header.''',
 
+                            readonly=True)
+
     getlastmodified = Text(title=u'''Contains the Last-Modified header\
                                 returned by a GET method without\
                                 accept headers''',
@@ -138,9 +144,11 @@
                                 MUST be defined on any DAV compliant
                                 resource that returns the
                                 Last-Modified header in response to a
-                                GET.''')
+                                GET.''',
 
+                            readonly=True)
 
+
 class IDAV1Schema(IGETDependentDAVSchema):
     """DAV properties required for Level 1 compliance"""
 
@@ -150,9 +158,11 @@
                                 The resourcetype property MUST be
                                 defined on all DAV compliant
                                 resources.  The default value is
-                                empty.''')
+                                empty.''',
 
+                            readonly=True)
 
+
 class IDAV2Schema(IDAV1Schema):
     """DAV properties required for Level 2 compliance"""
 
@@ -169,8 +179,10 @@
                                  of this information if the requesting
                                  principal does not have sufficient
                                  access rights to see the requested
-                                 data.''')
+                                 data.''',
 
+                            readonly=True)
+
     supportedlock = Text(title=u'''To provide a listing of the lock\
                                 capabilities supported by the\
                                 resource''',
@@ -185,17 +197,32 @@
                                 controlled by access controls so a
                                 server is not required to provide
                                 information the client is not
-                                authorized to see.''')
+                                authorized to see.''',
 
+                            readonly=True)
 
 class IDAVSchema(IOptionalDAVSchema, IDAV2Schema):
     """Full DAV properties schema"""
 
 
-class IDAVWidget(IWidget):
-    """A specialized widget used to render DAV properties output."""
+class IDAVWidget(IInputWidget):
+    """A specialized widget used to convert to and from DAV properties."""
+    
+    def __call__():
+        """Render the widget.
 
+        Optionally, this method could return a minidom DOM Node as the value;
+        this node will then be inersted into the resulting DAV XML response. 
+        Use a DocumentFragment if you want to include multiple nodes.
+        """
+    
+    def setRenderedValue(value):
+        """Set the DAV value for the property
 
+        value can be a DOM Element node representing the value.
+        """
+
+
 class ITextDAVWidget(IDAVWidget):
     """A DAV widget for text values."""
 

Modified: Zope3/trunk/src/zope/app/dav/opaquenamespaces.py
===================================================================
--- Zope3/trunk/src/zope/app/dav/opaquenamespaces.py	2004-10-14 14:22:55 UTC (rev 28187)
+++ Zope3/trunk/src/zope/app/dav/opaquenamespaces.py	2004-10-14 14:27:03 UTC (rev 28188)
@@ -108,6 +108,7 @@
     
     #
     # Convenience methods; storing and retrieving properties through WebDAV
+    # It may be better to use specialised IDAWWidget implementatins for this.
     #
     def renderProperty(self, ns, nsprefix, prop, propel):
         """Render a property as DOM elements"""

Modified: Zope3/trunk/src/zope/app/dav/propfind.py
===================================================================
--- Zope3/trunk/src/zope/app/dav/propfind.py	2004-10-14 14:22:55 UTC (rev 28187)
+++ Zope3/trunk/src/zope/app/dav/propfind.py	2004-10-14 14:27:03 UTC (rev 28188)
@@ -16,7 +16,7 @@
 __docformat__ = 'restructuredtext'
 
 from xml.dom import minidom
-from zope.schema import getFieldNamesInOrder
+from zope.schema import getFieldNamesInOrder, getFields
 from zope.app import zapi
 from zope.app.container.interfaces import IReadContainer
 from zope.app.form.utility import setUpWidgets
@@ -220,31 +220,30 @@
             
             # The registered namespace case
             initial = {}
-            adapter = iface(self.context, None)
-            for name in avail.get(ns):
-                value = getattr(adapter, name, None)
-                if value is not None:
+            for name, field in getFields(iface).items():
+                value = field.get(iface(self.context))
+                if value is not field.missing_value:
                     initial[name] = value
-            setUpWidgets(self, iface, IDAVWidget,
-                ignoreStickyValues=True, initial=initial, 
-                names=avail.get(ns))
+            setUpWidgets(self, iface, IDAVWidget, ignoreStickyValues=True,
+                         initial=initial, names=avail[ns])
                         
             for p in avail.get(ns):
                 el = resp.createElement('%s' % p )
                 if ns is not None and ns != self.default_ns:
                     el.setAttribute('xmlns', attr_name)
                 prop.appendChild(el)
-                value = getattr(self, p+'_widget')()
+                value = getattr(self, p + '_widget')()
                     
                 if isinstance(value, (unicode, str)):
                     # Get the widget value here
                     el.appendChild(resp.createTextNode(value))
                 else:
                     if zapi.isinstance(value, minidom.Node):
-                        el.appendChild(value)
+                        el.appendChild(
+                            el.ownerDocument.importNode(value, True))
                     else:
                         # Try to string-ify
-                        value = str(getattr(self, p+'_widget'))
+                        value = str(getattr(self, p + '_widget'))
                         # Get the widget value here
                         el.appendChild(resp.createTextNode(value))
 

Modified: Zope3/trunk/src/zope/app/dav/proppatch.py
===================================================================
--- Zope3/trunk/src/zope/app/dav/proppatch.py	2004-10-14 14:22:55 UTC (rev 28187)
+++ Zope3/trunk/src/zope/app/dav/proppatch.py	2004-10-14 14:27:03 UTC (rev 28188)
@@ -19,11 +19,12 @@
 
 import transaction
 from zope.app import zapi
-from zope.schema import getFieldNamesInOrder
+from zope.schema import getFieldNamesInOrder, getFields
 from zope.app.container.interfaces import IReadContainer
 from zope.publisher.http import status_reasons
+from zope.app.form.utility import setUpWidget, no_value
 
-from interfaces import IDAVNamespace
+from interfaces import IDAVNamespace, IDAVWidget
 from opaquenamespaces import IDAVOpaqueNamespaces
 
 class PROPPATCH(object):
@@ -156,10 +157,32 @@
                     props.append(prop.localName)
                 return 200
             return 403
-            
-        # XXX: Deal with registered ns interfaces here
-        return 403
+        
+        if not prop.localName in self.avail_props[ns]:
+            return 403 # Cannot add propeties to a registered schema
+        
+        fields = getFields(iface)
+        field = fields[prop.localName]
+        if field.readonly:
+            return 409 # RFC 2518 specifies 409 for readonly props
+        
+        value = field.get(iface(self.context))
+        if value is field.missing_value:
+            value = no_value
+        setUpWidget(self, prop.localName, field, IDAVWidget,
+            value=value, ignoreStickyValues=True)
+        
+        widget = getattr(self, prop.localName + '_widget')
+        widget.setRenderedValue(prop)
 
+        if not widget.hasValidInput():
+            return 409 # Didn't match the widget validation
+        
+        if widget.applyChanges(iface(self.context)):
+            return 200
+        
+        return 422 # Field didn't accept the value
+
     def _handleRemove(self, prop):
         ns = prop.namespaceURI
         if not prop.localName in self.avail_props.get(ns, []):
@@ -171,6 +194,20 @@
                 return 200
             self.oprops.removeProperty(ns, prop.localName)
             return 200
-            
-        # XXX: Deal with registered ns interfaces here
-        return 403
+        
+        # Registered interfaces
+        fields = getFields(iface)
+        field = fields[prop.localName]
+        if field.readonly:
+            return 409 # RFC 2518 specifies 409 for readonly props
+        
+        if field.required:
+            if field.default is None:
+                return 409 # Clearing a required property is a conflict
+            # Reset the field to the default if a value is required
+            field.set(iface(self.context), field.default)
+            return 200
+        
+        # Reset the field to it's defined missing_value
+        field.set(iface(self.context), field.missing_value)
+        return 200

Modified: Zope3/trunk/src/zope/app/dav/tests/test_proppatch.py
===================================================================
--- Zope3/trunk/src/zope/app/dav/tests/test_proppatch.py	2004-10-14 14:22:55 UTC (rev 28187)
+++ Zope3/trunk/src/zope/app/dav/tests/test_proppatch.py	2004-10-14 14:27:03 UTC (rev 28188)
@@ -20,7 +20,10 @@
 import unittest
 from StringIO import StringIO
 
+import transaction
+
 from zope.interface import Interface, implements, directlyProvides
+from zope.schema import Text
 from zope.publisher.interfaces.http import IHTTPRequest
 from zope.publisher.http import status_reasons
 from zope.pagetemplate.tests.util import normalize_xml
@@ -35,8 +38,10 @@
 from zope.app.traversing.browser import AbsoluteURL
 from zope.app.dublincore.interfaces import IZopeDublinCore
 from zope.app.dublincore.annotatableadapter import ZDCAnnotatableAdapter
+from zope.app.dublincore.zopedublincore import ScalarProperty
 from zope.app.annotation.interfaces import IAnnotatable, IAnnotations
 from zope.app.annotation.attribute import AttributeAnnotations
+from zope.schema.interfaces import IText, ISequence
 
 import zope.app.dav.tests
 from zope.app.dav.tests.unitfixtures import File, Folder, FooZPT
@@ -44,6 +49,8 @@
 from zope.app.dav import proppatch
 from zope.app.dav.interfaces import IDAVSchema
 from zope.app.dav.interfaces import IDAVNamespace
+from zope.app.dav.interfaces import IDAVWidget
+from zope.app.dav.widget import TextDAVWidget, SequenceDAVWidget
 from zope.app.dav.opaquenamespaces import DAVOpaqueNamespacesAdapter
 from zope.app.dav.opaquenamespaces import IDAVOpaqueNamespaces
 
@@ -86,6 +93,44 @@
     request = TestRequest(StringIO(body), StringIO(), _environ)
     return request
 
+
+class ITestSchema(Interface):
+    requiredNoDefault = Text(required=True, default=None)
+    requiredDefault = Text(required=True, default=u'Default Value')
+    unusualMissingValue = Text(required=False, missing_value=u'Missing Value')
+    constrained = Text(required=False, min_length=5)
+
+EmptyTestValue = object()
+TestKey = 'zope.app.dav.tests.test_proppatch'
+TestURI = 'uri://proppatch_tests'
+
+class TestSchemaAdapter(object):
+    implements(ITestSchema)
+    __used_for__ = IAnnotatable
+    annotations = None
+    
+    def __init__(self, context):
+        annotations = IAnnotations(context)
+        data = annotations.get(TestKey)
+        if data is None:
+            self.annotations = annotations
+            data =  {u'requiredNoDefault': (EmptyTestValue,),
+                     u'requiredDefault': (EmptyTestValue,),
+                     u'unusualMissingValue': (EmptyTestValue,),
+                     u'constrained': (EmptyTestValue,)}
+        self._mapping = data
+        
+    def _changed(self):
+        if self.annotations is not None:
+            self.annotations[TestKey] = self._mapping
+            self.annotations = None
+
+    requiredNoDefault = ScalarProperty(u'requiredNoDefault')
+    requiredDefault = ScalarProperty(u'requiredDefault')
+    unusualMissingValue = ScalarProperty(u'unusualMissingValue')
+    constrained = ScalarProperty(u'constrained')
+
+
 class PropFindTests(PlacefulSetup, unittest.TestCase):
 
     def setUp(self):
@@ -105,17 +150,22 @@
                           'absolute_url', AbsoluteURL)
         ztapi.provideView(None, IHTTPRequest, Interface,
                           'PROPPATCH', proppatch.PROPPATCH)
+        ztapi.browserViewProviding(IText, TextDAVWidget, IDAVWidget)
+        ztapi.browserViewProviding(ISequence, SequenceDAVWidget, IDAVWidget)
         ztapi.provideAdapter(IAnnotatable, IAnnotations, AttributeAnnotations)
         ztapi.provideAdapter(IAnnotatable, IZopeDublinCore,
                              ZDCAnnotatableAdapter)
         ztapi.provideAdapter(IAnnotatable, IDAVOpaqueNamespaces,
                              DAVOpaqueNamespacesAdapter)
+        ztapi.provideAdapter(IAnnotatable, ITestSchema, TestSchemaAdapter)
         utils = zapi.getGlobalService('Utilities')
         directlyProvides(IDAVSchema, IDAVNamespace)
         utils.provideUtility(IDAVNamespace, IDAVSchema, 'DAV:')
         directlyProvides(IZopeDublinCore, IDAVNamespace)
         utils.provideUtility(IDAVNamespace, IZopeDublinCore,
                              'http://www.purl.org/dc/1.1')
+        directlyProvides(ITestSchema, IDAVNamespace)
+        utils.provideUtility(IDAVNamespace, ITestSchema, TestURI)
         self.db = DB()
         self.conn = self.db.open()
         root = self.conn.root()
@@ -296,20 +346,90 @@
                                 {u'uri://foo': {u'bar': '<bar>spam</bar>'}})
         
     def test_proppatch_failure(self):
-        # XXX: This relies on the fact that only opaque properties can be set 
-        # for now. As soon as registered interfaces support is implemented, 
-        # this test will need to be rewritten.
         expect = self._makePropstat(
             ('uri://foo',), '<bar xmlns="a0"/>', 424)
         expect += self._makePropstat(
-            ('http://www.purl.org/dc/1.1',), '<title xmlns="a0"/>', 403)
+            ('http://www.purl.org/dc/1.1',), '<nonesuch xmlns="a0"/>', 403)
         self._checkProppatch(self.zpt, 
             ns=(('foo', 'uri://foo'), ('DC', 'http://www.purl.org/dc/1.1')),
-            set=('<foo:bar>spam</foo:bar>', '<DC:title>Test</DC:title>'),
+            set=('<foo:bar>spam</foo:bar>', '<DC:nonesuch>Test</DC:nonesuch>'),
             expect=expect)
         self._assertOPropsEqual(self.zpt, {})
+        
+    def test_nonexistent_dc(self):
+        expect = self._makePropstat(
+            ('http://www.purl.org/dc/1.1',), '<nonesuch xmlns="a0"/>', 403)
+        self._checkProppatch(self.zpt, 
+            ns=(('DC', 'http://www.purl.org/dc/1.1'),),
+            set=('<DC:nonesuch>Test</DC:nonesuch>',), expect=expect)
+        
+    def test_set_readonly(self):
+        expect = self._makePropstat((), '<getcontentlength/>', 409)
+        self._checkProppatch(self.zpt, 
+            set=('<getcontentlength>Test</getcontentlength>',), expect=expect)
+        
+    def test_remove_readonly(self):
+        expect = self._makePropstat((), '<getcontentlength/>', 409)
+        self._checkProppatch(self.zpt, rm=('<getcontentlength/>',), 
+                             expect=expect)
 
-    
+    def test_remove_required_no_default(self):
+        testprops = ITestSchema(self.zpt)
+        testprops.requiredNoDefault = u'foo'
+        transaction.commit()
+        expect = self._makePropstat((TestURI,), 
+                                    '<requiredNoDefault xmlns="a0"/>', 409)
+        self._checkProppatch(self.zpt, 
+            ns=(('tst', TestURI),), rm=('<tst:requiredNoDefault/>',), 
+            expect=expect)
+        self.assertEqual(ITestSchema(self.zpt).requiredNoDefault, u'foo')
+
+    def test_remove_required_default(self):
+        testprops = ITestSchema(self.zpt)
+        testprops.requiredDefault = u'foo'
+        transaction.commit()
+        expect = self._makePropstat((TestURI,), 
+                                    '<requiredDefault xmlns="a0"/>', 200)
+        self._checkProppatch(self.zpt, 
+            ns=(('tst', TestURI),), rm=('<tst:requiredDefault/>',), 
+            expect=expect)
+        self.assertEqual(testprops.requiredDefault, u'Default Value')
+
+    def test_remove_required_missing_value(self):
+        testprops = ITestSchema(self.zpt)
+        testprops.unusualMissingValue = u'foo'
+        transaction.commit()
+        expect = self._makePropstat((TestURI,), 
+                                    '<unusualMissingValue xmlns="a0"/>', 200)
+        self._checkProppatch(self.zpt, 
+            ns=(('tst', TestURI),), rm=('<tst:unusualMissingValue/>',), 
+            expect=expect)
+        self.assertEqual(testprops.unusualMissingValue, u'Missing Value')
+
+    def test_set_dctitle(self):
+        dc = IZopeDublinCore(self.zpt)
+        dc.title = u'Test Title'
+        transaction.commit()
+        expect = self._makePropstat(('http://www.purl.org/dc/1.1',), 
+                                    '<title xmlns="a0"/>', 200)
+        self._checkProppatch(self.zpt, 
+            ns=(('DC', 'http://www.purl.org/dc/1.1'),), 
+            set=('<DC:title>Foo Bar</DC:title>',), 
+            expect=expect)
+        self.assertEqual(dc.title, u'Foo Bar')
+
+    def test_set_dcsubjects(self):
+        dc = IZopeDublinCore(self.zpt)
+        dc.subjects = (u'Bla', u'Ble', u'Bli')
+        transaction.commit()
+        expect = self._makePropstat(('http://www.purl.org/dc/1.1',), 
+                                    '<subjects xmlns="a0"/>', 200)
+        self._checkProppatch(self.zpt, 
+            ns=(('DC', 'http://www.purl.org/dc/1.1'),), 
+            set=('<DC:subjects>Foo, Bar</DC:subjects>',), 
+            expect=expect)
+        self.assertEqual(dc.subjects, (u'Foo', u'Bar'))
+
 def test_suite():
     return unittest.TestSuite((
         unittest.makeSuite(PropFindTests),

Modified: Zope3/trunk/src/zope/app/dav/widget.py
===================================================================
--- Zope3/trunk/src/zope/app/dav/widget.py	2004-10-14 14:22:55 UTC (rev 28187)
+++ Zope3/trunk/src/zope/app/dav/widget.py	2004-10-14 14:27:03 UTC (rev 28188)
@@ -17,16 +17,16 @@
 """
 __docformat__ = 'restructuredtext'
 
+from xml.dom import minidom
+
 from zope.app.dav.interfaces import IDAVWidget
 from zope.app.dav.interfaces import ITextDAVWidget
 from zope.app.dav.interfaces import ISequenceDAVWidget
 
-from zope.app.form.interfaces import IWidget
-from zope.app.form import Widget
-from zope.component.interfaces import IViewFactory
+from zope.app.form import InputWidget
 from zope.interface import implements
 
-class DAVWidget(Widget):
+class DAVWidget(InputWidget):
 
     implements(IDAVWidget)
 
@@ -40,7 +40,18 @@
         return str(self._data)
 
     def __call__(self):
-        return self.getInputValue()
+        return str(self)
+    
+    def setRenderedValue(self, value):
+        if isinstance(value, minidom.Node):
+            text = u''
+            for node in value.childNodes:
+                if node.nodeType != node.TEXT_NODE:
+                    continue
+                text += node.nodeValue
+            value = text
+        
+        super(DAVWidget, self).setRenderedValue(value)
 
 class TextDAVWidget(DAVWidget):
 
@@ -52,3 +63,6 @@
 
     def __str__(self):
         return u', '.join(self._data)
+    
+    def getInputValue(self):
+        return [v.strip() for v in self._data.split(',')]



More information about the Zope3-Checkins mailing list