Products.cmfplone: PLIP: Add methods to plone.api to deal with relations

Created on 1 Jul 2020  路  17Comments  路  Source: plone/Products.CMFPlone

PLIP (Plone Improvement Proposal)

Responsible Persons

Proposer: Philip Bauer

Seconder:

Abstract

Add methods to plone.api that help deal with relations on dexterity content. Plone-Developer often model applications that make use of relations. zc.relation is very powerful but the api to work with relations is cumbersome at best and has quite some pitfalls.

Motivation

As discussed in https://community.plone.org/t/new-addon-collective-relationhelpers-to-manage-create-export-and-rebuild-relations/12365 most developer end up writing some version of these methods for all larger projects. It would be easier for them to simply use plone.api for these tasks.

Assumptions

Nobody develops new applications with Archetypes any more. This is why I would only support Dexterity.

Proposal & Implementation

Take the following methods from https://github.com/collective/collective.relationhelpers/blob/master/src/collective/relationhelpers/api.py and add them to the module content:

relations(obj, attribute=None, as_dict=False)
Get related objects.

unrestricted_relations(obj, attribute=None, as_dict=False)
Get related objects without permission checks.

backrelations(obj, attribute=None, as_dict=False)
Get objects with a relation to this object.

unrestricted_backrelations(obj, attribute=None, as_dict=False)
Get objects with a relation to this object without permission checks.

relation(obj, attribute)
Get related object. This is only valid if the attribute is the name of a relationChoice field on the object.

unrestricted_relation(obj, attribute)
Get related object without permission checks. See relation

backrelation(obj, attribute)
Get relating object. This only makes sense when one item has a relation of this type to the obj.
One example is parent -> child where only one parent can exist.

unrestricted_backrelation(obj, attribute)
Get relating object without permission checks. See backrelation

link_objects(source, target, relationship)
Link objects: Create a relation between two objects using the specified relationship.
From the parameter relationship the method will find out what kind of relationship you want to create (RelationChoice, RelationList) by inspecting the schema-field on the source-object.
The method also works for linkintegrity-relations and relations between working-copies.
Example: To use the default-behavior plone.relateditems use the field-name relatedItems as relationship: link_objects(obj, anotherobj, 'relatedItems').

I'm very open to suggestions as to which methods should be added, how the arguments should be called and what they should do exactly.

Questions:

  • attributes is maybe not the best choice to specify the relationship. Maybe relation?
  • backrelation and unrestricted_backrelation are weird and prbably do not fall unter the 80%/20% rule of plone.api. We could drop these.
  • The argument relationship in link_objects could maybe default to relatedItem.

Deliverables

Code, tests and documentation. The code exists, there are no tests yet.

Risks

Some people might attempt to use these methods with Archetypes-content. I suggest to throw a InvalidParameterError if a AT object is passed to any of these methods.

Participants

Philip Bauer

feature (plip)

Most helpful comment

unrestricted_relations(obj, attribute=None, as_dict=False)
Get related objects without permission checks.

unrestricted_backrelations(obj, attribute=None, as_dict=False)
Get objects with a relation to this object without permission checks.

I know we have unrestricted_traverse tradition, but can't we just add unrestricted=True as a parameter to the normal methods instead of duplicating them?

All 17 comments

What about using

api.relation.targets(source, attribute=None, as_dict=False)
api.relation.sources(target, attribute=None, as_dict=False)
api.relation.create(source, target, relationship)

instead of:

api.content.relations(obj, attribute=None, as_dict=False)
api.content.backrelations(obj, attribute=None, as_dict=False)
api.content.link_objects(source, target, relationship)

?

@ale-rt good point and still ... not sure

api.relation.targets to get where obj has relations to?
api.relation.sources to get where obj is related from?
api.relation.create is clear.

why not use that "from" and "to" as this is already used (to_object, from_object)?
api.relation.to(...)
api.relation.from(...)
api.relation.link(...) - as a replacement for .create()

not sure, if thats better, cause its closer to the implementation-methods or more stupid, as an api-user does not need to know that.

@pbauer kudos for the topic - i am looking forward to this :)

  • update (1min later):
    ok - i personally like api.relation(s?) more then api.content.relation*
  • relations are not content-only but also for forms
  • shorter import is easier to memorize.

why not use that "from" and "to" as this is already used (to_object, from_object)?

I picked source and target to be consistent with the proposed api.content.link_objects(source, target, relationship).
I see a problem in changing source with from and target to to because from is a reserved keyword.

... because from is a reserved keyword.

good point! didn't think about that.

why differentiate between forth and back relations? Just return what you ask for. For example, using a query string:
_api.relations.get_('') which means every object which relates to something
or
_api.relations.get_('<* * UID>') which means every object which has a relation to the object
or
_api.relations.get_('') which means every object which has a "myrelationtype" relation to the object

you can also ask for:
_api.relations.get_('') to see if there's a relation between this objects.

The same way you can do:
_api.relations.set_('')

to create the relation.

