[Zope3-Users] Dict Widget

Frank Burkhardt fbo2 at gmx.net
Thu Jun 15 06:01:44 EDT 2006


Hi,

On Wed, Jun 14, 2006 at 02:41:05PM -0500, mats.nordgren wrote:
> Frank,
> 
> That would be great.  If you wish you can email it to me.
> 
> This should be included in the trunk IMHO.
> 
> Thanks,

I attached the widgets, zcml-statements to configure them and
modified Dict-implementation.

Good luck,

Frank
-------------- next part --------------
<configure xmlns:zope="http://namespaces.zope.org/zope"
	xmlns:i18n="http://namespaces.zope.org/i18n"
	xmlns="http://namespaces.zope.org/browser" i18n_domain="mpgsite">

	<!-- Dictionary widget dispatcher -->
	<zope:view type="zope.publisher.interfaces.browser.IBrowserRequest"
		provides="zope.app.form.interfaces.IInputWidget"
		for="zope.schema.interfaces.IDict"
		factory="mpgsite.browser.widgets.DictionaryWidgetFactory"
		permission="zope.Public" />

	<!-- Choice()-keyed Dictionary Widget -->
	<zope:view type="zope.publisher.interfaces.browser.IBrowserRequest"
		provides="zope.app.form.interfaces.IInputWidget"
		for="zope.schema.interfaces.IDict zope.schema.interfaces.IChoice zope.schema.interfaces.IField"
		factory="mpgsite.browser.widgets.ChoicyDictionaryWidget"
		permission="zope.Public" />

	<!-- Arbitrary Dictionary Widget -->
	<zope:view type="zope.publisher.interfaces.browser.IBrowserRequest"
		provides="zope.app.form.interfaces.IInputWidget"
		for="zope.schema.interfaces.IDict zope.schema.interfaces.IField zope.schema.interfaces.IField"
		factory="mpgsite.browser.widgets.SimpleDictionaryWidget"
		permission="zope.Public" />
</configure>
-------------- next part --------------
from zope.app.form.browser import ObjectWidget, ListSequenceWidget
from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
from zope.interface import implements
from zope.app import zapi
from zope.app.form.browser.objectwidget import ObjectWidgetView, ObjectWidget
from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
from mpgsite.interfaces import IMpgSequenceField
from zope.app.form.browser.widget import BrowserWidget
from zope.app.form.interfaces import IDisplayWidget, IInputWidget
from zope.app.form import InputWidget
from zope.app.form.interfaces import WidgetInputError, MissingInputError
from zope.schema.interfaces import ValidationError, InvalidValue
from zope.app.i18n import MessageFactory
_=MessageFactory('mpgsite')
from zope.i18n import translate
from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
from zope.app.form.browser.widget import renderElement
from zope.app.form.interfaces import ConversionError
from zope.app.form.browser import TextWidget, SequenceDisplayWidget
from zope.security.proxy import removeSecurityProxy
import sys
from zope.schema import Object
from zope.annotation.interfaces import IAnnotations

class MpgTextWidget(TextWidget):
	def _toFieldValue(self, input):
		try:
			value = unicode(input)
		except ValueError, v:
			raise ConversionError(_("Invalid text data"), v)
		return value


class I18NTextLineWidget(MpgTextWidget):
   def __call__(self):
        value = self._getFormValue()
        if value is None or value == self.context.missing_value:
            value = ''

        kwargs = {'type': self.type,
                  'name': self.name,
                  'id': self.name,
                  'value': value,
                  'cssClass': self.cssClass,
                  'style': self.style,
                  'extra': self.extra}
        if self.displayMaxWidth:
            kwargs['maxlength'] = self.displayMaxWidth # TODO This is untested.

        return renderElement(self.tag, **kwargs)

class SimpleObjectWidget(ObjectWidget):
    """A Widget that shows all widgets of an object"""
    def __call__(self,context,request):
        xhtml=''
        for widget in context.subwidgets:
            xhtml +=widget()
        return xhtml


def ObjectInputWidgetDispatcher(context, request):
    """Dispatch widget for Object schema field to widget that is
    registered for (IObject, schema, IBrowserRequest) where schema
    is the schema of the object."""
    class Obj(object):
        implements(context.schema)

    widget=zapi.getMultiAdapter((context, Obj(), request), IInputWidget)
    return widget

