Dexterity, datagrids and relations

552 views
Skip to first unread message

Maurits van Rees

unread,
Apr 20, 2011, 10:54:37 AM4/20/11
to dexterity-...@googlegroups.com
Hi all,

This is my first post to the dexterity list and it is a long one, so I
apologize in advance. :-) I typed it as I went along testing. I list
several changes that I can probably safely commit. At the end there is
still a problem, so if you don't want to read all this, here is a simple
question instead: how do you get 'getSite()' to return the Plone Site
root instead of some Z3CFormValidation object that breaks expectations
in several parts of the code?

So now the long version. I am trying out
collective.z3cform.datagridfield with dexterity. Seems to work fine, so
thank you! But things go wrong once you want to add a RelationChoice in
the datagrid. Might be considered a corner case, but I assume this is
something that we ideally do want to work correctly.

My test setup is as follows. I am using the dexterity development
buildout at
https://svn.plone.org/svn/plone/plone.dexterity/buildouts/dev. I have
committed a change in the sources.cfg to add
collective.z3cform.datagridfield_demo as that seems handy for testing.
Locally in base.cfg I have added collective.z3cform.datagridfield and
collective.z3cform.datagridfield_demo to the auto-checkout and the
eggs. And in buildout.cfg I am extending plone41.cfg instead of
plone4.cfg (but I have seen the same behaviour with plone4).

The @@demo-collective.z3cform.datagrid page that the demo makes
available seems to work fine.

Now to test the RelationChoice within a datagridfield I changed
collective/z3cform/datagridfield_demo/browser/simple.py (not something I
should commit I think, just for testing locally):

Index: simple.py
===================================================================
--- simple.py (revision 237886)
+++ simple.py (working copy)
@@ -16,11 +16,18 @@
from collective.z3cform.datagridfield import DataGridFieldFactory
from collective.z3cform.datagridfield import DictRow

+from z3c.relationfield.schema import RelationChoice
+from plone.formwidget.contenttree import ObjPathSourceBinder

