43. Relations

Todo

  • Add screenshots for relationfields in Volto

  • Create relations between talk and speaker

  • Display relations (and backrelations)

You can model relationships between content items by placing them in a hierarchy (a folder speakers containing the (folderish) speakers and within each speaker the talks) or by linking them to each other in Richtext fields. But where would you store a talk that two speakers give together?

Relations allow developers to model relationships between objects without using links or a hierarchy. The behavior plone.app.relationfield.behavior.IRelatedItems provides the field Related Items in the tab Categorization. That field simply says a is somehow related to b.

By using custom relations you can model your data in a much more meaningful way.

43.1. Creating relations in a schema

Relate to one item only.

1from plone.app.vocabularies.catalog import CatalogSource
2from z3c.relationfield.schema import RelationChoice
3from z3c.relationfield.schema import RelationList
4
5evil_mastermind = RelationChoice(
6    title=_(u'The Evil Mastermind'),
7    vocabulary='plone.app.vocabularies.Catalog',
8    required=False,
9)

Relate to multiple items.

from z3c.relationfield.schema import RelationChoice
from z3c.relationfield.schema import RelationList

minions = RelationList(
    title=_(u'Minions'),
    default=[],
    value_type=RelationChoice(
        vocabulary='plone.app.vocabularies.Catalog',
    )
    required=False,
)

We can see that the code for the behavior IRelatedItems does exactly the same.

43.2. Controlling what to relate to

The best way to control wich item should be relatable to is to configure the widget with directives.widget(). In the following example you can only relate to Documents:

 1from plone.app.z3cform.widget import RelatedItemsFieldWidget
 2
 3relationchoice_field = RelationChoice(
 4    title=u"Relationchoice field",
 5    vocabulary='plone.app.vocabularies.Catalog',
 6    required=False,
 7)
 8directives.widget(
 9    "relationchoice_field",
10    RelatedItemsFieldWidget,
11    pattern_options={
12        "selectableTypes": ["Document"],
13    },
14)

The following example applies pattern-option basePath to force the widget to start browsing the site at the site-root using the method plone.app.multilingual.browser.interfaces.make_relation_root_path. By default the widget starts with the current context.

 1relationlist_field = RelationList(
 2    title=u"Relationlist Field",
 3    default=[],
 4    value_type=RelationChoice(vocabulary='plone.app.vocabularies.Catalog'),
 5    required=False,
 6    missing_value=[],
 7)
 8directives.widget(
 9    "relationlist_field",
10    RelatedItemsFieldWidget,
11    vocabulary='plone.app.vocabularies.Catalog',
12    pattern_options={
13        "basePath": make_relation_root_path,
14    },
15)

Instead of using a named vocabulary we can also use source:

 1from plone.app.vocabularies.catalog import CatalogSource
 2from z3c.relationfield.schema import RelationChoice
 3from z3c.relationfield.schema import RelationList
 4
 5minions = RelationList(
 6    title=_(u'Talks by this speaker'),
 7    value_type=RelationChoice(
 8        title=_(u'Talks'),
 9        source=CatalogSource(portal_type=['one_eyed_minion', 'minion'])),
10    required=False,
11)
12directives.widget(
13    'minions',
14    RelatedItemsFieldWidget,
15    pattern_options={'mode': 'search'},
16)

You can pass to CatalogSource the same arguments you use for catalog queries. This makes it very flexible for limiting relateable items by type, path, date, and so on.

Setting the mode of the widget to search makes it easier to select from the content that result form your catalog-query instead of having to navigate through your content-tree.

The RelatedItemsFieldWidget also allow you to set favorites:

1directives.widget(
2    'minions',
3    RelatedItemsFieldWidget,
4    pattern_options={
5        'favorites': [{'title': 'Minions', 'path': '/Plone/minions'}]
6    },
7)

favorites can also be a method that takes the current context. Here is a full example as a behavior:

 1from plone import api
 2from plone.app.vocabularies.catalog import CatalogSource
 3from plone.app.z3cform.widget import RelatedItemsFieldWidget
 4from plone.autoform import directives
 5from plone.autoform.interfaces import IFormFieldProvider
 6from plone.supermodel import model
 7from z3c.relationfield.schema import RelationChoice
 8from z3c.relationfield.schema import RelationList
 9from zope.interface import provider
10
11
12def minion_favorites(context):
13    portal = api.portal.get()
14    minions_path = '/'.join(portal['minions'].getPhysicalPath())
15    one_eyed_minions_path = '/'.join(portal['one-eyed-minions'].getPhysicalPath())
16    return [
17            {
18                'title': 'Current Content',
19                'path': '/'.join(context.getPhysicalPath())
20            }, {
21                'title': 'Minions',
22                'path': minions_path,
23            }, {
24                'title': 'One eyed minions',
25                'path': one_eyed_minions_path,
26            }
27        ]
28
29
30@provider(IFormFieldProvider)
31class IHaveMinions(model.Schema):
32
33    minions = RelationList(
34        title='My minions',
35        default=[],
36        value_type=RelationChoice(
37            source=CatalogSource(
38                portal_type=['one_eyed_minion', 'minion'],
39                review_state='published',
40            )
41        ),
42        required=False,
43    )
44    directives.widget(
45        'minions',
46        RelatedItemsFieldWidget,
47        pattern_options={
48            'mode': 'auto',
49            'favorites': minion_favorites,
50            }
51        )

For even more flexibility, you can create your own dynamic vocabularies.

For more examples how to use relationfields look at Dexterity: Reference.

43.3. Use a tailor shaped widget for relations

