relatedItems and 'back' (from/to object)

When Adding 'Page 2' as relatedItem of 'Page 1', it (Page 1) shows up in the 'relateditemes viewlet'.

If you also wanted 'Page 1' to show up as 'relatedItem' of 'Page 2' automatically (for all relatinons), what would be the best approach ( I can think of quite a few ways of doing this (customise the viewlet, customise the indexing, event handler)

A relation in the Plone sense is unidirectional and not bidirectional. Page 2 related to Page 1 but Page 1 is not related to Page 2.

Looking at https://docs.plone.org/external/plone.app.dexterity/docs/advanced/references.html#back-references
for retrieving back references.

You likely want to write your own viewlet or portlet if you have support a bidirectional relationship in someway.

Here's some "works for me" code...

In your view:

    def list_referers(self, context = None):
        """ Return a list of backreference relationvalues
        """
        catalog = getUtility(ICatalog)
        intids = getUtility(IIntIds)
        context = context and context or self.context
        rel_query = { 'to_id' : intids.getId(aq_inner(context)) }
        rel_items = list(catalog.findRelations(rel_query))
        return rel_items

    def get_brains_for_relation_ids(context, relationvalues, sort_key=None, direction='from', depth=0, portal_type=[], language='', keep_original_order=None):
        p_catalog = getToolByName(context, 'portal_catalog')
        results = []
        query = {}
    
        if portal_type:
            query['Type'] = portal_type
        if language:
            query['Language'] = language
    
        #log.info('%s Relationvalues ids: %s' % (get_linenumber(), [relationvalue.to_path.split('/')[-1] for relationvalue in relationvalues]))
    
        for relationvalue in relationvalues:
            r_path = direction == 'from' and relationvalue.from_path or relationvalue.to_path
    
            for search_depth in range(0, depth+1):
                query['path'] = {'query': r_path, 'depth': search_depth}
                brains = p_catalog(**query)
                if len(brains) > 0:
                    #log.info('%s brains ids: %s' % (get_linenumber(), [result.getId for result in brains]))
                    if keep_original_order:
                        for brain in brains:
                            if not brain in results:
                                results.append(brain)
                    else:
                        results = results and list(set(results + list(brains))) or list(brains)
    
                    #log.info('%s results ids: %s' % (get_linenumber(), [result.getId for result in results]))
    
        if results:
            results = sort_key and sorted(results, key=lambda match: match[sort_key]) or results
    return results

in your template:


        <fieldset id="pl-produktsortiment-alle"
                     tal:define="rel_list here/@@my_view/list_referers;
                                 get_brains nocall:here/@@my_view/get_brains_for_relation_ids;">
            <legend>
                <span tal:omit-tag i18n:translate="">Alle Produkte</span>
            </legend>
            <div class="template-pl_produkte_view" id="tmb-container" >
                <h3 i18n:translate="">Alle Produkte dieser Marke</h3>
                <table class="listing"
                       tal:define="brains nocall:python:get_brains(rel_list, sort_key='Title')" >
                    <thead>
                        <tr>
                            <th i18n:translate="">Title</th>
                            <th i18n:translate="">ID</th>
                            <th i18n:translate="">Language</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tal:items tal:repeat="item brains">
                            <tr metal:define-macro="listitem2"
                                tal:define="oddrow repeat/item/odd;"
                                tal:attributes="class python: oddrow and 'even' or 'odd'">
                                <td>
                                    <a tal:content="item/Title"
                                    tal:attributes="href item/getURL">PL Product Title</a>
                                </td>
                                <td tal:content="item/getId" />
                                <td tal:content="item/Language" />
                            </tr>
                        </tal:items>
                    </tbody>
                </table>
            </div>
        </fieldset>

Thanks. I used part of the code and I will do (something like) this:

def get_relateditems(self):
    refs = (self.context.relatedItems)
    to_objects = [ref.to_object for ref in refs]
    refers = self.get_referers(self.context)
    from_objects = [ref.from_object for ref in refers]
    ref_list = to_objects + from_objects
    #skip items that has been 'linked both ways'
    return OrderedDict( (x,1) for x in ref_list ).keys()

def get_referers(self, context = None):
    """ Return a list of backreference relationvalues
    """
    catalog = getUtility(ICatalog)
    intids = getUtility(IIntIds)
    context = context and context or self.context
    rel_query = { 'to_id' : intids.getId(aq_inner(context)) }
    rel_items = list(catalog.findRelations(rel_query))
    return rel_items

And in the view

<tal:repeat tal:repeat="ref view/get_relateditems">
    <a href="${ref/absolute_url}" title="link to project">
        ${ref/Title}
    </a>.
</tal:repeat>
1 Like

Note to_object wakes up your objects, if I understand correctly... to_path does not.

I think you are correct.

And also, it looks like one could end up with 'circular references' if two items are referenced 'both ways'.

Not this one by any chance?

  <monkey:patch description="Fix circular reference when field is RelationValue"
        class="plone.app.content.utils"
        original="custom_json_handler"
        replacement=".monkey_patches.pac_utils_custom_json_handler"
        />
def pac_utils_custom_json_handler(obj):
    if obj == Missing.Value:
        return None
    if type(obj) in (datetime.datetime, datetime.date):
        return obj.isoformat()
    if type(obj) == DateTime:
        dt = DateTime(obj)
        return dt.ISO()
    if type(obj) == set:
        return list(obj)
    # 20170418 fixed Circular reference
    # Should this return the actual object?
    if type(obj) == RelationValue:
        return obj.to_path.split('/')[-1]
    return obj

No, I made a custom viewlet.
As long as I use 'nocall' it seems to work OK