diff --git a/composer/schema/README.txt b/composer/schema/README.txt index 6fe1b64..995b996 100644 --- a/composer/schema/README.txt +++ b/composer/schema/README.txt @@ -62,6 +62,8 @@ Creating a schema from an interface >>> class Person(object): ... implements(IPerson) + ... def __init__(self, firstName=u'', lastName=u'', age=None): + ... self.firstName, self.lastName, self.age = firstName, lastName, age >>> from cybertools.composer.schema.interfaces import ISchemaFactory >>> factory = ISchemaFactory(Person()) @@ -77,9 +79,10 @@ Using a more specialized schema factory --------------------------------------- >>> class PersonSchemaFactory(SchemaFactory): - ... def __call__(self, manager=None): - ... schema = super(PersonSchemaFactory, self).__call__(manager) - ... del schema.fields['firstName'] # don't show first name + ... def __call__(self, interface, **kw): + ... schema = super(PersonSchemaFactory, self).__call__(interface) + ... if 'firstName' in schema.fields.keys(): + ... del schema.fields['firstName'] # don't show first name ... return schema >>> component.provideAdapter(PersonSchemaFactory, (IPerson,)) @@ -90,3 +93,71 @@ Using a more specialized schema factory lastName Last name textline age Age number +Access and update a context object using a schema-based form +------------------------------------------------------------ + + >>> from zope.publisher.browser import TestRequest + >>> from cybertools.composer.schema.browser.form import Form + +We first have to provide adapters for special field types ('number' in +this case) and an instance adapter that manages the access to the +context object. + + >>> from cybertools.composer.schema.field import NumberFieldInstance + >>> component.provideAdapter(NumberFieldInstance, name='number') + + >>> from cybertools.composer.schema.instance import Instance + >>> component.provideAdapter(Instance) + + >>> person = Person(u'John', u'Miller', 33) + +Note that the first name is not shown as we excluded it via the schema +factory above. The age field is a number, but is shown here as a +string as the instance is accessed using 'edit' mode, i.e. provide +data suitable for showing on an HTML form. + + >>> form = Form(person, TestRequest()) + >>> form.interface = IPerson + >>> form.data + {'lastName': u'Miller', 'age': '33'} + +For editing we have to provide another instance adapter. + + >>> from cybertools.composer.schema.instance import Editor + >>> component.provideAdapter(Editor, name='editor') + + >>> input = dict(lastName='Miller', age='40', action='update') + >>> request = TestRequest(form=input) + >>> form = Form(person, request) + >>> form.interface = IPerson + >>> form.nextUrl = 'dummy_url' # avoid hassle with IAbsoluteURL view... + + >>> form.update() + False + + >>> person.age + 40 + +Create a new object using a schema-based form +--------------------------------------------- + + >>> from cybertools.composer.schema.browser.form import CreateForm + >>> container = dict() + + >>> input = dict(lastName=u'Smith', age='28', action='update') + >>> form = CreateForm(container, TestRequest(form=input)) + >>> form.interface = IPerson + >>> form.factory = Person + >>> form.nextUrl = 'dummy_url' # avoid hassle with IAbsoluteURL view... + >>> form.getName = lambda x: x.lastName.lower() + + >>> form.data + {'lastName': u'Smith', 'age': '28'} + + >>> form.update() + False + + >>> p2 = container['smith'] + >>> p2.lastName, p2.age + (u'Smith', 28) + diff --git a/composer/schema/browser/common.py b/composer/schema/browser/common.py index 456cbac..ef19a39 100644 --- a/composer/schema/browser/common.py +++ b/composer/schema/browser/common.py @@ -124,8 +124,8 @@ class BaseView(object): submit=getCheckoutView, ) - #@Lazy - def nextUrl(self): + #@Lazy # must be method for Zope 2.9 compatibility :-( ??? + def getNextUrl(self): #viewName = 'thankyou.html' viewName = '' url = '' diff --git a/composer/schema/browser/form.py b/composer/schema/browser/form.py new file mode 100644 index 0000000..3d2c87e --- /dev/null +++ b/composer/schema/browser/form.py @@ -0,0 +1,153 @@ +# +# Copyright (c) 2007 Helmut Merz helmutm@cy55.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +View(s) for forms based on composer.schema. + +$Id$ +""" + +from zope import component +from zope.app.container.interfaces import INameChooser +from zope.cachedescriptors.property import Lazy +from zope.interface import Interface +from zope.event import notify +from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent +from zope.traversing.browser.absoluteurl import absoluteURL + +from cybertools.composer.interfaces import IInstance +from cybertools.composer.schema.browser.common import schema_macros, schema_edit_macros +from cybertools.composer.schema.interfaces import ISchemaFactory +from cybertools.composer.schema.schema import FormState + + +class Form(object): + + interface = Interface + fieldHandlers = {} # default, don't update! + formState = FormState() # dummy, don't update! + message = u'Object changed.' + + def __init__(self, context, request): + self.context = context + self.request = request + + @property + def schemaMacros(self): + return schema_macros.macros + + @property + def schemaEditMacros(self): + return schema_edit_macros.macros + + @Lazy + def object(self): + return self.context + + @Lazy + def schema(self): + schemaFactory = component.getAdapter(self.object, ISchemaFactory) + return schemaFactory(self.interface, manager=self, + request=self.request) + + @Lazy + def fields(self): + return self.schema.fields + + @Lazy + def data(self): + """ Provide data based on context object. + May be overwritten by subclass. + """ + instance = self.instance + instance.template = self.schema + data = instance.applyTemplate(mode='edit') + form = self.request.form + for k, v in data.items(): + #overwrite data with values from request.form + if k in form: + data[k] = form[k] + return data + + @Lazy + def instance(self): + return IInstance(self.object) + + def update(self): + """ Process form data - store in context object. + May be overwritten by subclass. + """ + form = self.request.form + if not form.get('action'): + return True + obj = self.object + instance = component.getAdapter(obj, IInstance, name='editor') + instance.template = self.schema + self.formState = formState = instance.applyTemplate(data=form, + fieldHandlers=self.fieldHandlers) + if formState.severity > 0: + # show form again + return True + if formState.changed: + notify(ObjectModifiedEvent(obj)) + url = '%s?messsage=%s' % (self.nextUrl, self.message) + self.request.response.redirect(url) + return False + + @Lazy + def nextUrl(self): + return absoluteURL(self.object, self.request) + + +class CreateForm(Form): + + factory = None # overwrite! + message = u'Object created.' + + @Lazy + def object(self): + return self.factory() + + @Lazy + def container(self): + return self.context + + def update(self): + form = self.request.form + if not form.get('action'): + return True + obj = self.object + instance = component.getAdapter(obj, IInstance, name='editor') + instance.template = self.schema + self.formState = formState = instance.applyTemplate(data=form, + fieldHandlers=self.fieldHandlers) + if formState.severity > 0: + # show form again + return True + container = self.container + name = self.getName(obj) + container[name] = obj + notify(ObjectCreatedEvent(obj)) + notify(ObjectModifiedEvent(obj)) + url = '%s?messsage=%s' % (self.nextUrl, self.message) + self.request.response.redirect(url) + return False + + def getName(self, obj): + name = getattr(obj, 'name', getattr(obj, 'title')) + return INameChooser(container).chooseName(name, obj) diff --git a/composer/schema/browser/schema.py b/composer/schema/browser/schema.py index e72ca8d..7813782 100644 --- a/composer/schema/browser/schema.py +++ b/composer/schema/browser/schema.py @@ -32,6 +32,8 @@ from cybertools.composer.schema.schema import FormState class SchemaView(BaseView): + """ View for schema objects. + """ formState = FormState() @@ -94,6 +96,6 @@ class SchemaView(BaseView): if newClient: clientName = manager.addClient(client) self.setClientName(clientName) - self.request.response.redirect(self.nextUrl()) + self.request.response.redirect(self.getNextUrl()) return False diff --git a/composer/schema/field.py b/composer/schema/field.py index b661c5d..dc355ba 100644 --- a/composer/schema/field.py +++ b/composer/schema/field.py @@ -117,7 +117,7 @@ class FieldInstance(object): def unmarshall(self, value): return toUnicode(value) or u'' - def validate(self, value): + def validate(self, value, data=None): if not value and self.context.required: self.setError('required_missing') @@ -144,7 +144,7 @@ class NumberFieldInstance(FieldInstance): return None return int(value) - def validate(self, value): + def validate(self, value, data=None): if value in ('', None): if self.context.required: self.setError('required_missing') diff --git a/composer/schema/instance.py b/composer/schema/instance.py index 0156d39..fca6772 100644 --- a/composer/schema/instance.py +++ b/composer/schema/instance.py @@ -102,7 +102,7 @@ class Editor(BaseInstance): for f in self.template.fields: fi = f.getFieldInstance() value = data.get(f.name) - fi.validate(value) + fi.validate(value, data) formState.fieldInstances.append(fi) formState.severity = max(formState.severity, fi.severity) return formState @@ -202,7 +202,7 @@ class ClientInstanceEditor(ClientInstance): fi = f.getFieldInstance() #value = fi.unmarshall(data.get(f.name)) value = data.get(f.name) - fi.validate(value) + fi.validate(value, data) formState.fieldInstances.append(fi) formState.severity = max(formState.severity, fi.severity) return formState diff --git a/composer/schema/interfaces.py b/composer/schema/interfaces.py index c4bb1e0..b0edb16 100644 --- a/composer/schema/interfaces.py +++ b/composer/schema/interfaces.py @@ -202,9 +202,12 @@ class IFieldInstance(Interface): value given. """ - def validate(value): + def validate(value, data=None): """ Check if the value given is valid. Return an object implementing IFieldState. + + Optionally, in addition the full data set may be given to + allow for checking more than one data element. """ diff --git a/composer/schema/schema.py b/composer/schema/schema.py index 146d19a..adab4ef 100644 --- a/composer/schema/schema.py +++ b/composer/schema/schema.py @@ -80,6 +80,9 @@ class FormError(object): def __str__(self): return self.title + def __repr__(self): + return "FormError('%s')" % self.title + formErrors = dict( required_missing=FormError(u'Missing data for required field', diff --git a/organize/browser/service.py b/organize/browser/service.py index eee753d..e5c3b16 100644 --- a/organize/browser/service.py +++ b/organize/browser/service.py @@ -340,5 +340,5 @@ class RegistrationTemplateView(BaseView): if s in allServices and s not in newServices] regs.unregister(toDelete) #return True - self.request.response.redirect(self.nextUrl()) + self.request.response.redirect(self.getNextUrl()) return False