[CMF-checkins] SVN: CMF/trunk/C CMFDefault.MetadataTool: support arbitrary additional schemas.

Tres Seaver tseaver at palladion.com
Mon Nov 28 22:29:20 EST 2005


Log message for revision 40396:
  CMFDefault.MetadataTool:  support arbitrary additional schemas.
  
  The "stock" DublinCore-specific API is still accessible, implemented via a
  special "DCMI" subobject.
  
  

Changed:
  U   CMF/trunk/CHANGES.txt
  U   CMF/trunk/CMFCalendar/setuphandlers.py
  U   CMF/trunk/CMFCore/interfaces/_tools.py
  U   CMF/trunk/CMFDefault/MetadataTool.py
  U   CMF/trunk/CMFDefault/dtml/metadataElementPolicies.dtml
  U   CMF/trunk/CMFDefault/dtml/metadataProperties.dtml
  U   CMF/trunk/CMFDefault/tests/test_MetadataTool.py

-=-
Modified: CMF/trunk/CHANGES.txt
===================================================================
--- CMF/trunk/CHANGES.txt	2005-11-29 01:58:24 UTC (rev 40395)
+++ CMF/trunk/CHANGES.txt	2005-11-29 03:29:18 UTC (rev 40396)
@@ -2,6 +2,10 @@
 
   New Features
 
+    - CMFDefault.MetadataTool:  support arbitrary additional schemas.
+      The "stock" DublinCore-specific API is still accessible, implemented
+      via a special "DCMI" subobject.
+
     - WorkflowTool and DCWorkflow: Improved add form for workflow objects.
       Presettings can now be loaded from workflow settings in setup profiles.
       This replaces the feature that did allow to load presettings from the

Modified: CMF/trunk/CMFCalendar/setuphandlers.py
===================================================================
--- CMF/trunk/CMFCalendar/setuphandlers.py	2005-11-29 01:58:24 UTC (rev 40395)
+++ CMF/trunk/CMFCalendar/setuphandlers.py	2005-11-29 03:29:18 UTC (rev 40396)
@@ -32,15 +32,19 @@
     # Set up a MetadataTool element policy for events
     try:
         _ = str # MetadataTool ist not aware of MessageIDs