Note: if you prefer, you can do _api.relations.get_(source, relation, target) instead of a query string.

This might be interesting to get the relations:

api.relations.get(source=None, relation=None, target=None)

Anyway most of the times I do not want to get the relations but the target object.

i have both cases lots of times. searching targets and sources

but i like the get a lot!
as far as i would understand that
relations.get(source=obj) -> a list relations where given object is related to
relations.get(target=obj) -> a list of relations where given object is related from
relations.get(source=obj, target=obj) -> a list (or better dict like {'from':[rels]}, {'to':[rels]} ?) of relations where obj is related to OR from
relations.get(relation=) -> i don't get this atm (but don't mind)

a relations.set(source=, relation=None, target=) would be great -> i don't get the relation here as well :D

unrestricted_relations(obj, attribute=None, as_dict=False)
Get related objects without permission checks.

unrestricted_backrelations(obj, attribute=None, as_dict=False)
Get objects with a relation to this object without permission checks.

I know we have unrestricted_traverse tradition, but can't we just add unrestricted=True as a parameter to the normal methods instead of duplicating them?

What about using

api.relation.targets(source, attribute=None, as_dict=False)
api.relation.sources(target, attribute=None, as_dict=False)
api.relation.create(source, target, relationship)

Relations as discussed are always between content items. Hoisting them in the upper api namespace might suggest to devs they are an independent feature. i.e. that I can use these methods to relate content to users, the portal or the env.

api.comtent.relations seems the intuitive location for me.

From the parameter relationship the method will find out what kind of relationship you want to create (RelationChoice, RelationList) by inspecting the schema-field on the source-object.

Is there a way to look up the types of relations available that you can set here? Or is it just a string used as an identifier and an agreement on the strings?

An extra method relationship_types could be handy here to list / query them.

What about using
api.relation.targets(source, attribute=None, as_dict=False)
api.relation.sources(target, attribute=None, as_dict=False)
api.relation.create(source, target, relationship)

Relations as discussed are always between content items. Hoisting them in the upper api namespace might suggest to devs they are an independent feature. i.e. that I can use these methods to relate content to users, the portal or the env.

api.comtent.relations seems the intuitive location for me.

i thought the same, but switched my mind.
relations are fields. on forms. not only allowed for content-forms. and they can point to whatever vocabulary you might add.

so their flexibility is beyond content-related usage.
(think about a controlpanel form for example.)
still: i get your point and it is still valid a valid

+1 on the PLIP from me.

Also:
I agree with @ale-rt about this having its own module relation

I agree with @fredvd about not needing an additional method for unrestricted search, and having an additional method for returning the type of relationships.

I was about to leave another way trying to make the interface more simple, but after reading all the opinions I agree with the direction of this PLIP.

Better than this just if it would be a property of the object where you seamlessly add or remove objects like if deal with a list, but this would require some black magic 馃槄

obj.relation.append(obj)

i thought the same, but switched my mind.
relations are fields. on forms. not only allowed for content-forms. and they can point to whatever vocabulary you might add.

Sorry but I don't understand this. If I read @pbauer 's abstract at the top this API deals with relations between dexterity content here. The purpose of the api is to save and retrieve these relations:

Add methods to plone.api that help deal with relations on dexterity content. Plone-Developer often model applications
that make use of relations. zc.relation is very powerful but the api to work with relations is cumbersome at best and has
quite some pitfalls.

Whatever happens in the presentation layer, forms, vocabularies etc. used to select content and how you expose the relations to the user interface has not been discussed so far.

+1 for the PLIP,

Putting it inside the content module makes sense to me.

Questions:
attributes is maybe not the best choice to specify the relationship. Maybe relation?

isn't it the field name? why not call it field or field_name?
If not i would prefer attribute_name over relation.

backrelation and unrestricted_backrelation are weird and prbably do not fall unter the 80%/20% rule of plone.api. We could drop these.

-1 for backrelation, does not hurt anybody and is more consistant.
+1 for not using unrestricted_backrelation, but this also applies to all other usage of unrestricted_* methos, here I'm with the others on having this as parameter.

The argument relationship in link_objects could maybe default to relatedItem.

+1

In general i thing it's better to have one or two more methods, that having to many parameters.
I like this examples:

content.relation.sources()
content.relation.targets()
content.relation.link()
content.relation.get() for a nested dict with relations, including backreferences.

Anyway most of the times I do not want to get the relations but the target object.

In case we have many linked object, one might want to iterate over the relation and not get all objects at once.
So it would be helpful to have at least one method to get the relations and not the objects it self.

relations.get(relation=) -> i don't get this atm (but don't mind)

a relations.set(source=, relation=None, target=) would be great -> i don't get the relation here as well :D

you can have different type of relation in general. In Plone we have only a generic "relatedto" relation (for example https://www.dublincore.org/specifications/dublin-core/dcmi-terms/#http://purl.org/dc/terms/relation) but there can be more types. BTW, you can dismiss relation parameter in the plone context.

+1

Was this page helpful?
0 / 5 - 0 ratings