[Zope3-checkins] SVN: Zope3/trunk/src/zope/app/apidoc/ New revised preference system. I still need to move it to another

Stephan Richter srichter at cosmos.phy.tufts.edu
Fri Apr 1 11:08:47 EST 2005


Log message for revision 29783:
  New revised preference system. I still need to move it to another 
  location, so I write a detailed message later.
  

Changed:
  U   Zope3/trunk/src/zope/app/apidoc/bookmodule/book.zcml
  U   Zope3/trunk/src/zope/app/apidoc/browser/modules.pt
  U   Zope3/trunk/src/zope/app/apidoc/configure.zcml
  U   Zope3/trunk/src/zope/app/apidoc/ifacemodule/configure.zcml
  U   Zope3/trunk/src/zope/app/apidoc/ifacemodule/index.pt
  U   Zope3/trunk/src/zope/app/apidoc/preference/README.txt
  U   Zope3/trunk/src/zope/app/apidoc/preference/browser.py
  U   Zope3/trunk/src/zope/app/apidoc/preference/configure.zcml
  A   Zope3/trunk/src/zope/app/apidoc/preference/default.py
  D   Zope3/trunk/src/zope/app/apidoc/preference/edit.pt
  A   Zope3/trunk/src/zope/app/apidoc/preference/index.pt
  U   Zope3/trunk/src/zope/app/apidoc/preference/interfaces.py
  A   Zope3/trunk/src/zope/app/apidoc/preference/macros.pt
  U   Zope3/trunk/src/zope/app/apidoc/preference/meta.zcml
  U   Zope3/trunk/src/zope/app/apidoc/preference/metaconfigure.py
  U   Zope3/trunk/src/zope/app/apidoc/preference/metadirectives.py
  U   Zope3/trunk/src/zope/app/apidoc/preference/preference.py
  A   Zope3/trunk/src/zope/app/apidoc/preference/subgroup.pt
  U   Zope3/trunk/src/zope/app/apidoc/preference/tests.py

-=-
Modified: Zope3/trunk/src/zope/app/apidoc/bookmodule/book.zcml
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/bookmodule/book.zcml	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/bookmodule/book.zcml	2005-04-01 16:08:47 UTC (rev 29783)
@@ -210,7 +210,7 @@
   <configure package="zope.testing">
     <bookchapter
         id="formparser"
-        title="Form Parse"
+        title="Form Parser"
         doc_path="formparser.txt"
         parent="test"
         />

Modified: Zope3/trunk/src/zope/app/apidoc/browser/modules.pt
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/browser/modules.pt	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/browser/modules.pt	2005-04-01 16:08:47 UTC (rev 29783)
@@ -18,9 +18,11 @@
       </li>
     </ul>
     <div id="preference_entry">
+<!--
       <a href="./++preferences++/@@menu.html" target="menu">
         User Preferences
       </a>
+-->
     </div>
   </div>
 

Modified: Zope3/trunk/src/zope/app/apidoc/configure.zcml
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/configure.zcml	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/configure.zcml	2005-04-01 16:08:47 UTC (rev 29783)
@@ -66,6 +66,15 @@
   <include package=".browser" />
   <include package=".preference" />
 
+  <apidoc:preferenceGroup
+      id="apidoc"
+      title="API Doc Tool" 
+      description="
+         These are all the preferences related to viewing the API
+         documtation."
+      category="True"
+      />
+
   <!-- API Documentation Modules -->
   <include package=".bookmodule" />
   <include package=".codemodule" />

Modified: Zope3/trunk/src/zope/app/apidoc/ifacemodule/configure.zcml
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/ifacemodule/configure.zcml	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/ifacemodule/configure.zcml	2005-04-01 16:08:47 UTC (rev 29783)
@@ -67,8 +67,8 @@
       template="menu.pt"
       />
 
-  <apidoc:preferencesGroup
-      name="InterfaceDetails"
+  <apidoc:preferenceGroup
+      id="apidoc.InterfaceDetails"
       schema=".interfaces.IInterfaceDetailsPreferences"
       title="Interface Details" 
       />

Modified: Zope3/trunk/src/zope/app/apidoc/ifacemodule/index.pt
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/ifacemodule/index.pt	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/ifacemodule/index.pt	2005-04-01 16:08:47 UTC (rev 29783)
@@ -69,7 +69,7 @@
     
       <div tal:define="
   elementId string:adapters.required.specific;
-  show context/++preferences++InterfaceDetails/showSpecificRequiredAdapters">
+  show context/++preferences++apidoc/InterfaceDetails/showSpecificRequiredAdapters">
 
         <h4 i18n:translate="">
           <metal:block use-macro="context/@@apidoc_macros/displaySwitch" />
@@ -93,7 +93,7 @@
     
       <div tal:define="
   elementId string:adapters.required.extended;
-  show context/++preferences++InterfaceDetails/showExtendedRequiredAdapters">
+  show context/++preferences++apidoc/InterfaceDetails/showExtendedRequiredAdapters">
 
         <h4>
           <metal:block use-macro="context/@@apidoc_macros/displaySwitch" />
@@ -117,7 +117,7 @@
 
       <div tal:define="
   elementId string:adapters.required.generic;
-  show context/++preferences++InterfaceDetails/showGenericRequiredAdapters">
+  show context/++preferences++apidoc/InterfaceDetails/showGenericRequiredAdapters">
 
         <h4>
           <metal:block use-macro="context/@@apidoc_macros/displaySwitch" />
@@ -169,17 +169,18 @@
     <div class="indent">
 
       <tal:block define="
-          type string:Browser;
-          specific_views view/specificBrowserViews;
-          extended_views view/extendedBrowserViews;
-          generic_views view/genericBrowserViews;
-          show context/++preferences++InterfaceDetails/showBrowserViews;
-          show_specific 
-            context/++preferences++InterfaceDetails/showSpecificBrowserViews;
-          show_extended
-            context/++preferences++InterfaceDetails/showExtendedBrowserViews;
-          show_generic
-            context/++preferences++InterfaceDetails/showGenericBrowserViews;
+        type string:Browser;
+        specific_views view/specificBrowserViews;
+        extended_views view/extendedBrowserViews;
+        generic_views view/genericBrowserViews;
+        show 
+        context/++preferences++apidoc/InterfaceDetails/showBrowserViews;
+        show_specific 
+        context/++preferences++apidoc/InterfaceDetails/showSpecificBrowserViews;
+        show_extended
+        context/++preferences++apidoc/InterfaceDetails/showExtendedBrowserViews;
+        show_generic
+        context/++preferences++apidoc/InterfaceDetails/showGenericBrowserViews;
           ">
 
         <metal:block use-macro="context/@@interface_macros/viewtype" />
@@ -187,18 +188,19 @@
       </tal:block>
 
       <tal:block define="
-          type string:XML-RPC;
-          specific_views view/specificXMLRPCViews;
-          extended_views view/extendedXMLRPCViews;
-          generic_views view/genericXMLRPCViews;
-          show context/++preferences++InterfaceDetails/showXMLRPCViews;
-          show_specific 
-            context/++preferences++InterfaceDetails/showSpecificXMLRPCViews;
-          show_extended
-            context/++preferences++InterfaceDetails/showExtendedXMLRPCViews;
-          show_generic
-            context/++preferences++InterfaceDetails/showGenericXMLRPCViews;
-          ">
+        type string:XML-RPC;
+        specific_views view/specificXMLRPCViews;
+        extended_views view/extendedXMLRPCViews;
+        generic_views view/genericXMLRPCViews;
+        show 
+        context/++preferences++apidoc/InterfaceDetails/showXMLRPCViews;
+        show_specific 
+        context/++preferences++apidoc/InterfaceDetails/showSpecificXMLRPCViews;
+        show_extended
+        context/++preferences++apidoc/InterfaceDetails/showExtendedXMLRPCViews;
+        show_generic
+        context/++preferences++apidoc/InterfaceDetails/showGenericXMLRPCViews;
+        ">
 
         <metal:block use-macro="context/@@interface_macros/viewtype" />
 