class ObjectInputWidget(ObjectWidget):
	def getInputValue(self):
		errors = []
		content = self.factory()
		for name in self.names:
			try:
				setattr(content, name, self.getSubWidget(name).getInputValue())
			except Exception, e:
				errors.append(e)
				if self._error is None:
					self._error = {}

				if name not in self._error:
					self._error[name] = e

		# Don't raise errors when widget operations (add to list, remove element, ...) are processed
		if ( 'mpgsite.no_form_action' not in IAnnotations(self.request) ) and errors:
			raise errors[0]

		return content

def FileLocal(filename,depth):
    path='/'.join(sys._getframe(depth).f_globals['__file__'].split('/')[:-1])
    return path + '/' +  filename
    

class TemplateObjectWidget(ObjectWidget):
    """A Widget that uses a page template"""
    def __init__(self, context, request, factory, template_, **kw):
        super(TemplateObjectWidget, self).__init__(context, request, factory, **kw)
        class TemplateObjectWidgetView(ObjectWidgetView):
            template = ViewPageTemplateFile(template_)
        self.view = TemplateObjectWidgetView(self, request)

def TemplateObjectWidgetFactory(context,request,factory,template):
    widget=TemplateObjectWidget(context,request,factory,FileLocal(template,2))
    return widget


class TemplateSequenceWidget(ListSequenceWidget):
    def __init__(self, context, field, request, subwidget=None):
        super(TemplateSequenceWidget, self).__init__(context, field, request, subwidget)

        # This isn't really related to an ObjectView but provides a convinient
        # way of providing a template base Widget.
        class TemplateObjectWidgetView(ObjectWidgetView):
            template = ViewPageTemplateFile("mpgl1sequence.pt")

        self.view = TemplateObjectWidgetView(self, request)

    def __call__(self):
        return self.view()


def DictionaryWidgetFactory(field,request):
	widget=zapi.getMultiAdapter((field,field.key_type,field.value_type,request),IInputWidget)
	return widget



