plone.api 2.2.5__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {plone_api-2.2.5 → plone_api-2.3.0}/CHANGES.rst +19 -0
  2. {plone_api-2.2.5 → plone_api-2.3.0}/PKG-INFO +20 -1
  3. {plone_api-2.2.5 → plone_api-2.3.0}/docs/content.md +56 -3
  4. {plone_api-2.2.5 → plone_api-2.3.0}/docs/contribute.md +26 -49
  5. {plone_api-2.2.5/src/plone/api/tests/doctests → plone_api-2.3.0/docs}/portal.md +43 -0
  6. {plone_api-2.2.5 → plone_api-2.3.0}/setup.py +1 -1
  7. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/content.py +30 -0
  8. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/portal.py +41 -0
  9. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/doctests/content.md +56 -3
  10. plone_api-2.3.0/src/plone/api/tests/doctests/contribute.md +217 -0
  11. {plone_api-2.2.5/docs → plone_api-2.3.0/src/plone/api/tests/doctests}/portal.md +43 -0
  12. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/test_content.py +63 -0
  13. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/test_doctests.py +1 -1
  14. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/test_portal.py +69 -0
  15. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone.api.egg-info/PKG-INFO +20 -1
  16. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone.api.egg-info/SOURCES.txt +1 -0
  17. {plone_api-2.2.5 → plone_api-2.3.0}/CONTRIBUTING.rst +0 -0
  18. {plone_api-2.2.5 → plone_api-2.3.0}/LICENSE +0 -0
  19. {plone_api-2.2.5 → plone_api-2.3.0}/MANIFEST.in +0 -0
  20. {plone_api-2.2.5 → plone_api-2.3.0}/README.md +0 -0
  21. {plone_api-2.2.5 → plone_api-2.3.0}/docs/about.md +0 -0
  22. {plone_api-2.2.5 → plone_api-2.3.0}/docs/env.md +0 -0
  23. {plone_api-2.2.5 → plone_api-2.3.0}/docs/group.md +0 -0
  24. {plone_api-2.2.5 → plone_api-2.3.0}/docs/index.md +0 -0
  25. {plone_api-2.2.5 → plone_api-2.3.0}/docs/relation.md +0 -0
  26. {plone_api-2.2.5 → plone_api-2.3.0}/docs/user.md +0 -0
  27. {plone_api-2.2.5 → plone_api-2.3.0}/pyproject.toml +0 -0
  28. {plone_api-2.2.5 → plone_api-2.3.0}/setup.cfg +0 -0
  29. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/__init__.py +0 -0
  30. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/__init__.py +0 -0
  31. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/configure.zcml +0 -0
  32. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/env.py +0 -0
  33. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/exc.py +0 -0
  34. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/group.py +0 -0
  35. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/profiles/testfixture/metadata.xml +0 -0
  36. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/profiles/testfixture/types/Dexterity_Folder.xml +0 -0
  37. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/profiles/testfixture/types/Dexterity_Item.xml +0 -0
  38. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/profiles/testfixture/types.xml +0 -0
  39. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/relation.py +0 -0
  40. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/testing.zcml +0 -0
  41. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/Dexterity_Folder.xml +0 -0
  42. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/Dexterity_Item.xml +0 -0
  43. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/__init__.py +0 -0
  44. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/base.py +0 -0
  45. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/doctests/about.md +0 -0
  46. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/doctests/env.md +0 -0
  47. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/doctests/group.md +0 -0
  48. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/doctests/relation.md +0 -0
  49. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/doctests/user.md +0 -0
  50. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/test_env.py +0 -0
  51. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/test_group.py +0 -0
  52. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/test_relation.py +0 -0
  53. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/test_user.py +0 -0
  54. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/tests/test_validation.py +0 -0
  55. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/user.py +0 -0
  56. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone/api/validation.py +0 -0
  57. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone.api.egg-info/dependency_links.txt +0 -0
  58. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone.api.egg-info/namespace_packages.txt +0 -0
  59. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone.api.egg-info/not-zip-safe +0 -0
  60. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone.api.egg-info/requires.txt +0 -0
  61. {plone_api-2.2.5 → plone_api-2.3.0}/src/plone.api.egg-info/top_level.txt +0 -0
  62. {plone_api-2.2.5 → plone_api-2.3.0}/tox.ini +0 -0
@@ -8,6 +8,25 @@ Changelog
8
8
 
9
9
  .. towncrier release notes start
10
10
 