@@ -209,13 +211,14 @@
           specific_views view/specificHTTPViews;
           extended_views view/extendedHTTPViews;
           generic_views view/genericHTTPViews;
-          show context/++preferences++InterfaceDetails/showHTTPViews;
+          show 
+          context/++preferences++apidoc/InterfaceDetails/showHTTPViews;
           show_specific 
-            context/++preferences++InterfaceDetails/showSpecificHTTPViews;
+          context/++preferences++apidoc/InterfaceDetails/showSpecificHTTPViews;
           show_extended
-            context/++preferences++InterfaceDetails/showExtendedHTTPViews;
+          context/++preferences++apidoc/InterfaceDetails/showExtendedHTTPViews;
           show_generic
-            context/++preferences++InterfaceDetails/showGenericHTTPViews;
+          context/++preferences++apidoc/InterfaceDetails/showGenericHTTPViews;
           ">
 
         <metal:block use-macro="context/@@interface_macros/viewtype" />
@@ -227,13 +230,14 @@
           specific_views view/specificFTPViews;
           extended_views view/extendedFTPViews;
           generic_views view/genericFTPViews;
-          show context/++preferences++InterfaceDetails/showFTPViews;
+          show 
+          context/++preferences++apidoc/InterfaceDetails/showFTPViews;
           show_specific 
-            context/++preferences++InterfaceDetails/showSpecificFTPViews;
+          context/++preferences++apidoc/InterfaceDetails/showSpecificFTPViews;
           show_extended
-            context/++preferences++InterfaceDetails/showExtendedFTPViews;
+          context/++preferences++apidoc/InterfaceDetails/showExtendedFTPViews;
           show_generic
-            context/++preferences++InterfaceDetails/showGenericFTPViews;
+          context/++preferences++apidoc/InterfaceDetails/showGenericFTPViews;
           ">
 
         <metal:block use-macro="context/@@interface_macros/viewtype" />
@@ -245,13 +249,14 @@
           specific_views view/specificOtherViews;
           extended_views view/extendedOtherViews;
           generic_views view/genericOtherViews;
-          show context/++preferences++InterfaceDetails/showOtherViews;
+          show 
+          context/++preferences++apidoc/InterfaceDetails/showOtherViews;
           show_specific 
-            context/++preferences++InterfaceDetails/showSpecificOtherViews;
+          context/++preferences++apidoc/InterfaceDetails/showSpecificOtherViews;
           show_extended
-            context/++preferences++InterfaceDetails/showExtendedOtherViews;
+          context/++preferences++apidoc/InterfaceDetails/showExtendedOtherViews;
           show_generic
-            context/++preferences++InterfaceDetails/showGenericOtherViews;
+          context/++preferences++apidoc/InterfaceDetails/showGenericOtherViews;
           ">
 
         <metal:block use-macro="context/@@interface_macros/viewtype" />

Modified: Zope3/trunk/src/zope/app/apidoc/preference/README.txt
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/README.txt	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/README.txt	2005-04-01 16:08:47 UTC (rev 29783)
@@ -12,7 +12,7 @@
 schemas to categorize and describe the preferences.
 
 
-Preferences Groups
+Preference Groups
 ------------------
 
 Preferences are grouped in preference groups and the preferences inside a
@@ -39,30 +39,31 @@
   ...                     u"at the top of the screen.",
   ...         default=True)
 
-Now we can instantiate the preference group. Each preference group must have a
-name by which it can be accessed and has an optional title field for UI
-purposes:
+Now we can instantiate the preference group. Each preference group must have
+an id by which it can be accessed and has an optional title and description
+field for UI purposes:
 
-  >>> settings = preference.PreferencesGroup(
-  ...     name="ZMISettings",
+  >>> settings = preference.PreferenceGroup(
+  ...     "ZMISettings",
   ...     schema=IZMIUserSettings,
-  ...     title=u"ZMI User Settings")
+  ...     title=u"ZMI User Settings",
+  ...     description=u"")
 
 Note that the preferences group provides the interface it is representing:
 
   >>> IZMIUserSettings.providedBy(settings)
   True
 
-and the name, schema and title of the group are directly available:
+and the id, schema and title of the group are directly available:
 
-  >>> settings.name
+  >>> settings.id
   'ZMISettings'
   >>> settings.schema
-  <InterfaceClass __builtin__.IZMIUserSettings>
+  <InterfaceClass zope.app.apidoc.preference.README.IZMIUserSettings>
   >>> settings.title
   u'ZMI User Settings'
 
-So let's ask the group for the skin setting:
+So let's ask the preference group for the `skin` setting:
 
   >>> settings.skin #doctest:+ELLIPSIS
   Traceback (most recent call last):
@@ -70,6 +71,7 @@
   ComponentLookupError: 
   (<InterfaceClass ...interfaces.IPrincipalAnnotationUtility>, '')
 
+
 So why did the lookup fail? Because we have not specified a principal yet, for
 which we want to lookup the preferences. To do that, we have to create a new
 interaction:
@@ -125,40 +127,269 @@
   ConstraintNotSatisfied: MySkin  
 
 
-User Preferences
-----------------
+Preference Group Trees
+----------------------
 
-The various preferences groups are collectively available via the user
-preferences object:
+The preferences would not be very powerful, if you could create a full
+preferences. So let's create a sub-group for our ZMI user settings, where we
+can adjust the look and feel of the folder contents view:
 
-  >>> prefs = preference.UserPreferences()
+  >>> import sets
+  >>> class IFolderSettings(zope.interface.Interface):
+  ...     """Basic User Preferences"""
+  ...
+  ...     shownFields = zope.schema.Set(
+  ...         title=u"Shown Fields",
+  ...         description=u"Fields shown in the table.",
+  ...         value_type=zope.schema.Choice(['name', 'size', 'creator']),
+  ...         default=sets.Set(['name', 'size']))
+  ...
+  ...     sortedBy = zope.schema.Choice(
+  ...         title=u"Sorted By",
+  ...         description=u"Data field to sort by.",
+  ...         values=['name', 'size', 'creator'],
+  ...         default='name')
 
-Using this objcet, you can access a list of all available groups
+  >>> folderSettings = preference.PreferenceGroup(
+  ...     "ZMISettings.Folder",
+  ...     schema=IFolderSettings,
+  ...     title=u"Folder Content View Settings")
 
-  >>> prefs.items()
-  []
+Note that the id was chosen so that the parent id is the prefix of the child's
+id. Our new preference sub-group should now be available as an attribute or an
+item on the parent group ...
 
-But why did our new ZMI user settings group not appear? This is because we
-have to register it first as a preferences group:
+  >>> settings.Folder
+  Traceback (most recent call last):
+  ...
+  AttributeError: 'Folder' is not a preference or sub-group.
 
-  >>> from zope.app.apidoc.preference.interfaces import IPreferencesGroup
-  >>> ztapi.provideUtility(IPreferencesGroup, settings, settings.name)
+... but not before we register the groups as utilities:
 
-Note that the name of the utility and the name saved in the group must be the
-same. Now let's try again:
+  >>> from zope.app.apidoc.preference import interfaces
+  >>> from zope.app.testing import ztapi
 
-  >>> prefs.items() #doctest:+ELLIPSIS
-  [(u'ZMISettings', 
-    <zope.app.apidoc.preference.preference.PreferencesGroup object at ...>)]
+  >>> ztapi.provideUtility(interfaces.IPreferenceGroup, settings,
+  ...                      name='ZMISettings')
+  >>> ztapi.provideUtility(interfaces.IPreferenceGroup, folderSettings,
+  ...                      name='ZMISettings.Folder')
 
-You can also just access one group at a time:
+If we now try to lookup the sub-group again, we should be successfull:
 
-  >>> prefs['ZMISettings'] #doctest:+ELLIPSIS
-  <zope.app.apidoc.preference.preference.PreferencesGroup object at ...>
+  >>> settings.Folder #doctest:+ELLIPSIS
+  <zope.app.apidoc.preference.preference.PreferenceGroup object at ...>
 
-The entire `IReadContainer` interface is available.
+  >>> settings['Folder'] #doctest:+ELLIPSIS
+  <zope.app.apidoc.preference.preference.PreferenceGroup object at ...>
 
+While the registry of the preference groups is flat, the careful naming of the
+ids allows us to have a tree of preferences. Note that this pattern is very
+similar to the way modules are handled in Python; they are stored in a flat
+dictionary in ``sys.modules``, but due to the naming they appear to be in a
+namespace tree.
 
+While we are at it, there are also preference categories that can be compared
+to Python packages. They basically are just a higher level grouping concept
+that is used by the UI to better organize the preferences. A preference group
+can be converted to a category by simply providing an additional interface:
+
+  >>> zope.interface.alsoProvides(settings, interfaces.IPreferenceCategory)
+
+  >>> interfaces.IPreferenceCategory.providedBy(settings)
+  True
+
+
+Default Preferences
+-------------------
+
+It sometimes desirable to define default settings on a site-by-site basis,
+instead of just using the default value from the schema. The preferences
+package provides a module
+ 
+  >>> from zope.app.apidoc.preference import default
+
+that implements a default preferences provider that can be added as a unnamed
+utility for each site. So the first step is to create a site:
+
+  >>> from zope.app.testing import setup
+
+  >>> import zope.app.component.hooks
+  >>> zope.app.component.hooks.setHooks()
+  >>> setup.setUpTraversal()
+  >>> setup.setUpSiteManagerLookup()
+  
+  >>> root = setup.buildSampleFolderTree()
+  >>> rsm = setup.createSiteManager(root, True)
+
+Now we can register the default preference provider with the root site:
+
+  >>> provider = setup.addUtility(rsm, '', 
+  ...                             interfaces.IDefaultPreferenceProvider, 
+  ...                             default.DefaultPreferenceProvider())
+
+So before we set an explicit default value for a preference, the schema field
+default is used:
+
+  >>> settings.Folder.sortedBy
+  'name'
+
+But if we now set a new default value with the provider,
+
+  >>> defaultFolder = provider.getDefaultPreferenceGroup('ZMISettings.Folder')
+  >>> defaultFolder.sortedBy = 'size'
+
+then the default of the setting changes:
+
+  >>> settings.Folder.sortedBy
+  'size'
+
+The default preference providers also implictly acquire default values from
+parent sites. So if we make `folder1` a site and set it as the active site
+
+  >>> folder1 = root['folder1']
+  >>> sm1 = setup.createSiteManager(folder1, True)
+
+and add a default provider there,
+
+  >>> provider1 = setup.addUtility(sm1, '', 
+  ...                              interfaces.IDefaultPreferenceProvider, 
+  ...                              default.DefaultPreferenceProvider())
+
+then we still get the root's default values, because we have not defined any
+in the higher default provider:
+
+  >>> settings.Folder.sortedBy
+  'size'
+
+But if we provide the new provider with a default value for `sortedBy`,
+
+  >>> defaultFolder1 = provider1.getDefaultPreferenceGroup('ZMISettings.Folder')
+  >>> defaultFolder1.sortedBy = 'creator'
+
+then it is used instead:
+
+  >>> settings.Folder.sortedBy
+  'creator'
+
+Of course, once the root site becomes our active site again
+
+  >>> zope.app.component.hooks.setSite(root)
+
+the default value of the root provider is used:
+
+  >>> settings.Folder.sortedBy
+  'size'
+
+Of course, all the defaults in the world are not relevant anymore as soon as
+the user actually provides a value:
+
+  >>> settings.Folder.sortedBy = 'name'
+  >>> settings.Folder.sortedBy
+  'name'
+
+Oh, and have I mentioned that entered values are always validated? So you
+cannot just assign any old value:
+
+  >>> settings.Folder.sortedBy = 'foo'
+  Traceback (most recent call last):
+  ...
+  ConstraintNotSatisfied: foo
+
+Finally, if the user deletes his/her explicit setting, we are back to the
+default value:
+
+  >>> del settings.Folder.sortedBy
+  >>> settings.Folder.sortedBy
+  'size'
+
+
+Creating Preference Groups Using ZCML
+-------------------------------------
+
+If you are using the user preference system in Zope 3, you will not have to
+manually setup the preference groups as we did above (of course). We will use
+ZCML instead. First, we need to register the directives:
+
+  >>> from zope.configuration import xmlconfig
+  >>> import zope.app.apidoc.preference
+  >>> context = xmlconfig.file('meta.zcml', zope.app.apidoc.preference)
+
+Then the system sets up a root preference group:
+
+  >>> context = xmlconfig.string('''
+  ...     <configure
+  ...         xmlns="http://namespaces.zope.org/apidoc"
+  ...         i18n_domain="test">
+  ...
+  ...       <preferenceGroup
+  ...           id=""
+  ...           title="User Preferences" 
+  ...           />
+  ...
+  ...     </configure>''', context)
+
+Now we can use the preference system in its intended way. We access the folder
+settings as follows:
+
+  >>> from zope.app import zapi
+  >>> prefs = zapi.getUtility(interfaces.IPreferenceGroup)
+  >>> prefs.ZMISettings.Folder.sortedBy
+  'size'
+
+Let's register the ZMI settings again under a new name via ZCML:
+
+  >>> context = xmlconfig.string('''
+  ...     <configure
+  ...         xmlns="http://namespaces.zope.org/apidoc"
+  ...         i18n_domain="test">
+  ...
+  ...       <preferenceGroup
+  ...           id="ZMISettings2"
+  ...           title="ZMI Settings NG"
+  ...           schema="zope.app.apidoc.preference.README.IZMIUserSettings"
+  ...           category="true"
+  ...           />
+  ...
+  ...     </configure>''', context)
+
+  >>> prefs.ZMISettings2 #doctest:+ELLIPSIS
+  <zope.app.apidoc.preference.preference.PreferenceGroup object at ...>
+
+  >>> prefs.ZMISettings2.title
+  u'ZMI Settings NG'
+
+  >>> IZMIUserSettings.providedBy(prefs.ZMISettings2)
+  True
+  >>> interfaces.IPreferenceCategory.providedBy(prefs.ZMISettings2)
+  True
+
+And the tree can built again by carefully cosntructing the id:
+
+  >>> context = xmlconfig.string('''
+  ...     <configure
+  ...         xmlns="http://namespaces.zope.org/apidoc"
+  ...         i18n_domain="test">
+  ...
+  ...       <preferenceGroup
+  ...           id="ZMISettings2.Folder"
+  ...           title="Folder Settings"
+  ...           schema="zope.app.apidoc.preference.README.IFolderSettings"
+  ...           />
+  ...
+  ...     </configure>''', context)
+
+  >>> prefs.ZMISettings2 #doctest:+ELLIPSIS
+  <zope.app.apidoc.preference.preference.PreferenceGroup object at ...>
+
+  >>> prefs.ZMISettings2.Folder.title
+  u'Folder Settings'
+
+  >>> IFolderSettings.providedBy(prefs.ZMISettings2.Folder)
+  True
+  >>> interfaces.IPreferenceCategory.providedBy(prefs.ZMISettings2.Folder)
+  False
+
+
 Traversal
 ---------
 