class SimpleDictionaryWidget(BrowserWidget, InputWidget):
	"""A widget for editing arbitrary dictionaries
	
		key_editsubwidget - optional edit subwidget for key components
		key_displaysubwidget - optional display subwidget for key components
		value_editsubwidget - optional edit subwidget for value components
	"""
	implements(IInputWidget)
	_type= dict
	
	def __init__(self,context,key_type,value_type,request,key_editsubwidget=None,key_displaysubwidget=None,value_editsubwidget=None):
		super(SimpleDictionaryWidget,self).__init__(context,request)
		self.key_editsubwidget=key_editsubwidget
		self.key_displaysubwidget=key_displaysubwidget
		self.value_editsubwidget=value_editsubwidget
		self.context.key_type.bind(object())
		self.mayadd=True

	def _widgetpostproc(self,widget,key,keyorvalue):
		"""For manipulating css classes of given elements"""

	def _sortkeys(self,keys):
		newkeys=[x for x in keys.__iter__()]
		newkeys.sort()
		return newkeys

	def _renderKeyAndCheckBox(self,render,key,i):
		render.append('<input class="editcheck" type="checkbox" name="%s.remove_%d" />' %(self.name,i))
		keydisplaywidget=self._getWidget(str(i),IDisplayWidget,self.context.key_type,self.key_displaysubwidget,'key-display')
		self._widgetpostproc(keydisplaywidget,key,'key-display')
		keydisplaywidget.setRenderedValue(key)
		render.append(keydisplaywidget())

	def _renderitems(self,render):
		keys=self._data.keys()
		keys=self._sortkeys(keys)
		for i in range(len(keys)):
			key=keys[i]
			value=self._data[key]
			render.append('<div>')
			render.append('<span>')
			self._renderKeyAndCheckBox(render,key,i)
			keyhiddenwidget=self._getWidget(str(i),IInputWidget,self.context.key_type,self.key_editsubwidget,'key')
			keyhiddenwidget.setRenderedValue(key)
			render.append(keyhiddenwidget.hidden())
			render.append('</span>')
			valuewidget=self._getWidget(str(i),IInputWidget,self.context.value_type,self.value_editsubwidget,'value')
			self._widgetpostproc(valuewidget,key,'value-edit')
			valuewidget.setRenderedValue(value)
			render.append('<span>' + valuewidget() + '</span></div>')
	
	def _renderbuttons(self,render):
		buttons = ''
		if ( len(self._data)>0 ) and len(self._data) > self.context.min_length:
			button_label = _('remove-selected-items', "Remove selected items")
			button_label = translate(button_label, context=self.request,default=button_label)
			buttons += ('<input type="submit" value="%s" name="%s.remove"/>' % (button_label, self.name))
		if (self.context.max_length is None or len(self._data) < self.context.max_length) and self.mayadd:
			field = self.context.value_type
			button_label = _('Add %s')
			button_label = translate(button_label, context=self.request, default=button_label)
			button_label = button_label % (field.title or field.__name__)
			buttons += '<input type="submit" name="%s.add" value="%s" />' % (self.name, button_label)
			self._keypreproc()
			newkeywidget=self._getWidget('new',IInputWidget,self.context.key_type,self.key_editsubwidget,'key')
			self._keypostproc()
			self._widgetpostproc(newkeywidget,'','key-edit')
			render.append('<div><span>%s</span></div>' %(newkeywidget(),) )
		if buttons:
			render.append('<div><span>%s</span></div>' % buttons)
	
	def __call__(self):
		"""Render the Widget"""
		assert self.context.key_type is not None
		assert self.context.value_type is not None
		render=[]
		render.append('<div><div id="%s">' % (self.name,))
		if not self._getRenderedValue():
			if self.context.default is not None:
				self._data=self.context.default
			else:
				self._data=self._type()
		
		self._renderitems(render)
		
		render.append('</div>')
		# possibly generate the "remove" and "add" buttons
		self._renderbuttons(render)
		
		render.append(self._getPresenceMarker(len(self._data)))
		render.append('</div>')
		text="\n".join(render)
		return text

	def _getWidget(self,i,interface,value_type,customwidget,mode):
		if customwidget is not None:
			widget=zapi.getMultiAdapter((value_type,self.request),interface,name=self.customwidget)
		else:
			widget=zapi.getMultiAdapter((value_type,self.request),interface)
		widget.setPrefix('%s.%s.%s.'%(self.name,i,mode))
		return widget

	def hidden(self):
		self._getRenderedValue()
		keys=self._data.keys()
		parts=[self._getPresenceMarker(len(self._data))]

		for i in range(len(keys)):
			key=keys[i]
			value=self._data[key]
			keywidget=self._getWidget(str(i),IInputWidget,self.context.key_type,self.key_displaysubwidget,'key')
			keywidget.setRenderedValue(key)
			valuewidget=self._getWidget(str(i),IInputWidget,self.context.value_type,self.value_editsubwidget,'value')
			parts.append(keywidget.hidden() + valuewidget.hidden())

		return "\n".join(parts)

	def _getPresenceMarker(self, count=0):
		return ('<input type="hidden" name="%s.count" value="%d" />'% (self.name, count))

	def _getRenderedValue(self):
		if not self._renderedValueSet():
			if self.hasInput():
				self._data=self._generateDict()
			else:
				self._data={}
		if self._data is None:
			self._data=self._type()
		if len(self._data) < self.context.min_length:
			"""Don't know, what to do here :-("""
		return self._data

	def getInputValue(self):
		if self.hasInput():
			dict=self._type(self._generateDict())
			if dict != self.context.missing_value:
				self.context.validate(dict)
			elif self.context.required:
				raise MissingInputError(self.context.__name__,self.context.title)
			return dict
		raise MissingInputError(self.context.__name__, self.context.title)
	
	def applyChanges(self,content):
		field=self.context
		value=self.getInputValue()
		change=field.query(content,self) != value
		if change:
			field.set(content,value)
		return change

	def hasInput(self):
		return (self.name+".count") in self.request.form

	def _generateDict(self):
		len_prefix=len(self.name)
		adding=False
		removing=[]
		if self.context.value_type is None:
			return []

		try:
			count=int(self.request.form[self.name+".count"])
		except ValueError:
			raise WidgetInputError(self.context.__name__, self.context.title)

		keys={}
		values={}
		for i in range(count):
			remove_key="%s.remove_%d" % (self.name,i)
			if remove_key not in self.request.form:
				keywidget=self._getWidget(str(i),IInputWidget,self.context.key_type,self.key_displaysubwidget,'key')
				valuewidget=self._getWidget(str(i),IInputWidget,self.context.value_type,self.value_editsubwidget,'value')
				keys[i]=keywidget.getInputValue()
				values[i]=valuewidget.getInputValue()
		adding=(self.name+".add") in self.request.form
		
		mykeys=keys.items()
		mykeys.sort()
		dict={}
		for (i,key) in mykeys:
			dict[key]=values[i]
			
		if adding:
			newkeywidget=self._getWidget('new',IInputWidget,self.context.key_type,self.key_displaysubwidget,'key')
			newkey=newkeywidget.getInputValue()
			self.context.key_type.validate(newkey)
			if dict.has_key(newkey):
				raise InvalidValue
			dict[newkey]=self.context.value_type.missing_value
			
		return dict

	def _keypreproc(self):
		"""Only for subclassing"""

	def _keypostproc(self):
		"""Only for Subclassing"""