-        mdtool.addElementPolicy(
+        mdtool.DCMI.addElementPolicy(
             element='Subject',
             content_type='Event',
             is_required=0,
             supply_default=0,
             default_value='',
             enforce_vocabulary=0,
-            allowed_vocabulary=(_('Appointment'), _('Convention'),
-                                _('Meeting'), _('Social Event'), _('Work')),
+            allowed_vocabulary=(_('Appointment'),
+                                _('Convention'),
+                                _('Meeting'),
+                                _('Social Event'),
+                                _('Work'),
+                               ),
             REQUEST=None)
     except MetadataError:
         pass

Modified: CMF/trunk/CMFCore/interfaces/_tools.py
===================================================================
--- CMF/trunk/CMFCore/interfaces/_tools.py	2005-11-29 01:58:24 UTC (rev 40395)
+++ CMF/trunk/CMFCore/interfaces/_tools.py	2005-11-29 03:29:18 UTC (rev 40396)
@@ -911,14 +911,17 @@
             o Must be set to 'portal_metadata'.
             """)
 
+    id = Attribute('id', 'Must be set to "portal_metadata"')
+
     #
-    #   Site-wide queries.
+    #   Site-wide queries, specific to Dublin Core metadata.
     #
     def getFullName(userid):
-        """ Convert an internal userid to a "formal" name, if possible
+        """ Convert an internal userid to a "formal" name.
+        
+        o Convert only if possible, perhaps using the 'portal_membership'
+          tool;  otherwise, return 'userid'.
 
-        o 'userid' is the ID of the user within the user folder.
-
         o Used to map userid's for Creator, Contributor DCMI queries.
         """
 
@@ -927,67 +930,100 @@
         """
 
     #
-    #   Content-specific queries.
+    #   Content-specific queries, for Dublin Core metadata.
     #
-    def listAllowedSubjects(content=None):
+    def listAllowedSubjects(content=None, content_type=None):
         """ List the allowed values of the 'Subject' DCMI element.
 
-        o If 'content' is not None, return only values appropriate for
-          content's type;  otherwise, return "default" values.
+        o 'Subject' elements should be keywords categorizing their resource.
 
-        o 'Subject' elements should be keywords categorizing their resource.
+        o Return only values appropriate for content's type, or all values if
+          both 'content' and 'content_type' are None.
         """
 
-    def listAllowedFormats(content=None):
+    def listAllowedFormats(content=None, content_type=None):
         """ List the allowed values of the 'Format' DCMI element.
 
-        o If 'content' is not None, return only values appropriate for
-          content's type;  otherwise, return "default" values.
+        o These items should be usable as HTTP 'Content-type' values.
 
-        o 'Format' elements should be usable as HTTP 'Content-type' values.
+        o Return only values appropriate for content's type, or all values if
+          both 'content' and 'content_type' are None.
         """
 
-    def listAllowedLanguages(content=None):
+    def listAllowedLanguages(content=None, content_type=None):
         """ List the allowed values of the 'Language' DCMI element.
 
-        o If 'content' is not None, return only values appropriate for
-          content's type;  otherwise, return "default" values.
-
         o 'Language' element values should be suitable for generating
           HTTP headers.
+
+        o Return only values appropriate for content's type, or all values if
+          both 'content' and 'content_type' are None.
         """
 
-    def listAllowedRights(content=None):
+    def listAllowedRights(content=None, content_type=None):
         """ List the allowed values of the 'Rights' DCMI element.
 
-        o If 'content' is not None, return only values appropriate for
-          content's type;  otherwise, return "default" values.
+        o The 'Rights' element describes copyright or other IP declarations
+          pertaining to a resource.
 
-        o The 'Rights' element describes copyright or other IP
-          declarations pertaining to a resource.
+        o Return only values appropriate for content's type, or all values if
+          both 'content' and 'content_type' are None.
         """
 
     #
-    #   Validation policy hooks.
+    #   Content-specific queries, for generic metadata.
     #
-    def setInitialMetadata(content):
-        """ Set default initial values for content metatdata.
+    def listAllowedVocabulary( schema
+                             , element
+                             , content=None
+                             , content_type=None
+                             ):
+        """ List allowed values for a given schema element and content object.
+        
+        o List possible keywords if both 'content' and 'content_type' are None.
         """
 
-    def validateMetadata(content):
-        """ Enforce portal-wide policies about DCMI elements.
+    #
+    #   Schema manipulation
+    #
+    def listSchemas():
+        """ Return a list of (id, schema) tuples enumerating our schema.
+        """
 
-        o Such policy may, e.g., require non-empty title/description, etc.
+    def addSchema( schema_id ):
+        """ Create a new schema with the given ID.
 
-        o Called by the CMF immediately before saving changes to the
-          metadata of an object.
+        o Return the newly-created schema object.
 
-        o XXX:  Note that the default skins / edit methods do *not*
-          call this method;  the choice of when to apply the validation
-          is policy.
+        o Raise KeyError if such a schema already exists.
         """
 
+    def removeSchema( schema_id ):
+        """ Remove an existing schema with the given ID.
 
+        o Raise KeyError if no such schema exists.
+        """
+
+    #
+    #   Validation policy hooks.
+    #
+    def setInitialMetadata(content):
+        """ Set initial values for content metatdata.
+        
+        o Supply any site-specific defaults.
+        """
+
+    def validateMetadata(content):
+        """ Enforce portal-wide policies about metadata.
+        
+        o E.g., policies may require non-empty title/description, etc.
+        
+        o This method may be called by view / workflow code at "appropriate"
+          times, such as immediately before saving changes to the metadata of
+          an object.
+        """
+
+
 #
 #   Site Properties tool interface
 #

Modified: CMF/trunk/CMFDefault/MetadataTool.py
===================================================================
--- CMF/trunk/CMFDefault/MetadataTool.py	2005-11-29 01:58:24 UTC (rev 40395)
+++ CMF/trunk/CMFDefault/MetadataTool.py	2005-11-29 03:29:18 UTC (rev 40396)
@@ -20,6 +20,7 @@
 from Globals import DTMLFile
 from Globals import InitializeClass
 from Globals import PersistentMapping
+from OFS.Folder import Folder
 from OFS.SimpleItem import SimpleItem
 from zope.interface import implements
 
@@ -37,10 +38,8 @@
 
 
 class MetadataElementPolicy( SimpleItem ):
-
+    """ Represent a type-specific policy about a particular metadata element.
     """
-        Represent a type-specific policy about a particular DCMI element.
-    """
 
     security = ClassSecurityInfo()
     #
@@ -77,62 +76,47 @@
     #
     security.declareProtected(View , 'isMultiValued')
     def isMultiValued( self ):
+        """ Can this element hold multiple values?
         """
-            Can this element hold multiple values?
-        """
         return self.is_multi_valued
 
     security.declareProtected(View , 'isRequired')
     def isRequired( self ):
+        """ Must this element be supplied?
         """
-            Must this element be supplied?
-        """
         return self.is_required
 
     security.declareProtected(View , 'supplyDefault')
     def supplyDefault( self ):
+        """ Should the tool supply a default?
         """
-            Should the tool supply a default?
-        """
         return self.supply_default
 
     security.declareProtected(View , 'defaultValue')
     def defaultValue( self ):
+        """ If so, what is the default?
         """
-            If so, what is the default?
-        """
         return self.default_value
 
     security.declareProtected(View , 'enforceVocabulary')
     def enforceVocabulary( self ):
+        """ Should the tool enforce the policy's vocabulary?
         """
-        """
         return self.enforce_vocabulary
 
     security.declareProtected(View , 'allowedVocabulary')
     def allowedVocabulary( self ):
+        """ What are the allowed values?
         """
-        """
         return self.allowed_vocabulary
 
 InitializeClass( MetadataElementPolicy )
 
 
-DEFAULT_ELEMENT_SPECS = ( ( 'Title', 0 )
-                        , ( 'Description', 0 )
-                        , ( 'Subject', 1 )
-                        , ( 'Format', 0 )
-                        , ( 'Language', 0 )
-                        , ( 'Rights', 0 )
-                        )
 
-
 class ElementSpec( SimpleItem ):
-
+    """ Represent all the tool knows about a single metadata element.
     """
-        Represent all the tool knows about a single metadata element.
-    """
-
     security = ClassSecurityInfo()
 
     #
@@ -158,10 +142,10 @@
 
     security.declareProtected(View , 'getPolicy')
     def getPolicy( self, typ=None ):
+        """ Find the policy for this element for objects of the given type.
+        
+        o Return a default, if none found.
         """
-            Find the policy this element for objects whose type
-            object name is 'typ';  return a default, if none found.
-        """
         try:
             return self.policies[ typ ].__of__(self)
         except KeyError:
@@ -169,9 +153,8 @@
 
     security.declareProtected(View , 'listPolicies')
     def listPolicies( self ):
+        """ Return a list of all policies for this element.
         """
-            Return a list of all policies for this element.
-        """
         res = []
         for k, v in self.policies.items():
             res.append((k, v.__of__(self)))
@@ -179,10 +162,8 @@
 
     security.declareProtected(ManagePortal , 'addPolicy')
     def addPolicy( self, typ ):
+        """ Add a policy to this element for objects of the given type.
         """
-            Add a policy to this element for objects whose type
-            object name is 'typ'.
-        """
         if typ is None:
             raise MetadataError, "Can't replace default policy."
 
@@ -193,10 +174,10 @@
 
     security.declareProtected(ManagePortal, 'removePolicy')
     def removePolicy( self, typ ):
+        """ Remove the policy from this element for objects of the given type.
+        
+        o Do *not* remvoe the default, however.
         """
-            Remove the policy from this element for objects whose type
-            object name is 'typ' (*not* the default, however).
-        """
         if typ is None:
             raise MetadataError, "Can't remove default policy."
         del self.policies[ typ ]
@@ -204,90 +185,32 @@
 InitializeClass( ElementSpec )
 
 
-class MetadataTool( UniqueObject, SimpleItem, ActionProviderBase ):
-
-    implements(IMetadataTool)
-    __implements__ = (z2IMetadataTool, ActionProviderBase.__implements__)
-
-    id = 'portal_metadata'
-    meta_type = 'Default Metadata Tool'
-
-    #
-    #   Default values.
-    #
-    publisher           = ''
-    element_specs       = None
-    #initial_values_hook = None
-    #validation_hook     = None
-
+class MetadataSchema( SimpleItem ):
+    """ Describe a metadata schema.
+    """
     security = ClassSecurityInfo()
 
-    def __init__( self
-                , publisher=None
-               #, initial_values_hook=None
-               #, validation_hook=None
-                , element_specs=DEFAULT_ELEMENT_SPECS
-                ):
+    meta_type = 'Metadata Schema'
+    publisher = ''
 
-        self.editProperties( publisher
-                          #, initial_values_hook
-                          #, validation_hook
-                           )
-
+    def __init__( self, id, element_specs=() ):
+        self._setId( id )
         self.element_specs = PersistentMapping()
-
         for name, is_multi_valued in element_specs:
             self.element_specs[ name ] = ElementSpec( is_multi_valued )
 
+
     #
     #   ZMI methods
     #
-    manage_options = ( ActionProviderBase.manage_options +
-                     ( { 'label'      : 'Overview'
-                         , 'action'     : 'manage_overview'
-                         }
-                       , { 'label'      : 'Properties'
-                         , 'action'     : 'propertiesForm'
-                         }
-                       , { 'label'      : 'Elements'
+    manage_options = ( ( { 'label'      : 'Elements'
                          , 'action'     : 'elementPoliciesForm'
                          }
-            # TODO     , { 'label'      : 'Types'
-            #            , 'action'     : 'typesForm'
-            #            }
+                       ,
                        )
                      + SimpleItem.manage_options
                      )
 
-    security.declareProtected(ManagePortal, 'manage_overview')
-    manage_overview = DTMLFile( 'explainMetadataTool', _dtmldir )
-
-    security.declareProtected(ManagePortal, 'propertiesForm')
-    propertiesForm = DTMLFile( 'metadataProperties', _dtmldir )
-
-    security.declareProtected(ManagePortal, 'editProperties')
-    def editProperties( self
-                      , publisher=None
-               # TODO , initial_values_hook=None
-               # TODO , validation_hook=None
-                      , REQUEST=None
-                      ):
-        """
-            Form handler for "tool-wide" properties (including list of
-            metadata elements).
-        """
-        if publisher is not None:
-            self.publisher = publisher
-
-        # TODO self.initial_values_hook = initial_values_hook
-        # TODO self.validation_hook = validation_hook
-
-        if REQUEST is not None:
-            REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
-                                        + '/propertiesForm'
-                                        + '?manage_tabs_message=Tool+updated.'
-                                        )
-
     security.declareProtected(ManagePortal, 'elementPoliciesForm')
     elementPoliciesForm = DTMLFile( 'metadataElementPolicies', _dtmldir )
 
@@ -302,9 +225,8 @@
                         , allowed_vocabulary
                         , REQUEST=None
                         ):
+        """ Add a type-specific policy for one of our elements.
         """
-            Add a type-specific policy for one of our elements.
-        """
         if content_type == '<default>':
             content_type = None
 
@@ -330,9 +252,8 @@
                            , content_type
                            , REQUEST=None
                            ):
+        """ Remvoe a type-specific policy for one of our elements.
         """
-            Remvoe a type-specific policy for one of our elements.
-        """
         if content_type == '<default>':
             content_type = None
 
@@ -356,10 +277,10 @@
                            , allowed_vocabulary
                            , REQUEST=None
                            ):
+        """ Update a policy for one of our elements 
+        
+        o 'content_type' will be '<default>' when we edit the default.
         """
-            Update a policy for one of our elements ('content_type'
-            will be '<default>' when we edit the default).
-        """
         if content_type == '<default>':
             content_type = None
         spec = self.getElementSpec( element )
@@ -383,10 +304,8 @@
     #
     security.declareProtected(ManagePortal, 'listElementSpecs')
     def listElementSpecs( self ):
+        """ Return a list of ElementSpecs representing the elements we manage.
         """
-            Return a list of ElementSpecs representing
-            the elements managed by the tool.
-        """
         res = []
         for k, v in self.element_specs.items():
             res.append((k, v.__of__(self)))
@@ -394,17 +313,14 @@
 
     security.declareProtected(ManagePortal, 'getElementSpec')
     def getElementSpec( self, element ):
+        """ Return an ElementSpec for the given 'element'.
         """
-            Return an ElementSpec representing the tool's knowledge
-            of 'element'.
-        """
         return self.element_specs[ element ].__of__( self )
 
     security.declareProtected(ManagePortal, 'addElementSpec')
     def addElementSpec( self, element, is_multi_valued, REQUEST=None ):
+        """ Add 'element' to our list of managed elements.
         """
-            Add 'element' to our list of managed elements.
-        """
         # Don't replace.
         if self.element_specs.has_key( element ):
            return
@@ -419,9 +335,8 @@
 
     security.declareProtected(ManagePortal, 'removeElementSpec')
     def removeElementSpec( self, element, REQUEST=None ):
+        """ Remove 'element' from our list of managed elements.
         """
-            Remove 'element' from our list of managed elements.
-        """
         del self.element_specs[ element ]
 
         if REQUEST is not None:
@@ -432,122 +347,276 @@
 
     security.declareProtected(ManagePortal, 'listPolicies')
     def listPolicies( self, typ=None ):
