Dexterity object as field of another dexterity object: Works out of the box, but introspection not

Currently I am working on an open source generic DCAT compatible RDF marshalling for Plone5 based on the work of EEA.RDFmarshaller. This metadata schema consists of 22 entities around three main content types. http://www.dcat-ap.de/def/dcatde/1_0/uml/modelio.pdf

I modelled this schema with properties as well as with containment. The main content-types are folderish and contain each other.
The properties I modelled utilizing fields which are

  • plain fields
  • other Dexterity objects
  • lists of Dexterity objects

E..g a catalog object has a property "additional_titles" which is a list of RDF:Literal which is a content_type consisting of a string and a language.

And tat ta ta! It works right out of the box. [if you follow my post on How to use dexterity/forms to add a list of custom objects?]

This is quite cool! Plone developers, you did a great job! I have add, edit masks and anything runs smoothly.

But.

When such a complex property is to be exported via RDF I grab it and try to determine its portal_type and it answers with None. If I summon the portal_types tool it raises an attribute error.

>>> getToolByName(self.context, 'portal_types')
Traceback (most recent call last):
  File "/home/volker/workspace/pycharm-community-2017.1.2/helpers/pydev/_pydevd_bundle/pydevd_exec.py", line 3, in Exec
    exec exp in global_vars, local_vars
  File "<input>", line 1, in <module>
  File "<string>", line 13, in check_getToolByName
  File "/home/volker/workspace/PKAN2/buildout-cache/eggs/Products.CMFCore-2.2.10-py2.7.egg/Products/CMFCore/utils.py", line 120, in getToolByName
    raise AttributeError, name
AttributeError: portal_types

Guys we are so close to perfection. Please help me making Plone even better.

class ICatalog(model.Schema):
    """ Marker interfce and Dexterity Python Schema for Catalog
    """

    publisher = schema.Object(
        title=_(u'Publisher'),
        required=True,
        schema=IFoafagent
    )

alsoProvides(ILiteral, IFoafagent)

@implementer(ICatalog)
class Catalog(Container):
    """
    """

from z3c.form.object import registerFactoryAdapter
registerFactoryAdapter(ICatalog, Catalog)

class IFoafagent(model.Schema):
""" Marker interfce and Dexterity Python Schema for Foafagent
"""

name = schema.TextLine(
     title=_(u'Name'),
     required=True
)


@implementer(IFoafagent)
class Foafagent(Item):
"""
"""

from z3c.form.object import registerFactoryAdapter
registerFactoryAdapter(IFoafagent, Foafagent)

Any help or ideas welcome

Volker

In principle, there are too ways to access a so called "tool" (like portal_types): via acquisition and via a local utility lookup. I do not know which one is used by Plone 5 for portal_types access.

Accessing a "tool" via acquisition requires that the first argument to getToolByName is acquisition wrapped; accessing it via a local utility lookup requires that you are in a portal context (i.e. that setSite has been called for this portal).

Dear Dieter!
Uh Uh! Guess I forgot about the aquisition wrapper. Will come back with new findings soon.

I am a bit lost.
The context of Type "Catalog" has an attribute "portal_type" explicitly set as well as lots of other attributes.
The complex property of "Catalog" of type FOAFAgent has no attribute "portal_type" and lacks all the other goodies.

>>> self.context
<Catalog at /PKAN/organisationen/hobbit-presse-gesamtausgabe>

>>> self.context.__dict__
{'_v__providedBy__': (1513373658.087768, 1513365428.9656048, 0, 8781894840713, <implementedBy ?>), 'description': '', 'portal_type': 'catalog', '_mt_index': <BTrees.OOBTree.OOBTree object at 0x7fcb1b830f50>, 'creation_date': DateTime('2017/12/15 22:34:18.011812 GMT+1'), '_Modify_portal_content_Permission': ('Manager', 'Owner', 'Editor', 'Site Administrator'), '_tree': <BTrees.OOBTree.OOBTree object at 0x7fcb1b830e50>, '_Access_contents_information_Permission': ('Manager', 'Owner', 'Editor', 'Reader', 'Contributor', 'Site Administrator'), '_View_Permission': ('Manager', 'Owner', 'Editor', 'Reader', 'Contributor', 'Site Administrator'), 'add_title': [<Literal at >], 'publisher': <Foafagent at >, '_count': <BTrees.Length.Length object at 0x7fcb1b902578>, 'language': u'de', 'license': 'http://modor.de', 'title': u'Hobbit Presse Gesamtausgabe', 'workflow_history': {'simple_publication_workflow': ({'action': None, 'review_state': 'private', 'actor': 'admin', 'comments': '', 'time': DateTime('2017/12/15 22:34:18.034978 GMT+1')},)}, 'rights': None, 'modification_date': DateTime('2017/12/15 22:34:18.011812 GMT+1'), 'id': 'hobbit-presse-gesamtausgabe', '_plone.uuid': '9a8f1e9f7a234dd3a65083b1d2db6ce8', 'creators': ('admin',), '__ac_local_roles__': {'admin': ['Owner']}}