class FilterVocabulary(SimpleVocabulary):
	"""Removes All terms from a vocabulary that are contained
		in a given dictionary. This is useful for filtering
		the vocabulary that is used to fill the new Choice-Widget
		of a Dict-Widget"""
	def __init__(self,vocabulary,dictionary):
		terms=[]
		self.empty=True
		for term in vocabulary._terms:
			if not dictionary.has_key(term.value):
				terms.append(term)
				self.empty=False
		SimpleVocabulary.__init__(self,terms)

class ChoicyDictionaryWidget(SimpleDictionaryWidget):
	"""This widget reduces available choices in a key_value-choice to only
		include values not already used"""
	def _keypreproc(self):
		"""We use the keypreproc-hook to install a filter vocabulary
			that removes all choices from the original vocabulary that
			are already used."""
		if self.context.key_type.vocabulary is None:
			return
		self.old_key_vocabulary=self.context.key_type.vocabulary
		self.context.key_type.vocabulary=FilterVocabulary(self.context.key_type.vocabulary,self._data)
		if self.context.key_type.vocabulary.empty:
			self.mayadd=False
			

	def _keypostproc(self):
		"""Reinstall the original dictionary"""
		if self.context.key_type.vocabulary is None:
			return
		self.context.key_type.vocabulary=self.old_key_vocabulary

	def _renderbuttons(self,render):
		self._keypreproc()
		super(ChoicyDictionaryWidget,self)._renderbuttons(render)
		self._keypostproc()

class PathWidget(MpgTextWidget):
    """A Widget from zope.app.homefolder for entering absolute paths to objects"""
    def _toFieldValue(self, input):
        path = super(PathWidget, self)._toFieldValue(input)
        root = zapi.getRoot(self.context.context)
        try:
            proxy = zapi.traverse(root, path)
        except TraversalError, e:
            raise ConversionError(_('path is not correct !'), e)
        else:
            return removeSecurityProxy(proxy)

    def _toFormValue(self, value):
        if value is None:
            return ''
        return zapi.getPath(value)

class MpgSetInputWidget(ListSequenceWidget):
	_type=set
	def _generateSequence(self):
		"""This is a modified method to provide functionality
			for a +/- -controlled SetWidget"""
		if self.context.value_type is None:
			return set([])
		try:
			count = int(self.request.form[self.name + ".count"])
		except ValueError:
			raise WidgetInputError(self.context.__name__, self.context.title)

		# pre-populate
		sequence=[]

		for i in range(count):
			widget = self._getWidget(i)
			if widget.hasValidInput():
				# catch and set sequence widget errors to ``_error`` attribute
				try:
					sequence.append(widget.getInputValue())
				except WidgetInputError, error:
					self._error = error
					raise self._error

			remove_key = "%s.remove_%d" % (self.name, i)
			add_key = "%s.add_%d" % (self.name, i)
			if add_key in self.request.form:
				sequence.append(self.context.value_type.missing_value)
			if remove_key in self.request.form:
				del sequence[i]
		
		if (self.name + '.add') in self.request.form:
			sequence.append(self.context.value_type.missing_value)
		return set(sequence)
	
	def _getRenderedValue(self):
		value=super(MpgSetInputWidget,self)._getRenderedValue()
		return set(value)
	
	def __call__(self):
		self._update()
		class TemplateObjectWidgetView(ObjectWidgetView):
			template = ViewPageTemplateFile("mpgpml1set.pt")
		template = TemplateObjectWidgetView(self, self.request)
		
		return template()

class SetDisplayWidget(SequenceDisplayWidget):
	tag='ul'
	cssClass="setWidget"
	# TODO: missing-value-messages abgleichen