+        """ Show all policies for a given content type
+        
+        o If 'typ' is none, return the list of default policies.
         """
-            Show all policies for a given content type, or the default
-            if None.
-        """
         result = []
         for element, spec in self.listElementSpecs():
             result.append( ( element, spec.getPolicy( typ ) ) )
         return result
 
+InitializeClass(MetadataSchema)
+
+
+_DCMI_ELEMENT_SPECS = ( ( 'Title', 0 )
+                      , ( 'Description', 0 )
+                      , ( 'Subject', 1 )
+                      , ( 'Format', 0 )
+                      , ( 'Language', 0 )
+                      , ( 'Rights', 0 )
+                      )
+
+class MetadataTool( UniqueObject, Folder, ActionProviderBase ):
+
+    implements(IMetadataTool)
+    __implements__ = (z2IMetadataTool, ActionProviderBase.__implements__)
+
+    id = 'portal_metadata'
+    meta_type = 'Default Metadata Tool'
+    _actions = ()
+
+    _DCMI = None
+    def _get_DCMI( self ):
+
+        if self._DCMI is None:
+            dcmi = self._DCMI = MetadataSchema( 'DCMI', _DCMI_ELEMENT_SPECS )
+
+            old_specs = getattr( self, 'element_specs', None )
+            if old_specs is not None:
+                del self.element_specs
+                for element_id, old_spec in old_specs.items():
+                    new_spec = dcmi.getElementSpec( element_id )
+                    for typ, policy in old_spec.listPolicies():
+                        if typ is not None:
+                            new_spec.addPolicy( typ )
+                        tp = new_spec.getPolicy( typ )
+                        tp.edit( is_required=policy.isRequired()
+                               , supply_default=policy.supplyDefault()
+                               , default_value=policy.defaultValue()
+                               , enforce_vocabulary=policy.enforceVocabulary()
+                               , allowed_vocabulary=policy.allowedVocabulary()
+                               )
+
+        return self._DCMI
+
+    DCMI = property(_get_DCMI, None)
+
     #
-    #   'portal_metadata' interface
+    #   Default values.
     #
+    publisher           = ''
+
+    security = ClassSecurityInfo()
+
+    def __init__( self, publisher=None ):
+
+        self.editProperties( publisher )
+
+    #
+    #   ZMI methods
+    #
+    manage_options = ( ( { 'label'      : 'Schema'
+                         , 'action'     : 'propertiesForm'
+                         }
+                       , { 'label'      : 'Overview'
+                         , 'action'     : 'manage_overview'
+                         }
+                       )
+                     + Folder.manage_options[:1]
+                     + ActionProviderBase.manage_options +
+                       Folder.manage_options[1:]
+                     )
+
+    security.declareProtected(ManagePortal, 'manage_overview')
+    manage_overview = DTMLFile( 'explainMetadataTool', _dtmldir )
+
+    security.declareProtected(ManagePortal, 'propertiesForm')
+    propertiesForm = DTMLFile( 'metadataProperties', _dtmldir )
+
+    security.declareProtected(ManagePortal, 'editProperties')
+    def editProperties( self
+                      , publisher=None
+                      , REQUEST=None
+                      ):
+        """ Form handler for "tool-wide" properties 
+        """
+        if publisher is not None:
+            self.publisher = publisher
+
+        if REQUEST is not None:
+            REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
+                                        + '/propertiesForm'
+                                        + '?manage_tabs_message=Tool+updated.'
+                                        )
+
+    security.declareProtected(ManagePortal, 'manage_addSchema')
+    def manage_addSchema( self, schema_id, elements, REQUEST ):
+        """ ZMI wrapper around addSchema
+        """
+        massaged = []
+        for element in elements:
+            if isinstance(element, basestring):
+                element = element.split(',')
+                if len( element ) < 2:
+                    element.append(0)
+            massaged.append( element )
+        self.addSchema( schema_id, massaged )
+
+        REQUEST['RESPONSE'].redirect( self.absolute_url()
+                                    + '/propertiesForm'
+                                    + '?manage_tabs_message=Schema+added.'
+                                    )
+
+    security.declareProtected(ManagePortal, 'manage_removeSchemas')
+    def manage_removeSchemas( self, schema_ids, REQUEST ):
+        """ ZMI wrapper around removeSchema
+        """
+        if not schema_ids:
+            raise ValueError, 'No schemas selected!'
+
+        for schema_id in schema_ids:
+            self.removeSchema( schema_id )
+
+        REQUEST['RESPONSE'].redirect( self.absolute_url()
+                                    + '/propertiesForm'
+                                    + '?manage_tabs_message=Schemas+removed.'
+                                    )
+
     security.declarePrivate( 'getFullName' )
     def getFullName( self, userid ):
+        """ See IMetadataTool.
         """
-            Convert an internal userid to a "formal" name, if
-            possible, perhaps using the 'portal_membership' tool.
-
-            Used to map userid's for Creator, Contributor DCMI
-            queries.
-        """
         return userid   # TODO: do lookup here
 
     security.declarePublic( 'getPublisher' )
     def getPublisher( self ):
+        """ See IMetadataTool.
         """
-            Return the "formal" name of the publisher of the
-            portal.
-        """
         return self.publisher
 
-    security.declarePublic( 'listAllowedVocabulary' )
-    def listAllowedVocabulary( self, element, content=None, content_type=None ):
-        """
-            List allowed keywords for a given portal_type, or all
-            possible keywords if none supplied.
-        """
-        spec = self.getElementSpec( element )
-        if content_type is None and content:
-            content_type = content.getPortalTypeName()
-        return spec.getPolicy( content_type ).allowedVocabulary()
-
     security.declarePublic( 'listAllowedSubjects' )
     def listAllowedSubjects( self, content=None, content_type=None ):
+        """ See IMetadataTool.
         """
-            List allowed keywords for a given portal_type, or all
-            possible keywords if none supplied.
-        """
-        return self.listAllowedVocabulary( 'Subject', content, content_type )
+        return self.listAllowedVocabulary( 'DCMI'
+                                         , 'Subject'
+                                         , content
+                                         , content_type
+                                         )
 
     security.declarePublic( 'listAllowedFormats' )
     def listAllowedFormats( self, content=None, content_type=None ):
+        """ See IMetadataTool.
         """
-            List the allowed 'Content-type' values for a particular
-            portal_type, or all possible formats if none supplied.
-        """
-        return self.listAllowedVocabulary( 'Format', content, content_type )
+        return self.listAllowedVocabulary( 'DCMI'
+                                         , 'Format'
+                                         , content
+                                         , content_type
+                                         )
 
     security.declarePublic( 'listAllowedLanguages' )
     def listAllowedLanguages( self, content=None, content_type=None ):
+        """ See IMetadataTool.
         """
-            List the allowed language values.
-        """
-        return self.listAllowedVocabulary( 'Language', content, content_type )
+        return self.listAllowedVocabulary( 'DCMI'
+                                         , 'Language'
+                                         , content
+                                         , content_type
+                                         )
 
     security.declarePublic( 'listAllowedRights' )
     def listAllowedRights( self, content=None, content_type=None ):
+        """ See IMetadata Tool.
         """
-            List the allowed values for a "Rights"
-            selection list;  this gets especially important where
-            syndication is involved.
+        return self.listAllowedVocabulary( 'DCMI'
+                                         , 'Rights'
+                                         , content
+                                         , content_type
+                                         )
+
+    security.declarePublic( 'listAllowedVocabulary' )
+    def listAllowedVocabulary( self
+                             , schema
+                             , element
+                             , content=None
+                             , content_type=None
+                             ):
+        """ See IMetadataTool.
         """
-        return self.listAllowedVocabulary( 'Rights', content, content_type )
+        schema_def = getattr( self, schema )
+        spec = schema_def.getElementSpec( element )
+        if content_type is None and content:
+            content_type = content.getPortalTypeName()
+        return spec.getPolicy( content_type ).allowedVocabulary()
 
+    security.declarePublic( 'listSchemas' )
+    def listSchemas( self ):
+        """ See IMetadataTool.
+        """
+        result = [ ( 'DCMI', self.DCMI ) ]
+        result.extend( self.objectItems( [ MetadataSchema.meta_type ] ) )
+        return result
+
+    security.declareProtected(ModifyPortalContent, 'addSchema')
+    def addSchema( self, schema_id, elements=() ):
+        """ See IMetadataTool.
+        """
+        if schema_id == 'DCMI' or schema_id in self.objectIds():
+            raise KeyError, 'Duplicate schema ID: %s' % schema_id
+
+        schema = MetadataSchema( schema_id, elements )
+        self._setObject( schema_id, schema )
+
+        return self._getOb( schema_id )
+
+    security.declareProtected(ModifyPortalContent, 'removeSchema')
+    def removeSchema( self, schema_id ):
+        """ See IMetadataTool.
+        """
+        if schema_id == 'DCMI' or schema_id not in self.objectIds():
+            raise KeyError, 'Invalid schema ID: %s' % schema_id
+
+        self._delObject( schema_id )
+
     security.declareProtected(ModifyPortalContent, 'setInitialMetadata')
     def setInitialMetadata( self, content ):