Sometimes the widget for relations is not what you want since it can be hard to navigate to the content you want to relate to. With SelectFieldWidget and a custom vocabulary you can shape a widget for an easier selection of related items:

 1from plone.app.z3cform.widget import SelectFieldWidget
 2from plone.autoform import directives
 3from z3c.relationfield.schema import RelationChoice
 4from z3c.relationfield.schema import RelationList
 5
 6relationlist_field_select = RelationList(
 7    title=u'Relationlist with select widget',
 8    default=[],
 9    value_type=RelationChoice(vocabulary='ploneconf.site.vocabularies.documents'),
10    required=False,
11    missing_value=[],
12)
13directives.widget(
14    'relationlist_field_select',
15    SelectFieldWidget,
16)

Register the vocabulary like this in configure.zcml:

<utility
    name="ploneconf.site.vocabularies.documents"
    component="ploneconf.site.vocabularies.DocumentVocabularyFactory" />

Note that the value is the object itself, not the uuid. This is a requirement of the field-type:

 1from plone import api
 2from zope.interface import implementer
 3from zope.schema.interfaces import IVocabularyFactory
 4from zope.schema.vocabulary import SimpleTerm
 5from zope.schema.vocabulary import SimpleVocabulary
 6
 7@implementer(IVocabularyFactory)
 8class DocumentVocabulary(object):
 9    def __call__(self, context=None):
10        terms = []
11        # Use getObject since the DataConverter expects a real object.
12        for brain in api.content.find(portal_type='Document', sort_on='sortable_title'):
13            terms.append(SimpleTerm(
14                value=brain.getObject(),
15                token=brain.UID,
16                title=u'{} ({})'.format(brain.Title, brain.getPath()),
17            ))
18        return SimpleVocabulary(terms)
19
20DocumentVocabularyFactory = DocumentVocabulary()

The field should then look like this:

RelationList field with select widget SelectFieldWidget

RelationList field with select widget SelectFieldWidget and custom vocabulary

Warning

This approach is bad for performance if the vocabulary will contain a lot of content.

43.5. Creating RelationFields through the web

It is surprisingly easy to create RelationFields through the web

  • Using the Dexterity schema editor, add a new field and select Relation List or Relation Choice, depending on whether you want to relate to multiple items or not.

  • When configuring the field you can even select the content type the relation should be limited to.

When you click on Edit XML field model you will see the fields in the XML schema:

RelationChoice:

<field name="boss" type="z3c.relationfield.schema.RelationChoice">
  <description/>
  <required>False</required>
  <title>Boss</title>
</field>

RelationList:

 1<field name="underlings" type="z3c.relationfield.schema.RelationList">
 2  <description/>
 3  <required>False</required>
 4  <title>Underlings</title>
 5  <value_type type="z3c.relationfield.schema.RelationChoice">
 6    <title i18n:translate="">Relation Choice</title>
 7    <portal_type>
 8      <element>Document</element>
 9      <element>News Item</element>
10    </portal_type>
11  </value_type>
12</field>

43.6. Accessing relations and backrelations from code

The recommended way to create and read relations and backrelations as a developer is to use collective.relationhelpers.

43.7. The stack

Relations are based on zc.relation. This package stores transitive and intransitive relationships. It allows complex relationships and searches along them. Because of this functionality, the package is a bit complicated.

The package zc.relation provides its own catalog, a relation catalog. This is a storage optimized for the queries needed. zc.relation is sort of an outlier with regards to Zope documentation. It has extensive documentation, with a good level of doctests for explaining things.

You can use zc.relation to store the objects and its relations directly into the catalog. But the additional packages that make up the relation functionality don’t use the catalog this way.

We want to work with schemas to get auto generated forms. The logic for this is provided by the package z3c.relationfield. This package contains the RelationValue object and everything needed to define a relation schema, and all the code that is necessary to automatically update the catalog.

A RelationValue Object does not reference all objects directly. For the target, it uses an id it gets from the IntId Utility. This id allows direct recovery of the object. The source object stores it directly.

Widgets are provided by plone.app.z3cform and some converters are provided by plone.app.relationfield. The widget that Plone uses can also store objects directly. Because of this, the following happens when saving a relation via a form:

  1. The HTML shows some nice representation of selectable objects.

  2. When the user submits the form, selected items are submitted by their UUIDs.

  3. The Widget retrieves the original object with the UUID.

  4. Some datamanager gets another unique ID from an IntID Tool.

  5. The same datamanager creates a RelationValue from this id, and stores this relation value on the source object.

  6. Some Event handlers update the catalogs.

You could delete a Relation like this delattr(rel.from_object, rel.from_attribute)

This is a terrible idea by the way, because when you define in your schema that one can store multiple RelationValues, your Relation is stored in a list on this attribute.

Relations depend on a lot of infrastructure to work. This infrastructure in turn depends a lot on event handlers being thrown properly. When this is not the case things can break. Because of this, there is a method isBroken which you can use to check if the target is available.

There are alternatives to using Relations. You could instead just store the UUID of an object. But using real relations and the catalog allows for very powerful things. The simplest concrete advantage is the possibility to see what links to your object.

The built-in linkintegrity feature of Plone 5 is also implemented using relations.

43.8. RelationValues

RelationValue objects have a fairly complete API. For both target and source, you can receive the IntId, the object and the path. On a RelationValue, the terms source and target aren’t used. Instead, they are from and to. So the API for getting the target is:

  • to_id

  • to_path

  • to_object

In addition, the relation value knows under which attribute it has been stored as from_attribute. It is usually the name of the field with which the relation is created. But it can also be the name of a relation that is created by code, e.g. linkintegrity relations (isReferencing) or the relation between a working copy and the original (iterate-working-copy).