+
class IAddress(Interface):
address_type = schema.Choice(
title = u'Address Type', required=True,
values=[u'Work', u'Home'])
+ affiliate = RelationChoice(
+ title=u"Affiliation",
+ source=ObjPathSourceBinder(),
+ required=True)
line1 = schema.TextLine(
title = u'Line 1', required=True)
line2 = schema.TextLine(


Now when I restart Plone and visit @@demo-collective.z3cform.datagrid I
get this traceback:

2011-04-20 12:28:31 ERROR Zope.SiteErrorLog 1303295311.30.655257484827
http://localhost:8080/Plone/@@demo-collective.z3cform.datagrid
Traceback (innermost last):
Module ZPublisher.Publish, line 126, in publish
Module ZPublisher.mapply, line 77, in mapply
Module Products.PDBDebugMode.runcall, line 70, in pdb_runcall
Module ZPublisher.Publish, line 46, in call_object
Module z3c.form.form, line 215, in __call__
Module z3c.form.form, line 208, in update
Module plone.z3cform.patch, line 21, in BaseForm_update
Module z3c.form.form, line 149, in update
Module collective.z3cform.datagridfield_demo.browser.simple, line 94,
in updateWidgets
Module z3c.form.form, line 134, in updateWidgets
Module z3c.form.field, line 275, in update
Module z3c.form.browser.multi, line 61, in update
Module z3c.form.browser.widget, line 70, in update
Module z3c.form.widget, line 123, in update
Module z3c.form.widget, line 383, in set
Module collective.z3cform.datagridfield.datagridfield, line 96, in
updateWidgets
Module z3c.form.widget, line 352, in updateWidgets
Module z3c.form.widget, line 341, in applyValue
Module collective.z3cform.datagridfield.datagridfield, line 198, in set
Module z3c.form.object, line 229, in applyValue
Module z3c.form.validator, line 67, in validate
Module zope.schema._bootstrapfields, line 153, in validate
Module zope.schema._field, line 325, in _validate
TypeError: argument of type 'ObjPathSourceBinder' is not iterable

Looks like that can be fixed in plone/formwidget/contenttree/source.py
by adding this to the PathSourceBinder class:

def __contains__(self, value):
return value in self(context=None)

Reloading the page gives the next partial traceback:

Module collective.z3cform.datagridfield.datagridfield, line 198, in set
Module z3c.form.object, line 229, in applyValue
Module z3c.form.validator, line 67, in validate
Module zope.schema._bootstrapfields, line 153, in validate
Module zope.schema._field, line 325, in _validate
Module plone.formwidget.contenttree.source, line 185, in __contains__
Module plone.formwidget.contenttree.source, line 95, in __contains__
Module plone.formwidget.contenttree.source, line 153, in _path_for_value
AttributeError: 'NO_VALUE' object has no attribute 'getPhysicalPath'

A few more changes in source.py and we can fix that as well:

Index: source.py
===================================================================
--- source.py (revision 48972)
+++ source.py (working copy)
@@ -93,18 +93,24 @@
def __contains__(self, value):
try:
brain = self._brain_for_path(self._path_for_value(value))
+ if brain is None:
+ return False
return self._filter(brain)
except KeyError:
return False

def getTermByToken(self, token):
brain = self._brain_for_path(self._path_for_token(token))
+ if brain is None:
+ raise LookupError(token)
if not self._filter(brain):
raise LookupError(token)
return self._term_for_brain(brain)

def getTerm(self, value):
brain = self._brain_for_path(self._path_for_value(value))
+ if brain is None:
+ raise LookupError(value)
if not self._filter(brain):
raise LookupError(value)
return self._term_for_brain(brain)
@@ -140,7 +146,8 @@

def _brain_for_path(self, path):
rid = self.catalog.getrid(path)
- return self.catalog._catalog[rid]
+ if rid is not None:
+ return self.catalog._catalog[rid]

def _term_for_brain(self, brain, real_value=True):
path = brain.getPath()[len(self.portal_path):]
@@ -150,7 +157,10 @@
class ObjPathSource(PathSource):

def _path_for_value(self, value):
- return '/'.join(value.getPhysicalPath())
+ try:
+ return '/'.join(value.getPhysicalPath())
+ except AttributeError:
+ return

def _term_for_brain(self, brain, real_value=True):
path = brain.getPath()[len(self.portal_path):]


After this, the @@demo-collective.z3cform.datagridfield page loads
correctly. Within the datagrid I now have the RelationChoice field and
I can use that to link each row to a content item; at least on Plone 4.1
this works for both Archetypes and Dexterity content.

So far so good actually. But on Plone 4.0 I tried this in a custom
dexterity content type and it did not work. I tried a few more changes
there but still got tracebacks. Let's see if I can show what goes wrong
in this setup with 4.1 as well. If it works after all, then I am happy
too. :-)

Okay, I installed all of the example packages that are in the dexterity
buildout. In example/conference/program.py I added the same IAddress
interface as in the changed
collective/z3cform/datagridfield_demo/browser/simple.py from above:

from zope.interface import Interface
from collective.z3cform.datagridfield import DictRow
from plone.formwidget.contenttree import ObjPathSourceBinder
from z3c.relationfield.schema import RelationChoice
from collective.z3cform.datagridfield.datagridfield import
DataGridFieldFactory


class IAddress(Interface):
address_type = schema.Choice(
title = u'Address Type', required=True,
values=[u'Work', u'Home'])
affiliate = RelationChoice(
title=u"Affiliation",
source=ObjPathSourceBinder(),
required=True)
line1 = schema.TextLine(
title = u'Line 1', required=True)
line2 = schema.TextLine(
title = u'Line 2', required=False)
city = schema.TextLine(
title = u'City / Town', required=True)
country = schema.TextLine(
title = u'Country', required=True)

And in the IProgram schema I added this field:

form.widget(address=DataGridFieldFactory)
address = schema.List(title=u'Addresses',
value_type=DictRow(title=u'Address', schema=IAddress),
required=True)

Restart Plone. Now while adding or editing a Program in the Plone Site
root the validation is triggered and gives tracebacks in the error log:

