Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #533: Implement API get_vocabulary & get_vocabularies_name #557

Merged
merged 15 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion docs/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,6 @@ portal = api.portal.get()
api.content.transition(obj=portal['about'], transition='reject', comment='You had a typo on your page.')
```


(content-disable-roles-acquisition-example)=

## Disable local roles acquisition
Expand Down Expand Up @@ -536,6 +535,65 @@ view = api.content.get_view(
%
% self.assertEqual(view.__name__, u'plone')

(content-get-vocabulary-example)=

## Get vocabulary

To get a vocabulary by name, use {meth}`api.content.get_vocabulary`.

```python
from plone import api

# Get vocabulary using default portal context
vocab = api.content.get_vocabulary(name='plone.app.vocabularies.PortalTypes')

# Get vocabulary with specific context
context = api.portal.get()['about']
states_vocab = api.content.get_vocabulary(
name='plone.app.vocabularies.WorkflowStates',
context=context
)

# Access vocabulary terms
for term in vocab:
print(f"Value: {term.value}, Title: {term.title}")
```

% invisible-code-block: python
%
% self.assertTrue(vocab)
% self.assertTrue(hasattr(vocab, '__iter__'))
% self.assertTrue(states_vocab)

(content-get-vocabularies-names-example)=

## Get all vocabulary names

To get a list of all available vocabulary names in your Plone site, use {meth}`api.content.get_vocabularies_names`.

```python
from plone import api

# Get all vocabulary names
vocab_names = api.content.get_vocabularies_names()

# Common vocabularies that should be available
common_vocabs = [
'plone.app.vocabularies.PortalTypes',
'plone.app.vocabularies.WorkflowStates',
'plone.app.vocabularies.WorkflowTransitions'
]

