diff --git a/src/z3c/configurator/CHANGES.txt b/src/z3c/configurator/CHANGES.txt new file mode 100644 index 0000000..1717c70 --- /dev/null +++ b/src/z3c/configurator/CHANGES.txt @@ -0,0 +1,27 @@ +======================== +z3c.configurator Changes +======================== + +This file contains change information for the current z3c.configurator +package. + +After 1.0 (trunk only) +====================== + +New features +------------ + +- Added possibility to apply only specific named plugins in confugure. + +- New option to configure allows to have namespaced data to resolve + naming conflicts. + +- Added a page to call configurators TTW. This is the first step + towards mergin z3c.configurator and z3c.sampledata into one package. + +Bug fixes +--------- + +- SchemaConfigurationPluginBase now implements + ISchemaConfigurationPluginBase. + diff --git a/src/z3c/configurator/README.txt b/src/z3c/configurator/README.txt index 22daaf5..d9f1bfe 100644 --- a/src/z3c/configurator/README.txt +++ b/src/z3c/configurator/README.txt @@ -125,3 +125,58 @@ The value must exist and be valid: ... WrongType: (1, ) +Data Namespaces +--------------- + +In order to not confuse attribute names if two plugins share a common +name it is possible to pass data as a dictionary of dictionaries. The +keys of the dictionary is the name under which the plugins are +registered. + + >>> something = Something() + >>> data = {u'add foo': {'foo': u'foo value'}, + ... u'add bar': {'bar': u'bar value'}} + >>> configurator.configure(something, data, useNameSpaces=True) + >>> something.foo, something.bar + (u'Text: foo value', u'bar value') + +Named Configuration +------------------- + +Sometimes we do not want all registered configuration plugins to be +executed. This can be achieved by providing the names argument to the +configure function. + +Let us create a new something: + + >>> something = Something() + +If we now configure it without names we get both attributes set. + + >>> configurator.configure(something, {'foo': u'my value', 'bar': u'asdf'}) + >>> something.__dict__ + {'foo': u'Text: my value', 'bar': u'asdf'} + +Now let us just configure the plugin 'add bar'. + + >>> something = Something() + >>> configurator.configure(something, {'foo': u'my value', 'bar': u'asdf'}, + ... names=['add bar']) + >>> something.__dict__ + {'bar': u'asdf'} + +Dependencies of plugins are always executed - they don't have to be +added to the ```names``` argument. + + >>> something = Something() + >>> configurator.configure(something, {'foo': u'my value'}, + ... names=['extend foo']) + >>> something.foo + u'Text: my value' + +Named configurations are usefull when called manually through the web +(see browser/README.txt). The configurator package does not look if a +configuration is already applied if called twice. It is the +responsibility of the plugin to be aware that it doesn't do things +twice or delete things. + diff --git a/src/z3c/configurator/SETUP.cfg b/src/z3c/configurator/SETUP.cfg new file mode 100644 index 0000000..1056925 --- /dev/null +++ b/src/z3c/configurator/SETUP.cfg @@ -0,0 +1,3 @@ + + z3c.configurator-*.zcml + diff --git a/src/z3c/configurator/browser/README.txt b/src/z3c/configurator/browser/README.txt new file mode 100644 index 0000000..e85b7ac --- /dev/null +++ b/src/z3c/configurator/browser/README.txt @@ -0,0 +1,46 @@ +========================= +Calling Configurators TTW +========================= + +A configuration view is registered to apply named configuration on any +object. We defined two example configurators which we now gonna apply +to the site object. + + >>> from zope.testbrowser.testing import Browser + >>> browser = Browser() + >>> browser.addHeader('Authorization','Basic mgr:mgrpw') + >>> browser.handleErrors = False + + >>> browser.open('http://localhost/manage') + >>> browser.url + 'http://localhost/@@contents.html' + +The view is registered in the zmi_views menu + + >>> browser.getLink(u'Configurators').click() + >>> viewURL = browser.url + >>> viewURL + 'http://localhost/@@configurators.html' + + >>> sel = browser.getControl(name="form.pluginNames.to") + +First we can choose from the registered named plugins. + + >>> plugs = browser.getControl(name="form.pluginNames.from").options + >>> sorted(plugs) + ['z3c.configurator.testing.setdescription', + 'z3c.configurator.testing.settitle'] + >>> browser.open(viewURL + '?form.pluginNames=z3c.configurator.testing.settitle') + +We have choosen a plugin, so now we have a form for the arguments needed. + + >>> browser.getControl('Some Argument').value + '' + >>> browser.getControl('Some Argument').value = "New Title" + >>> browser.getControl('Apply').click() + + +XXX form.pluginNames have to be set, but we can't because the widget +uses javascript. + + diff --git a/src/z3c/configurator/browser/__init__.py b/src/z3c/configurator/browser/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/src/z3c/configurator/browser/__init__.py @@ -0,0 +1 @@ +# diff --git a/src/z3c/configurator/browser/configure.pt b/src/z3c/configurator/browser/configure.pt new file mode 100644 index 0000000..15acaf4 --- /dev/null +++ b/src/z3c/configurator/browser/configure.pt @@ -0,0 +1,9 @@ +
+
+ +

