Computed Field for Dexterity

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.

What if you add a readonly textline or testfield, and add a property to your class?
ie something like

ImALumberjack(zope.interface.Interface):
   isleep = zope.schema.TextLine(readonly=True)

class OK:
  @property
   def isleep(self):
       return u"All night, and I work all day"

I haven't tested this, I'm just guessing this should work :slight_smile:

3 Likes

Darn, you are right. I would not have expected that to simply work.

And if you want to show some html:
return chameleon.utils.Markup('<blink>Congrats 1_000_000th visitor!</blink>')

4 Likes

It would be great if this was added to the official docs. Just saying... :hugs:

1 Like

I'm clueless on where that should/could be added, so please help yourself :slight_smile:
https://docs.plone.org/about/contributing/index.html should help you get setup and ready to help out and contribute. Have fun!

There is a slight problem for advanced use-cases: In a @property the object itself (self) is not Acquisition-aware.
You can either use @ComputedAttribute instead (see https://stackoverflow.com/questions/12544129/zope-cannot-access-request-under-property-decorator) or use a hack like wrapped = api.content.get(UID=self.UID()) which makes wrapped the same as self but now it has aq-related features like __parent__ and absolute_url().

1 Like

I'd use ComputedAttribute, as it is more clear than wrapping via plone.api.

5 posts were split to a new topic: Computed Field on RichText