@@ -169,9 +400,6 @@
 register all necessary traversal components and the special `preferences`
 namespace:
 
-  >>> from zope.app.testing import setup
-  >>> setup.setUpTraversal()
-
   >>> import zope.app.traversing.interfaces
   >>> ztapi.provideAdapter(None,
   ...                      zope.app.traversing.interfaces.ITraversable,
@@ -180,8 +408,6 @@
 
 We can now access the preferences as follows:
 
-  >>> from zope.app import zapi
-
   >>> zapi.traverse(None, '++preferences++ZMISettings/skin')
   'Basic'
   >>> zapi.traverse(None, '++preferences++/ZMISettings/skin')
@@ -200,18 +426,18 @@
 Let's create a checker using the function that the security machinery is
 actually using:
 
-  >>> checker = preference.PreferencesGroupChecker(settings)
+  >>> checker = preference.PreferenceGroupChecker(settings)
   >>> checker.permission_id('skin')
   Global(CheckerPublic,zope.security.checker)
   >>> checker.setattr_permission_id('skin')
   Global(CheckerPublic,zope.security.checker)
 
-The name, title and schema are publically available for access, but are not
-available for mutation at all:
+The id, title, description, and schema are publically available for access,
+but are not available for mutation at all:
 
-  >>> checker.permission_id('name')
+  >>> checker.permission_id('id')
   Global(CheckerPublic,zope.security.checker)
-  >>> checker.setattr_permission_id('name') is None
+  >>> checker.setattr_permission_id('id') is None
   True
 
 

Modified: Zope3/trunk/src/zope/app/apidoc/preference/browser.py
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/browser.py	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/browser.py	2005-04-01 16:08:47 UTC (rev 29783)
@@ -16,22 +16,78 @@
 $Id: menu.py 29269 2005-02-23 22:22:48Z srichter $
 """
 __docformat__ = 'restructuredtext'
+import re
+import zope.interface
+import zope.schema
 from zope.security.proxy import removeSecurityProxy
+
+from zope.app import zapi
+from zope.app.basicskin.standardmacros import StandardMacros
+from zope.app.container.interfaces import IObjectFindFilter
+from zope.app.form.browser.editview import EditView
 from zope.app.pagetemplate.simpleviewclass import simple
 from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
-from zope.app.form.browser.editview import EditView
+from zope.app.tree.browser.cookie import CookieTreeView
 
-from zope.app.apidoc import utilities
+from zope.app.apidoc.preference import interfaces
 
+NoneInterface = zope.interface.interface.InterfaceClass('None')
 
-class EditPreferencesGroup(EditView):
+class PreferencesMacros(StandardMacros):
+    """Page Template METAL macros for preferences"""
+    macro_pages = ('preference_macro_definitions',)
 
+
+class PreferenceGroupFilter(object):
+    """A special filter for """
+    zope.interface.implements(IObjectFindFilter)
+
+    def matches(self, obj):
+        """Decide whether the object is shown in the tree."""
+        if interfaces.IPreferenceCategory.providedBy(obj):
+            return True
+
+        if interfaces.IPreferenceGroup.providedBy(obj):
+            parent = zapi.getParent(obj)
+            if interfaces.IPreferenceCategory.providedBy(parent):
+                return True
+
+        return False
+        
+
+class PreferencesTree(CookieTreeView):
+    """Preferences Tree using the stateful cookie tree."""
+
+    def tree(self):
+        root = zapi.getRoot(self.context)
+        filter = PreferenceGroupFilter()
+        return self.cookieTree(root, filter)
+
+
+class EditPreferenceGroup(EditView):
+
     def __init__(self, context, request):
         self.__used_for__ = removeSecurityProxy(context.schema)
         self.schema = removeSecurityProxy(context.schema)
+
+        if self.schema is None:
+            self.schema = NoneInterface 
+            zope.interface.alsoProvides(removeSecurityProxy(context),
+                                        NoneInterface)
+            
         self.label = context.title + ' Preferences'
-        super(EditPreferencesGroup, self).__init__(context, request)
+        super(EditPreferenceGroup, self).__init__(context, request)
+        self.setPrefix(context.id)
 
     def getIntroduction(self):
-        return utilities.renderText(self.schema.__doc__,
-                                    self.schema.__module__)
+        # TODO: Remove dependency
+        from zope.app.apidoc import utilities
+        text = self.context.description or self.schema.__doc__
+
+        # Determine common whitespace ...
+        cols = len(re.match('^[ ]*', text).group())
+        # ... and clean it up.
+        text = re.sub('\n[ ]{%i}' %cols, '\n', text)
+
+        return utilities.renderText(text.strip(), self.schema.__module__)
+

Modified: Zope3/trunk/src/zope/app/apidoc/preference/configure.zcml
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/configure.zcml	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/configure.zcml	2005-04-01 16:08:47 UTC (rev 29783)
@@ -4,42 +4,108 @@
   xmlns:apidoc="http://namespaces.zope.org/apidoc"
   i18n_domain="zope">
 
-  <class class=".preference.UserPreferences">
-    <allow interface=".interfaces.IUserPreferences" 
-           attributes="__parent__ __name__" />
-  </class>
-
   <view
-      name="preferences" type="*"
-      provides="zope.app.traversing.interfaces.ITraversable" for="*"
+      name="preferences" 
+      for="*"
+      type="*"
+      provides="zope.app.traversing.interfaces.ITraversable"
       factory=".preference.preferencesNamespace"
       />
 
   <adapter
       name="preferences"
-      provides="zope.app.traversing.interfaces.ITraversable" for="*"
+      for="*"
+      provides="zope.app.traversing.interfaces.ITraversable"
       factory=".preference.preferencesNamespace"
       />
 
-  <!-- Browser Views -->
+  <!-- Root preference group -->
+  <apidoc:preferenceGroup
+      id=""
+      title="User Preferences" 
+      />
 
+
+  <!-- Preference Groups -->
   <browser:page
-      for=".interfaces.IUserPreferences"
+      name="index.html"
+      for=".interfaces.IPreferenceGroup"
+      class=".browser.EditPreferenceGroup"
+      template="index.pt"
       permission="zope.Public"
-      name="menu.html"
-      template="menu.pt"
       />
 
   <browser:page
-      name="edit.html"
-      for=".interfaces.IPreferencesGroup"
-      class=".browser.EditPreferencesGroup"
-      template="edit.pt"
+      name="editAsSubGroup"
+      for=".interfaces.IPreferenceGroup"
+      class=".browser.EditPreferenceGroup"
+      template="subgroup.pt"
       permission="zope.Public"
       />
 
-  <!-- Books Chapter -->
 
+  <!-- Default Preference Provider -->
+  <localUtility class=".default.DefaultPreferenceProvider">
+    <require
+        permission="zope.ManageSite"
+        interface=".interfaces.IDefaultPreferenceProvider"
+        />
+  </localUtility>
+
+  <view
+      name="preferences" 
+      for=".interfaces.IDefaultPreferenceProvider"
+      type="*"
+      provides="zope.interface.Interface"
+      factory=".default.DefaultPreferences"
+      />
+
+  <browser:addMenuItem
+       class=".default.DefaultPreferenceProvider"
+       title="Default User Preferences Provider"
+       description="A Default User Preferences Provider"
+       permission="zope.ManageSite"
+       />
+
+  <browser:tool
+      interface=".interfaces.IDefaultPreferenceProvider"
+      title="Default User Preferences Provider"
+      description="
+          This component lets you define the local default user
+          preferences. The values of this provider are used, if the
+          user has not made a selection yet."
+      unique="true"
+      />
+
+  <!-- Preferences-specific macros -->
+  <browser:page
+      for="*"
+      name="preferences_macros"
+      permission="zope.View"
+      class=".browser.PreferencesMacros"
+      allowed_interface="zope.interface.common.mapping.IItemMapping" 
+      />
+  
+  <browser:page
+      for="*"
+      name="preference_macro_definitions"
+      permission="zope.View"
+      template="macros.pt"
+      />
+
+
+  <!-- Preferences Tree -->
+
+  <browser:page
+      name="tree"
+      for="zope.app.apidoc.preference.interfaces.IPreferenceGroup"
+      class=".browser.PreferencesTree"
+      permission="zope.View"
+      attribute="tree" 
+      />
+
+  <!-- Book Chapter -->
+
   <apidoc:bookchapter 
       id="preferences"
       title="User Preferences API"

Added: Zope3/trunk/src/zope/app/apidoc/preference/default.py
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/default.py	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/default.py	2005-04-01 16:08:47 UTC (rev 29783)
@@ -0,0 +1,115 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Default Preferences Provider
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import persistent
+from BTrees.OOBTree import OOBTree
+
+import zope.interface
+from zope.security.checker import defineChecker
+
+from zope.app import component
+from zope.app import zapi
+from zope.app.container.contained import Contained
+from zope.app.location import locate
+from zope.app.traversing.interfaces import IContainmentRoot
+
+from zope.app.apidoc.preference import preference, interfaces
+
+
+class DefaultPreferenceProvider(persistent.Persistent, Contained):
+    zope.interface.implements(interfaces.IDefaultPreferenceProvider)
+
+    def __init__(self):
+        self.data = OOBTree()
+
+    def getDefaultPreferenceGroup(self, id=''):
+        group = zapi.getUtility(interfaces.IPreferenceGroup, name=id)
+        group = group.__bind__(self)
+        default = DefaultPreferenceGroup(group, self)
+        zope.interface.alsoProvides(default, IContainmentRoot)
+        locate(default, self, 'preferences')
+        return default
+
+    preferences = property(getDefaultPreferenceGroup)
+
+
+def DefaultPreferences(context, request):
+    return context.preferences
+
+
+class DefaultPreferenceGroup(preference.PreferenceGroup):
+    """A preference group representing the site-wide default values."""
+
+    def __init__(self, group, provider):
+        self.provider = provider
+        super(DefaultPreferenceGroup, self).__init__(
+            group.id, group.schema, group.title, group.description)
+
+        # Make sure that we also mark the default group as category if the
+        # actual group is one; this is important for the UI.
+        if interfaces.IPreferenceCategory.providedBy(group):
+            zope.interface.alsoProvides(self, interfaces.IPreferenceCategory)
+
+    def get(self, key, default=None):
+        group = super(DefaultPreferenceGroup, self).get(key, default)
+        if group is default:
+            return default
+        return DefaultPreferenceGroup(group, self.provider).__bind__(self)
+    
+    def items(self):
+        return [
+            (id, DefaultPreferenceGroup(group, self.provider).__bind__(self))
+            for id, group in super(DefaultPreferenceGroup, self).items()]
+
+    def __getattr__(self, key):
+        # Try to find a sub-group of the given id
+        group = self.get(key)
+        if group is not None:
+            return group
+
+        # Try to find a preference of the given name
+        if self.schema and key in self.schema:
+            marker = object()
+            value = self.data.get(key, marker)
+            if value is not marker:
+                return value
+
+            # There is currently no local entry, so let's go to the next
+            # provider and lookup the group and value there.
+            nextProvider = component.queryNextUtility(
+                self.provider, interfaces.IDefaultPreferenceProvider)
+
+            # No more providers found, so return the schema's default
+            if nextProvider is None: 
+                return self.schema[key].default
+
+            nextGroup = nextProvider.getDefaultPreferenceGroup(self.id)
+            return getattr(nextGroup, key, self.schema[key].default)
+
+        # Nothing found, raise an attribute error
+        raise AttributeError, "'%s' is not a preference or sub-group." %key
+
+    def data(self):
+        if self.id not in self.provider.data:
+            self.provider.data[self.id] = OOBTree()
+
+        return self.provider.data[self.id]
+    data = property(data)
+
+
+defineChecker(DefaultPreferenceGroup, preference.PreferenceGroupChecker)


Property changes on: Zope3/trunk/src/zope/app/apidoc/preference/default.py
___________________________________________________________________
Name: svn:eol-style
   + native

Deleted: Zope3/trunk/src/zope/app/apidoc/preference/edit.pt
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/edit.pt	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/edit.pt	2005-04-01 16:08:47 UTC (rev 29783)
@@ -1,123 +0,0 @@
-<html metal:use-macro="context/@@apidoc_macros/details">
-<head>
-  <style type="text/css" media="all"
-         metal:fill-slot="style_slot">
- 
-table.prefs {
-  border: 0pt;
-  width: 80%;
-}    
-
-tr {
-  margin: 0pt;
-  padding: 0pt;
-  border: 0pt;
-}
-
-tr.odd {
-  background: #fffbbe;
-}
-
-tr.first td {
-  border-top: 1pt solid #0000C0;
-}
-
-td {
-  padding: 3pt;
-  border-bottom: 1pt solid #0000C0;
-}
-
-td.input {
-  vertical-align: middle;
-  text-align: center;
-}
-
-td.description {
-
-}
-
-td.controls {
-  margin-top: 10pt;
-  border: 0pt;
-  padding: 4pt;
-  background: #ccf;
-  text-align: right;
-}
-
-td.spacer {
-  padding: 5pt;
-  border: 0pt;
-}
-
-div.documentation blockquote {
-  margin: 0pt;
-  padding: 0pt;
-}
-
-div.error {
-  font-weight: bold;
-  margin: 3pt 0pt;
-  color: #ffaa22;
-}
-  </style>
-</head>
-<body metal:fill-slot="contents">
-
-  <h1 tal:content="view/label">Edit something</h1>
-
-  <div class="documentation" tal:content="structure view/getIntroduction">
-    Here is the doc string
-  </div>
-
-  <p tal:define="status view/update"
-     tal:condition="status"
-     tal:content="status" />
-
-  <p tal:condition="view/errors" i18n:translate="">
-    There are <strong tal:content="python:len(view.errors)"
-                      i18n:name="num_errors">6</strong> input errors.
-  </p>
-  <br />
-
-  <form action="." tal:attributes="action request/URL" method="post"
-        enctype="multipart/form-data">
-      
-  <table class="prefs" cellspacing="0" cellpadding="0">
-    <tal:block repeat="widget view/widgets" >
-    <tr class=""
-        tal:define="parity repeat/widget/parity;
-                    firstrow repeat/widget/start"
-        tal:attributes="class python: parity + 
-                                      (firstrow and ' first' or '')">
-      <td class="description">
-        <b tal:content="widget/label">Option</b>
-        <div class="indent small">
-          <div tal:content="widget/hint">
-            Explanation
-          </div>
-          <div class="error" tal:define="error widget/error"
-            tal:condition="error" tal:content="structure error">
-            The Error
-          </div>
-        </div>
-      </td>
-      <td class="input" tal:content="structure widget">
-        <input type="text" style="width:100%"/>
-      </td>
-    </tr>
-    </tal:block>
-    <tr><td class="spacer"></td></tr>
-    <tr>
-      <td colspan="2" class="controls">
-        <input type="submit" value="Refresh" 
-            i18n:attributes="value refresh-button" />
-        <input type="submit" name="UPDATE_SUBMIT" value="Change" 
-            i18n:attributes="value submit-button"/>
-      </td>
-    </tr>
-  </table>
-
-  </form>
-
-</body>
-</html>

Added: Zope3/trunk/src/zope/app/apidoc/preference/index.pt
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/index.pt	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/index.pt	2005-04-01 16:08:47 UTC (rev 29783)
@@ -0,0 +1,26 @@
+<html metal:use-macro="context/@@preferences_macros/pref_view">
+
+<div metal:fill-slot="body">
+
+  <form action="." tal:attributes="action request/URL" method="post"
+        enctype="multipart/form-data">
+
+  <div metal:use-macro="context/@@preferences_macros/edit_pref_group" />
+
+  <table class="prefs" cellspacing="0" cellpadding="0">
+    <tr><td class="spacer"></td></tr>
+    <tr>
+      <td colspan="2" class="controls">
+        <input type="submit" value="Refresh" 
+            i18n:attributes="value refresh-button" />
+        <input type="submit" name="UPDATE_SUBMIT" value="Change" 
+            i18n:attributes="value submit-button"/>
+      </td>
+    </tr>
+  </table>
+
+  </form>
+
+</div>
+
+</html>


Property changes on: Zope3/trunk/src/zope/app/apidoc/preference/index.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: Zope3/trunk/src/zope/app/apidoc/preference/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/interfaces.py	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/interfaces.py	2005-04-01 16:08:47 UTC (rev 29783)
@@ -11,7 +11,7 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-"""User Preferences API
+"""User Preferences Interfaces
 
 $Id$
 """
@@ -21,32 +21,60 @@
 import zope.schema
 
 from zope.app.container.interfaces import IReadContainer
+from zope.app.location.interfaces import ILocation
 
-class IPreferencesGroup(zope.interface.Interface):
+
+class IPreferenceGroup(ILocation):
     """A group of preferences.
 
     This component represents a logical group of preferences. The preferences
     contained by this group is defined through the schema. The group has also
     a name by which it can be accessed.
+
+    The fields specified in the schema *must* be available as attributes and
+    items of the group instance. It is up to the implementation how this is
+    realized, however, most often one will implement __setattr__ and
+    __getattr__ as well as the common mapping API. 
     """
 
-    name = zope.schema.TextLine(
-        title=u"Name",
-        description=u"The name of the group.",
+    id = zope.schema.TextLine(
+        title=u"Id",
+        description=u"The id of the group.",
         required=True)
 
     schema = zope.schema.InterfaceField(
         title=u"Schema",
         description=u"Schema describing the preferences of the group.",
-        required=True)
+        required=False)
 
     title = zope.schema.TextLine(
         title=u"Title",
         description=u"The title of the group used in the UI.",
         required=True)
 
+    description = zope.schema.Text(
+        title=u"Description",
+        description=u"The description of the group used in the UI.",
+        required=False)
 
-class IUserPreferences(IReadContainer):
-    """Component that manages all preference groups."""
 
-    
+class IPreferenceCategory(zope.interface.Interface):
+    """A collection of preference groups.
+
+    Objects providing this interface serve as groups of preference
+    groups. This allows UIs to distinguish between high- and low-level
+    prefernce groups.
+    """
+
+
+class IDefaultPreferenceProvider(zope.interface.Interface):
+    """A root object providing default values for the entire preferences tree.
+
+    Default preference providers are responsible for providing default values
+    for all preferences. The way they get these values are up to the
+    implementation.
+    """
+
+    preferences = zope.schema.Field(
+        title = u"Default Preferences Root",
+        description = u"Link to the default preferences")

Added: Zope3/trunk/src/zope/app/apidoc/preference/macros.pt
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/macros.pt	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/macros.pt	2005-04-01 16:08:47 UTC (rev 29783)
@@ -0,0 +1,146 @@
+<metal:block define-macro="tree">
+
+<table cellspacing="0" cellpadding="0"
+       tal:define="root           context/@@tree;
+                   result         root/getFlatDicts;
+                   nodeDictList   python:result[0];
+                   maxDepth       python:result[1]">
+
+<tr>
+  <td class="list-item"
+      tal:attributes="colspan python:maxDepth+2">
+    Preferences
+  </td>
+</tr>
+
+<tr tal:repeat="nodeInfo nodeDictList">
+<tal:block tal:define="node nodeInfo/node">
+
+  <td style="width:16px" tal:repeat="state nodeInfo/row-state">
+    <img tal:attributes="src context/++resource++tree_images/vline.png"
+         tal:condition="state" alt="|" border="0" />
+  </td>
+
+  <td style="width:16px">
+    <a href=""
+       tal:attributes="href string:?tree-state=${nodeInfo/tree-state}"
+       tal:condition="node/hasChildren">
+      <tal:block condition="not:nodeInfo/last-level-node">
+        <img tal:attributes="src context/++resource++tree_images/plus_vline.png"
+             tal:condition="not:node/expanded" alt="+" border="0" />
+        <img tal:attributes="src context/++resource++tree_images/minus_vline.png"
+             tal:condition="node/expanded" alt="-" border="0" />
+      </tal:block>
+      <tal:block condition="nodeInfo/last-level-node">
+        <img tal:attributes="src context/++resource++tree_images/plus.png"
+             tal:condition="not:node/expanded" alt="+" border="0" />
+        <img tal:attributes="src context/++resource++tree_images/minus.png"
+             tal:condition="node/expanded" alt="-" border="0" />
+      </tal:block>
+    </a>
+    <tal:block condition="not:node/hasChildren">
+      <img tal:attributes="src context/++resource++tree_images/tline.png"
+           tal:condition="not:nodeInfo/last-level-node" alt="" border="0" />
+      <img tal:attributes="src context/++resource++tree_images/lline.png"
+           tal:condition="nodeInfo/last-level-node" alt="" border="0" />
+    </tal:block>
+  </td>
+
+  <td class="list-item"
+      tal:attributes="colspan python:maxDepth-len(nodeInfo['row-state'])+1">
+    &nbsp;<a href=""
+       tal:attributes="href 
+           string:${node/context/@@absolute_url}/@@index.html"
+       tal:content="node/context/zope:name">
+      node/id
+    </a>
+  </td>
+
+</tal:block>
+</tr>
+
+</table>
+  
+</metal:block>
+
+
+<metal:block define-macro="pref_view">
+
+<html metal:use-macro="context/@@standard_macros/view">
+<body>
+
+<div id="navigators" metal:fill-slot="navigators">
+ <div class="box">
+   <h4>Preferences</h4>
+   <div class="body">
+    <metal:block use-macro="context/@@preferences_macros/tree" />
+   </div>
+ </div>
+</div>
+
+<div metal:fill-slot="tabs">
+  <h1 tal:content="context/title">User Preferences</h1>
+</div>
+
+<div metal:fill-slot="body">
+
+  <div metal:define-slot="body">
+     <p>Body here</p>
+  </div>
+
+</div>
+
+</body>
+
+</html>
+</metal:block>
+
+
+<metal:block define-macro="edit_pref_group">
+
+  <div tal:content="structure view/getIntroduction">
+    Category Description goes here.
+  </div>  
+  <br/>
+
+  <p tal:define="status view/update"
+     tal:condition="status"
+     tal:content="status" />
+
+  <p tal:condition="view/errors" i18n:translate="">
+    There are <strong tal:content="python:len(view.errors)"
+                      i18n:name="num_errors">6</strong> input errors.
+  </p>
+  <br />
+      
+  <table class="prefs" cellspacing="0" cellpadding="0">
+    <tal:block repeat="widget view/widgets" >
+    <tr class=""
+        tal:define="parity repeat/widget/parity;
+                    firstrow repeat/widget/start"
+        tal:attributes="class python: parity + 
+                                      (firstrow and ' first' or '')">
+      <td class="description">
+        <b tal:content="widget/label">Option</b>
+        <div class="indent small">
+          <div tal:content="widget/hint">
+            Explanation
+          </div>
+          <div class="error" tal:define="error widget/error"
+            tal:condition="error" tal:content="structure error">
+            The Error
+          </div>
+        </div>
+      </td>
+      <td class="input" tal:content="structure widget">
+        <input type="text" style="width:100%"/>
+      </td>
+    </tr>
+    </tal:block>
+  </table>
+
+  <div tal:repeat="subgroup context/values">
+    <tal:block replace="structure subgroup/@@editAsSubGroup" />
+  </div>
+
+</metal:block>


Property changes on: Zope3/trunk/src/zope/app/apidoc/preference/macros.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: Zope3/trunk/src/zope/app/apidoc/preference/meta.zcml
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/meta.zcml	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/meta.zcml	2005-04-01 16:08:47 UTC (rev 29783)
@@ -6,9 +6,9 @@
   <meta:directives namespace="http://namespaces.zope.org/apidoc">
 
     <meta:directive
-        name="preferencesGroup"
-        schema=".metadirectives.IPreferencesGroupDirective"
-        handler=".metaconfigure.preferencesGroup"
+        name="preferenceGroup"
+        schema=".metadirectives.IPreferenceGroupDirective"
+        handler=".metaconfigure.preferenceGroup"
         />
 
   </meta:directives>

Modified: Zope3/trunk/src/zope/app/apidoc/preference/metaconfigure.py
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/metaconfigure.py	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/metaconfigure.py	2005-04-01 16:08:47 UTC (rev 29783)
@@ -18,9 +18,13 @@
 __docformat__ = 'restructuredtext'
 from zope.app.component.metaconfigure import utility
 
-from zope.app.apidoc.preference import preference, interfaces
+from zope.app.apidoc.preference.interfaces import IPreferenceGroup
+from zope.app.apidoc.preference.preference import PreferenceGroup
 
 
-def preferencesGroup(_context, name, schema, title):
-    group = preference.PreferencesGroup(name, schema, title)
-    utility(_context, interfaces.IPreferencesGroup, group, name=name)
+def preferenceGroup(_context, id=None, schema=None,
+                    title=u'', description=u'', category=False):
+    if id is None:
+        id = ''
+    group = PreferenceGroup(id, schema, title, description, category)
+    utility(_context, IPreferenceGroup, group, name=id)

Modified: Zope3/trunk/src/zope/app/apidoc/preference/metadirectives.py
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/metadirectives.py	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/metadirectives.py	2005-04-01 16:08:47 UTC (rev 29783)
@@ -19,20 +19,23 @@
 from zope.interface import Interface
 from zope.configuration import fields
 
-class IPreferencesGroupDirective(Interface):
+class IPreferenceGroupDirective(Interface):
     """Register a preference group."""
 
-    name = fields.PythonIdentifier(
-        title=u"Name",
-        description=u"Name of the preference group used to access the group.",
-        required=True
+    # The id is not required, since the root group has an empty id.
+    id = fields.PythonIdentifier(
+        title=u"Id",
+        description=u"""
+            Id of the preference group used to access the group. The id should
+            be a valid path in the preferences tree.""",
+        required=False,
         )
 
     schema = fields.GlobalInterface(
         title=u"Schema",
         description=u"Schema of the preference group used defining the "
                     u"preferences of the group.",
-        required=True        
+        required=False        
         )
 
     title = fields.MessageID(
@@ -40,3 +43,16 @@
         description=u"Title of the preference group used in UIs.",
         required=True
         )
+
+    description = fields.MessageID(
+        title=u"Description",
+        description=u"Description of the preference group used in UIs.",
+        required=False
+        )
+
+    category = fields.Bool(
+        title=u"Is Group a Category",
+        description=u"Denotes whether this preferences group is a category.",
+        required=False,
+        default=False
+        )

Modified: Zope3/trunk/src/zope/app/apidoc/preference/preference.py
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/preference.py	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/preference.py	2005-04-01 16:08:47 UTC (rev 29783)
@@ -11,7 +11,7 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-"""User Preference System
+"""User Preferences System
 
 $Id$
 """
@@ -23,107 +23,206 @@
 from zope.security.checker import CheckerPublic, Checker, defineChecker
 from zope.security.management import getInteraction
 
+from zope.app.container.interfaces import IReadContainer
 from zope.app import zapi
 from zope.app.container.contained import Contained
-from zope.app.location import LocationProxy, locate
+from zope.app.location import LocationProxy, locate, Location
 from zope.app.principalannotation.interfaces import IPrincipalAnnotationUtility
+from zope.app.traversing.interfaces import IContainmentRoot
 
-from zope.app.apidoc.utilities import ReadContainerBase
-from zope.app.apidoc.preference import interfaces
+from zope.app.apidoc.preference.interfaces import IPreferenceGroup 
+from zope.app.apidoc.preference.interfaces import IPreferenceCategory 
+from zope.app.apidoc.preference.interfaces import IDefaultPreferenceProvider 
 
 pref_key = 'zope.app.user.UserPreferences'
 
-class PreferencesGroup(object):
 
-    zope.interface.implements(interfaces.IPreferencesGroup)
+class PreferenceGroup(Location):
+    """A feature-rich ``IPreferenceGroup`` implementation.
 
-    name = None
+    This class implements the 
+    """
+    zope.interface.implements(IPreferenceGroup, IReadContainer)
+
+    # Declare attributes here, so that they are always available.
+    id = None
     schema = None
     title = None
+    description = None
 
-    def __init__(self, name, schema, title=u''):
-        self.name = name
+    def __init__(self, id, schema=None, title=u'', description=u'',
+                 isCategory=False):
+        self.id = id
         self.schema = schema
         self.title = title
-        zope.interface.directlyProvides(self, self.schema)
+        self.description = description
 
+        # The last part of the id is the name.
+        self.__name__ = id.split('.')[-1]
+
+        # Make sure this group provides all important interfaces.
+        directlyProvided = ()
+        if isCategory:
+            directlyProvided += (IPreferenceCategory,)
+        if schema:
+            directlyProvided += (schema,)
+        zope.interface.directlyProvides(self, directlyProvided)
+
+
+    def __bind__(self, parent):
+        clone = self.__class__.__new__(self.__class__)
+        clone.__dict__.update(self.__dict__)
+        clone.__parent__ = parent
+        return clone
+
+
+    def get(self, key, default=None):
+        id = self.id and self.id + '.' + key or key
+        group = zapi.queryUtility(IPreferenceGroup, id, default)
+        if group is default:
+            return default
+        return group.__bind__(self)
+    
+
+    def items(self):
+        cutoff = self.id and len(self.id)+1 or 0
+        return [(id[cutoff:], group.__bind__(self))
+                for id, group in zapi.getUtilitiesFor(IPreferenceGroup)
+                if id != self.id and \
+                   id.startswith(self.id) and \
+                   id[cutoff:].find('.') == -1]
+
+
+    def __getitem__(self, key):
+        """See zope.app.container.interfaces.IReadContainer"""
+        default = object()
+        obj = self.get(key, default)
+        if obj is default:
+            raise KeyError, key
+        return obj
+
+    def __contains__(self, key):
+        """See zope.app.container.interfaces.IReadContainer"""
+        return self.get(key) is not None
+
+    def keys(self):
+        """See zope.app.container.interfaces.IReadContainer"""
+        return [id for id, group in self.items()]
+
+    def __iter__(self):
+        """See zope.app.container.interfaces.IReadContainer"""
+        return self.values().__iter__()
+        
+    def values(self):
+        """See zope.app.container.interfaces.IReadContainer"""
+        return [group for id, group in self.items()]
+
+    def __len__(self):
+        """See zope.app.container.interfaces.IReadContainer"""
+        return len(self.items())    
+
     def __getattr__(self, key):
-        if key in self.schema:
+        # Try to find a sub-group of the given id
+        group = self.get(key)
+        if group is not None:
+            return group
+
+        # Try to find a preference of the given name
+        if self.schema and key in self.schema:
             marker = object()
-            value = self.annotations.get(key, marker)
+            value = self.data.get(key, marker)
             if value is marker:
-                return self.schema[key].default
+                # Try to find a default preference provider
+                provider = zapi.queryUtility(IDefaultPreferenceProvider)
+                if provider is None:
+                    return self.schema[key].default
+                defaultGroup = provider.getDefaultPreferenceGroup(self.id)
+                return getattr(defaultGroup, key)
             return value
-        raise AttributeError, "'%s' is not a preference." %key
 
+        # Nothing found, raise an attribute error
+        raise AttributeError, "'%s' is not a preference or sub-group." %key
+
     def __setattr__(self, key, value):
         if self.schema and key in self.schema:
             # Validate the value
             bound = self.schema[key].bind(self)
             bound.validate(value)
             # Assign value
-            self.annotations[key] = value
+            self.data[key] = value
         else:
             self.__dict__[key] = value
-            
-    def annotations(self):
+
+    def __delattr__(self, key):
+        if self.schema and key in self.schema:
+            del self.data[key]
+        else:
+            del self.__dict__[key]
+
+    def data(self):
         utility = zapi.getUtility(IPrincipalAnnotationUtility)
-        # TODO: what if we have multiple participations
+        # TODO: what if we have multiple participations?
         principal = getInteraction().participations[0].principal
         ann = utility.getAnnotations(principal)
 
+        # If no preferences exist, create the root preferences object.
         if  ann.get(pref_key) is None:
             ann[pref_key] = OOBTree()
         prefs = ann[pref_key]
 
-        if self.name not in prefs.keys():
-            prefs[self.name] = OOBTree()
+        # If no entry for the group exists, create a new entry.
+        if self.id not in prefs.keys():
+            prefs[self.id] = OOBTree()
 
-        return prefs[self.name]
-    annotations = property(annotations)
+        return prefs[self.id]
+    data = property(data)
 
 
-def PreferencesGroupChecker(instance):
-    """A function that can be registered as a Checker in defineChecker()"""
-    read_perm_dict = {'name': CheckerPublic, 'schema': CheckerPublic,
-                      'title': CheckerPublic}
+
+def PreferenceGroupChecker(instance):
+    """A function that can be registered as a Checker in defineChecker()
+
+    The attributes available in a preference group are dynamically generated
+    based on the group schema and the available sub-groups. Thus, the
+    permission dictionaries have to be generated at runtime and are unique for
+    each preference group instance.
+    """
+    read_perm_dict = {}
     write_perm_dict = {}
 
-    for name in getFields(instance.schema):
+    # Make sure that the attributes from IPreferenceGroup and IReadContainer
+    # are public.
+    for attrName in ('id', 'schema', 'title', 'description',
+                     'get', 'items', 'keys', 'values',
+                     '__getitem__', '__contains__', '__iter__', '__len__'):
+        read_perm_dict[attrName] = CheckerPublic
+
+    # Make the attributes generated from the schema available as well.
+    if instance.schema is not None:
+        for name in getFields(instance.schema):
+            read_perm_dict[name] = CheckerPublic
+            write_perm_dict[name] = CheckerPublic
+
+    # Make all sub-groups available as well.
+    for name in instance.keys():
         read_perm_dict[name] = CheckerPublic
         write_perm_dict[name] = CheckerPublic
 
     return Checker(read_perm_dict, write_perm_dict)
 
-defineChecker(PreferencesGroup, PreferencesGroupChecker)
+defineChecker(PreferenceGroup, PreferenceGroupChecker)
 
 
-class UserPreferences(ReadContainerBase, Contained):
 
-    zope.interface.implements(interfaces.IUserPreferences)
+class preferencesNamespace(object):
+    """Used to traverse to the root preferences group."""
 
-    def get(self, key, default=None):
-        """See zope.app.container.interfaces.IReadContainer"""
-        group = zapi.queryUtility(interfaces.IPreferencesGroup, key, default)
-        if group == default:
-            return default
-        return LocationProxy(group, self, key)
-
-    def items(self):
-        """See zope.app.container.interfaces.IReadContainer"""
-        items = list(zapi.getUtilitiesFor(interfaces.IPreferencesGroup))
-        return [(key, LocationProxy(group, self, key))
-                for key, group in items]
-
-
-class preferencesNamespace(object):
-    """Used to traverse to an User Preferences."""
     def __init__(self, ob, request=None):
         self.context = ob
         
     def traverse(self, name, ignore):
-        prefs = UserPreferences()
-        locate(prefs, self.context, '++preferences++')
-        if not name:
-            return prefs
-        return prefs[name]
+        rootGroup = zapi.getUtility(IPreferenceGroup)
+        rootGroup = rootGroup.__bind__(self.context)
+        rootGroup.__name__ = '++preferences++'
+        zope.interface.alsoProvides(rootGroup, IContainmentRoot)
+        return name and rootGroup[name] or rootGroup

Added: Zope3/trunk/src/zope/app/apidoc/preference/subgroup.pt
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/subgroup.pt	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/subgroup.pt	2005-04-01 16:08:47 UTC (rev 29783)
@@ -0,0 +1,6 @@
+<fieldset>
+  <legend tal:content="context/title">Title</legend>
+
+  <div metal:use-macro="context/@@preferences_macros/edit_pref_group" />
+
+</fieldset>
\ No newline at end of file


Property changes on: Zope3/trunk/src/zope/app/apidoc/preference/subgroup.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: Zope3/trunk/src/zope/app/apidoc/preference/tests.py
===================================================================
--- Zope3/trunk/src/zope/app/apidoc/preference/tests.py	2005-04-01 16:07:53 UTC (rev 29782)
+++ Zope3/trunk/src/zope/app/apidoc/preference/tests.py	2005-04-01 16:08:47 UTC (rev 29783)
@@ -17,8 +17,17 @@
 """
 import unittest
 from zope.testing import doctest, doctestunit
-from zope.component.testing import setUp, tearDown
+from zope.component import testing
+from zope.app.testing import setup
 
+def setUp(test):
+    testing.setUp(test)
+    setup.setUpTestAsModule(test, 'zope.app.apidoc.preference.README')
+
+def tearDown(test):
+    testing.tearDown(test)
+    setup.tearDownTestAsModule(test)
+
 def test_suite():
     return unittest.TestSuite((
         doctest.DocFileSuite('README.txt',



More information about the Zope3-Checkins mailing list