Traceback (innermost last):
Module ZPublisher.Publish, line 126, in publish
Module ZPublisher.mapply, line 77, in mapply
Module Products.PDBDebugMode.runcall, line 70, in pdb_runcall
Module ZPublisher.Publish, line 46, in call_object
Module <wrapper>, line 5, in wrapper
Module kss.core.actionwrapper, line 236, in apply
Module plone.app.z3cform.kss.validation, line 48, in validate_input
Module plone.dexterity.browser.edit, line 46, in update
Module plone.z3cform.fieldsets.extensible, line 59, in update
Module plone.z3cform.patch, line 30, in GroupForm_update
Module z3c.form.group, line 125, in update
Module z3c.form.form, line 134, in updateWidgets
Module z3c.form.field, line 275, in update
Module z3c.form.browser.multi, line 61, in update
Module z3c.form.browser.widget, line 70, in update
Module z3c.form.widget, line 84, in update
Module z3c.form.widget, line 402, in extract
Module z3c.form.widget, line 294, in getWidget
Module z3c.form.browser.widget, line 70, in update
Module z3c.form.object, line 213, in update
Module z3c.form.widget, line 84, in update
Module z3c.form.object, line 266, in extract
Module z3c.form.object, line 205, in updateWidgets
Module z3c.form.object, line 85, in update
Module plone.z3cform.patch, line 21, in BaseForm_update
Module z3c.form.form, line 149, in update
Module z3c.form.form, line 134, in updateWidgets
Module z3c.form.field, line 275, in update
Module z3c.formwidget.query.widget, line 98, in update
Module z3c.formwidget.query.widget, line 85, in bound_source
Module z3c.formwidget.query.widget, line 80, in source
Module zope.schema._field, line 287, in bind
Module plone.formwidget.contenttree.source, line 192, in __call__
Module plone.formwidget.contenttree.source, line 66, in __init__
Module plone.formwidget.contenttree.navtree, line 43, in __call__
Module plone.app.layout.navigation.root, line 32, in getNavigationRoot
AttributeError: 'Z3CFormValidation' object has no attribute
'getPhysicalPath'

Actually, that one is without using the DataGridFieldFactory. With that
factory, the traceback is very similar though:

Traceback (innermost last):
...
Module collective.z3cform.datagridfield.datagridfield, line 217, in
updateWidgets
Module z3c.form.form, line 134, in updateWidgets
Module z3c.form.field, line 275, in update
Module z3c.formwidget.query.widget, line 98, in update
Module z3c.formwidget.query.widget, line 85, in bound_source
Module z3c.formwidget.query.widget, line 80, in source
Module zope.schema._field, line 287, in bind
Module plone.formwidget.contenttree.source, line 192, in __call__
Module plone.formwidget.contenttree.source, line 66, in __init__
Module plone.formwidget.contenttree.navtree, line 43, in __call__
Module plone.app.layout.navigation.root, line 32, in getNavigationRoot
AttributeError: 'Z3CFormValidation' object has no attribute
'getPhysicalPath'

The problem here is that the context that is used here is not the
Program content item that I am editing, but a Z3CFormValidation object:

(Pdb) aq_chain(context)
[<Products.Five.metaclass.Z3CFormValidation object at 0x10a536990>,
<Container at /Plone/plone-conference>, <PloneSite at /Plone>,
<Application at >, <ZPublisher.BaseRequest.RequestContainer object at
0x10a536dd0>]

The problem is apparent in the call method of the PathSourceBinder class:

def __call__(self, context):
content = context
if not IAcquirer.providedBy(content):
content = getSite()
return self.path_source(
content,
selectable_filter=self.selectable_filter,
navigation_tree_query=self.navigation_tree_query)

Here content can be Z3CFormValidation (also seen:
z3c.form.interfaces.NO_VALUE) and then the getSite call does not return
the Plone Site root:

(Pdb) getSite()
<Products.Five.metaclass.Z3CFormValidation object at 0x108253250>

This object does not provide the required IAcquirer interface, so the
call to self.path_source ends with the above traceback. Okay, we can
try working around that. Put this in the call method:

if not IAcquirer.providedBy(content):
content = getSite()
if not IAcquirer.providedBy(content):
return EmptySource(content)

And define the EmptySource class:

class EmptySource(object):
implements(IContentSource)

