Today I had the requirement to display computed values when iterating over the schema of a dexterity type. I usually use properties or instance-methods to get such data but these values will not show up in the DefaultView of dexterity objects since that is generated by iterating over the schema.
Thus I implemented a ComputedField
similar to the same field in Archetypes. Here is the implementation and a example for use-case.
I would like to hear if you think there are valid use-cases for such a field. If there is interest I'm happy to release that as a collective package.
I also heard that you can achieve a similar result with a FieldProperty
(from zope.schema.fieldproperty
) but I still have to look into that. Does anyone have a code-example for that?
So here it is:
The configure.zcml
:
<include package="z3c.form" file="meta.zcml" />
<include package="z3c.form" />
<adapter factory=".computedfield.ComputedFieldWidget" />
<adapter factory=".computedfield.ComputedFieldDataConverter" />
<z3c:widgetTemplate
mode="display"
widget=".computedfield.IComputedWidget"
layer="z3c.form.interfaces.IFormLayer"
template="computed_display.pt"
/>
The Field and DataConverter (computedfield.py
):
# -*- coding: UTF-8 -*-
from plone.app.z3cform.interfaces import IPloneFormLayer
from z3c.form.browser.text import TextWidget
from z3c.form.interfaces import IDataConverter
from z3c.form.interfaces import IFieldWidget
from z3c.form.interfaces import ITextWidget
from z3c.form.interfaces import IWidget
from z3c.form.widget import FieldWidget
from zope.component import adapter
from zope.interface import implementer
from zope.schema import Field
from zope.schema._bootstrapfields import TextLine
from zope.schema.interfaces import IField
class IComputedField(IField):
prop = TextLine(
title="Instance Property",
description="A property",
required=False,
)
method = TextLine(
title="Instance Method",
description="A method",
required=False,
)
@implementer(IComputedField)
class ComputedField(Field):
"""A field that stores nothing but points to either
a property or a method of the instance.
Useful to access data when you want to access data that is not stored
on the instance when iterating over a schema. Use cases might be
Similar to the Archetypes Fields ComputedField or BackRef.
"""
def __init__(self, readonly=True, **kw):
self.prop = kw.pop('prop', '')
self.method = kw.pop('method', '')
if not self.prop and not self.method:
raise ValueError("ComputedField requires prop or method.")
if self.prop and self.method:
raise ValueError("ComputedField requires prop or method, not both.")
super(ComputedField, self).__init__(readonly=readonly, **kw)
class IComputedWidget(ITextWidget):
""" Computed Widget """
@implementer(IComputedWidget)
class ComputedWidget(TextWidget):
klass = u'computedfield-widget'
value = None
@adapter(IComputedField, IPloneFormLayer)
@implementer(IFieldWidget)
def ComputedFieldWidget(field, request):
return FieldWidget(field, ComputedWidget(request))
@adapter(IComputedField, IWidget)
@implementer(IDataConverter)
class ComputedFieldDataConverter(object):
"""A data converter that returns."""
def __init__(self, field, widget):
self.field = field
self.widget = widget
def toWidgetValue(self, value):
obj = self.widget.context
prop = self.field.prop
method = self.field.method
if prop:
return getattr(obj, prop, None)
if method:
method = getattr(obj, method, None)
if method:
return method()
The widget-template computed_display.pt
:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
tal:omit-tag="">
<span id="" class=""
tal:attributes="id view/id;
class view/klass;">
<span tal:condition="view/value"
tal:replace="structure view/value" />
</span>
</html>
An example use:
# -*- coding: utf-8 -*-
from collective.z3c.computedfield import ComputedField
from plone.app.textfield import RichText
from plone.dexterity.content import Container
from plone.supermodel import model
from zc.relation.interfaces import ICatalog
from zope import schema
from zope.component import getUtility
from zope.i18nmessageid import MessageFactory
from zope.interface import Invalid
from zope.interface import implementer
from zope.intid.interfaces import IIntIds
class IExample(model.Schema):
text = RichText(title='Some Text')
foo = ComputedField(
title='A computed Field using a property',
prop='some_property')
text2 = schema.TextLine(title='Some Text')
bar = ComputedField(
title='A computed Field using a method',
method='some_method')
incoming_links = ComputedField(
title='A computed Field using a method',
method='incoming_links')
@implementer(IExample)
class Example(Container):
@property
def some_property(self):
return 'Foo Property'
def some_method(self):
return 'Bar Method'
def incoming_links(self):
intids = getUtility(IIntIds)
int_id = intids.getId(self)
relation_catalog = getUtility(ICatalog)
results = []
for relation in relation_catalog.findRelations({'to_id': int_id}):
obj = relation.from_object
link = '<li><a href="{}">{} ({})</a></li>'.format(
obj.absolute_url(),
obj.title,
relation.from_attribute)
results.append(link)
return '<ul>{}</ul>'.format(''.join(results))
The method incoming_links
is probably a strech for that field. It would be better to write amother custom field like Products.ATBackRef
with a custom template and pass the relation in the field-definition of the schema.