>>> self.context.publisher
<Foafagent at /PKAN/organisationen/hobbit-presse-gesamtausgabe/>

>>> self.context.publisher.__dict__
{'creators': ('admin',), 'creation_date': DateTime('2017/12/15 22:34:17.999355 GMT+1'), 'name': u'otto', 'modification_date': DateTime('2017/12/15 22:34:18.000149 GMT+1'), '_plone.uuid': '1ad5a4460fcc4d1aa177188a81d58655'}

Why is this the case?

I also checked:

>>> self.context.publisher.__parent__
<Catalog at /PKAN/organisationen/hobbit-presse-gesamtausgabe>

>>> import Acquisition
>>> Acquisition.aq_parent(self.context.publisher)
<Catalog at /PKAN/organisationen/hobbit-presse-gesamtausgabe>

>>> IDexterityContent.providedBy(self.context.publisher)
True

I also checked that the Foafagent content type works well if not used as a complex property of an other contenttype. That is the case.

Am I guessing right that the "Catalog" object gets its attribute 'portal_type' when it is put into its final destination? And the complex property "Publisher" of the "Catalog" object does not get the attribute portal_type since it does land elsewhere in the ZODB?

How does dexterity look up not existing attributes? Surely not via acquisition, since then it should return the portal_type of the "Catalog" instead, but it returns "None".

Also I noticed that the Publisher property instance is not listed in the portal_catalog.
If I do a simple self.context.publisher.reindexObject() the it goes in to the portal_catalog with portal_type "unkown" and the location of its parent (Which may not be entirely correct).

If I cheat a bit and set its portal_type forehand:

>>> self.context.publisher.portal_type = 'foafagent'
>>> self.context.publisher.reindexObject()
>>> transaction.commit()
>>> self.context.publisher.__dict__
{'name': u'otto', 'modification_date': DateTime('2017/12/16 20:46:42.070806 GMT+1'), 'portal_type': 'foafagent', 'creation_date': DateTime('2017/12/15 22:34:17.999355 GMT+1'), '_plone.uuid': '1ad5a4460fcc4d1aa177188a81d58655', 'creators': ('admin',), '_v__providedBy__': (1513453524.6802478, 1513365428.9656048, 0, 8781895345601, <implementedBy ?>)}

It lands in the portal_catalog with the right portal_type.

If I then delete it attribute portal_type and try to acces it

>>> del self.context.publisher.portal_type
>>> self.context.publisher.portal_type
>>>

Again I get returned "None".
So this is IMHO the prove that dexterity does not queries the portal_catalog for not set attributes. Since in the portal_catalog the portal_type for the publisher instance is now correctly set. asuming that the query to the portal_catalog is not done via the URL (which is not correct), but by the UID which is correct.

Here the prove that the portal_catalog is correct :

>>> portal_catalog.searchResults(portal_type='foafagent')[1].getRID()
981909210
>>> portal_catalog.getIndexDataForRID(981909210)
{'total_comments': 0, 'Title': [], 'effectiveRange': (-1500, None), 'path': '/PKAN/organisationen/hobbit-presse-gesamtausgabe/', 'commentators': '', 'Type': u'FOAFAgent', 'id': '', 'cmf_uid': 2, 'end': '', 'Description': [], 'is_folderish': 0, 'sync_uid': '', 'getId': '', 'start': '', 'is_default_page': 0, 'Date': 1081026466, 'review_state': 'private', 'getRawRelatedItems': '', 'portal_type': 'foafagent', 'expires': 1339244580, 'allowedRolesAndUsers': ['Site Administrator', 'Manager', 'user:admin', 'Editor', 'Reader', 'Contributor'], 'getObjPositionInParent': [], 'object_provides': ['Products.CMFCore.interfaces._content.IContentish', 'pkan.dcatapde.content.foafagent.IFoafagent', 'plone.app.content.interfaces.INameFromTitle', 'plone.app.dexterity.behaviors.discussion.IAllowDiscussion', 'plone.app.dexterity.behaviors.exclfromnav.IExcludeFromNavigation', 'plone.app.iterate.dexterity.interfaces.IDexterityIterateAware', 'plone.app.relationfield.interfaces.IDexterityHasRelations', 'plone.dexterity.interfaces.IDexterityContent', 'plone.dexterity.interfaces.IDexterityItem', 'plone.namedfile.interfaces.IImageScaleTraversable', 'plone.supermodel.model.Schema', 'plone.uuid.interfaces.IAttributeUUID', 'webdav.interfaces.IWriteLock', 'z3c.relationfield.interfaces.IHasIncomingRelations', 'z3c.relationfield.interfaces.IHasOutgoingRelations', 'z3c.relationfield.interfaces.IHasRelations', 'zope.location.interfaces.IContained', 'zope.location.interfaces.ILocation'], 'in_reply_to': '', 'UID': '1ad5a4460fcc4d1aa177188a81d58655', 'effective': 1055334180, 'created': 1081025134, 'Creator': 'admin', 'modified': 1081026466, 'SearchableText': [], 'sortable_title': '', 'meta_type': 'Dexterity Item', 'Subject': ''}