+

+ +
+
+ diff --git a/src/z3c/configurator/browser/configure.zcml b/src/z3c/configurator/browser/configure.zcml new file mode 100644 index 0000000..1380413 --- /dev/null +++ b/src/z3c/configurator/browser/configure.zcml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/src/z3c/configurator/browser/ftesting.zcml b/src/z3c/configurator/browser/ftesting.zcml new file mode 100644 index 0000000..362fa4f --- /dev/null +++ b/src/z3c/configurator/browser/ftesting.zcml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/z3c/configurator/browser/ftests.py b/src/z3c/configurator/browser/ftests.py new file mode 100644 index 0000000..a978156 --- /dev/null +++ b/src/z3c/configurator/browser/ftests.py @@ -0,0 +1,29 @@ +import unittest +from zope.app.testing import functional + +functional.defineLayer('TestLayer', 'ftesting.zcml') + + +def setUp(test): + """Setup a reasonable environment for the category tests""" + pass + + +def tearDown(test): + pass + + +def test_suite(): + suite = unittest.TestSuite() + suites = ( + functional.FunctionalDocFileSuite('README.txt', + setUp=setUp, tearDown=tearDown, + ), + ) + for s in suites: + s.layer=TestLayer + suite.addTest(s) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/src/z3c/configurator/browser/testing.py b/src/z3c/configurator/browser/testing.py new file mode 100644 index 0000000..ec1a2c9 --- /dev/null +++ b/src/z3c/configurator/browser/testing.py @@ -0,0 +1,32 @@ +"""Some test classes +""" +from z3c.configurator import configurator +from zope import interface +from zope import component +from zope import schema +from zope.dublincore.interfaces import IZopeDublinCore +from zope.annotation.interfaces import IAttributeAnnotatable + +class ISingleArg(interface.Interface): + + arg = schema.TextLine(title=u'Some Argument') + +class SetTitle(configurator.SchemaConfigurationPluginBase): + """makes an object implement IFoo""" + component.adapts(IAttributeAnnotatable) + schema = ISingleArg + + def __call__(self, data): + dc = IZopeDublinCore(self.context) + dc.title = data.get('arg') + +class SetDescription(configurator.SchemaConfigurationPluginBase): + + component.adapts(IAttributeAnnotatable) + schema = ISingleArg + + def __call__(self, data): + dc = IZopeDublinCore(self.context) + dc.description = data.get('arg') + + diff --git a/src/z3c/configurator/browser/views.py b/src/z3c/configurator/browser/views.py new file mode 100644 index 0000000..7b4ee58 --- /dev/null +++ b/src/z3c/configurator/browser/views.py @@ -0,0 +1,125 @@ +from zope import component +from zope import interface +from zope import schema + +from zope import formlib +from zope.formlib import form +from zope.app.pagetemplate import ViewPageTemplateFile +from zope.cachedescriptors.property import Lazy +from z3c.configurator import interfaces +from z3c.configurator.i18n import _ +from z3c.configurator import configurator + + +class SelectPlugins(form.PageForm): + + """a form to choose plugins, to be applied""" + + form_fields = form.Fields( + schema.Choice(__name__=u'pluginName', + title=_(u'Plugin Name'), + vocabulary="Configurator Plugin Names") + ) + + @form.action(label=_(u'Apply Configuration')) + def selectPlugins(self, action, data): + pluginName = data.get('pluginName') + configurator.configure(self.context, names=[pluginName]) + self.status = _('Configuration applied') + +class IGenerateSchema(interface.Interface): + """Schema for the minimal generator parameters""" + + seed = schema.TextLine( + title = _(u'Seed'), + description = _(u'A seed for the random generator'), + default = u'sample', + required=False, + ) + + +class ConfigureForm(form.PageForm): + """Configurator Plugin form""" + + base_template = form.EditForm.template + template = ViewPageTemplateFile('configure.pt') + subforms = [] + + form_fields = form.Fields( + schema.List(__name__=u'pluginNames', + title=u'Plugin Names', + value_type=schema.Choice( + __name__=u'pluginName', + title=_(u'Plugin Name'), + vocabulary="Configurator Plugin Names") + )) + + workDone = False + + @Lazy + def _pluginNames(self): + names = self.request.form.get(self.prefix + '.pluginNames') + if names and not type(names) is type([]): + return [names] + return names + + def setUpWidgets(self, ignore_request=False): + if self._pluginNames: + plugins = configurator.requiredPlugins(self.context, + self._pluginNames) + self.subforms = [] + for name, plugin in plugins: + if not interfaces.ISchemaConfigurationPlugin.providedBy( + plugin): + continue + subform = PluginSchemaForm(context=self.context, + request=self.request, + plugin=plugin, + prefix=name) + subform.form_fields = form.Fields(plugin.schema) + self.subforms.append(subform) + super(ConfigureForm, self).setUpWidgets(ignore_request=ignore_request) + + @form.action(_("Update")) + def handleUpdate(self, action, data): + if not self._pluginNames: + return + self.setUpWidgets(ignore_request=False) + result = self.template() + return result + + def _pluginsSelected(self, action): + return not not self.request.form.get(self.prefix + '.pluginNames') + + @form.action(_("Apply"), condition='_pluginsSelected') + def handleApply(self, action, data): + plugins = configurator.requiredPlugins(self._pluginNames) + configuratorData = {} + for subform in self.subforms: + subform.update() + formData = {} + errors = form.getWidgetsData(subform.widgets, + subform.prefix, + formData) + configuratorData[subform.prefix] = formData + + configurator.configure(self.context, + configuratorData, + names=self._pluginNames, + useNameSpaces=True) + self.status = u'Applied: %s' % u' '.join(self._pluginNames) + + +class PluginSchemaForm(form.AddForm): + """An editor for a single schema based plugin""" + interface.implements(formlib.interfaces.ISubPageForm) + template = formlib.namedtemplate.NamedTemplate('default') + actions = [] + + def __init__(self, context, request, plugin=None, + schema=None, prefix=''): + self.plugin = plugin + self.schema = schema + self.prefix = prefix + super(PluginSchemaForm, self).__init__(context, request) + diff --git a/src/z3c/configurator/configurator.py b/src/z3c/configurator/configurator.py index 9860c0a..3e4bfbf 100644 --- a/src/z3c/configurator/configurator.py +++ b/src/z3c/configurator/configurator.py @@ -23,44 +23,39 @@ from z3c.configurator import interfaces -# Stati values -NEW = 1 -OPEN = 2 -CLOSED = 3 - -def configure(component, data): +def requiredPlugins(component, names=[]): + """returns a list of tuples (name, plugin) in the right order to + be executed""" + plugins = dict(zope.component.getAdapters( (component,), interfaces.IConfigurationPlugin)) - - # status is a dict plugin names as keys and stati as values. - status = dict([(name, NEW) for name in plugins]) - - def visit(name): - """The recursive part of the topological sort - - Raises a CyclicDependencyError if cyclic depencencies are found. - """ - if status[name] == NEW: - status[name] = OPEN - plugin = plugins[name] - for dep in getattr(plugin, 'dependencies', ()): - visit(dep) - plugin(data) - status[name] = CLOSED - - elif status[name] == CLOSED: - return - - # Stumbling over an OPEN node means there is a cyclic dependency - elif status[name] == OPEN: - raise interfaces.CyclicDependencyError( - "cyclic dependency at '%s'" % name) - - - for name in plugins: - visit(name) - + # if we have no names we return them all + if not names: + return [(name, plugins[name]) for name in sorted(plugins.keys())] + + def _add(name, res): + deps = getattr(plugins[name], 'dependencies', ()) + for dep in deps: + if not dep in res: + _add(dep, res) + if name not in res: + res.append(name) + res = [] + for name in names: + _add(name, res) + return [(name, plugins[name]) for name in res] + +def configure(component, data, names=[], useNameSpaces=False): + + plugins = requiredPlugins(component, names) + for name, plugin in plugins: + if useNameSpaces is True: + d = data.get(name, {}) + else: + d = data + + plugin(d) class ConfigurationPluginBase(object): zope.interface.implements(interfaces.IConfigurationPlugin) @@ -72,7 +67,7 @@ def __call__(self, data): raise NotImplemented class SchemaConfigurationPluginBase(object): - zope.interface.implements(interfaces.IConfigurationPlugin) + zope.interface.implements(interfaces.ISchemaConfigurationPlugin) schema = zope.interface.Interface def __init__(self, context): diff --git a/src/z3c/configurator/configure.zcml b/src/z3c/configurator/configure.zcml new file mode 100644 index 0000000..b8756fb --- /dev/null +++ b/src/z3c/configurator/configure.zcml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/z3c/configurator/i18n.py b/src/z3c/configurator/i18n.py new file mode 100644 index 0000000..340a5f7 --- /dev/null +++ b/src/z3c/configurator/i18n.py @@ -0,0 +1,10 @@ +"""The i18n definitions. + +$Id$ +""" +__docformat__ = "reStructuredText" + +import zope.i18nmessageid + +_ = zope.i18nmessageid.MessageFactory('z3c.configurator') + diff --git a/src/z3c/configurator/interfaces.py b/src/z3c/configurator/interfaces.py index 89140b3..77160f9 100644 --- a/src/z3c/configurator/interfaces.py +++ b/src/z3c/configurator/interfaces.py @@ -43,7 +43,6 @@ def __call__(self, data): then raise a ``DataMissingError`` error. """ - class ISchemaConfigurationPlugin(IConfigurationPlugin): """A configuration plugin that provides a data schema.""" diff --git a/src/z3c/configurator/vocabulary.py b/src/z3c/configurator/vocabulary.py new file mode 100644 index 0000000..7b2e0f8 --- /dev/null +++ b/src/z3c/configurator/vocabulary.py @@ -0,0 +1,29 @@ +############################################################################## +# +# 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. +# +############################################################################## +"""Vocabularies + +$Id$ +""" +__docformat__ = "reStructuredText" +from zope import component +from zope.schema import vocabulary +import interfaces + +def pluginNamesVocabulary(context): + """a vocabulary that returns all names of registered configuration + plugins""" + terms = [] + plugins = dict(component.getAdapters( + (context,), interfaces.IConfigurationPlugin)) + return vocabulary.SimpleVocabulary.fromValues(plugins.keys()) diff --git a/src/z3c/configurator/z3c.configurator-configure.zcml b/src/z3c/configurator/z3c.configurator-configure.zcml new file mode 100644 index 0000000..0c706d7 --- /dev/null +++ b/src/z3c/configurator/z3c.configurator-configure.zcml @@ -0,0 +1 @@ + \ No newline at end of file