+        """ See IMetadataTool.
         """
-            Set initial values for content metatdata, supplying
-            any site-specific defaults.
-        """
-        for element, policy in self.listPolicies(content.getPortalTypeName()):
+        for schema_id, schema in self.listSchemas():
+            for element, policy in schema.listPolicies(
+                                    content.getPortalTypeName()):
 
-            if not getattr( content, element )():
+                if not getattr( content, element )():
 
-                if policy.supplyDefault():
-                    setter = getattr( content, 'set%s' % element )
-                    setter( policy.defaultValue() )
-                elif policy.isRequired():
-                    raise MetadataError, \
-                          'Metadata element %s is required.' % element
+                    if policy.supplyDefault():
+                        setter = getattr( content, 'set%s' % element )
+                        setter( policy.defaultValue() )
+                    elif policy.isRequired():
+                        raise MetadataError, \
+                            'Metadata element %s is required.' % element
 
         # TODO:  Call initial_values_hook, if present
 
     security.declareProtected(View, 'validateMetadata')
     def validateMetadata( self, content ):
+        """ See IMetadataTool.
         """
-            Enforce portal-wide policies about DCI, e.g.,
-            requiring non-empty title/description, etc.  Called
-            by the CMF immediately before saving changes to the
-            metadata of an object.
-        """
-        for element, policy in self.listPolicies(content.getPortalTypeName()):
+        for schema_id, schema in self.listSchemas():
+            for element, policy in schema.listPolicies(
+                                    content.getPortalTypeName()):
 
-            value = getattr( content, element )()
-            if not value and policy.isRequired():
-                raise MetadataError, \
-                        'Metadata element %s is required.' % element
+                value = getattr( content, element )()
+                if not value and policy.isRequired():
+                    raise MetadataError, \
+                            'Metadata element %s is required.' % element
 
-            if value and policy.enforceVocabulary():
-                values = policy.isMultiValued() and value or [ value ]
-                for value in values:
-                    if not value in policy.allowedVocabulary():
-                        raise MetadataError, \
-                        'Value %s is not in allowed vocabulary for ' \
-                        'metadata element %s.' % ( value, element )
+                if value and policy.enforceVocabulary():
+                    values = policy.isMultiValued() and value or [ value ]
+                    for value in values:
+                        if not value in policy.allowedVocabulary():
+                            raise MetadataError, \
+                            'Value %s is not in allowed vocabulary for ' \
+                            'metadata element %s.' % ( value, element )
 
-        # TODO:  Call validation_hook, if present
-
 InitializeClass( MetadataTool )

Modified: CMF/trunk/CMFDefault/dtml/metadataElementPolicies.dtml
===================================================================
--- CMF/trunk/CMFDefault/dtml/metadataElementPolicies.dtml	2005-11-29 01:58:24 UTC (rev 40395)
+++ CMF/trunk/CMFDefault/dtml/metadataElementPolicies.dtml	2005-11-29 03:29:18 UTC (rev 40396)
@@ -1,9 +1,9 @@
 <dtml-var manage_page_header>
 <dtml-var manage_tabs>
 
-<dtml-unless expr="REQUEST.has_key( 'element' )">
-<dtml-call expr="REQUEST.set( 'element', listElementSpecs()[0][0] )">
-</dtml-unless>
+<dtml-let specs=listElementSpecs
+          def_spec="specs and specs[0][0] or None"
+          current="REQUEST.get( 'element', def_spec )">
 
 <h3> Update Element Metadata Policies </h3>
 
@@ -14,7 +14,7 @@
   <td colspan="3"> 
    <dtml-in listElementSpecs>
     <dtml-let key=sequence-key>
-     <dtml-if expr="key == REQUEST[ 'element' ]">
+     <dtml-if expr="key == current">
      &dtml-key;  &nbsp;
      <dtml-else>
       <a href="&dtml-URL;?element=&dtml-key;"> &dtml-key; </a> &nbsp;
@@ -24,14 +24,14 @@
   </td>
  </tr>
 
- <dtml-let spec="getElementSpec( element=REQUEST[ 'element' ] )"
+ <dtml-if def_spec>
+ <dtml-let spec="getElementSpec( element=current )"
            multi="spec.isMultiValued()"
            tokenz="multi and ':tokens' or ''"
  >
 
  <dtml-in expr="spec.listPolicies()" sort>
- <dtml-let element="REQUEST[ 'element']"
-           key=sequence-key
+ <dtml-let key=sequence-key
            typ="key or '<default>'"
            policy=sequence-item
            rqd="policy.isRequired() and 'checked' or ''"
@@ -44,7 +44,7 @@
  >
 
  <form action="&dtml-absolute_url;" method="POST">
- <input type="hidden" name="element" value="&dtml-element;"> 
+ <input type="hidden" name="element" value="&dtml-current;"> 
  <input type="hidden" name="content_type" value="&dtml-typ;"> 
 
  <tr style="background-color: DarkGray; color: DarkBlue">
@@ -110,7 +110,7 @@
  </dtml-in>
 
  <form action="&dtml-absolute_url;" method="POST">
- <input type="hidden" name="element" value="&dtml-element;"> 
+ <input type="hidden" name="element" value="&dtml-current;"> 
 
  <tr style="background-color: DarkGray; color: DarkBlue">
   <th colspan="4"> &lt;new type&gt; </th>
@@ -171,6 +171,9 @@
  </tr>
 
  </dtml-let>
+ </dtml-if>
 </table>
 
+</dtml-let>
+
 <dtml-var manage_page_footer>

Modified: CMF/trunk/CMFDefault/dtml/metadataProperties.dtml
===================================================================
--- CMF/trunk/CMFDefault/dtml/metadataProperties.dtml	2005-11-29 01:58:24 UTC (rev 40395)
+++ CMF/trunk/CMFDefault/dtml/metadataProperties.dtml	2005-11-29 03:29:18 UTC (rev 40396)
@@ -20,54 +20,76 @@
 </table>
 </form>
 
-<h3> Add Metadata Element </h3>
+<h3> Metadata Schemas </h3>
 
-<form action="&dtml-absolute_url;/addElementSpec" method="POST">
-<input type="hidden" name="is_multi_valued:int:default" value="0">
+<form action="&dtml-absolute_url;" method="POST">
 <table>
 
+ <dtml-in listSchemas>
+ <dtml-if sequence-start>
+
  <tr valign="middle">
-  <th width="100" align="right"> Element: </th>
-  <td> <input type="text" name="element" size="20"> </td>
+  <td width="16">
+    <br /> <!-- can't remove DCMI schema! -->
+  </td>
+  <td>
+   <a href="&dtml-absolute_url;/manage_workspace">DCMI</a>
+  </td>
  </tr>
 
+ <dtml-else>
+
  <tr valign="middle">
-  <th width="100" align="right"> Multi-valued? </th>
-  <td> <input type="checkbox" name="is_multi_valued:boolean"> </td>
+  <td>
+   <input type="checkbox" name="schema_ids:list" value="&dtml-sequence-key;" />
+  </td>
+  <td>
+   <a href="&dtml-absolute_url;/manage_workspace"
+   > &dtml-sequence-key; </a>
+  </td>
  </tr>
 
+ </dtml-if>
+ </dtml-in>
+
  <tr valign="middle">
-  <td> <br> </td>
-  <td> <input type="submit" value=" Add "> </td>
+  <td colspan="2">
+    <input type="submit" name="manage_removeSchemas:method"
+        value=" Remove Schemas ">
+  </td>
  </tr>
-
 </table>
-</form>
 
-<h3> Remove Metadata Element </h3>
+<br />
 
-<form action="&dtml-absolute_url;/removeElementSpec" method="POST">
 <table>
 
- <tr valign="middle">
-  <th width="100" align="right"> Element: </th>
-  <td> <dtml-in listElementSpecs>
-       <dtml-if sequence-start>
-       <select name="element">
-       </dtml-if>
-       <option value="&dtml-sequence-key;"> &dtml-sequence-key; </option>
-       <dtml-if sequence-end>
-       </select>
-       </dtml-if>
-       </dtml-in>
+ <tr>
+  <th colspan="2"
+      style="background-color:#CCCCCC; color:#000088">
+   Add a Schema
+  </th>
+ <tr>
+  <th> Schema ID </th>
+  <td>
+    <input type="text" name="schema_id" size="20"><br />
   </td>
  </tr>
 
