32. Relations – Mastering Plone 6 development

32. Relations#

You can model relationships between content items by placing them in a hierarchy (for example a (folderish) page speakers containing the (folderish) speakers and within each speaker the talks) or by linking them to each other in blocks. But where would you then 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 section 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.

Backend and frontend chapter

Check out the code at the relevant tags!

Code for the beginning of this chapter:

# frontend
git checkout sponsors
# backend
git checkout user_generated_content

Code for the end of this chapter:

# frontend
git checkout relations
# backend
git checkout relations

More info in The code for the training

32.1. Creating and configuring relations in a schema#

Relate to one item only with RelationChoice.

1from z3c.relationfield.schema import RelationChoice
2
3
4    speaker = RelationChoice(
5        title="Speaker",
6        description="The speaker of the talk",
7        vocabulary="plone.app.vocabularies.Catalog",
8        required=False
9    )

Relate to multiple items with RelationList.

 1from z3c.relationfield.schema import RelationChoice
 2from z3c.relationfield.schema import RelationList
 3
 4
 5    speaker = RelationList(
 6        title="Speaker",
 7        description="Speakers of the talk",
 8        value_type=RelationChoice(
 9            vocabulary="plone.app.vocabularies.Catalog",
10        ),
11        required=False,
12        default=[]
13    )

Controlling what to relate to#

The vocabulary controls which content instances can be related to from the field.

1    speaker = RelationList(
2        title="Speaker",
3        description="Speakers of the talk",
4        value_type=RelationChoice(
5            vocabulary="ploneconf.speakers"
6        ),
7        required=False,
8        default=[]
9    )

We want to relate to content instances of type 'speaker'. So we define a vocabulary of speakers.

src/ploneconf/site/vocabularies/configure.zcml

1    <utility
2        name="ploneconf.speakers"
3        component="ploneconf.site.vocabularies.speaker.SpeakerVocabularyFactory"
4        />

src/ploneconf/site/vocabularies/speaker.py

 1from plone.app.vocabularies.catalog import StaticCatalogVocabulary
 2from zope.interface import provider
 3from zope.schema.interfaces import IVocabularyFactory
 4
 5
 6@provider(IVocabularyFactory)
 7def SpeakerVocabularyFactory(context=None):
 8    return StaticCatalogVocabulary(
 9        {
10            "portal_type": ["speaker"],
11            "review_state": "published",
12            "sort_on": "sortable_title",
13        }
14    )

32.2. The widget#

The widget allows the editor to edit the relations.

The default and only widget by now in Volto is the select widget. It opens the tree of content to be selected by the editor. On saving the talk, the selection is validated according the vocabulary.

For a more sophisticated widget see the explanation on how to write a custom widget in Forms and widgets.

32.4. Inspecting relations#

In Plone 6 Volto you can inspect all relations and inverse relations in your site using the control panel relations http://localhost:3000/controlpanel/relations. You can even edit the relations.

The relations controlpanel

The relations controlpanel#

You can find the inverse relations of a content instance via the menu item Links and references

Links and references menu

Links and references menu#

Links and references

Links and references#

32.5. Programming with relations#

Since Plone 6 plone.api has methods to create, read, and delete relations and inverse relations.

1from plone import api
2
3portal = api.portal.get()
4source = portal.schedule["workflows-made-easy"]
5target = portal.speakers["urs-herbst"]
6api.relation.create(source=source, target=target, relationship="speaker")
1from plone import api
2
3api.relation.get(source=portal.schedule["workflows-made-easy"])
4api.relation.get(relationship="speaker")
5api.relation.get(target=portal.speakers["urs-herbst"])

List all relations of name "speaker":

>>> for rel in api.relation.get(relationship="speaker"): rel.from_object, rel.to_object, rel.from_attribute
... 
(<Talk at /Plone/schedule/talkli>, <Speaker at /Plone/speakers/urs-herbst>, 'speaker')
(<Talk at /Plone/schedule/advanced-relations>, <Speaker at /Plone/speakers/katja-i-e-suss>, 'speaker')

See the chapter Relations of the docs for plone.api for more details.

Plone 5.2 and older#

In older Plone versions you can use collective.relationhelpers to create and read relations and inverse relations in a very similar way.

32.6. Exercise 1#

Add a content type speaker and modify the content type talk to relate to speakers. Write an upgrade step for the change of the field 'speaker'.

The code can be found in backend add-on ploneconf.site at tag relations.

32.7. Exercise 2#

The speaker is now a relation on talk. Available on the TalkView is a subset of attributes of the speaker. How would you achieve to show the GitHub handle of the speaker? It is by now not included in the available attributes.

Solution

Add the name of the field to the relevant serializer implementing IJSONSummarySerializerMetadata in src/ploneconf/site/serializers/summary.py