11
+ 2.3.0 (2025-02-21)
12
+ ------------------
13
+
14
+ New features:
15
+
16
+
17
+ - Added the content API helper function ``api.content.get_path``, which gets either the relative or absolute path of an object. @ujsquared (#532)
18
+ - Added two new portal API functions:
19
+ - ``api.portal.get_vocabulary``: Get a vocabulary by name
20
+ - ``api.portal.get_vocabulary_names``: Get a list of all available vocabulary names
21
+ @ujsquared (#533)
22
+
23
+
24
+ Internal:
25
+
26
+
27
+ - Making it easier for new contributors to get started with a simple Makefile and a tidy 'contributing' chapter. @ksuess (#558)
28
+
29
+
11
30
  2.2.5 (2025-01-24)
12
31
  ------------------
13
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: plone.api
3
- Version: 2.2.5
3
+ Version: 2.3.0
4
4
  Summary: A Plone API.
5
5
  Home-page: https://github.com/plone/plone.api
6
6
  Author: Plone Foundation
@@ -110,6 +110,25 @@ Changelog
110
110
 
111
111
  .. towncrier release notes start
112
112
 
113
+ 2.3.0 (2025-02-21)
114
+ ------------------
115
+
116
+ New features:
117
+
118
+
119
+ - Added the content API helper function ``api.content.get_path``, which gets either the relative or absolute path of an object. @ujsquared (#532)
120
+ - Added two new portal API functions:
121
+ - ``api.portal.get_vocabulary``: Get a vocabulary by name
122
+ - ``api.portal.get_vocabulary_names``: Get a list of all available vocabulary names
123
+ @ujsquared (#533)
124
+
125
+
126
+ Internal:
127
+
128
+
129
+ - Making it easier for new contributors to get started with a simple Makefile and a tidy 'contributing' chapter. @ksuess (#558)
130
+
131
+
113
132
  2.2.5 (2025-01-24)
114
133
  ------------------
115
134
 
@@ -78,7 +78,7 @@ plone (portal root)
78
78
  % api.content.create(container=events, type='Event', id='conference')
79
79
  % api.content.create(container=events, type='Event', id='sprint')
80
80
 
81
- The following operations will get objects from the structure above, including using {meth}`api.content.get`.
81
+ The following operations will get objects from the structure above, using {meth}`api.content.get`.
82
82
 
83
83
  ```python
84
84
  # let's first get the portal object
@@ -118,7 +118,7 @@ not_found = api.content.get(UID='notfound')
118
118
 
119
119
  ## Find content objects
120
120
 
121
- You can use the find function to search for content.
121
+ You can use the {func}`api.content.find` function to search for content.
122
122
 
123
123
  Finding all Documents:
124
124
 
@@ -460,7 +460,6 @@ portal = api.portal.get()
460
460
  api.content.transition(obj=portal['about'], transition='reject', comment='You had a typo on your page.')
461
461
  ```
462
462
 
463
-
464
463
  (content-disable-roles-acquisition-example)=
465
464
 
466
465
  ## Disable local roles acquisition
@@ -536,6 +535,60 @@ view = api.content.get_view(
536
535
  %
537
536
  % self.assertEqual(view.__name__, u'plone')
538
537
 
538
+ (content-get-path-example)=
539
+
540
+ ## Get content path
541
+
542
+ To get the path of a content object, use {func}`api.content.get_path`.
543
+ This function accepts an object for which you want to get its path as the required parameter `obj`, and an optional boolean parameter `relative` whose default is `False`.
544
+
545
+ It returns either an absolute path from the Zope root by default or when `relative` is set to `False`, or a relative path from the portal root when `relative` is set to `True`.
546
+
547
+ The following example shows how to get the absolute path from the Zope root.
548
+
549
+ ```python
550
+ from plone import api
551
+ portal = api.portal.get()
552
+
553
+ folder = portal['events']['training']
554
+ path = api.content.get_path(obj=folder)
555
+ assert path == '/plone/events/training'
556
+ ```
557
+
558
+ The following example shows how to get the portal-relative path.
559
+
560
+ ```python
561
+ rel_path = api.content.get_path(obj=folder, relative=True)
562
+ assert rel_path == 'events/training'
563
+ ```
564
+
565
+ If the API is used to fetch an object with the `relative` parameter set as `True`, and the object is outside the portal, it throws an `InvalidParameterError` error.
566
+
567
+ % invisible-code-block: python
568
+ %
569
+ % # Setup an object outside portal for testing error case
570
+ % app = portal.aq_parent
571
+ % app.manage_addFolder('outside_folder')
572
+ %
573
+ % # Test that getting relative path for object outside portal raises error
574
+ % from plone.api.exc import InvalidParameterError
575
+ % with self.assertRaises(InvalidParameterError):
576
+ % api.content.get_path(obj=app.outside_folder, relative=True)
577
+
578
+ ```python
579
+ from plone.api.exc import InvalidParameterError
580
+
581
+ # Getting path of an object outside portal raises InvalidParameterError
582
+ try:
583
+ outside_path = api.content.get_path(
584
+ obj=app.outside_folder,
585
+ relative=True
586
+ )
587
+ assert False, "Should raise InvalidParameterError and not reach this code"
588
+ except InvalidParameterError as e:
589
+ assert "Object not in portal path" in str(e)
590
+ ```
591
+
539
592
  ## Further reading
540
593
 
541
594
  For more information on possible flags and usage options please see the full {ref}`plone-api-content` specification.
@@ -18,65 +18,42 @@ Prepare your system by installing prerequisites.
18
18
 
19
19
  ### System libraries
20
20
 
21
- You need to install system libraries, as described in {ref}`plone:plone-prerequisites-label`, with the exception of GNU make.
21
+ You need to install system libraries, as described in {ref}`plone:plone-prerequisites-label`.
22
22
 
23
- ### tox
24
-
25
- [tox](https://tox.wiki/en/stable/index.html) automates and standardizes testing in Python.
26
- Install tox into your Python user space with the following command.
27
-
28
- ```shell
29
- python -m pip install --user tox
30
- ```
31
-
32
- ### pre-commit
33
-
34
- `plone.api` uses [pre-commit](https://pre-commit.com/) to automate code quality checks before every commit.
35
-
36
- Install pre-commit either with your system package manager.
37
- Alternatively you can install pre-commit into your Python user.
38
-
39
- ```shell
40
- python -m pip install --user pre-commit
41
- ```
42
-
43
- Once installed, set up the git hook scripts to run on every commit.
44
-
45
- ```shell
46
- pre-commit install
47
- ```
48
23
 
49
24
  ## Create development environment
50
25
 
51
26
  After satisfying the prerequisites, you are ready to create your development environment.
52
27
  `plone.api` uses `tox` as a wrapper around `coredev.buildout` to simplify development, whereas Plone core uses `coredev.buildout` directly.
28
+ Additionally `plone.api` uses `make` commands that invoke the `tox` commands as a convenience and for consistency across Plone packages.
53
29
 
54
- Start by changing your working directory to your project folder, and download the latest `plone.api` source code.
30
+ Start by changing your working directory to your project folder, and check out the latest `plone.api` source code.
55
31
 
56
32
  ```shell
57
33
  cd <your_project_folder>
58
34
  git clone https://github.com/plone/plone.api.git
59
35
  ```
60
36
 
61
- Next go into the newly created directory, and build your environment.
37
+ Run `make help` to see the available `make` commands.
62
38
 
63
- ```shell
64
- cd plone.api
65
- tox
39
+ ```console
40
+ check Check code base according to Plone standards
41
+ clean Clean environment
42
+ help This help message
43
+ linkcheck Check links in documentation
44
+ livehtml Build docs and watch for changes
45
+ test Run tests
66
46
  ```
67
47
 
68
- Go make some tea while `tox` runs all tasks listed by issuing the command `tox -l`.
48
+ The `make` commands `check`, `livehtml`, and `test` will create a development environment, if it does not already exist.
69
49
 
70
- Open `tox.ini` in your code editor to see all configured commands and what they do.
71
- Some helpful `tox` commands are shown below.
50
+ Test your code changes with the following command.
72
51
 
73
52
  ```shell
74
- tox -e py39-plone-60 # run all tests for Python 3.9 and Plone 6
75
- tox -e plone6docs # build documentation
76
- tox -e livehtml # build, serve, and reload changes to documentation
77
- tox -l # list all tox environments
53
+ make test
78
54
  ```
79
55
 
56
+
80
57
  (git-workflow)=
81
58
 
82
59
  ## git
@@ -109,22 +86,17 @@ For every feature change or addition to `plone.api`, you must add documentation
109
86
  {doc}`plone:contributing/documentation/index`
110
87
  ```
111
88
 
112
- After adding or modifying documentation, you can build the documentation with the following command.
89
+ When adding or modifying documentation, you can build the documentation with the following command.
90
+ As you edit the documentation, the started process automatically reloads your changes in the web browser.
113
91
 
114
92
  ```shell
115
- tox -e plone6docs
116
- ```
117
-
118
- Alternatively, you can automatically reload changes to the documentation as you edit it in a web browser.
119
-
120
- ```shell
121
- tox -e livehtml
93
+ make livehtml
122
94
  ```
123
95
 
124
96
  You can run a link checker on documentation.
125
97
 
126
98
  ```shell
127
- tox -e linkcheck
99
+ make linkcheck
128
100
  ```
129
101
 
130
102
  The [`plone.api` documentation](https://6.docs.plone.org/plone.api) is automatically generated from the documentation source files when its submodule is updated in the [main Plone `documentation` repository](https://github.com/plone/documentation/).
@@ -172,6 +144,10 @@ def foo(path=None, UID=None):
172
144
  % InvalidParameterError,
173
145
  % lambda: foo("/plone/blog", "abcd001")
174
146
  % )
147
+ %
148
+ % # Make it available for testing below
149
+ % from plone import api
150
+ % api.content.foo = foo
175
151
 
176
152
  Add documentation in {file}`docs/api/content.md`.
177
153
  Narrative documentation should describe what your function does.
@@ -198,7 +174,8 @@ blog_foo = api.content.foo(path="/plone/blog")
198
174
 
199
175
  % invisible-code-block: python
200
176
  %
201
- % self.assertEqual(blog_foo,"foo")
177
+ % self.assertEqual(blog_foo, "foo")
178
+
202
179
  ````
203
180
 
204
181
  Code blocks are rendered in documentation.
@@ -215,7 +192,7 @@ Invisible code blocks are not rendered in documentation and can be used for test
215
192
  ```markdown
216
193
  % invisible-code-block: python
217
194
  %
218
- % self.assertEqual(blog_foo,"foo")
195
+ % self.assertEqual(blog_foo, "foo")
219
196
  ```
220
197
 
221
198
  Invisible code blocks are also handy for enriching the namespace without cluttering the narrative documentation.
@@ -416,6 +416,49 @@ api.portal.set_registry_record('field_one', 'new value', interface=IMyRegistrySe
416
416
  % 'new value'
417
417
  % )
418
418
 
419
+ (portal-get-vocabulary-example)=
420
+
421
+ ## Get vocabulary
422
+
423
+ To get a vocabulary by name, use {func}`api.portal.get_vocabulary`.
424
+
425
+ ```python
426
+ from plone import api
427
+
428
+ # Get vocabulary using default portal context
429
+ vocabulary = api.portal.get_vocabulary(name='plone.app.vocabularies.PortalTypes')
430
+
431
+ # Get vocabulary with specific context
432
+ context = api.portal.get()
433
+ states_vocabulary = api.portal.get_vocabulary(
434
+ name='plone.app.vocabularies.WorkflowStates',
435
+ context=context
436
+ )
437
+ ```
438
+
439
+ (portal-get-all-vocabulary-names-example)=
440
+
441
+ ## Get all vocabulary names
442
+
443
+ To get a list of all available vocabulary names in your Plone site, use {meth}`api.portal.get_vocabulary_names`.
444
+
445
+ ```python
446
+ from plone import api
447
+
448
+ # Get all vocabulary names
449
+ vocabulary_names = api.portal.get_vocabulary_names()
450
+
451
+ # Common vocabularies that should be available
452
+ common_vocabularies = [
453
+ 'plone.app.vocabularies.PortalTypes',
454
+ 'plone.app.vocabularies.WorkflowStates',
455
+ 'plone.app.vocabularies.WorkflowTransitions'
456
+ ]
457
+
458
+ for vocabulary_name in common_vocabularies:
459
+ assert vocabulary_name in vocabulary_names
460
+ ```
461
+
419
462
  ## Further reading
420
463
 
421
464
  For more information on possible flags and usage options please see the full {ref}`plone-api-portal` specification.
@@ -3,7 +3,7 @@ from setuptools import find_packages
3
3
  from setuptools import setup
4
4
 
5
5
 
6
- version = "2.2.5"
6
+ version = "2.3.0"
7
7
 
8
8
  long_description = (
9
9
  f"{Path('README.md').read_text()}\n"
@@ -569,6 +569,36 @@ def get_uuid(obj=None):
569
569
  return IUUID(obj)
570
570
 
571
571
 
572
+ @required_parameters("obj")
573
+ def get_path(obj=None, relative=False):
574
+ """Get the path of an object.
575
+
576
+ :param obj: [required] Object for which to get its path
577
+ :type obj: Content object
578
+ :param relative: Return a relative path from the portal root
579
+ :type relative: boolean
580
+ :returns: Path to the object
581
+ :rtype: string
582
+ :raises:
583
+ InvalidParameterError
584
+ :Example: :ref:`content-get-path-example`
585
+ """
586
+ if not hasattr(obj, "getPhysicalPath"):
587
+ raise InvalidParameterError(f"Cannot get path of object {obj!r}")
588
+
589
+ if not relative:
590
+ return "/".join(obj.getPhysicalPath())
591
+ site = portal.get()
592
+ site_path = site.getPhysicalPath()
593
+ obj_path = obj.getPhysicalPath()
594
+ if obj_path[: len(site_path)] != site_path:
595
+ raise InvalidParameterError(
596
+ "Object not in portal path. Object path: {}".format("/".join(obj_path))
597
+ )
598
+ rel_path = obj_path[len(site_path) :]
599
+ return "/".join(rel_path) if rel_path else ""
600
+
601
+
572
602
  def _parse_object_provides_query(query):
573
603
  """Create a query for the object_provides index.
574
604
 
@@ -12,11 +12,14 @@ from plone.registry.interfaces import IRegistry
12
12
  from Products.CMFCore.interfaces import ISiteRoot
13
13
  from Products.CMFCore.utils import getToolByName
14
14
  from Products.statusmessages.interfaces import IStatusMessage
15
+ from zope.component import ComponentLookupError
16
+ from zope.component import getUtilitiesFor
15
17
  from zope.component import getUtility
16
18
  from zope.component import providedBy
17
19
  from zope.component.hooks import getSite
18
20
  from zope.globalrequest import getRequest
19
21
  from zope.interface.interfaces import IInterface
22
+ from zope.schema.interfaces import IVocabularyFactory
20
23
 
21
24
  import datetime as dtime
22
25
  import re
@@ -431,3 +434,41 @@ def translate(msgid, domain="plone", lang=None):
431
434
  # Pass the request, so zope.i18n.translate can negotiate the language.
432
435
  query["context"] = getRequest()
433
436
  return translation_service.utranslate(**query)
437
+
438
+
439
+ @required_parameters("name")
440
+ def get_vocabulary(name=None, context=None):
441
+ """Return a vocabulary object with the given name.
442
+
443
+ :param name: Name of the vocabulary.
444
+ :type name: str
445
+ :param context: Context to be applied to the vocabulary. Default: portal root.
446
+ :type context: object
447
+ :returns: A SimpleVocabulary instance that implements IVocabularyTokenized.
448
+ :rtype: zope.schema.vocabulary.SimpleVocabulary
449
+ :Example: :ref:`portal-get-vocabulary-example`
450
+ """
451
+ if context is None:
452
+ context = get()
453
+ try:
454
+ vocabulary = getUtility(IVocabularyFactory, name)
455
+ except ComponentLookupError:
456
+ raise InvalidParameterError(
457
+ "Cannot find a vocabulary with name '{name}'.\n"
458
+ "Available vocabularies are:\n"
459
+ "{vocabularies}".format(
460
+ name=name,
461
+ vocabularies="\n".join(get_vocabulary_names()),
462
+ ),
463
+ )
464
+ return vocabulary(context)
465
+
466
+
467
+ def get_vocabulary_names():
468
+ """Return a list of vocabulary names.
469
+
470
+ :returns: A sorted list of vocabulary names.
471
+ :rtype: list[str]
472
+ :Example: :ref:`portal-get-all-vocabulary-names-example`
473
+ """
474
+ return sorted([name for name, vocabulary in getUtilitiesFor(IVocabularyFactory)])
@@ -78,7 +78,7 @@ plone (portal root)
78
78
  % api.content.create(container=events, type='Event', id='conference')
79
79
  % api.content.create(container=events, type='Event', id='sprint')
80
80
 
81
- The following operations will get objects from the structure above, including using {meth}`api.content.get`.
81
+ The following operations will get objects from the structure above, using {meth}`api.content.get`.
82
82
 
83
83
  ```python
84
84
  # let's first get the portal object
@@ -118,7 +118,7 @@ not_found = api.content.get(UID='notfound')
118
118
 
119
119
  ## Find content objects
120
120
 
121
- You can use the find function to search for content.
121
+ You can use the {func}`api.content.find` function to search for content.
122
122
 
123
123
  Finding all Documents:
124
124
 
@@ -460,7 +460,6 @@ portal = api.portal.get()
460
460
  api.content.transition(obj=portal['about'], transition='reject', comment='You had a typo on your page.')
461
461
  ```
462
462
 
463
-
464
463
  (content-disable-roles-acquisition-example)=
465
464
 
466
465
  ## Disable local roles acquisition
@@ -536,6 +535,60 @@ view = api.content.get_view(
536
535
  %
537
536
  % self.assertEqual(view.__name__, u'plone')
538
537
 
538
+ (content-get-path-example)=
539
+
540
+ ## Get content path
541
+
542
+ To get the path of a content object, use {func}`api.content.get_path`.
543
+ This function accepts an object for which you want to get its path as the required parameter `obj`, and an optional boolean parameter `relative` whose default is `False`.
544
+
545
+ It returns either an absolute path from the Zope root by default or when `relative` is set to `False`, or a relative path from the portal root when `relative` is set to `True`.
546
+
547
+ The following example shows how to get the absolute path from the Zope root.
548
+
549
+ ```python
550
+ from plone import api
551
+ portal = api.portal.get()
552
+
553
+ folder = portal['events']['training']
554
+ path = api.content.get_path(obj=folder)
555
+ assert path == '/plone/events/training'
556
+ ```
557
+
558
+ The following example shows how to get the portal-relative path.
559
+
560
+ ```python
561
+ rel_path = api.content.get_path(obj=folder, relative=True)
562
+ assert rel_path == 'events/training'
563
+ ```
564
+
565
+ If the API is used to fetch an object with the `relative` parameter set as `True`, and the object is outside the portal, it throws an `InvalidParameterError` error.
566
+
567
+ % invisible-code-block: python
568
+ %
569
+ % # Setup an object outside portal for testing error case
570
+ % app = portal.aq_parent
571
+ % app.manage_addFolder('outside_folder')
572
+ %
573
+ % # Test that getting relative path for object outside portal raises error
574
+ % from plone.api.exc import InvalidParameterError
575
+ % with self.assertRaises(InvalidParameterError):
576
+ % api.content.get_path(obj=app.outside_folder, relative=True)
577
+
578
+ ```python
579
+ from plone.api.exc import InvalidParameterError
580
+
581
+ # Getting path of an object outside portal raises InvalidParameterError
582
+ try:
583
+ outside_path = api.content.get_path(
584
+ obj=app.outside_folder,
585
+ relative=True
586
+ )
587
+ assert False, "Should raise InvalidParameterError and not reach this code"
588
+ except InvalidParameterError as e:
589
+ assert "Object not in portal path" in str(e)
590
+ ```
591
+
539
592
  ## Further reading
540
593
 
541
594
  For more information on possible flags and usage options please see the full {ref}`plone-api-content` specification.
@@ -0,0 +1,217 @@
1
+ ---
2
+ myst:
3
+ html_meta:
4
+ "description": "Contribute to plone.api"
5
+ "property=og:description": "Contribute to plone.api"
6
+ "property=og:title": "Contribute to plone.api"
7
+ "keywords": "plone.api, contribute, Plone, API, development"
8
+ ---
9
+
10
+ # Contribute to `plone.api`
11
+
12
+ This section describes how to contribute to the `plone.api` project.
13
+ It extends {doc}`plone:contributing/index`.
14
+
15
+ ## Prerequisites
16
+
17
+ Prepare your system by installing prerequisites.
18
+
19
+ ### System libraries
20
+
21
+ You need to install system libraries, as described in {ref}`plone:plone-prerequisites-label`.
22
+
23
+
24
+ ## Create development environment
25
+
26
+ After satisfying the prerequisites, you are ready to create your development environment.
27
+ `plone.api` uses `tox` as a wrapper around `coredev.buildout` to simplify development, whereas Plone core uses `coredev.buildout` directly.
28
+ Additionally `plone.api` uses `make` commands that invoke the `tox` commands as a convenience and for consistency across Plone packages.
29
+
30
+ Start by changing your working directory to your project folder, and check out the latest `plone.api` source code.
31
+
32
+ ```shell
33
+ cd <your_project_folder>
34
+ git clone https://github.com/plone/plone.api.git
35
+ ```
36
+
37
+ Run `make help` to see the available `make` commands.
38
+
39
+ ```console
40
+ check Check code base according to Plone standards
41
+ clean Clean environment
42
+ help This help message
43
+ linkcheck Check links in documentation
44
+ livehtml Build docs and watch for changes
45
+ test Run tests
46
+ ```
47
+
48
+ The `make` commands `check`, `livehtml`, and `test` will create a development environment, if it does not already exist.
49
+
50
+ Test your code changes with the following command.
51
+
52
+ ```shell
53
+ make test
54
+ ```
55
+
56
+
57
+ (git-workflow)=
58
+
59
+ ## git
60
+
61
+ Use the following git branches when contributing to `plone.api`.
62
+
63
+ feature branches
64
+ : All development for a new feature or bug fix must be done on a new branch.
65
+
66
+ `main`
67
+ : Pull requests should be made from a feature branch against the `main` branch.
68
+ When features and bug fixes are complete and approved, they are merged into the `main` branch.
69
+
70
+ ```{seealso}
71
+ {ref}`plone:contributing-core-work-with-git-label`
72
+ ```
73
+
74
+ ## Continuous integration
75
+
76
+ `plone.api` uses GitHub workflows for continuous integration.
77
+ On every push to the `main` branch, GitHub runs its workflows for all tests and code quality checks.
78
+ GitHub workflows are configured in the directory `.github/workflows` at the root of this package.
79
+
80
+ ## Documentation
81
+
82
+ For every feature change or addition to `plone.api`, you must add documentation of it.
83
+ `plone.api` uses [MyST](https://myst-parser.readthedocs.io/en/latest/) for documentation syntax.
84
+
85
+ ```{seealso}
86
+ {doc}`plone:contributing/documentation/index`
87
+ ```
88
+
89
+ When adding or modifying documentation, you can build the documentation with the following command.
90
+ As you edit the documentation, the started process automatically reloads your changes in the web browser.
91
+
92
+ ```shell
93
+ make livehtml
94
+ ```
95
+
96
+ You can run a link checker on documentation.
97
+
98
+ ```shell
99
+ make linkcheck
100
+ ```
101
+
102
+ The [`plone.api` documentation](https://6.docs.plone.org/plone.api) is automatically generated from the documentation source files when its submodule is updated in the [main Plone `documentation` repository](https://github.com/plone/documentation/).
103
+
104
+ ## Add a function to an existing module
105
+
106
+ This section describes how to add a new function `foo` to `plone.api`.
107
+
108
+ The function would go in the module `plone.api.content`, located in the file {file}`src/plone/api/content.py`.
109
+
110
+ % invisible-code-block: python
111
+ %
112
+ % from plone.api.validation import at_least_one_of
113
+ % from plone.api.validation import mutually_exclusive_parameters
114
+
115
+ ```python
116
+ @mutually_exclusive_parameters('path', 'UID')
117
+ @at_least_one_of('path', 'UID')
118
+ def foo(path=None, UID=None):
119
+ """Do foo.
120
+
121
+ :param path: Path to the object we want to get,
122
+ relative to the portal root.
123
+ :type path: string
124
+
125
+ :param UID: UID of the object we want to get.
126
+ :type UID: string
127
+
128
+ :returns: String
129
+ :raises:
130
+ :class:`~plone.api.exc.MissingParameterError`,
131
+ :class:`~plone.api.exc.InvalidParameterError`
132
+ :Example: :ref:`content-foo-example`
133
+ """
134
+ return "foo"
135
+ ```
136
+
137
+ % invisible-code-block: python
138
+ %
139
+ % bar = foo('/plone/blog')
140
+ % self.assertEqual(bar,"foo")
141
+ %
142
+ % from plone.api.exc import InvalidParameterError
143
+ % self.assertRaises(
144
+ % InvalidParameterError,
145
+ % lambda: foo("/plone/blog", "abcd001")
146
+ % )
147
+ %
148
+ % # Make it available for testing below
149
+ % from plone import api
150
+ % api.content.foo = foo
151
+
152
+ Add documentation in {file}`docs/api/content.md`.
153
+ Narrative documentation should describe what your function does.
154
+
155
+ You should also write some tests in code blocks.
156
+ `TestCase` methods, such as `self.assertEqual()`, are available in `doctests`.
157
+ See [unittest.TestCase assert methods](https://docs.python.org/3/library/unittest.html#unittest.TestCase.debug) for all available methods.
158
+ The file is linked in `/src/plone/api/tests/doctests/`, which includes the doctests in `plone.api`'s test setup.
159
+ The package `manuel` allows you to write doctests as common Python code in code blocks.
160
+
161
+ The following example shows narrative documentation and doctests.
162
+
163
+ ````markdown
164
+ (content-foo-example)=
165
+
166
+ ## Get the foo of an object
167
+
168
+ You can use the {meth}`api.content.foo` function to get the `foo` of an object.
169
+
170
+ ```python
171
+ from plone import api
172
+ blog_foo = api.content.foo(path="/plone/blog")
173
+ ```
174
+
175
+ % invisible-code-block: python
176
+ %
177
+ % self.assertEqual(blog_foo, "foo")
178
+
179
+ ````
180
+
181
+ Code blocks are rendered in documentation.
182
+
183
+ ````markdown
184
+ ```python
185
+ from plone import api
186
+ blog_foo = api.content.foo(path="/plone/blog")
187
+ ```
188
+ ````
189
+
190
+ Invisible code blocks are not rendered in documentation and can be used for tests.
191
+
192
+ ```markdown
193
+ % invisible-code-block: python
194
+ %
195
+ % self.assertEqual(blog_foo, "foo")
196
+ ```
197
+
198
+ Invisible code blocks are also handy for enriching the namespace without cluttering the narrative documentation.
199
+
200
+ ```markdown
201
+ % invisible-code-block: python
202
+ %
203
+ % portal = api.portal.get()
204
+ % image = api.content.create(type='Image', id='image', container=portal)
205
+ % blog = api.content.create(type='Link', id='blog', container=portal)
206
+ ```
207
+
208
+ Functions and examples in documentation are mutually referenced.
209
+ The function references the narrative documentation via the label `content-foo-example`.
210
+ The narrative documentation references the API function documentation via `` {meth}`api.content.foo` ``.
211
+ The documentation is rendered with a link from the API reference to the narrative documentation, which in turn links back to the API reference.
212
+
213
+ ## Resources
214
+
215
+ - {doc}`plone:index`
216
+ - [Source code](https://github.com/plone/plone.api)
217
+ - [Issue tracker](https://github.com/plone/plone.api/issues)
@@ -416,6 +416,49 @@ api.portal.set_registry_record('field_one', 'new value', interface=IMyRegistrySe
416
416
  % 'new value'
417
417
  % )
418
418
 
419
+ (portal-get-vocabulary-example)=
420
+
421
+ ## Get vocabulary
422
+
423
+ To get a vocabulary by name, use {func}`api.portal.get_vocabulary`.
424
+
425
+ ```python
426
+ from plone import api
427
+
428
+ # Get vocabulary using default portal context
429
+ vocabulary = api.portal.get_vocabulary(name='plone.app.vocabularies.PortalTypes')
430
+
431
+ # Get vocabulary with specific context
432
+ context = api.portal.get()
433
+ states_vocabulary = api.portal.get_vocabulary(
434
+ name='plone.app.vocabularies.WorkflowStates',
435
+ context=context
436
+ )
437
+ ```
438
+
439
+ (portal-get-all-vocabulary-names-example)=
440
+
441
+ ## Get all vocabulary names
442
+
443
+ To get a list of all available vocabulary names in your Plone site, use {meth}`api.portal.get_vocabulary_names`.
444
+
445
+ ```python
446
+ from plone import api
447
+
448
+ # Get all vocabulary names
449
+ vocabulary_names = api.portal.get_vocabulary_names()
450
+
451
+ # Common vocabularies that should be available
452
+ common_vocabularies = [
453
+ 'plone.app.vocabularies.PortalTypes',
454
+ 'plone.app.vocabularies.WorkflowStates',
455
+ 'plone.app.vocabularies.WorkflowTransitions'
456
+ ]
457
+
458
+ for vocabulary_name in common_vocabularies:
459
+ assert vocabulary_name in vocabulary_names
460
+ ```
461
+
419
462
  ## Further reading
420
463
 
421
464
  For more information on possible flags and usage options please see the full {ref}`plone-api-portal` specification.
@@ -1447,3 +1447,66 @@ class TestPloneApiContent(unittest.TestCase):
1447
1447
 
1448
1448
  for should_be_there in should_be_theres:
1449
1449
  self.assertIn((should_be_there + "\n"), str(cm.exception))
1450
+
1451
+ def test_get_path_absolute(self):
1452
+ """Test getting the path of a content object with relative parameter set to False."""
1453
+ from plone.api.exc import InvalidParameterError
1454
+
1455
+ portal = self.layer["portal"]
1456
+
1457
+ # Test portal root
1458
+ self.assertEqual(
1459
+ api.content.get_path(portal), "/plone" # This assumes default Plone site id
1460
+ )
1461
+
1462
+ # Test folder structure
1463
+ folder = api.content.create(container=portal, type="Folder", id="test-folder")
1464
+ self.assertEqual(api.content.get_path(folder), "/plone/test-folder")
1465
+
1466
+ # Test nested content
1467
+ document = api.content.create(
1468
+ container=folder, type="Document", id="test-document"
1469
+ )
1470
+ self.assertEqual(
1471
+ api.content.get_path(document), "/plone/test-folder/test-document"
1472
+ )
1473
+
1474
+ # Test invalid object
1475
+ invalid_obj = object()
1476
+ with self.assertRaises(InvalidParameterError):
1477
+ api.content.get_path(invalid_obj)
1478
+ self.assertRaisesRegex(
1479
+ InvalidParameterError,
1480
+ r"^Cannot get path of object <object object at 0x[0-9a-f]+>$",
1481
+ )
1482
+
1483
+ def test_get_path_relative(self):
1484
+ from plone.api.exc import InvalidParameterError
1485
+
1486
+ portal = self.layer["portal"]
1487
+
1488
+ # Test portal root
1489
+ self.assertEqual(api.content.get_path(portal, relative=True), "")
1490
+
1491
+ # Test folder structure
1492
+ folder = api.content.create(container=portal, type="Folder", id="test-folder")
1493
+ self.assertEqual(api.content.get_path(folder, relative=True), "test-folder")
1494
+
1495
+ # Test nested content
1496
+ document = api.content.create(
1497
+ container=folder, type="Document", id="test-document"
1498
+ )
1499
+ self.assertEqual(
1500
+ api.content.get_path(document, relative=True),
1501
+ "test-folder/test-document",
1502
+ )
1503
+
1504
+ # Test object outside portal
1505
+ class FauxObject:
1506
+ def getPhysicalPath(self):
1507
+ return ("", "foo", "bar")
1508
+
1509
+ outside_obj = FauxObject()
1510
+ with self.assertRaises(InvalidParameterError) as cm:
1511
+ api.content.get_path(outside_obj, relative=True)
1512
+ self.assertIn("Object not in portal path", str(cm.exception))
@@ -115,7 +115,7 @@ def DocFileSuite(
115
115
 
116
116
 
117
117
  def test_suite():
118
- """Find .rst files and test code examples in them."""
118
+ """Find .md files and test code examples in them."""
119
119
  path = "doctests"
120
120
  doctests = []
121
121
  docs_path = os.path.join(os.path.dirname(__file__), path)
@@ -19,6 +19,7 @@ from zope import schema
19
19
  from zope.component import getUtility
20
20
  from zope.component.hooks import setSite
21
21
  from zope.interface import Interface
22
+ from zope.schema.vocabulary import SimpleVocabulary
22
23
  from zope.site import LocalSiteManager
23
24
 
24
25
  import DateTime
@@ -894,3 +895,71 @@ class TestPloneApiPortal(unittest.TestCase):
894
895
  ),
895
896
  "Página",
896
897
  )
898
+
899
+ def test_get_vocabulary(self):
900
+ """Test getting a vocabulary by name."""
901
+ from plone.api.exc import InvalidParameterError
902
+ from plone.api.exc import MissingParameterError
903
+
904
+ # The vocabulary name must be given as parameter
905
+ with self.assertRaises(MissingParameterError):
906
+ portal.get_vocabulary()
907
+
908
+ # Test getting a commonly available vocabulary
909
+ vocabulary = portal.get_vocabulary(name="plone.app.vocabularies.PortalTypes")
910
+ self.assertIsInstance(vocabulary, SimpleVocabulary)
911
+
912
+ # Test with invalid vocabulary name
913
+ with self.assertRaises(InvalidParameterError) as cm:
914
+ portal.get_vocabulary(name="non.existing.vocabulary")
915
+
916
+ expected_msg = (
917
+ "Cannot find a vocabulary with name 'non.existing.vocabulary'.\n"
918
+ "Available vocabularies are:\n"
919
+ )
920
+ self.assertTrue(str(cm.exception).startswith(expected_msg))
921
+
922
+ # Test with context
923
+ vocabulary_with_context = portal.get_vocabulary(
924
+ name="plone.app.vocabularies.PortalTypes", context=self.portal
925
+ )
926
+ self.assertIsInstance(vocabulary_with_context, SimpleVocabulary)
927
+
928
+ def test_get_vocabulary_names(self):
929
+ """Test getting list of vocabulary names."""
930
+ names = portal.get_vocabulary_names()
931
+
932
+ # Test we get a list of strings
933
+ self.assertIsInstance(names, list)
934
+ self.assertTrue(len(names) > 0)
935
+ self.assertIsInstance(names[0], str)
936
+
937
+ # Test that common vocabularies are included
938
+ common_vocabularies = [
939
+ "plone.app.vocabularies.PortalTypes",
940
+ "plone.app.vocabularies.WorkflowStates",
941
+ "plone.app.vocabularies.WorkflowTransitions",
942
+ ]
943
+
944
+ for vocabulary_name in common_vocabularies:
945
+ self.assertIn(vocabulary_name, names)
946
+
947
+ def test_vocabulary_terms(self):
948
+ """Test the actual content of retrieved vocabularies."""
949
+ # Get portal types vocabulary
950
+ types_vocabulary = portal.get_vocabulary("plone.app.vocabularies.PortalTypes")
951
+
952
+ # Check that we have some common content types
953
+ types = [term.value for term in types_vocabulary]
954
+ self.assertIn("Document", types)
955
+ self.assertIn("Folder", types)
956
+
957
+ # Get workflow states vocabulary
958
+ states_vocabulary = portal.get_vocabulary(
959
+ "plone.app.vocabularies.WorkflowStates"
960
+ )
961
+
962
+ # Check that we have some common workflow states
963
+ states = [term.value for term in states_vocabulary]
964
+ self.assertIn("private", states)
965
+ self.assertIn("published", states)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: plone.api
3
- Version: 2.2.5
3
+ Version: 2.3.0
4
4
  Summary: A Plone API.
5
5
  Home-page: https://github.com/plone/plone.api
6
6
  Author: Plone Foundation
@@ -110,6 +110,25 @@ Changelog
110
110
 
111
111
  .. towncrier release notes start
112
112
 
113
+ 2.3.0 (2025-02-21)
114
+ ------------------
115
+
116
+ New features:
117
+
118
+
119
+ - Added the content API helper function ``api.content.get_path``, which gets either the relative or absolute path of an object. @ujsquared (#532)
120
+ - Added two new portal API functions:
121
+ - ``api.portal.get_vocabulary``: Get a vocabulary by name
122
+ - ``api.portal.get_vocabulary_names``: Get a list of all available vocabulary names
123
+ @ujsquared (#533)
124
+
125
+
126
+ Internal:
127
+
128
+
129
+ - Making it easier for new contributors to get started with a simple Makefile and a tidy 'contributing' chapter. @ksuess (#558)
130
+
131
+
113
132
  2.2.5 (2025-01-24)
114
133
  ------------------
115
134
 
@@ -53,6 +53,7 @@ src/plone/api/tests/test_user.py
53
53
  src/plone/api/tests/test_validation.py
54
54
  src/plone/api/tests/doctests/about.md
55
55
  src/plone/api/tests/doctests/content.md
56
+ src/plone/api/tests/doctests/contribute.md
56
57
  src/plone/api/tests/doctests/env.md
57
58
  src/plone/api/tests/doctests/group.md
58
59
  src/plone/api/tests/doctests/portal.md
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes