Indexing UUID of RelationValue Fields

I have a relation field defined as follows:

    application = RelationChoice(
        title="Application",
        vocabulary="service.app.CompletedApplications",
        required=False,
    )

And an index set as:
type: FieldIndex
name: application
indexed attribute: application

Since the catalog will index relation values by default, I have a custom indexer which returns the UUID of the target object.

This works. However, when I add another content type with a field defined the same as above, I get the error below:

Traceback (innermost last):
  Module ZPublisher.WSGIPublisher, line 187, in transaction_pubevents
  Module transaction._manager, line 255, in commit
  Module transaction._manager, line 132, in commit
  Module transaction._transaction, line 267, in commit
  Module transaction._transaction, line 333, in _callBeforeCommitHooks
  Module transaction._transaction, line 372, in _call_hooks
  Module Products.CMFCore.indexing, line 317, in before_commit
  Module Products.CMFCore.indexing, line 227, in process
  Module Products.CMFCore.indexing, line 49, in reindex
  Module Products.CMFCore.CatalogTool, line 368, in _reindexObject
  Module Products.CMFPlone.CatalogTool, line 320, in catalog_object
  Module Products.ZCatalog.ZCatalog, line 495, in catalog_object
  Module Products.ZCatalog.Catalog, line 362, in catalogObject
  Module Products.PluginIndexes.unindex, line 237, in index_object
  Module Products.PluginIndexes.unindex, line 282, in _index_object
  Module Products.PluginIndexes.unindex, line 213, in insertForwardIndexEntry
  Module functools, line 91, in _gt_from_lt
  Module z3c.relationfield.relation, line 91, in __lt__
AttributeError: 'str' object has no attribute 'from_attribute'

Looking at the source code of z3c.relationfield, the error occurred because in line 89:

     84     def __lt__(self, other):

     89         if (self.from_attribute or '') < (other.from_attribute or ''):

"self" is a RelationValue object, while "other" is the UUID value of the target object. The UUID string does not have a from_attribute method, hence the error.

I am not sure if this is a bug. My work around is to redefine my index as follows:
type: FieldIndex
name: application_uid
indexed attribute: application_uid

So far this seems to work. The field id and index name are different. Is this correct? Or is there a better way to index the UUID of RelationValue fields?

From left field: what happens if you use a UUIDIndex rather than a fieldindex?

Edit/add Also have a look at the 2023 version of the training. The latest one focuses on Volto only for reasons unknown...

If I use a UUIDIndex without a custom indexer, I get RelationValue objects on the left column and the path on the right.

If I use a UUIDIndex with my custom indexer, I get UUIDs on the left column and the item path on the right column.

Both set a 1 to 1 relationship and not 1 to many.

1 Like

I haven't upgraded the site where I use RelationValues and a custom indexer, so don't know what the status is for Plone 6. FWIW in a Plone 5.1 site, I resorted to storing the path of the source object rather than the UUID / UID. The why has long faded from memory...

Added:

mdb.py

@provider(IFormFieldProvider)
class IMasterdatenblatt(model.Schema):
    """ MDB Schema, converted from TTW XML 20200302
    """
    urprodukt_verweis = RelationChoice(
        title       = _(u'Urprodukt Verweis'),
        required    = True,
        vocabulary  = 'plone.app.vocabularies.Catalog',
    )
    directives.widget(
		'urprodukt_verweis',
		RelatedItemsFieldWidget,
        pattern_options={
			'recentlyUsed': False,
            'basePath': default_base_path,
            'mode': 'auto',
            'favorites': default_favorites,
            'folderTypes': ['Folder', 'LIF', 'LRF'],
            'selectableTypes': ['urprodukt'],
        },
    )
alsoProvides(IMasterdatenblatt['urprodukt_verweis'], ILanguageIndependentField)

indexers.py

"""Custom PNZ indexers
    - enabled via behavior in dexterity type xml
    - registered via adapter in configure.zcml
    - implemented (indexed) via keywordindex or fieldindex entry in catalog.xml
See: https://docs.plone.org/external/plone.app.dexterity/docs/advanced/catalog-indexing-strategies.html
"""
from plone import api
from zope.component import getUtility
from Products.CMFPlone.utils import safe_unicode
from zope.schema.interfaces import IVocabularyFactory
from plone.indexer.decorator import indexer
from .interfaces import ICustomSearch

@indexer(ICustomSearch)
def getRelatedURPPath(obj):
    field = obj.urprodukt_verweis
    obj_path = getattr(field,'to_path', None)
    return obj_path

interfaces.py

class ICustomSearch(Interface):
    """ Marker interface to enable custom indices from datagrid field (Urprodukt, Marke)
    """

configure.zcml
  <adapter name="urprodukt_verweis"
        factory=".indexers.getRelatedURPPath"
        />

Thanks. Your code is similar to my implementation except I am storing UUIDs in an index. I am used to using UUIDs to establish relationships. I have sparingly used relationfields and it is only now that I need to index the target object set via a relationfield. The error seems like a bug.

Possibly. I apparently also patched the value converters for both RelationChoice and RelationItems but I doubt this related to what you are seeing. Just in case:

Interested in finding out what caused your issue, using UUIDs is likely more robust.

def RelationChoice_toWidgetValue(self, value):
    if not value:
        return self.field.missing_value
    try:
        return IUUID(value)

    except TypeError:
        uuid = IUUID(value, None)
        if uuid is None and hasattr(aq_base(value), 'UID'):
            uuid = value.UID()
            return uuid
        pass

def RelatedItems_toWidgetValue(self, value):
    """Converts from field value to widget.

    :param value: List of catalog brains.
    :type value: list

    :returns: List of of UID separated by separator defined on widget.
    :rtype: string
    """
    if not value:
        return self.field.missing_value
    separator = getattr(self.widget, 'separator', ';')

    if IRelationList.providedBy(self.field):
        try:
            return separator.join([IUUID(o) for o in value if value])
        except TypeError:
            uuid_list = []
            if value:
                for o in value:
                    uuid = IUUID(o, None)
                    if uuid is None and hasattr(aq_base(value), 'UID'):
                        uuid_list.append(o.UID())
                return separator.join(uuid_list)
            pass
    else:
        return separator.join(v for v in value if v)

I suspect that the normal index processing is triggered and the RelationValue of the field is being compared with the available UUID index value in the catalog or the value returned by my custom indexer. The __lt__() method was expecting both to be RelationValue objects.

Edit: I just confirmed that the UUID value being compared is from my custom indexer and not from the catalog.