class MpgListInputWidget(ListSequenceWidget):
	def _generateSequence(self):
		"""This is a modified method to provide functionality
			for a +/- -controlled SequenceWidget"""
		if self.context.value_type is None:
			return []
		try:
			count = int(self.request.form[self.name + ".count"])
		except ValueError:
			raise WidgetInputError(self.context.__name__, self.context.title)

		# pre-populate
		sequence = [None] * count
		found_up=None
		found_down=None
		found_remove=False
		found_add=None
		for i in reversed(range(count)):
			widget = self._getWidget(i)
			if widget.hasValidInput():
				# catch and set sequence widget errors to ``_error`` attribute
				try:
					sequence[i] = widget.getInputValue()
				except WidgetInputError, error:
					self._error = error
					raise self._error

			remove_key = "%s.remove_%d" % (self.name, i)
			add_key = "%s.add_%d" % (self.name, i)
			up_key = "%s.up_%d" % (self.name,i)
			down_key = "%s.down_%d" % (self.name,i)
			if add_key in self.request.form:
				found_add=i
			if remove_key in self.request.form:
				del sequence[i]
				found_remove=True
			if down_key in self.request.form:
				found_down=i
			if up_key in self.request.form:
				found_up=i
		if not found_remove:
			if found_up is not None:
				temp=sequence[found_up-1]
				sequence[found_up-1]=sequence[found_up]
				sequence[found_up]=temp
			if found_down is not None:
				temp=sequence[found_down+1]
				sequence[found_down+1]=sequence[found_down]
				sequence[found_down+1]=temp
			if found_add is not None:
				sequence[found_add:0]=[self.context.value_type.default]
					
		if (self.name + '.add') in self.request.form:
			new=self.context.value_type.default
			import pdb;pdb.set_trace()
			if (new is None) and isinstance(self.context.value_type,Object):
				widget=zapi.getMultiAdapter((self.context.value_type,self.request),IInputWidget)
				new=widget.factory()
			sequence.append(new)
			
		return sequence
	
	def __call__(self):
		self._update()
		self.listcontrollerclasses=''
		if isinstance(self.context.value_type ,Object):
			self.listcontrollerclasses='Object'

		class TemplateObjectWidgetView(ObjectWidgetView):
			template = ViewPageTemplateFile("mpgpml1sequence.pt")
		template = TemplateObjectWidgetView(self, self.request)
		return template()

	def _getRenderedValue(self):
		"""Returns a sequence from the request or _data"""
		if self._renderedValueSet():
			if self._data is None:
				sequence=[]
			else:
				sequence = list(self._data)
		elif self.hasInput():
			sequence = self._generateSequence()
		else:
			sequence = []
		# ensure minimum number of items in the form
		while len(sequence) < self.context.min_length:
			# Shouldn't this use self.field.value_type.missing_value,
			# instead of None?
			sequence.append(self.context.value_type.default)
		return sequence


def ObjectSequenceWidget(listfield,objectfield,request):
	"""Dispatcher Widget that tries to find a specialized list widget for a
		given Object()-schema with fallback to Object()-default-widget"""
	widget=zapi.queryMultiAdapter((listfield,objectfield.schema,request),IInputWidget)
	if widget is None:
		return MpgListInputWidget(listfield,objectfield,request)
	return widget

-------------- next part --------------
from zope.schema.interfaces import WrongContainedType, ValidationError
from zope.schema import Dict
from zope.schema._field import AbstractCollection

def _validate_dict(value_type,key_type, value, errors=None):
	if errors is None:
		errors=[]
	if value_type is None:
		return errors
	if key_type is None:
		return errors
	
	for (key,vl) in value.items():
		try:
			key_type.validate(key)
		except ValidationError, error:
			errors.append(error)
		try:
			value_type.validate(vl)
		except ValidationError, error:
			errors.append(error)
	return errors

class FixedDict(Dict,AbstractCollection):
	"""We have to fix the bind-method of Dict"""
	def bind(self, object):
		clone=AbstractCollection.bind(self,object)
		if clone.key_type is not None:
			clone.key_type = clone.key_type.bind(object)
		return clone

	def _validate(self,value):
		errors=_validate_dict(self.value_type,self.key_type,value)
		if len(errors) > 0:
			raise WrongContainedType(errors)


More information about the Zope3-users mailing list