In summary: Complex properties of dexterity content_types are not indexed into the portal_catalog. Nor did they get some attributes set like "portal_type" a normal dexterity instance has. This looks IMHO at least inconsistent.

Any help appreciated.

Volker

Not sure, but accessing .__dict__ does not load a persistent object from the ZODB and it may appear empty (event though the persistent object has many attributes). Usually, printing of an object accesses attributes which ensure an object load; thus, this may not be the explanation in your case.

It gets portal_type in Products.CMFCore.TypesTool.FactoryTypeInformation._constructInstance (provided it has the method _setPortalTypeName).

security.declarePrivate('_constructInstance')
def _constructInstance(self, container, id, *args, **kw):
    """Build a bare instance of the appropriate type.

    Does not do any security checks.
    """
    # XXX: this method violates the rules for tools/utilities:
    # it depends on self.REQUEST
    import pdb; pdb.set_trace()

Creating a dexterity object does not stop here???

The "dexterity" FTI might have overridden the _constructInstance method (although my superficial exploration has not noticed such an override).

To find out where its _constructInstance method is defined, you could access the FTI (= "FactoryTypeInformation") in your portal_types (e.g. in a bin/client1 debug session) and look at fti._constructInstance.im_func.func_globals["__file__"]. In my Plone instnace (5.0.x), it comes from Products.CMFCore.TypesTool.

Other possibilities: you do not create your object in a CMF compatible way or "dexterity" uses something of its own (and not the CMF way) to create the object.

The _constructInstance definition in TypesTool demonstrates that the CMF does not expect that the bare object constructor sets the correct portal_type (it likely remains None) but that portal_type is set when the instance is constructed via the fti (which is natural as the CMF types (identified by portal_type) add differentiating behaviour to the same base content types).
Thus, if you do not create a content object via an fti, it may not have a propery portal_type value.

Dear Dieter, you angel!

You saved my day!

<property name="klass">pkan.dcatapde.content.foafagent.Foafagent</property>

I told dexterity to not create a class for my CT but use my custom class

@implementer(IFoafagent)
class Foafagent(Item):
    """
    """

If I add the class attribute

    portal_type = 'foafagent'

to this class everything works like a charm.

The responsiblilty/sideeffects that come with utilizing a custom class for a dexterity content_type should be added to the documentation.

Cheers,
from a really happy Volker

Tracked the problem finally down.

Here I stated that if one will user complex properties a ContentFactory for z3c.form has to be registered.

https://community.plone.org/t/how-to-use-dexterity-forms-to-add-a-list-of-custom-objects/3656/2

@implementer(IFoafagent)
class Foafagent(Item):
    """
    """
    portal_type = 'foafagent'

from z3c.form.object import registerFactoryAdapter
registerFactoryAdapter(IFoafgent, FoafAgent)

This is correct but also causes the problems shown in this thread. As Dieter stated the complex properties are not created in the "plone way". And the reason is obviously that the FactoryAdapter I utilized is from z3c.form and does not know much about plone and dexterity.

Here I present a prototype to utilize the dexterity contentFactory instead. There may surely be more elegant solutions.

from z3c.form.object import FactoryAdapter, getIfName
import zope.component
from zope.component.interfaces import IFactory

@implementer(IObjectFactory)
class FoafagentFactory(FactoryAdapter):
    """
    """

    def __call__(self, value):
        factory = queryUtility(IFactory, name='foafagent')
        return factory()

name = getIfName(IFoafagent)
zope.component.provideAdapter(FoafagentFactory, name=name)

This piece of code grabs the registered plone factory utility for the content type (In fact an IDexterityFactory representive aka DexterityFactory instance) and uses it as IObjectFactory adapter for z3c.form.

Utilizing this method complex dexterity properties (as far as I have tested) are working with schema introspection.

Maybe someone is able to help me to intergrate this into dexterity. This registration probably could be made at the time the FTI registration takes place.

Finally thousand thanks @dieter. Without your generous help I would never have discovered this.

you have to register the Factory Type Information (FTI) using an XML file like the one described in the training.

Hi,
same problem here.
I found a very clean solution here:

It is enough to provide to the factoryclass all the sub-schema field-properties:

@implementer(IFoo)
class Foo(object):
    field1 = FieldProperty(IFoo('field1'))
    field2 = FieldProperty(IFoo('field2'))

registerFactoryAdapter(IFoo, Foo)

then is straightforward to use IFoo as sub-schema in schema.Object field

class IMyDexteritySchema(model.Schema):
    firstFoo = schema.Object(schema=IFoo, title="First Foo")
    secondFoo = schema.Object(schema=IFoo, title="secondFoo")

it works.

2 Likes