for vocab_name in common_vocabs:
assert vocab_name in vocab_names
```

% invisible-code-block: python
%
% self.assertTrue(isinstance(vocab_names, list))
% self.assertTrue(len(vocab_names) > 0)
% self.assertTrue('plone.app.vocabularies.PortalTypes' in vocab_names)

## Further reading

For more information on possible flags and usage options please see the full {ref}`plone-api-content` specification.
6 changes: 6 additions & 0 deletions news/533.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Added two new content API functions:
- api.content.get_vocabulary: Get a vocabulary by name
- api.content.get_vocabularies_names: Get a list of all available vocabulary names

This enhances vocabulary management capabilities in plone.api by providing
simple access to Plone's vocabulary system.
29 changes: 29 additions & 0 deletions src/plone/api/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
from zope.component import ComponentLookupError
from zope.component import getMultiAdapter
from zope.component import getSiteManager
from zope.component import getUtilitiesFor
from zope.component import getUtility
from zope.container.interfaces import INameChooser
from zope.globalrequest import getRequest
from zope.interface import Interface
from zope.interface import providedBy
from zope.schema.interfaces import IVocabularyFactory

import random
import transaction
Expand Down Expand Up @@ -666,3 +669,29 @@ def find(context=None, depth=None, unrestricted=False, **kwargs):
return catalog.unrestrictedSearchResults(**query)
else:
return catalog(**query)


@required_parameters("name")
def get_vocabulary(name=None, context=None):
"""Return a vocabulary object with given name.

:param name: Name of the vocabulary.
:param context: Context to be applied to the vocabulary. Default: portal root
:return: A vocabulary that implements the IVocabularyTokenized interface.
"""
if not context:
context = portal.get()
try:
vocab = getUtility(IVocabularyFactory, name)
except ComponentLookupError:
raise InvalidParameterError(f"No vocabulary with name '{name}' available.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for your contribution!
It might be interesting to have an error message consistent with the one raised by the get_view function:

except ComponentLookupError:
# Getting all available views
sm = getSiteManager()
available_views = sm.adapters.lookupAll(
required=(providedBy(context), providedBy(request)),
provided=Interface,
)
# Check if the requested view is available
# by getting the names of all available views
available_view_names = [view[0] for view in available_views]
if name not in available_view_names:
# Raise an error if the requested view is not available.
raise InvalidParameterError(
"Cannot find a view with name '{name}'.\n"
"Available views are:\n"
"{views}".format(
name=name,
views="\n".join(sorted(available_view_names)),
),
)

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Making the changes.

return vocab(context)


def get_vocabularies_names():
"""Return a list of vocabularies names.

:return: A list of vocabularies names.
"""
all_vocabs = getUtilitiesFor(IVocabularyFactory)
return [v[0] for v in all_vocabs]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about:

Suggested change
return [v[0] for v in all_vocabs]
return sorted([name for name, vocabulary in getUtilitiesFor(IVocabularyFactory)])

?

87 changes: 87 additions & 0 deletions src/plone/api/tests/test_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -1447,3 +1447,90 @@ def test_get_view_view_not_found(self):

for should_be_there in should_be_theres:
self.assertIn((should_be_there + "\n"), str(cm.exception))

def test_get_vocabulary(self):
"""Test getting a vocabulary by name."""
from plone.api.exc import InvalidParameterError
from plone.api.exc import MissingParameterError

# The vocabulary name must be given as parameter
with self.assertRaises(MissingParameterError):
api.content.get_vocabulary()

# Test getting a commonly available vocabulary
vocab = api.content.get_vocabulary(name="plone.app.vocabularies.PortalTypes")
self.assertTrue(vocab)
self.assertTrue(hasattr(vocab, "__iter__")) # Verify it's iterable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would replace these lines with a self.assertIsInstance

Copy link
Contributor Author

@ujsquared ujsquared Feb 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you suggest importing collections.abc just to replace current approach?
i can try going with this
image
but i'm not sure if vocabularies with length 0 are to be supported or not.

Copy link
Member

@ale-rt ale-rt Feb 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, sorry. I was referring to something like:

Suggested change
self.assertTrue(vocab)
self.assertTrue(hasattr(vocab, "__iter__")) # Verify it's iterable
self.assertIsInstance(vocabulary, SimpleVocabulary)


# Test with invalid vocabulary name
with self.assertRaises(InvalidParameterError) as cm:
api.content.get_vocabulary(name="non.existing.vocabulary")

self.assertEqual(
str(cm.exception),
"No vocabulary with name 'non.existing.vocabulary' available.",
)

# Test with context
vocab_with_context = api.content.get_vocabulary(
name="plone.app.vocabularies.PortalTypes", context=self.portal
)
self.assertTrue(vocab_with_context)

def test_get_vocabularies_names(self):
"""Test getting list of vocabulary names."""
names = api.content.get_vocabularies_names()

# Test we get a list of strings
self.assertIsInstance(names, list)
self.assertTrue(len(names) > 0)
self.assertIsInstance(names[0], str)

# Test that common vocabularies are included
common_vocabs = [
"plone.app.vocabularies.PortalTypes",
"plone.app.vocabularies.WorkflowStates",
"plone.app.vocabularies.WorkflowTransitions",
]

for vocab_name in common_vocabs:
self.assertIn(vocab_name, names)

def test_vocabulary_terms(self):
"""Test the actual content of retrieved vocabularies."""
# Get portal types vocabulary
types_vocab = api.content.get_vocabulary("plone.app.vocabularies.PortalTypes")

# Check that we have some common content types
types = [term.value for term in types_vocab]
self.assertIn("Document", types)
self.assertIn("Folder", types)

# Get workflow states vocabulary
states_vocab = api.content.get_vocabulary(
"plone.app.vocabularies.WorkflowStates"
)

# Check that we have some common workflow states
states = [term.value for term in states_vocab]
self.assertIn("private", states)
self.assertIn("published", states)

def test_vocabulary_context_sensitivity(self):
Copy link
Member

@ksuess ksuess Feb 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test on context sensitivity would need a vocabulary that is context sensitive to be meaningful.
I would say this test is not really needed, as the new plone.api method api.content.get_vocabulary is simply calling getUtility(IVocabularyFactory, name) and applying this vocabulary to the context. So the underlying code is testing the context sensitivity already.
Further opinions are welcome.

"""Test that vocabularies respect their context."""
# Create some content to test with
folder = api.content.create(
container=self.portal, type="Folder", id="test-folder"
)

# Get vocabulary in different contexts
root_vocab = api.content.get_vocabulary(
name="plone.app.vocabularies.PortalTypes", context=self.portal
)
folder_vocab = api.content.get_vocabulary(
name="plone.app.vocabularies.PortalTypes", context=folder
)

# Both vocabularies should be valid
self.assertTrue(root_vocab)
self.assertTrue(folder_vocab)