- <tr valign="middle">
-  <td> <br> </td>
-  <td> <input type="submit" value=" Remove "> </td>
+ <tr>
+  <th> Elements </th>
+  <td>
+    <textarea name="elements:lines" cols="30" rows="10"></textarea>
+  </td>
  </tr>
 
+ <tr>
+  <td> <br /> </td>
+  <td>
+    <input type="submit" name="manage_addSchema:method" value=" Add Schema ">
+  </td>
+ </tr>
+
 </table>
 </form>
 

Modified: CMF/trunk/CMFDefault/tests/test_MetadataTool.py
===================================================================
--- CMF/trunk/CMFDefault/tests/test_MetadataTool.py	2005-11-29 01:58:24 UTC (rev 40395)
+++ CMF/trunk/CMFDefault/tests/test_MetadataTool.py	2005-11-29 03:29:18 UTC (rev 40396)
@@ -14,118 +14,132 @@
 
 $Id$
 """
-
-from unittest import TestCase, TestSuite, makeSuite, main
+import unittest
 import Testing
 import Zope2
 Zope2.startup()
 
 from Acquisition import aq_base
 
-from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
-from Products.CMFDefault.exceptions import MetadataError
-from Products.CMFDefault.MetadataTool import DEFAULT_ELEMENT_SPECS
-from Products.CMFDefault.MetadataTool import ElementSpec
-from Products.CMFDefault.MetadataTool import MetadataElementPolicy
-from Products.CMFDefault.MetadataTool import MetadataTool
 
+class TestMetadataElementPolicy( unittest.TestCase ):
 
-class TestMetadataElementPolicy( TestCase ):
+    def _getTargetClass( self ):
+        from Products.CMFDefault.MetadataTool import MetadataElementPolicy
+        return MetadataElementPolicy
 
-    def setUp( self ):
-        self.sv_policy = MetadataElementPolicy( 0 )
-        self.mv_policy = MetadataElementPolicy( 1 )
+    def _makeOne( self, *args, **kw ):
+        return self._getTargetClass()( *args, **kw )
 
-    def tearDown( self ):
-        del self.sv_policy
-        del self.mv_policy
+    def test_empty_single_valued( self ):
+        sv_policy = self._makeOne( 0 )
+        self.failIf( sv_policy.isMultiValued() )
+        self.failIf( sv_policy.isRequired() )
+        self.failIf( sv_policy.supplyDefault() )
+        self.failIf( sv_policy.defaultValue() )
+        self.failIf( sv_policy.enforceVocabulary() )
+        self.failIf( sv_policy.allowedVocabulary() )
 
-    def test_emptySV( self ):
-        assert not self.sv_policy.isMultiValued()
-        assert not self.sv_policy.isRequired()
-        assert not self.sv_policy.supplyDefault()
-        assert not self.sv_policy.defaultValue()
-        assert not self.sv_policy.enforceVocabulary()
-        assert not self.sv_policy.allowedVocabulary()
+    def test_edit_single_valued( self ):
+        sv_policy = self._makeOne( 0 )
+        sv_policy.edit( 1, 1, 'xxx', 0, '' ) 
+        self.failIf( sv_policy.isMultiValued() )
+        self.failUnless( sv_policy.isRequired() )
+        self.failUnless( sv_policy.supplyDefault() )
+        self.assertEquals( sv_policy.defaultValue(), 'xxx' )
+        self.failIf( sv_policy.enforceVocabulary() )
+        self.failIf( sv_policy.allowedVocabulary() )
 
-    def test_editSV( self ):
-        self.sv_policy.edit( 1, 1, 'xxx', 0, '' )
-        assert not self.sv_policy.isMultiValued()
-        assert self.sv_policy.isRequired()
-        assert self.sv_policy.supplyDefault()
-        assert self.sv_policy.defaultValue() == 'xxx'
-        assert not self.sv_policy.enforceVocabulary()
-        assert not self.sv_policy.allowedVocabulary()
+    def test_empty_multi_valued( self ):
+        mv_policy = self._makeOne( 1 )
+        self.failUnless( mv_policy.isMultiValued() )
+        self.failIf( mv_policy.isRequired() )
+        self.failIf( mv_policy.supplyDefault() )
+        self.failIf( mv_policy.defaultValue() )
+        self.failIf( mv_policy.enforceVocabulary() )
+        self.failIf( mv_policy.allowedVocabulary() )
 
-    def test_emptyMV( self ):
-        assert self.mv_policy.isMultiValued()
-        assert not self.mv_policy.isRequired()
-        assert not self.mv_policy.supplyDefault()
-        assert not self.mv_policy.defaultValue()
-        assert not self.mv_policy.enforceVocabulary()
-        assert not self.mv_policy.allowedVocabulary()
+    def test_edit_multi_valued( self ):
+        mv_policy = self._makeOne( 1 )
+        mv_policy.edit( 1, 1, 'xxx', 1, ( 'xxx', 'yyy' ) )
+        self.failUnless( mv_policy.isMultiValued() )
+        self.failUnless( mv_policy.isRequired() )
+        self.failUnless( mv_policy.supplyDefault() )
+        self.assertEqual( mv_policy.defaultValue(), 'xxx' )
+        self.failUnless( mv_policy.enforceVocabulary() )
+        self.assertEqual( len( mv_policy.allowedVocabulary() ), 2 )
+        self.failUnless( 'xxx' in mv_policy.allowedVocabulary() )
+        self.failUnless( 'yyy' in mv_policy.allowedVocabulary() )
 
-    def test_editMV( self ):
-        self.mv_policy.edit( 1, 1, 'xxx', 1, ( 'xxx', 'yyy' ) )
-        assert self.mv_policy.isMultiValued()
-        assert self.mv_policy.isRequired()
-        assert self.mv_policy.supplyDefault()
-        assert self.mv_policy.defaultValue() == 'xxx'
-        assert self.mv_policy.enforceVocabulary()
-        assert len( self.mv_policy.allowedVocabulary() ) == 2
-        assert 'xxx' in self.mv_policy.allowedVocabulary()
-        assert 'yyy' in self.mv_policy.allowedVocabulary()
 
-class TestElementSpec( TestCase ):
+class TestElementSpec( unittest.TestCase ):
 
-    def setUp( self ):
-        self.sv_spec    = ElementSpec( 0 )
-        self.mv_spec    = ElementSpec( 1 )
+    def _getTargetClass( self ):
+        from Products.CMFDefault.MetadataTool import ElementSpec
+        return ElementSpec
 
-    def tearDown( self ):
-        del self.sv_spec
-        del self.mv_spec
+    def _makeOne( self, *args, **kw ):
+        return self._getTargetClass()( *args, **kw )
 
-    def test_empty( self ):
-        assert not self.sv_spec.isMultiValued()
-        assert self.sv_spec.getPolicy() == self.sv_spec.getPolicy( 'XYZ' )
-        policies = self.sv_spec.listPolicies()
-        assert len( policies ) == 1
-        assert policies[0][0] is None
+    def test_empty_single_valued( self ):
+        sv_spec = self._makeOne( 0 )
+        self.failIf( sv_spec.isMultiValued() )
+        self.assertEqual( sv_spec.getPolicy(), sv_spec.getPolicy( 'XYZ' ) )
+        policies = sv_spec.listPolicies()
+        self.assertEqual( len( policies ), 1 )
+        self.assertEqual( policies[0][0], None )
 
-        assert self.mv_spec.isMultiValued()
-        assert self.mv_spec.getPolicy() == self.mv_spec.getPolicy( 'XYZ' )
-        policies = self.mv_spec.listPolicies()
-        assert len( policies ) == 1
-        assert policies[0][0] is None
+    def test_empty_multi_valued( self ):
+        mv_spec = self._makeOne( 1 )
+        self.failUnless( mv_spec.isMultiValued() )
+        self.assertEqual( mv_spec.getPolicy(), mv_spec.getPolicy( 'XYZ' ) )
+        policies = mv_spec.listPolicies()
+        self.assertEqual( len( policies ), 1 )
+        self.assertEqual( policies[0][0], None )
 
 
-class Foo( DefaultDublinCoreImpl ):
 
-    description = title = language = format = rights = ''
-    subject = ()
+class TestMetadataSchema( unittest.TestCase ):
 
-    def __init__( self ):
-        pass # skip DDCI's default values
+    def _getTargetClass( self ):
+        from Products.CMFDefault.MetadataTool import MetadataSchema
+        return MetadataSchema
 
-    def getPortalTypeName( self ):
-        return 'Foo'
+    def _makeOne( self, *args, **kw ):
+        return self._getTargetClass()( *args, **kw )
 
 
-class Bar( Foo ):
+class TestMetadataTool( unittest.TestCase ):
 
-    def getPortalTypeName( self ):
-        return 'Bar'
+    def _getTargetClass( self ):
+        from Products.CMFDefault.MetadataTool import MetadataTool
+        return MetadataTool
 
+    def _makeOne( self, *args, **kw ):
+        return self._getTargetClass()( *args, **kw )
 
-class TestMetadataTool( TestCase ):
+    def _makeTestObjects( self ):
 
-    def setUp( self ):
-        self.tool = MetadataTool()
+        from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
+        class Foo( DefaultDublinCoreImpl ):
 
-    def tearDown( self ):
-        del self.tool
+            description = title = language = format = rights = ''
+            subject = ()
 
+            def __init__( self ):
+                pass # skip DDCI's default values
+
+            def getPortalTypeName( self ):
+                return 'Foo'
+
+
+        class Bar( Foo ):
+
+            def getPortalTypeName( self ):
+                return 'Bar'
+
+        return Foo(), Bar()
+
     def test_z2interfaces(self):
         from Interface.Verify import verifyClass
         from Products.CMFCore.interfaces.portal_actions \
@@ -133,89 +147,92 @@
         from Products.CMFCore.interfaces.portal_metadata \
                 import portal_metadata as IMetadataTool
 
-        verifyClass(IActionProvider, MetadataTool)
-        verifyClass(IMetadataTool, MetadataTool)
+        verifyClass(IActionProvider, self._getTargetClass())
+        verifyClass(IMetadataTool, self._getTargetClass())
 
     def test_z3interfaces(self):
         from zope.interface.verify import verifyClass
         from Products.CMFCore.interfaces import IActionProvider
         from Products.CMFCore.interfaces import IMetadataTool
 
-        verifyClass(IActionProvider, MetadataTool)
-        verifyClass(IMetadataTool, MetadataTool)
+        verifyClass(IActionProvider, self._getTargetClass())
+        verifyClass(IMetadataTool, self._getTargetClass())
 
     def test_empty( self ):
+        from Products.CMFDefault.MetadataTool import _DCMI_ELEMENT_SPECS
 
-        assert not self.tool.getPublisher()
-        assert self.tool.getFullName( 'foo' ) == 'foo'
+        tool = self._makeOne()
+        self.failIf( tool.getPublisher() )
+        self.assertEqual( tool.getFullName( 'foo' ), 'foo' )
 
-        specs = list( self.tool.listElementSpecs() )
-        defaults = list( DEFAULT_ELEMENT_SPECS )
+        dcmi = tool.DCMI
+        specs = list( dcmi.DCMI.listElementSpecs() )
+        defaults = list( _DCMI_ELEMENT_SPECS )
         specs.sort(); defaults.sort()
 
-        assert len( specs ) == len( defaults )
+        self.assertEqual( len( specs ), len( defaults ) )
         for i in range( len( specs ) ):
-            assert specs[i][0] == defaults[i][0]
-            assert specs[i][1].isMultiValued() == defaults[i][1]
+            self.assertEqual( specs[i][0], defaults[i][0] )
+            self.assertEqual( specs[i][1].isMultiValued(), defaults[i][1] )
             policies = specs[i][1].listPolicies()
-            assert len( policies ) == 1
-            assert policies[0][0] is None
+            self.assertEqual( len( policies ), 1 )
+            self.failUnless( policies[0][0] is None )
 
-        assert not self.tool.getElementSpec( 'Title'        ).isMultiValued()
-        assert not self.tool.getElementSpec( 'Description'  ).isMultiValued()
-        assert     self.tool.getElementSpec( 'Subject'      ).isMultiValued()
-        assert not self.tool.getElementSpec( 'Format'       ).isMultiValued()
-        assert not self.tool.getElementSpec( 'Language'     ).isMultiValued()
-        assert not self.tool.getElementSpec( 'Rights'       ).isMultiValued()
+        self.failIf( dcmi.getElementSpec( 'Title' ).isMultiValued() )
+        self.failIf( dcmi.getElementSpec( 'Description' ).isMultiValued() )
+        self.failUnless( dcmi.getElementSpec( 'Subject' ).isMultiValued() )
+        self.failIf( dcmi.getElementSpec( 'Format' ).isMultiValued() )
+        self.failIf( dcmi.getElementSpec( 'Language' ).isMultiValued() )
+        self.failIf( dcmi.getElementSpec( 'Rights' ).isMultiValued() )
 
         try:
-            dummy = self.tool.getElementSpec( 'Foo' )
+            dummy = dcmi.getElementSpec( 'Foo' )
         except KeyError:
             pass
         else:
-            assert 0, "Expected KeyError"
+            self.failUnless( 0, "Expected KeyError" )
 
-        assert not self.tool.listAllowedSubjects()
-        assert not self.tool.listAllowedFormats()
-        assert not self.tool.listAllowedLanguages()
-        assert not self.tool.listAllowedRights()
+        self.failIf( dcmi.listAllowedSubjects() )
+        self.failIf( dcmi.listAllowedFormats() )
+        self.failIf( dcmi.listAllowedLanguages() )
+        self.failIf( dcmi.listAllowedRights() )
 
-    def test_add( self ):
-        self.tool.addElementSpec( 'Rating', 1 )
-        assert len( self.tool.listElementSpecs() ) \
-            == len( DEFAULT_ELEMENT_SPECS ) + 1
-        rating = self.tool.getElementSpec( 'Rating' )
-        assert rating.isMultiValued()
+    def test_DCMI_addElementSpec( self ):
+        from Products.CMFDefault.MetadataTool import _DCMI_ELEMENT_SPECS
 
-    def test_remove( self ):
-        self.tool.removeElementSpec( 'Rights' )
+        tool = self._makeOne()
+        dcmi = tool.DCMI
+        dcmi.addElementSpec( 'Rating', 1 )
+        self.assertEqual( len( dcmi.listElementSpecs() )
+                        , len( _DCMI_ELEMENT_SPECS ) + 1 )
+        rating = dcmi.getElementSpec( 'Rating' )
+        self.failUnless( rating.isMultiValued() )
 
-        assert len( self.tool.listElementSpecs() ) \
-            == len( DEFAULT_ELEMENT_SPECS ) - 1
+    def test_DCMI_removeElementSpec( self ):
+        from Products.CMFDefault.MetadataTool import _DCMI_ELEMENT_SPECS
 
-        try:
-            dummy = self.tool.getElementSpec( 'Rights' )
-        except KeyError:
-            pass
-        else:
-            assert 0, "Expected KeyError"
+        tool = self._makeOne()
+        dcmi = tool.DCMI
+        dcmi.removeElementSpec( 'Rights' )
 
-        try:
-            self.tool.removeElementSpec( 'Foo' )
-        except KeyError:
-            pass
-        else:
-            assert 0, "Expected KeyError"
+        self.assertEqual( len( dcmi.listElementSpecs() )
+                        , len( _DCMI_ELEMENT_SPECS ) - 1
+                        )
 
+        self.assertRaises( KeyError, dcmi.getElementSpec, 'Rights' )
+        self.assertRaises( KeyError, dcmi.removeElementSpec, 'Foo' )
+
     def test_simplePolicies( self ):
 
-        tSpec = self.tool.getElementSpec( 'Title' )
+        tool = self._makeOne()
+        dcmi = tool.DCMI
+        tSpec = dcmi.getElementSpec( 'Title' )
 
         # Fetch default policy.
         tDef  = tSpec.getPolicy()
-        assert not tDef.isRequired()
-        assert not tDef.supplyDefault()
-        assert not tDef.defaultValue()
+        self.failIf( tDef.isRequired() )
+        self.failIf( tDef.supplyDefault() )
+        self.failIf( tDef.defaultValue() )
 
         # Fetch (default) policy for a type.
         tDoc  = tSpec.getPolicy( 'Document' )
@@ -223,40 +240,42 @@
 
         # Changing default changes policies found from there.
         tDef.edit( 1, 1, 'xyz', 0, () )
-        assert tDef.isRequired()
-        assert tDef.supplyDefault()
-        assert tDef.defaultValue() == 'xyz'
-        assert tDoc.isRequired()
-        assert tDoc.supplyDefault()
-        assert tDoc.defaultValue() == 'xyz'
+        self.failUnless( tDef.isRequired() )
+        self.failUnless( tDef.supplyDefault() )
+        self.assertEqual( tDef.defaultValue(), 'xyz' )
+        self.failUnless( tDoc.isRequired() )
+        self.failUnless( tDoc.supplyDefault() )
+        self.assertEqual( tDoc.defaultValue(), 'xyz' )
 
         tSpec.addPolicy( 'Document' )
-        assert len( tSpec.listPolicies() ) == 2
+        self.assertEqual( len( tSpec.listPolicies() ), 2 )
 
         tDoc  = tSpec.getPolicy( 'Document' )
         self.assertNotEqual(aq_base(tDoc), aq_base(tDef))
-        assert not tDoc.isRequired()
-        assert not tDoc.supplyDefault()
-        assert not tDoc.defaultValue()
+        self.failIf( tDoc.isRequired() )
+        self.failIf( tDoc.supplyDefault() )
+        self.failIf( tDoc.defaultValue() )
 
         tSpec.removePolicy( 'Document' )
         tDoc  = tSpec.getPolicy( 'Document' )
         self.assertEqual(aq_base(tDoc), aq_base(tDef))
-        assert tDoc.isRequired()
-        assert tDoc.supplyDefault()
-        assert tDoc.defaultValue() == 'xyz'
+        self.failUnless( tDoc.isRequired() )
+        self.failUnless( tDoc.supplyDefault() )
+        self.assertEqual( tDoc.defaultValue(), 'xyz' )
 
     def test_multiValuedPolicies( self ):
 
-        sSpec = self.tool.getElementSpec( 'Subject' )
+        tool = self._makeOne()
+        dcmi = tool.DCMI
+        sSpec = dcmi.getElementSpec( 'Subject' )
 
         # Fetch default policy.
         sDef  = sSpec.getPolicy()
-        assert not sDef.isRequired()
-        assert not sDef.supplyDefault()
-        assert not sDef.defaultValue()
-        assert not sDef.enforceVocabulary()
-        assert not sDef.allowedVocabulary()
+        self.failIf( sDef.isRequired() )
+        self.failIf( sDef.supplyDefault() )
+        self.failIf( sDef.defaultValue() )
+        self.failIf( sDef.enforceVocabulary() )
+        self.failIf( sDef.allowedVocabulary() )
 
         # Fetch (default) policy for a type.
         sDoc  = sSpec.getPolicy( 'Document' )
@@ -264,150 +283,266 @@
 
         # Changing default changes policies found from there.
         sDef.edit( 1, 1, 'xyz', 1, ( 'foo', 'bar' ) )
-        assert sDef.isRequired()
-        assert sDef.supplyDefault()
-        assert sDef.defaultValue() == 'xyz'
-        assert sDoc.isRequired()
-        assert sDoc.supplyDefault()
-        assert sDoc.defaultValue() == 'xyz'
-        assert sDef.enforceVocabulary()
-        assert len( sDef.allowedVocabulary() ) == 2
-        assert 'foo' in sDef.allowedVocabulary()
-        assert 'bar' in sDef.allowedVocabulary()
-        assert sDoc.enforceVocabulary()
-        assert len( sDoc.allowedVocabulary() ) == 2
-        assert 'foo' in sDoc.allowedVocabulary()
-        assert 'bar' in sDoc.allowedVocabulary()
+        self.failUnless( sDef.isRequired() )
+        self.failUnless( sDef.supplyDefault() )
+        self.assertEqual( sDef.defaultValue(), 'xyz' )
+        self.failUnless( sDoc.isRequired() )
+        self.failUnless( sDoc.supplyDefault() )
+        self.assertEqual( sDoc.defaultValue(), 'xyz' )
+        self.failUnless( sDef.enforceVocabulary() )
+        self.assertEqual( len( sDef.allowedVocabulary() ), 2 )
+        self.failUnless( 'foo' in sDef.allowedVocabulary() )
+        self.failUnless( 'bar' in sDef.allowedVocabulary() )
+        self.failUnless( sDoc.enforceVocabulary() )
+        self.assertEqual( len( sDoc.allowedVocabulary() ), 2 )
+        self.failUnless( 'foo' in sDoc.allowedVocabulary() )
+        self.failUnless( 'bar' in sDoc.allowedVocabulary() )
 
         sSpec.addPolicy( 'Document' )
-        assert len( sSpec.listPolicies() ) == 2
+        self.assertEqual( len( sSpec.listPolicies() ), 2 )
 
         sDoc  = sSpec.getPolicy( 'Document' )
         self.assertNotEqual(aq_base(sDoc), aq_base(sDef))
-        assert not sDoc.isRequired()
-        assert not sDoc.supplyDefault()
-        assert not sDoc.defaultValue()
-        assert not sDoc.enforceVocabulary()
-        assert not sDoc.allowedVocabulary()
+        self.failIf( sDoc.isRequired() )
+        self.failIf( sDoc.supplyDefault() )
+        self.failIf( sDoc.defaultValue() )
+        self.failIf( sDoc.enforceVocabulary() )
+        self.failIf( sDoc.allowedVocabulary() )
 
         sSpec.removePolicy( 'Document' )
         sDoc  = sSpec.getPolicy( 'Document' )
         self.assertEqual(aq_base(sDoc), aq_base(sDef))
-        assert sDoc.isRequired()
-        assert sDoc.supplyDefault()
-        assert sDoc.defaultValue() == 'xyz'
-        assert sDoc.enforceVocabulary()
-        assert len( sDoc.allowedVocabulary() ) == 2
-        assert 'foo' in sDoc.allowedVocabulary()
-        assert 'bar' in sDoc.allowedVocabulary()
+        self.failUnless( sDoc.isRequired() )
+        self.failUnless( sDoc.supplyDefault() )
+        self.assertEqual( sDoc.defaultValue(), 'xyz' )
+        self.failUnless( sDoc.enforceVocabulary() )
+        self.assertEqual( len( sDoc.allowedVocabulary() ), 2 )
+        self.failUnless( 'foo' in sDoc.allowedVocabulary() )
+        self.failUnless( 'bar' in sDoc.allowedVocabulary() )
 
     def test_vocabularies( self ):
-        fSpec   = self.tool.getElementSpec( 'Format' )
+        tool = self._makeOne()
+        dcmi = tool.DCMI
+        fSpec   = dcmi.getElementSpec( 'Format' )
         fDef    = fSpec.getPolicy()
         formats = ( 'text/plain', 'text/html' )
         fDef.edit( 0, 0, '', 0, ( 'text/plain', 'text/html' ) )
-        assert self.tool.listAllowedFormats() == formats
+        self.assertEqual( tool.listAllowedFormats(), formats )
 
-        foo = Foo()
-        assert self.tool.listAllowedFormats( foo ) == formats
+        foo, bar = self._makeTestObjects()
+
+        self.assertEqual( tool.listAllowedFormats( foo ), formats )
+
         fSpec.addPolicy( 'Foo' )
-        assert not self.tool.listAllowedFormats( foo )
+        self.failIf( tool.listAllowedFormats( foo ) )
+
         foo_formats = ( 'image/jpeg', 'image/gif', 'image/png' )
         fFoo        = fSpec.getPolicy( 'Foo' )
         fFoo.edit( 0, 0, '', 0, foo_formats )
-        assert self.tool.listAllowedFormats( foo ) == foo_formats
+        self.assertEqual( tool.listAllowedFormats( foo ), foo_formats )
 
-    def test_initialValues( self ):
-        foo = Foo()
-        assert not foo.Title()
-        assert not foo.Description()
-        assert not foo.Subject()
-        assert not foo.Format(), foo.Format()
-        assert not foo.Language()
-        assert not foo.Rights()
+    def test_initialValues_defaults( self ):
+        tool = self._makeOne()
+        foo, bar = self._makeTestObjects()
+        self.failIf( foo.Title() )
+        self.failIf( foo.Description() )
+        self.failIf( foo.Subject() )
+        self.failIf( foo.Format(), foo.Format() )
+        self.failIf( foo.Language() )
+        self.failIf( foo.Rights() )
 
-        self.tool.setInitialMetadata( foo )
-        assert not foo.Title()
-        assert not foo.Description()
-        assert not foo.Subject()
-        assert not foo.Format()
-        assert not foo.Language()
-        assert not foo.Rights()
+        tool.setInitialMetadata( foo )
+        self.failIf( foo.Title() )
+        self.failIf( foo.Description() )
+        self.failIf( foo.Subject() )
+        self.failIf( foo.Format() )
+        self.failIf( foo.Language() )
+        self.failIf( foo.Rights() )
 
+    def test_initialValues_implicit( self ):
         # Test default policy.
-        foo     = Foo()
-        fSpec   = self.tool.getElementSpec( 'Format' )
+        tool = self._makeOne()
+        dcmi = tool.DCMI
+        foo, bar = self._makeTestObjects()
+        fSpec   = dcmi.getElementSpec( 'Format' )
         fPolicy = fSpec.getPolicy()
         fPolicy.edit( 0, 1, 'text/plain', 0, () )
-        self.tool.setInitialMetadata( foo )
-        assert not foo.Title()
-        assert not foo.Description()
-        assert not foo.Subject()
-        assert foo.Format() == 'text/plain'
-        assert not foo.Language()
-        assert not foo.Rights()
+        tool.setInitialMetadata( foo )
+        self.failIf( foo.Title() )
+        self.failIf( foo.Description() )
+        self.failIf( foo.Subject() )
+        self.assertEqual( foo.Format(), 'text/plain' )
+        self.failIf( foo.Language() )
+        self.failIf( foo.Rights() )
 
+    def test_initialValues_explicit_raises_if_constraint_fails( self ):
+        from Products.CMFDefault.exceptions import MetadataError
+
         # Test type-specific policy.
-        foo     = Foo()
-        tSpec   = self.tool.getElementSpec( 'Title' )
+        tool = self._makeOne()
+        dcmi = tool.DCMI
+        foo, bar = self._makeTestObjects()
+        tSpec   = dcmi.getElementSpec( 'Title' )
         tSpec.addPolicy( 'Foo' )
         tPolicy = tSpec.getPolicy( foo.getPortalTypeName() )
         tPolicy.edit( 1, 0, '', 0, () )
 
-        try:
-            self.tool.setInitialMetadata( foo )
-        except MetadataError:
-            pass
-        else:
-            assert 0, "Expected MetadataError"
+        self.assertRaises( MetadataError, tool.setInitialMetadata, foo )
 
+    def test_initialValues_explicit_mutliple_types( self ):
+        from Products.CMFDefault.exceptions import MetadataError
+
+        tool = self._makeOne()
+        dcmi = tool.DCMI
+        foo, bar = self._makeTestObjects()
         foo.setTitle( 'Foo title' )
-        self.tool.setInitialMetadata( foo )
-        assert foo.Title() == 'Foo title'
-        assert not foo.Description()
-        assert not foo.Subject()
-        assert foo.Format() == 'text/plain'
-        assert not foo.Language()
-        assert not foo.Rights()
 
+        fSpec   = dcmi.getElementSpec( 'Format' )
+        fSpec.addPolicy( foo.getPortalTypeName() )
+        fPolicy = fSpec.getPolicy( foo.getPortalTypeName() )
+        fPolicy.edit( 0, 1, 'text/plain', 0, () )
+
+        tool.setInitialMetadata( foo )
+        self.assertEqual( foo.Title(), 'Foo title' )
+        self.failIf( foo.Description() )
+        self.failIf( foo.Subject() )
+        self.assertEqual( foo.Format(), 'text/plain' )
+        self.failIf( foo.Language() )
+        self.failIf( foo.Rights() )
+
         #   Ensure Foo's policy doesn't interfere with other types.
-        bar = Bar()
-        self.tool.setInitialMetadata( bar )
-        assert not bar.Title()
-        assert not bar.Description()
-        assert not bar.Subject()
-        assert bar.Format() == 'text/plain'
-        assert not bar.Language()
-        assert not bar.Rights()
+        tool.setInitialMetadata( bar )
+        self.failIf( bar.Title() )
+        self.failIf( bar.Description() )
+        self.failIf( bar.Subject() )
+        self.assertEqual( bar.Format(), '' )
+        self.failIf( bar.Language() )
+        self.failIf( bar.Rights() )
 
     def test_validation( self ):
+        from Products.CMFDefault.exceptions import MetadataError
 
-        foo = Foo()
-        self.tool.setInitialMetadata( foo )
-        self.tool.validateMetadata( foo )
+        tool = self._makeOne()
+        foo, bar = self._makeTestObjects()
+        tool.setInitialMetadata( foo )
+        tool.validateMetadata( foo )
 
-        tSpec   = self.tool.getElementSpec( 'Title' )
+        dcmi = tool.DCMI
+        tSpec   = dcmi.getElementSpec( 'Title' )
         tSpec.addPolicy( 'Foo' )
         tPolicy = tSpec.getPolicy( foo.getPortalTypeName() )
         tPolicy.edit( 1, 0, '', 0, () )
 
-        try:
-            self.tool.validateMetadata( foo )
-        except MetadataError:
-            pass
-        else:
-            assert 0, "Expected MetadataError"
+        self.assertRaises( MetadataError, tool.validateMetadata, foo )
 
         foo.setTitle( 'Foo title' )
-        self.tool.validateMetadata( foo )
+        tool.validateMetadata( foo )
 
+    def test_addSchema_normal( self ):
+        from Products.CMFDefault.MetadataTool import MetadataSchema
 
+        tool = self._makeOne()
+        before = tool.listSchemas()
+        self.assertEqual( len( before ), 1 )
+        self.assertEqual( before[0][0], 'DCMI' )
+        self.failUnless( isinstance( before[0][1], MetadataSchema ) )
+
+        tool.addSchema( 'Arbitrary' )
+
+        after = tool.listSchemas()
+        self.assertEqual( len( after ), 2 )
+        self.assertEqual( after[0][0], 'DCMI' )
+        self.failUnless( isinstance( after[0][1], MetadataSchema ) )
+        self.assertEqual( after[1][0], 'Arbitrary' )
+        self.failUnless( isinstance( after[1][1], MetadataSchema ) )
+
+    def test_addSchema_duplicate( self ):
+        tool = self._makeOne()
+        before = tool.listSchemas()
+        tool.addSchema( 'Arbitrary' )
+        self.assertRaises( KeyError, tool.addSchema, 'Arbitrary' )
+        self.assertRaises( KeyError, tool.addSchema, 'DCMI' )
+
+    def test_removeSchema_normal( self ):
+        tool = self._makeOne()
+        before = tool.listSchemas()
+        self.assertEqual( len( before ), 1 )
+        self.assertEqual( before[0][0], 'DCMI' )
+
+        tool.addSchema( 'Arbitrary' )
+        tool.addSchema( 'Beneficent' )
+        tool.addSchema( 'Grouchy' )
+
+        middle = tool.listSchemas()
+        self.assertEqual( len( middle ), 4 )
+        self.assertEqual( middle[0][0], 'DCMI' )
+        self.assertEqual( middle[1][0], 'Arbitrary' )
+        self.assertEqual( middle[2][0], 'Beneficent' )
+        self.assertEqual( middle[3][0], 'Grouchy' )
+
+        tool.removeSchema( 'Beneficent' )
+
+        after = tool.listSchemas()
+        self.assertEqual( len( after ), 3 )
+        self.assertEqual( after[0][0], 'DCMI' )
+        self.assertEqual( after[1][0], 'Arbitrary' )
+        self.assertEqual( after[2][0], 'Grouchy' )
+
+    def test_removeSchema_invalid( self ):
+        tool = self._makeOne()
+        self.assertRaises( KeyError, tool.removeSchema, 'DCMI' )
+        tool.addSchema( 'Arbitrary' )
+        tool.removeSchema( 'Arbitrary' )
+        self.assertRaises( KeyError, tool.removeSchema, 'Arbitrary' )
+
+    def test_migration( self ):
+        # Test that we forward-migrate old-style DCMI policies.
+        from Products.CMFDefault.MetadataTool import ElementSpec
+        from Products.CMFDefault.MetadataTool import _DCMI_ELEMENT_SPECS
+
+        tool = self._makeOne()
+        tool.element_specs = { 'Title' : ElementSpec( 0 )
+                             , 'Description' : ElementSpec( 0 )
+                             , 'Subject' : ElementSpec( 1 )
+                             , 'Format' : ElementSpec( 0 )
+                             , 'Language' : ElementSpec( 0 )
+                             , 'Rights' : ElementSpec( 0 )
+                             }
+        subj = tool.element_specs[ 'Subject' ]
+        subj.addPolicy( 'Foo' )
+        subj.getPolicy( 'Foo' ).edit( False
+                                    , False
+                                    , None
+                                    , True
+                                    , ( 'bar', 'baz' )
+                                    )
+
+        dcmi = tool.DCMI
+
+        self.assertEqual( dcmi.getId(), 'DCMI' )
+
+        # Accessing the DCMI property converts and clears 'element_specs'
+        self.assertRaises(AttributeError, lambda: tool.element_specs )
+
+        subj2 = dcmi.getElementSpec( 'Subject' )
+        subj_default = subj2.getPolicy( None )
+        subj_foo = subj2.getPolicy( 'Foo' )
+
+        self.assertEqual( subj_foo.isRequired(), False )
+        self.assertEqual( subj_foo.supplyDefault(), False )
+        self.assertEqual( subj_foo.defaultValue(), None )
+        self.assertEqual( subj_foo.enforceVocabulary(), True )
+        self.assertEqual( len( subj_foo.allowedVocabulary() ), 2 )
+        self.failUnless( 'bar' in subj_foo.allowedVocabulary() )
+        self.failUnless( 'baz' in subj_foo.allowedVocabulary() )
+
+
 def test_suite():
-    return TestSuite((
-        makeSuite(TestMetadataElementPolicy),
-        makeSuite(TestElementSpec),
-        makeSuite(TestMetadataTool),
+    return unittest.TestSuite((
+        unittest.makeSuite(TestMetadataElementPolicy),
+        unittest.makeSuite(TestElementSpec),
+        unittest.makeSuite(TestMetadataTool),
         ))
 
 if __name__ == '__main__':
-    main(defaultTest='test_suite')
+    unittest.main(defaultTest='test_suite')



More information about the CMF-checkins mailing list