def __init__(self, context, selectable_filter=None,
navigation_tree_query=None):
self.context = context
# Maybe just always set these to None, ignoring keyword arguments.
self.selectable_filter = selectable_filter
self.navigation_tree_query = navigation_tree_query

# Tokenised vocabulary API

def __iter__(self):
return [].__iter__()

def __contains__(self, value):
return False

def getTermByToken(self, token):
raise LookupError(token)

def getTerm(self, value):
raise LookupError(value)

# Query API - used to locate content, e.g. in non-JS mode

def search(self, query, limit=20):
return []


With this code it works a bit. Except that while editing you get red
borders as the validation is failing. Ah, and now when editing you also
get an error because the content item you selected cannot be found as
the value of the field is an item without acquisition chain so it's
brain cannot be found by its physical path. And it only works
reasonable with the datagridwidget because I first started editing
without that widget so there was already some data; in a completely new
add-form the addresses field just gives the headers for the datagrid
columns and you cannot add any rows.

In other words, when you want to have a RelationChoice column in a
datagrid field, or probab ly in any schema that has such a table-like
field, it is brittle at best. I think the first few changes listed
above are probably fine. For the last part I think the key question
might be: how do you get 'getSite()' to work correctly when you do not
have a proper context?

Any ideas?

For the record, a simple RelationChoice field outside of a datagrid
works fine for me, but the client wants to include some more information
for each chosen relation (a string or selection field and a boolean
field), so a datagrid-like solution sounds like a good fit if it works.
An alternative idea would be welcome as well.

Cheers,

--
Maurits van Rees
Web App Programmer at Zest Software: http://zestsoftware.nl
Personal website: http://maurits.vanrees.org/

Martin Aspeli

unread,
Apr 20, 2011, 11:19:35 AM4/20/11
to dexterity-...@googlegroups.com
On 20 April 2011 15:54, Maurits van Rees <mau...@vanrees.org> wrote:
> Hi all,
>
> This is my first post to the dexterity list and it is a long one, so I
> apologize in advance. :-)  I typed it as I went along testing. I list
> several changes that I can probably safely commit.  At the end there is
> still a problem, so if you don't want to read all this, here is a simple
> question instead: how do you get 'getSite()' to return the Plone Site root
> instead of some Z3CFormValidation object that breaks expectations in several
> parts of the code?

WTF is the Z3CFormValidation object?

Is it to do with KSS inline validation?

> So now the long version.  I am trying out collective.z3cform.datagridfield
> with dexterity.  Seems to work fine, so thank you!  But things go wrong once
> you want to add a RelationChoice in the datagrid.  Might be considered a
> corner case, but I assume this is something that we ideally do want to work
> correctly.

I think it's an important use case.

That's pretty nuts. It means Z3CFormValidation is a local site manager
that is set upon traversal (zope.site.hooks.setSite()). Why it would
need to be, I have no idea.

Inline validation of data grid fields seems really tricky anyway. I'd
probably just find a way to turn it off and not worry about it.

> In other words, when you want to have a RelationChoice column in a datagrid
> field, or probab ly in any schema that has such a table-like field, it is
> brittle at best.  I think the first few changes listed above are probably
> fine.  For the last part I think the key question might be: how do you get
> 'getSite()' to work correctly when you do not have a proper context?
>
> Any ideas?
>
> For the record, a simple RelationChoice field outside of a datagrid works
> fine for me, but the client wants to include some more information for each
> chosen relation (a string or selection field and a boolean field), so a
> datagrid-like solution sounds like a good fit if it works. An alternative
> idea would be welcome as well.

I think we probably have to fix/disable the inline validation stuff in
the first instance.

Martin

Baumann Jonas

unread,
Apr 21, 2011, 7:05:59 AM4/21/11
to dexterity-...@googlegroups.com

Am 20.04.2011 um 17:19 schrieb Martin Aspeli:

> On 20 April 2011 15:54, Maurits van Rees <mau...@vanrees.org> wrote:
>> Hi all,
>>
>> This is my first post to the dexterity list and it is a long one, so I
>> apologize in advance. :-) I typed it as I went along testing. I list
>> several changes that I can probably safely commit. At the end there is
>> still a problem, so if you don't want to read all this, here is a simple
>> question instead: how do you get 'getSite()' to return the Plone Site root
>> instead of some Z3CFormValidation object that breaks expectations in several
>> parts of the code?
>
> WTF is the Z3CFormValidation object?
>
> Is it to do with KSS inline validation?

