diff --git a/docs/backend/vocabularies.md b/docs/backend/vocabularies.md index 780644ad6..8e89e5909 100644 --- a/docs/backend/vocabularies.md +++ b/docs/backend/vocabularies.md @@ -1,27 +1,679 @@ --- myst: html_meta: - "description": "Vocabularies are often used for select fields. They allow editing through the user interface or can be updated dynamically." - "property=og:description": "Vocabularies are often used for select fields. They allow editing through the user interface or can be updated dynamically." + "description": "How to create and use vocabularies in Plone" + "property=og:description": "How to create and use vocabularies in Plone" "property=og:title": "Vocabularies" - "keywords": "Vocabularies, schema, select" + "keywords": "Plone, vocabularies, zope.schema, Choice, plonecli" --- -(backend-vocabularies-label)= +(vocabularies-label)= # Vocabularies -```{seealso} -See the chapter {ref}`training:vocabularies-label` from the Mastering Plone 6 Training. +Vocabularies are lists of value/title pairs that provide options for selection fields. +They are commonly used with `zope.schema.Choice` fields to populate dropdown menus, checkboxes, and radio buttons in forms. + +The `zope.schema` package provides tools to programmatically construct vocabularies using `SimpleVocabulary` and `SimpleTerm` objects. + + +## Vocabulary Terms + +A vocabulary consists of terms. Each term represents one selectable option and has three key attributes: + +`SimpleTerm.token` +: Must be an ASCII string. + This is the value passed with the request when the form is submitted. + A token must uniquely identify a term. + +`SimpleTerm.value` +: The actual value stored on the object. + This is not passed to the browser or used in the form. + The value is often a unicode string, but can be any type of object. + +`SimpleTerm.title` +: A unicode string or translatable message. + It is used for display in the form. + +```{note} +If you need internationalized texts, only the `title` should be translated. +The `value` and `token` must always carry the same value. +``` + + +## Creating a Static Vocabulary + +### From a List of Tuples + +```python +from zope.schema.vocabulary import SimpleTerm +from zope.schema.vocabulary import SimpleVocabulary + +items = [ + ('value1', 'This is label for item'), + ('value2', 'This is label for value 2'), +] + +terms = [ + SimpleTerm(value=pair[0], token=pair[0], title=pair[1]) + for pair in items +] + +my_vocabulary = SimpleVocabulary(terms) +``` + +### From a List of Values + +For simple cases where value, token, and title are the same: + +```python +from zope.schema.vocabulary import SimpleVocabulary + +values = ['foo', 'bar', 'baz'] +my_vocabulary = SimpleVocabulary.fromValues(values) +``` + +### Using the Vocabulary in a Schema + +```python +from plone.supermodel import model +from zope import schema + +class ISampleSchema(model.Schema): + + content_type = schema.Choice( + title='Content Type', + vocabulary=my_vocabulary, + required=True, + ) +``` + + +## Registering a Named Vocabulary + +To make a vocabulary reusable across your application, register it as a named utility. + +### Creating a Vocabulary Factory + +```python +from zope.interface import provider +from zope.schema.interfaces import IVocabularyFactory +from zope.schema.vocabulary import SimpleTerm +from zope.schema.vocabulary import SimpleVocabulary + + +@provider(IVocabularyFactory) +def priority_vocabulary_factory(context): + """A vocabulary of priority levels.""" + items = [ + ('low', 'Low Priority'), + ('normal', 'Normal Priority'), + ('high', 'High Priority'), + ('urgent', 'Urgent'), + ] + terms = [ + SimpleTerm(value=pair[0], token=pair[0], title=pair[1]) + for pair in items + ] + return SimpleVocabulary(terms) +``` + +### Registering in ZCML + +Register the vocabulary factory in your `configure.zcml`: + +```xml + +``` + +### Using a Named Vocabulary in a Schema + +Reference the vocabulary by its registered name: + +```python +from plone.supermodel import model +from zope import schema + + +class ITask(model.Schema): + + priority = schema.Choice( + title='Priority', + vocabulary='example.vocabularies.priority', + required=True, + ) +``` + + +## Retrieving a Vocabulary Programmatically + +```python +from zope.component import getUtility +from zope.schema.interfaces import IVocabularyFactory + +factory = getUtility(IVocabularyFactory, 'example.vocabularies.priority') +vocabulary = factory(context) +``` + + +## Working with Vocabulary Terms + +### Getting a Term by Value + +```python +term = vocabulary.getTerm('high') +print(term.value) # 'high' +print(term.token) # 'high' +print(term.title) # 'High Priority' +``` + +### Getting a Term by Token + +```python +term = vocabulary.getTermByToken('high') +``` + +### Iterating Over a Vocabulary + +```python +for term in vocabulary: + print(f"Value: {term.value}, Token: {term.token}, Title: {term.title}") +``` + +### Checking if a Value Exists + +```python +if 'high' in vocabulary: + print("Value exists in vocabulary") +``` + + +## Dynamic Vocabularies + +Dynamic vocabularies generate their values at runtime based on context data, such as catalog queries or registry settings. + +### Context Source Binder + +Use `IContextSourceBinder` when your vocabulary depends on the current context: + +```python +from zope.interface import provider +from zope.schema.interfaces import IContextSourceBinder +from zope.schema.vocabulary import SimpleTerm +from zope.schema.vocabulary import SimpleVocabulary + + +@provider(IContextSourceBinder) +def available_documents_source(context): + """Return a vocabulary of documents in the current folder.""" + # Get the portal catalog + catalog = context.portal_catalog + + # Query for documents + brains = catalog.searchResults( + portal_type='Document', + path={ + 'query': '/'.join(context.getPhysicalPath()), + 'depth': 1, + }, + ) + + # Build vocabulary terms + terms = [ + SimpleTerm( + value=brain.UID, + token=brain.UID, + title=brain.Title, + ) + for brain in brains + ] + + return SimpleVocabulary(terms) +``` + +### Using a Source in a Schema + +Use `source` instead of `vocabulary` when using a context source binder: + +```python +from plone.supermodel import model +from zope import schema + + +class IMyContent(model.Schema): + + related_document = schema.Choice( + title='Related Document', + source=available_documents_source, + required=False, + ) +``` + +### Dynamic Vocabulary from Registry + +A common pattern is to populate vocabulary options from the Plone registry: + +```python +from plone import api +from zope.interface import provider +from zope.schema.interfaces import IVocabularyFactory +from zope.schema.vocabulary import SimpleTerm +from zope.schema.vocabulary import SimpleVocabulary + + +@provider(IVocabularyFactory) +def talk_types_vocabulary_factory(context): + """Vocabulary of talk types from registry.""" + types = api.portal.get_registry_record( + 'example.conference.talk_types', + default=['Talk', 'Keynote', 'Workshop'], + ) + terms = [ + SimpleTerm(value=t, token=t, title=t) + for t in types + ] + return SimpleVocabulary(terms) +``` + + +## Built-in Plone Vocabularies + +Plone provides many useful vocabularies in the `plone.app.vocabularies` package. + +### Common Vocabularies + +| Vocabulary Name | Description | +|----------------|-------------| +| `plone.app.vocabularies.PortalTypes` | All portal types installed | +| `plone.app.vocabularies.ReallyUserFriendlyTypes` | User-friendly content types | +| `plone.app.vocabularies.UserFriendlyTypes` | Types filtered by the Types Tool | +| `plone.app.vocabularies.Workflows` | All installed workflows | +| `plone.app.vocabularies.WorkflowStates` | All workflow states | +| `plone.app.vocabularies.WorkflowTransitions` | All workflow transitions | +| `plone.app.vocabularies.AvailableContentLanguages` | Available content languages | +| `plone.app.vocabularies.SupportedContentLanguages` | Supported content languages | +| `plone.app.vocabularies.Roles` | User roles available in the site | +| `plone.app.vocabularies.Groups` | All groups in the site | +| `plone.app.vocabularies.Users` | All users in the site | +| `plone.app.vocabularies.Skins` | Available skins/themes | +| `plone.app.vocabularies.Actions` | All action category ids | +| `plone.app.vocabularies.Timezones` | Common timezones from pytz | +| `plone.app.vocabularies.AvailableEditors` | Configured WYSIWYG editors | +| `plone.app.vocabularies.ImagesScales` | All available image scales | +| `plone.app.vocabularies.Permissions` | All available permissions | +| `plone.app.vocabularies.Catalog` | All catalog indexes | + +### Using Built-in Vocabularies + +```python +from plone.supermodel import model +from zope import schema + + +class IMyContent(model.Schema): + + allowed_types = schema.List( + title='Allowed Content Types', + value_type=schema.Choice( + vocabulary='plone.app.vocabularies.ReallyUserFriendlyTypes', + ), + required=False, + ) + + workflow = schema.Choice( + title='Workflow', + vocabulary='plone.app.vocabularies.Workflows', + required=False, + ) +``` + + +## Creating a Vocabulary with plonecli + +The easiest way to create a vocabulary in a Plone add-on is using `plonecli` with `bobtemplates.plone`. + +### Installation + +```bash +UV tool install plonecli +``` + +### Creating an Add-on with a Vocabulary + +First, create a new Plone add-on: + +```bash +plonecli create addon src/collective.myaddon +``` + +Then, navigate to the package directory and add a vocabulary: + +```bash +cd src/collective.myaddon +plonecli add vocabulary +``` + +The CLI will prompt you for the vocabulary class name. Enter a name like `MyCustomVocabulary`. + +### Generated File Structure + +The `plonecli add vocabulary` command creates the following structure: + +``` +src/collective.myaddon/ +└── src/ + └── collective/ + └── myaddon/ + ├── configure.zcml # Updated with vocabulary registration + └── vocabularies/ + ├── __init__.py + ├── configure.zcml # Vocabulary ZCML configuration + └── my_custom_vocabulary.py # Your vocabulary implementation +``` + +### Generated Vocabulary Code + +The generated vocabulary file looks like this: + +```python +# -*- coding: utf-8 -*- +from collective.myaddon import _ +from plone import api +from zope.interface import provider +from zope.schema.interfaces import IVocabularyFactory +from zope.schema.vocabulary import SimpleTerm +from zope.schema.vocabulary import SimpleVocabulary + + +@provider(IVocabularyFactory) +def MyCustomVocabularyFactory(context): + """Vocabulary factory for my custom vocabulary.""" + # TODO: Replace with your vocabulary terms + values = [ + ('value1', _('Title 1')), + ('value2', _('Title 2')), + ('value3', _('Title 3')), + ] + terms = [ + SimpleTerm(value=pair[0], token=pair[0], title=pair[1]) + for pair in values + ] + return SimpleVocabulary(terms) +``` + +### Generated ZCML Registration + +The vocabulary is automatically registered in `vocabularies/configure.zcml`: + +```xml + + + + + +``` + +### Using the Generated Vocabulary + +After building and installing your add-on, use the vocabulary in your schemas: + +```python +from plone.supermodel import model +from zope import schema + + +class IMyContent(model.Schema): + + my_field = schema.Choice( + title='My Field', + vocabulary='collective.myaddon.MyCustomVocabulary', + required=True, + ) +``` + +The vocabulary will also appear in the Dexterity schema editor in your browser. + + +## Multiple Choice Fields + +For fields that allow multiple selections, use `schema.List` or `schema.Set` with a `Choice` value type: + +### List with Multiple Choices + +```python +from plone.supermodel import model +from zope import schema + + +class IArticle(model.Schema): + + categories = schema.List( + title='Categories', + description='Select one or more categories', + value_type=schema.Choice( + vocabulary='example.vocabularies.categories', + ), + required=False, + ) +``` + +### Set with Multiple Choices (No Duplicates) + +```python +from plone.supermodel import model +from zope import schema + + +class IArticle(model.Schema): + + tags = schema.Set( + title='Tags', + description='Select one or more tags', + value_type=schema.Choice( + vocabulary='example.vocabularies.tags', + ), + required=False, + ) +``` + + +## Customizing Vocabulary Widgets + +### Using Checkboxes for Multiple Choice + +```python +from plone.autoform import directives +from plone.supermodel import model +from z3c.form.browser.checkbox import CheckBoxFieldWidget +from zope import schema + + +class IMyContent(model.Schema): + + directives.widget('options', CheckBoxFieldWidget) + options = schema.List( + title='Options', + value_type=schema.Choice( + vocabulary='example.vocabularies.options', + ), + required=False, + ) +``` + +### Using Radio Buttons for Single Choice + +```python +from plone.autoform import directives +from plone.supermodel import model +from z3c.form.browser.radio import RadioFieldWidget +from zope import schema + + +class IMyContent(model.Schema): + + directives.widget('priority', RadioFieldWidget) + priority = schema.Choice( + title='Priority', + vocabulary='example.vocabularies.priority', + required=True, + ) ``` -```{todo} -Contribute to this documentation! -See issue [Backend > Vocabularies needs content](https://github.com/plone/documentation/issues/1306). + +## Translated Vocabulary Titles + +For internationalized vocabulary titles, use message factories: + +```python +from collective.myaddon import _ +from zope.interface import provider +from zope.schema.interfaces import IVocabularyFactory +from zope.schema.vocabulary import SimpleTerm +from zope.schema.vocabulary import SimpleVocabulary + + +@provider(IVocabularyFactory) +def status_vocabulary_factory(context): + """Vocabulary with translated titles.""" + items = [ + ('draft', _('Draft')), + ('pending', _('Pending Review')), + ('published', _('Published')), + ('archived', _('Archived')), + ] + terms = [ + SimpleTerm(value=pair[0], token=pair[0], title=pair[1]) + for pair in items + ] + return SimpleVocabulary(terms) +``` + +Don't forget to add translations to your `.po` files for each supported language. + + +## Vocabulary with Catalog Query + +A complete example showing a vocabulary populated from a catalog query: + +```python +from plone import api +from zope.interface import provider +from zope.schema.interfaces import IVocabularyFactory +from zope.schema.vocabulary import SimpleTerm +from zope.schema.vocabulary import SimpleVocabulary + + +@provider(IVocabularyFactory) +def authors_vocabulary_factory(context): + """Vocabulary of authors from existing content.""" + catalog = api.portal.get_tool('portal_catalog') + + # Get unique creators from all content + brains = catalog.searchResults( + portal_type=['Document', 'News Item', 'Event'], + ) + + # Collect unique authors + authors = set() + for brain in brains: + if brain.Creator: + authors.add(brain.Creator) + + # Build terms sorted alphabetically + terms = [] + for author_id in sorted(authors): + # Try to get the user's full name + user = api.user.get(author_id) + if user: + fullname = user.getProperty('fullname') or author_id + else: + fullname = author_id + + terms.append( + SimpleTerm( + value=author_id, + token=author_id, + title=fullname, + ) + ) + + return SimpleVocabulary(terms) ``` + +## REST API Access + +Vocabularies are accessible via the Plone REST API at the `@vocabularies` endpoint. + +### List All Vocabularies + +```bash +curl -X GET http://localhost:8080/Plone/@vocabularies \ + -H "Accept: application/json" \ + --user admin:secret +``` + +### Get Vocabulary Terms + +```bash +curl -X GET http://localhost:8080/Plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes \ + -H "Accept: application/json" \ + --user admin:secret +``` + +The response includes the `token` and `title` for each term: + +```json +{ + "@id": "http://localhost:8080/Plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes", + "items": [ + {"token": "Document", "title": "Page"}, + {"token": "News Item", "title": "News Item"}, + {"token": "Event", "title": "Event"} + ], + "items_total": 3 +} +``` + + +## Best Practices + +1. **Use named vocabularies** for reusable options that appear in multiple schemas. + +2. **Use context source binders** when the vocabulary values depend on the current context. + +3. **Keep tokens ASCII-safe** since they are passed via HTTP requests. + +4. **Make titles translatable** using message factories for internationalization. + +5. **Cache expensive vocabularies** if they query the catalog or external services. + +6. **Use plonecli** to scaffold vocabularies in your add-ons to follow best practices. + +7. **Document your vocabularies** with docstrings explaining what they contain and when to use them. + + +## See Also + +- {ref}`fields-label` for field types that use vocabularies +- {doc}`../forms/index` for form handling +- [zope.schema documentation](https://zopeschema.readthedocs.io/) +- [plone.app.vocabularies source code](https://github.com/plone/plone.app.vocabularies) +- [bobtemplates.plone documentation](https://bobtemplatesplone.readthedocs.io/) + + ## Related content - {doc}`/backend/fields` - {doc}`/backend/schemas` -- {doc}`/backend/content-types/index` \ No newline at end of file +- {doc}`/backend/content-types/index`