The Z3CFormValidation object is set with setSite() while KSS validation is
running. Using some kind of vocabularies and structures may cause problems
then.

I'd propose to just use a IVocabularyFactory (see Named vocabularies section at
http://plone.org/products/dexterity/documentation/manual/developer-manual/advanced/vocabularies),
which will get a context passed in __call__.

If you need to call other stuff from there which use getSite(), you may need to set
the site temporarily.
Example:

class MyVocabulary(grok.GlobalUtility):

grok.provides(IVocabularyFactory)
grok.name('myVocabulary')

def __call__(self, context):
# kss validation overrides getSite() hook with a bad object
# but we need getSite to work properly, so we fix it.
site = getSite()
if site.__class__.__name__ == 'Z3CFormValidation':
fixed_site = getToolByName(self.context,
'portal_url').getPortalObject()

setSite(fixed_site)
vocab = self.generate_vocab()
setSite(site)
return vocab

else:
return self.generate_vocab()

def generate_vocab(self):
return do_some_stuff_using_getSite()

We had the same problem and "resolved" it that way, but it would be indeed better
to stop KSS overwriting the site hook (or at least make the Z3CFormValidation validation
object aq-wrapped, which would make it possible to use other stuff like getToolByName then).

Cheers
Jonas
>

JeanMichel FRANCOIS

unread,
Apr 21, 2011, 7:16:38 AM4/21/11
to dexterity-...@googlegroups.com
I had an issue one month ago related to this using plone.app.registry
I was not able to use a vocabulary and was not able to find why
getsite do not return plonesite.


it is weird !

we need to fix this

--
Regards / Cordialement
JeanMichel FRANCOIS

> --
> You received this message because you are subscribed to the Google Groups "Dexterity development" group.
> To post to this group, send email to dexterity-...@googlegroups.com.
> To unsubscribe from this group, send email to dexterity-develo...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/dexterity-development?hl=en.
>

Jamie Lentin

unread,
Apr 21, 2011, 2:00:35 PM4/21/11
to dexterity-...@googlegroups.com
On Wed, 20 Apr 2011, Maurits van Rees wrote:

> This is my first post to the dexterity list and it is a long one, so I
> apologize in advance. :-) I typed it as I went along testing. I list several
> changes that I can probably safely commit. At the end there is still a
> problem, so if you don't want to read all this, here is a simple question
> instead: how do you get 'getSite()' to return the Plone Site root instead of
> some Z3CFormValidation object that breaks expectations in several parts of
> the code?

Me & Laurence Rowe have been doing similar things, putting a field to
store a link to another page within a datagridfield. However, we've been
trying to store plone.uuid instead of intids. The field within the
datagridfield is a:-
schema.Choice(source=ArchetypesContentSourceBinder())

We have a branch here that largely works, that contains the
ArchetypesContentSourceBinder we use:-
https://svn.plone.org/svn/plone/plone.formwidget.contenttree/branches/archetypes-content-source

We also spent some time battling with lack of suitable context, in the end
this worked best for us:-

from zope.globalrequest import getRequest
.
.
.
if not IAcquirer.providedBy(content):
content = getattr(getRequest(),'PUBLISHED',getSite())


if not IAcquirer.providedBy(content):
content = getSite()

The second fallback being necessary as PUBLISHED doesn't contain anything
useful during traversal (the form gets created to resolve ++widget++
urls).

I've also refactored the javascript events in contenttree (and
autocomplete) so they continue to work for cloned fields. Blindly cloning
events along with DOM elements to create new rows is somewhat tricky to
allow for in JS code though, not entirely sure what the solution is.

With another hack, validation can work(although it's obviously not the
right solution):-

Index: collective/z3cform/datagridfield/datagridfield.py
===================================================================
--- collective/z3cform/datagridfield/datagridfield.py (revision 236760)
+++ collective/z3cform/datagridfield/datagridfield.py (working copy)
@@ -194,8 +194,12 @@
active_names = self.subform.fields.keys()
for name in getFieldNames(self.field.schema):
if name in active_names:
- self.applyValue(self.subform.widgets[name],
- value.get(name, interfaces.NO_VALUE))
+ v = value.get(name, interfaces.NO_VALUE)
+ # HACK: if trying to set a widget value to [], use NO_VALUE instead.
+ # The data converter should probably have done this by now, but is
+ # either not a SequenceDataConverter or not being called at all.
+ if type(v) is list and len(v) == 0: v = interfaces.NO_VALUE
+ self.applyValue(self.subform.widgets[name],v)

Hope some of this is useful,

--
Jamie Lentin

Maurits van Rees

unread,
Apr 27, 2011, 5:10:14 PM4/27/11
to dexterity-...@googlegroups.com
Op 20-04-11 16:54, Maurits van Rees schreef:

> In other words, when you want to have a RelationChoice column in a
> datagrid field, or probably in any schema that has such a table-like
> field, it is brittle at best. I think the first few changes listed
> above are probably fine. For the last part I think the key question
> might be: how do you get 'getSite()' to work correctly when you do not
> have a proper context?
>
> Any ideas?
>
> For the record, a simple RelationChoice field outside of a datagrid
> works fine for me, but the client wants to include some more
> information for each chosen relation (a string or selection field and
> a boolean field), so a datagrid-like solution sounds like a good fit
> if it works. An alternative idea would be welcome as well.

Thanks for all the suggestions in this thread. I think I tried all of
them, but still failed to get it working. Either inline validation
failed, or with that turned off the normal validation failed, for
example with an error like this:
"ValueError: No IObjectFactory adapter registered for
example.conference.program.IAddress", or the getPhysicalPath of an item
had lost it context so it did not give the path from the zope root to
that object. Or no rows could be added while editing, or none showed in
the view, or an empty row was there for editing but no archetypes or
dexterity items could be selected. Sigh...

Nah, I'm not whining (I hope), just a tad frustrated. But I know a
beverage or two that can solve that. ;-)

Am I overlooking a possibly simpler solution? Perhaps without a
datagridfield? The idea would be to get an alternative for something
like this:

from collective.z3cform.datagridfield import DictRow
from collective.z3cform.datagridfield.datagridfield import
DataGridFieldFactory

class IAddress(Interface):
address_type = schema.Choice(
title = u'Address Type', required=True,
values=[u'Work', u'Home'])
affiliate = RelationChoice(
title=u"Affiliation",
source=ObjPathSourceBinder(),
required=True)

class IProgram(form.Schema):
#optional: form.widget(address=DataGridFieldFactory)


address = schema.List(title=u'Addresses',
value_type=DictRow(title=u'Address', schema=IAddress),
required=True)

It might be possible to use a normal zope.schema.Dict as value_type, but
it looks like that requires both keys of the same type (strings/unicode,
fine) and values of the same type, so either e.g. all unicode or all
integers, which does not fit this use case.

Juan Pablo Giménez

unread,
May 7, 2013, 4:11:59 PM5/7/13
to dexterity-...@googlegroups.com
Hi,

  I know than this thread is two years old, but is hitting me now... I have some dexterity related code than defines a local registry, and I seeing some exceptions related to getSite getting the Z3CFormValidation object. So, my question is, there is a real solution to this problem? Why kss setSite to the Z3CFormValidation? I should patch my code to workaround this problem?


thanks!
jpg

David Glick (Plone)

unread,
May 7, 2013, 5:55:03 PM5/7/13
to dexterity-...@googlegroups.com, Juan Pablo Giménez
getSite returns the location of the active site manager (component
registry). That's often the portal but you can't count on it. I never
really looked into why KSS needed its own component registry; I just
removed KSS from Plone. :)
David
> --
> You received this message because you are subscribed to the Google
> Groups "Dexterity development" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to dexterity-develo...@googlegroups.com.
> To post to this group, send email to
> dexterity-...@googlegroups.com.
> Visit this group at
> http://groups.google.com/group/dexterity-development?hl=en.
> For more options, visit https://groups.google.com/groups/opt_out.
>
>

Reply all
Reply to author
Forward
0 new messages