plone.api 2.5.3__tar.gz → 3.0.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 (66) hide show
  1. {plone_api-2.5.3 → plone_api-3.0.0}/CHANGES.md +48 -0
  2. {plone_api-2.5.3 → plone_api-3.0.0}/PKG-INFO +57 -7
  3. {plone_api-2.5.3 → plone_api-3.0.0}/docs/portal.md +103 -0
  4. {plone_api-2.5.3 → plone_api-3.0.0}/pyproject.toml +12 -11
  5. {plone_api-2.5.3 → plone_api-3.0.0}/setup.py +7 -11
  6. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/addon.py +14 -18
  7. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/content.py +0 -1
  8. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/env.py +0 -1
  9. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/portal.py +0 -1
  10. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/relation.py +0 -1
  11. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/doctests/portal.md +103 -0
  12. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/test_addon.py +0 -1
  13. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/test_content.py +6 -0
  14. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/test_doctests.py +0 -1
  15. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/test_portal.py +0 -1
  16. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/test_user.py +3 -0
  17. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/user.py +24 -4
  18. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone.api.egg-info/PKG-INFO +57 -7
  19. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone.api.egg-info/SOURCES.txt +0 -2
  20. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone.api.egg-info/requires.txt +1 -1
  21. {plone_api-2.5.3 → plone_api-3.0.0}/tox.ini +85 -108
  22. plone_api-2.5.3/src/plone/__init__.py +0 -1
  23. plone_api-2.5.3/src/plone.api.egg-info/namespace_packages.txt +0 -1
  24. {plone_api-2.5.3 → plone_api-3.0.0}/CONTRIBUTING.md +0 -0
  25. {plone_api-2.5.3 → plone_api-3.0.0}/LICENSE +0 -0
  26. {plone_api-2.5.3 → plone_api-3.0.0}/MANIFEST.in +0 -0
  27. {plone_api-2.5.3 → plone_api-3.0.0}/README.md +0 -0
  28. {plone_api-2.5.3 → plone_api-3.0.0}/docs/about.md +0 -0
  29. {plone_api-2.5.3 → plone_api-3.0.0}/docs/addon.md +0 -0
  30. {plone_api-2.5.3 → plone_api-3.0.0}/docs/content.md +0 -0
  31. {plone_api-2.5.3 → plone_api-3.0.0}/docs/contribute.md +0 -0
  32. {plone_api-2.5.3 → plone_api-3.0.0}/docs/env.md +0 -0
  33. {plone_api-2.5.3 → plone_api-3.0.0}/docs/group.md +0 -0
  34. {plone_api-2.5.3 → plone_api-3.0.0}/docs/index.md +0 -0
  35. {plone_api-2.5.3 → plone_api-3.0.0}/docs/relation.md +0 -0
  36. {plone_api-2.5.3 → plone_api-3.0.0}/docs/user.md +0 -0
  37. {plone_api-2.5.3 → plone_api-3.0.0}/setup.cfg +0 -0
  38. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/__init__.py +0 -0
  39. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/configure.zcml +0 -0
  40. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/exc.py +0 -0
  41. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/group.py +0 -0
  42. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/profiles/testfixture/metadata.xml +0 -0
  43. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/profiles/testfixture/types/Dexterity_Folder.xml +0 -0
  44. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/profiles/testfixture/types/Dexterity_Item.xml +0 -0
  45. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/profiles/testfixture/types.xml +0 -0
  46. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/testing.zcml +0 -0
  47. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/Dexterity_Folder.xml +0 -0
  48. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/Dexterity_Item.xml +0 -0
  49. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/__init__.py +0 -0
  50. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/base.py +0 -0
  51. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/doctests/about.md +0 -0
  52. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/doctests/addon.md +0 -0
  53. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/doctests/content.md +0 -0
  54. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/doctests/contribute.md +0 -0
  55. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/doctests/env.md +0 -0
  56. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/doctests/group.md +0 -0
  57. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/doctests/relation.md +0 -0
  58. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/doctests/user.md +0 -0
  59. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/test_env.py +0 -0
  60. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/test_group.py +0 -0
  61. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/test_relation.py +0 -0
  62. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/tests/test_validation.py +0 -0
  63. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone/api/validation.py +0 -0
  64. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone.api.egg-info/dependency_links.txt +0 -0
  65. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone.api.egg-info/not-zip-safe +0 -0
  66. {plone_api-2.5.3 → plone_api-3.0.0}/src/plone.api.egg-info/top_level.txt +0 -0
@@ -9,6 +9,54 @@
9
9
 
10
10
  <!-- towncrier release notes start -->
11
11
 
12
+ ## 3.0.0 (2026-05-07)
13
+
14
+
15
+ ### Documentation
16
+
17
+ - Added example to construct a complex email. @1letter
18
+
19
+ ## 3.0.0a3 (2026-03-25)
20
+
21
+
22
+ ### Bug fixes
23
+
24
+ - Fix ``plone.api.user.create()`` to respect ``use_uuid_as_userid`` and
25
+ ``use_email_as_login`` registry settings, as well as custom ``IUserIdGenerator``
26
+ and ``ILoginNameGenerator`` utilities. #4292
27
+
28
+
29
+ ### Internal
30
+
31
+ - Update configuration files @plone
32
+
33
+
34
+ ### Tests
35
+
36
+ - Fix `test_create_raises_unicodedecodeerror` for deferred indexing queue processing since Products.CMFCore 3.9.
37
+ @jensens #602
38
+
39
+ ## 3.0.0a2 (2025-12-18)
40
+
41
+
42
+ ### Bug fixes:
43
+
44
+ - Fix deprecation warnings for `INonInstallable` and `get_installer`. @mauritsvanrees
45
+
46
+
47
+ ### Internal:
48
+
49
+ - Improve trigger for RTD PR preview builds. @stevepiercy #591
50
+ - Ignore the `/_build` directory from building documentation. @stevepiercy #598
51
+
52
+ ## 3.0.0a1 (2025-11-26)
53
+
54
+
55
+ ### Breaking changes:
56
+
57
+ - Replace ``pkg_resources`` namespace with PEP 420 native namespace.
58
+ Support only Plone 6.2 and Python 3.10+. #3928
59
+
12
60
  ## 2.5.3 (2025-09-10)
13
61
 
14
62
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: plone.api
3
- Version: 2.5.3
3
+ Version: 3.0.0
4
4
  Summary: A Plone API.
5
5
  Home-page: https://github.com/plone/plone.api
6
6
  Author: Plone Foundation
@@ -19,17 +19,18 @@ Platform: Any
19
19
  Classifier: Development Status :: 5 - Production/Stable
20
20
  Classifier: Environment :: Web Environment
21
21
  Classifier: Framework :: Plone
22
- Classifier: Framework :: Plone :: 6.0
22
+ Classifier: Framework :: Plone :: 6.2
23
23
  Classifier: Framework :: Plone :: Core
24
24
  Classifier: Intended Audience :: Developers
25
25
  Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
26
26
  Classifier: Operating System :: OS Independent
27
27
  Classifier: Programming Language :: Python
28
- Classifier: Programming Language :: Python :: 3.8
29
- Classifier: Programming Language :: Python :: 3.9
30
28
  Classifier: Programming Language :: Python :: 3.10
31
29
  Classifier: Programming Language :: Python :: 3.11
32
- Requires-Python: >=3.8
30
+ Classifier: Programming Language :: Python :: 3.12
31
+ Classifier: Programming Language :: Python :: 3.13
32
+ Classifier: Programming Language :: Python :: 3.14
33
+ Requires-Python: >=3.10
33
34
  Description-Content-Type: text/markdown
34
35
  License-File: LICENSE
35
36
  Requires-Dist: Acquisition
@@ -37,6 +38,7 @@ Requires-Dist: Products.statusmessages
37
38
  Requires-Dist: Products.PlonePAS
38
39
  Requires-Dist: Products.CMFPlone
39
40
  Requires-Dist: decorator
41
+ Requires-Dist: plone.app.users
40
42
  Requires-Dist: plone.app.uuid
41
43
  Requires-Dist: plone.app.dexterity
42
44
  Requires-Dist: plone.app.intid
@@ -46,7 +48,6 @@ Requires-Dist: plone.dexterity
46
48
  Requires-Dist: plone.i18n
47
49
  Requires-Dist: plone.registry
48
50
  Requires-Dist: plone.uuid
49
- Requires-Dist: setuptools
50
51
  Requires-Dist: zope.globalrequest
51
52
  Requires-Dist: Products.CMFCore
52
53
  Requires-Dist: z3c.relationfield
@@ -71,6 +72,7 @@ Dynamic: description-content-type
71
72
  Dynamic: home-page
72
73
  Dynamic: keywords
73
74
  Dynamic: license
75
+ Dynamic: license-file
74
76
  Dynamic: platform
75
77
  Dynamic: project-url
76
78
  Dynamic: provides-extra
@@ -132,6 +134,54 @@ Continuous Integration
132
134
 
133
135
  <!-- towncrier release notes start -->
134
136
 
137
+ ## 3.0.0 (2026-05-07)
138
+
139
+
140
+ ### Documentation
141
+
142
+ - Added example to construct a complex email. @1letter
143
+
144
+ ## 3.0.0a3 (2026-03-25)
145
+
146
+
147
+ ### Bug fixes
148
+
149
+ - Fix ``plone.api.user.create()`` to respect ``use_uuid_as_userid`` and
150
+ ``use_email_as_login`` registry settings, as well as custom ``IUserIdGenerator``
151
+ and ``ILoginNameGenerator`` utilities. #4292
152
+
153
+
154
+ ### Internal
155
+
156
+ - Update configuration files @plone
157
+
158
+
159
+ ### Tests
160
+
161
+ - Fix `test_create_raises_unicodedecodeerror` for deferred indexing queue processing since Products.CMFCore 3.9.
162
+ @jensens #602
163
+
164
+ ## 3.0.0a2 (2025-12-18)
165
+
166
+
167
+ ### Bug fixes:
168
+
169
+ - Fix deprecation warnings for `INonInstallable` and `get_installer`. @mauritsvanrees
170
+
171
+
172
+ ### Internal:
173
+
174
+ - Improve trigger for RTD PR preview builds. @stevepiercy #591
175
+ - Ignore the `/_build` directory from building documentation. @stevepiercy #598
176
+
177
+ ## 3.0.0a1 (2025-11-26)
178
+
179
+
180
+ ### Breaking changes:
181
+
182
+ - Replace ``pkg_resources`` namespace with PEP 420 native namespace.
183
+ Support only Plone 6.2 and Python 3.10+. #3928
184
+
135
185
  ## 2.5.3 (2025-09-10)
136
186
 
137
187
 
@@ -257,6 +257,109 @@ api.portal.send_email(
257
257
  % 'attachment; filename="report.xml',
258
258
  % payloads[1]['Content-Disposition']
259
259
  % )
260
+ % mailhost.messages.clear()
261
+
262
+ The following code is a more complex example that constructs an email with a file attachment, HTML, plain text, and mail headers to control the mail response.
263
+
264
+ ```python
265
+ from email.encoders import encode_base64
266
+ from email.header import Header
267
+ from email.mime.base import MIMEBase
268
+ from email.mime.multipart import MIMEMultipart
269
+ from email.mime.text import MIMEText
270
+ from plone import api
271
+
272
+ # we need a message part to bundle the HTML and Plain Text
273
+ textmsgpart = MIMEMultipart("alternative")
274
+
275
+ # create plain text part of email
276
+ plaintextpart = MIMEText("Fill out your plain text", "plain", "utf-8")
277
+
278
+ # create html text of email
279
+ html = "<html><body><p>fill out your text in HTML</p></body></html>"
280
+ htmlpart = MIMEText(html, "html", "utf-8")
281
+
282
+ # bundle the parts
283
+ textmsgpart.attach(plaintextpart)
284
+ textmsgpart.attach(htmlpart)
285
+
286
+ # handle the file attachment
287
+
288
+ # Create a dummy PDF as NamedBlobFile
289
+ from plone.namedfile.file import NamedBlobFile
290
+
291
+ # Minimal PDF content (a valid but nearly empty PDF)
292
+ pdf_content = b"""%PDF-1.4
293
+ 1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
294
+ 2 0 obj<</Type/Pages/Count 1/Kids[3 0 R]>>endobj
295
+ 3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]/Contents 4 0 R>>endobj
296
+ 4 0 obj<</Length 44>>stream
297
+ BT /F1 12 Tf 100 700 Td (Hello World) Tj ET
298
+ endstream endobj
299
+ xref
300
+ 0 5
301
+ 0000000000 65535 f
302
+ 0000000009 00000 n
303
+ 0000000056 00000 n
304
+ 0000000115 00000 n
305
+ 0000000214 00000 n
306
+ trailer<</Size 5/Root 1 0 R>>
307
+ startxref
308
+ 315
309
+ %%EOF"""
310
+
311
+ attachment = NamedBlobFile(
312
+ data=pdf_content,
313
+ contentType='application/pdf',
314
+ filename='document.pdf'
315
+ )
316
+
317
+ filepart = MIMEBase("application", "pdf")
318
+ filepart.set_payload(attachment.data)
319
+ encode_base64(filepart)
320
+ filepart.add_header(
321
+ "Content-Disposition",
322
+ "attachment",
323
+ filename=(Header(attachment.filename, "utf-8").encode()),
324
+ )
325
+
326
+ # we need a mixed Multipart Message to bundle the text bundle and the attachment
327
+ msg = MIMEMultipart("mixed")
328
+ msg["From"] = "sender@abc"
329
+ msg["To"] = "recipient@zzz"
330
+ msg["Subject"] = "The Mail Subject"
331
+ msg["Reply-To"] = "community@plone.org"
332
+ msg["Return-Path"] = "error@xxx"
333
+
334
+ # add the text message bundle
335
+ msg.attach(textmsgpart)
336
+
337
+ # add the file attachment
338
+ msg.attach(filepart)
339
+
340
+ # send with plone.api
341
+ api.portal.send_email(
342
+ sender=msg["From"],
343
+ recipient=msg["To"],
344
+ subject=msg["Subject"],
345
+ body=msg.as_string()
346
+ )
347
+ ```
348
+
349
+ % invisible-code-block: python
350
+ %
351
+ % self.assertEqual(len(mailhost.messages), 1)
352
+ %
353
+ % msg = message_from_bytes(mailhost.messages[0])
354
+ % payloads = msg.get_payload()
355
+ % self.assertTrue(len(payloads) == 2)
356
+ % self.assertTrue(msg['Reply-To'] == 'community@plone.org')
357
+ % text_payloads = payloads[0].get_payload()
358
+ % self.assertEqual(len(text_payloads), 2)
359
+ % self.assertIn(
360
+ % 'attachment; filename',
361
+ % payloads[1]['Content-Disposition']
362
+ % )
260
363
  % api.portal.PRINTINGMAILHOST_ENABLED = False
261
364
  % mailhost.reset()
262
365
 
@@ -1,8 +1,8 @@
1
1
  # Generated from:
2
- # https://github.com/plone/meta/tree/main/config/default
2
+ # https://github.com/plone/meta/tree/main/src/plone/meta/default
3
3
  # See the inline comments on how to expand/tweak this configuration file
4
4
  [build-system]
5
- requires = ["setuptools>=68.2,<77"]
5
+ requires = ["setuptools>=68.2,<83", "wheel"]
6
6
 
7
7
  [tool.towncrier]
8
8
  directory = "news/"
@@ -14,27 +14,27 @@ underlines = ["", "", ""]
14
14
 
15
15
  [[tool.towncrier.type]]
16
16
  directory = "breaking"
17
- name = "Breaking changes:"
17
+ name = "Breaking changes"
18
18
  showcontent = true
19
19
 
20
20
  [[tool.towncrier.type]]
21
21
  directory = "feature"
22
- name = "New features:"
22
+ name = "New features"
23
23
  showcontent = true
24
24
 
25
25
  [[tool.towncrier.type]]
26
26
  directory = "bugfix"
27
- name = "Bug fixes:"
27
+ name = "Bug fixes"
28
28
  showcontent = true
29
29
 
30
30
  [[tool.towncrier.type]]
31
31
  directory = "internal"
32
- name = "Internal:"
32
+ name = "Internal"
33
33
  showcontent = true
34
34
 
35
35
  [[tool.towncrier.type]]
36
36
  directory = "documentation"
37
- name = "Documentation:"
37
+ name = "Documentation"
38
38
  showcontent = true
39
39
 
40
40
  [[tool.towncrier.type]]
@@ -62,7 +62,7 @@ profile = "plone"
62
62
  ##
63
63
 
64
64
  [tool.black]
65
- target-version = ["py38"]
65
+ target-version = ["py310"]
66
66
 
67
67
  ##
68
68
  # Add extra configuration options in .meta.toml:
@@ -73,7 +73,7 @@ target-version = ["py38"]
73
73
  ##
74
74
 
75
75
  [tool.codespell]
76
- ignore-words-list = "discreet,manuel"
76
+ ignore-words-list = "discreet,assertin,thet,manuel,checkin"
77
77
  skip = "*.po,"
78
78
  ##
79
79
  # Add extra configuration options in .meta.toml:
@@ -121,6 +121,7 @@ Zope = [
121
121
  'Products.CMFCore', 'Products.CMFDynamicViewFTI',
122
122
  ]
123
123
  python-dateutil = ['dateutil']
124
+ pytest-plone = ['pytest', 'zope.pytestlayer', 'plone.testing', 'plone.app.testing']
124
125
  ignore-packages = ['Products.PrintingMailHost', 'plone.app.iterate',]
125
126
 
126
127
  ##
@@ -142,11 +143,11 @@ ignore = [
142
143
  "dependabot.yml",
143
144
  "mx.ini",
144
145
  "tox.ini",
146
+ # From `.meta.toml`, pyproject.check_manifest_ignores.
145
147
  "*.cfg",
146
148
  ".editorconfig",
147
149
  ".readthedocs.yaml",
148
- "constraints_plone52.txt",
149
- "constraints_plone60.txt",
150
+ "constraints_plone62.txt",
150
151
  "constraints.txt",
151
152
  "fix-converted-myst.py",
152
153
  "Makefile",
@@ -1,9 +1,7 @@
1
1
  from pathlib import Path
2
- from setuptools import find_packages
3
2
  from setuptools import setup
4
3
 
5
-
6
- version = "2.5.3"
4
+ version = "3.0.0"
7
5
 
8
6
  long_description = "\n".join(
9
7
  [Path("README.md").read_text(), Path("CHANGES.md").read_text()]
@@ -18,20 +16,18 @@ setup(
18
16
  author="Plone Foundation",
19
17
  author_email="plone-developers@lists.sourceforge.net",
20
18
  license="GPL version 2",
21
- packages=find_packages("src"),
22
- package_dir={"": "src"},
23
- namespace_packages=["plone"],
24
19
  include_package_data=True,
25
20
  zip_safe=False,
26
21
  url="https://github.com/plone/plone.api",
27
22
  keywords="plone api",
28
- python_requires=">=3.8",
23
+ python_requires=">=3.10",
29
24
  install_requires=[
30
25
  "Acquisition",
31
26
  "Products.statusmessages",
32
27
  "Products.PlonePAS",
33
28
  "Products.CMFPlone",
34
29
  "decorator",
30
+ "plone.app.users",
35
31
  "plone.app.uuid",
36
32
  "plone.app.dexterity",
37
33
  "plone.app.intid",
@@ -41,7 +37,6 @@ setup(
41
37
  "plone.i18n",
42
38
  "plone.registry",
43
39
  "plone.uuid",
44
- "setuptools",
45
40
  "zope.globalrequest",
46
41
  "Products.CMFCore",
47
42
  "z3c.relationfield",
@@ -68,16 +63,17 @@ setup(
68
63
  "Development Status :: 5 - Production/Stable",
69
64
  "Environment :: Web Environment",
70
65
  "Framework :: Plone",
71
- "Framework :: Plone :: 6.0",
66
+ "Framework :: Plone :: 6.2",
72
67
  "Framework :: Plone :: Core",
73
68
  "Intended Audience :: Developers",
74
69
  "License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
75
70
  "Operating System :: OS Independent",
76
71
  "Programming Language :: Python",
77
- "Programming Language :: Python :: 3.8",
78
- "Programming Language :: Python :: 3.9",
79
72
  "Programming Language :: Python :: 3.10",
80
73
  "Programming Language :: Python :: 3.11",
74
+ "Programming Language :: Python :: 3.12",
75
+ "Programming Language :: Python :: 3.13",
76
+ "Programming Language :: Python :: 3.14",
81
77
  ],
82
78
  platforms="Any",
83
79
  project_urls={
@@ -7,19 +7,15 @@ from importlib.metadata import PackageNotFoundError
7
7
  from plone.api import portal
8
8
  from plone.api.exc import InvalidParameterError
9
9
  from plone.api.validation import required_parameters
10
+ from plone.base.interfaces import INonInstallable
11
+ from plone.base.utils import get_installer
10
12
  from Products.CMFPlone.controlpanel.browser.quickinstaller import InstallerView
11
- from Products.CMFPlone.interfaces import INonInstallable
12
- from Products.CMFPlone.utils import get_installer
13
13
  from Products.GenericSetup import EXTENSION
14
- from typing import Dict
15
- from typing import List
16
- from typing import Tuple
17
14
  from zope.component import getAllUtilitiesRegisteredFor
18
15
  from zope.globalrequest import getRequest
19
16
 
20
17
  import logging
21
18
 
22
-
23
19
  logger = logging.getLogger("plone.api.addon")
24
20
 
25
21
 
@@ -39,8 +35,8 @@ __all__ = [
39
35
  class NonInstallableAddons:
40
36
  """Set of add-ons not available for installation."""
41
37
 
42
- profiles: List[str]
43
- products: List[str]
38
+ profiles: list[str]
39
+ products: list[str]
44
40
 
45
41
 
46
42
  @dataclass
@@ -52,14 +48,14 @@ class AddonInformation:
52
48
  title: str
53
49
  description: str
54
50
 
55
- upgrade_profiles: Dict
56
- other_profiles: List[List]
57
- install_profile: Dict
58
- uninstall_profile: Dict
51
+ upgrade_profiles: dict
52
+ other_profiles: list[list]
53
+ install_profile: dict
54
+ uninstall_profile: dict
59
55
  profile_type: str
60
- upgrade_info: Dict
56
+ upgrade_info: dict
61
57
  valid: bool
62
- flags: List[str]
58
+ flags: list[str]
63
59
 
64
60
  def __repr__(self) -> str:
65
61
  """Return a string representation of this object."""
@@ -98,7 +94,7 @@ def _get_non_installable_addons() -> NonInstallableAddons:
98
94
 
99
95
 
100
96
  @lru_cache(maxsize=1)
101
- def _cached_addons() -> Tuple[Tuple[str, AddonInformation]]:
97
+ def _cached_addons() -> tuple[tuple[str, AddonInformation]]:
102
98
  """Return information about add-ons in this installation.
103
99
 
104
100
  :returns: Tuple of tuples with add-on id and AddonInformation.
@@ -198,7 +194,7 @@ def _update_addon_info(
198
194
  return addon
199
195
 
200
196
 
201
- def _get_addons() -> List[AddonInformation]:
197
+ def _get_addons() -> list[AddonInformation]:
202
198
  """Return an updated list of add-on information.
203
199
 
204
200
  :returns: List of AddonInformation.
@@ -212,7 +208,7 @@ def _get_addons() -> List[AddonInformation]:
212
208
  return result
213
209
 
214
210
 
215
- def get_addons(limit: str = "") -> List[AddonInformation]:
211
+ def get_addons(limit: str = "") -> list[AddonInformation]:
216
212
  """List add-ons in this Plone site.
217
213
 
218
214
  :param limit: Limit list of add-ons.
@@ -239,7 +235,7 @@ def get_addons(limit: str = "") -> List[AddonInformation]:
239
235
  return addons
240
236
 
241
237
 
242
- def get_addon_ids(limit: str = "") -> List[str]:
238
+ def get_addon_ids(limit: str = "") -> list[str]:
243
239
  """List add-ons ids in this Plone site.
244
240
 
245
241
  :param limit: Limit list of add-ons.
@@ -25,7 +25,6 @@ from zope.interface import providedBy
25
25
  import transaction
26
26
  import uuid
27
27
 
28
-
29
28
  _marker = []
30
29
 
31
30
  # Maximum number of attempts to generate a unique random ID
@@ -18,7 +18,6 @@ from zope.globalrequest import getRequest
18
18
  import traceback
19
19
  import Zope2
20
20
 
21
-
22
21
  IS_TEST = None
23
22
 
24
23
 
@@ -24,7 +24,6 @@ from zope.schema.interfaces import IVocabularyFactory
24
24
  import datetime as dtime
25
25
  import re
26
26
 
27
-
28
27
  logger = getLogger("plone.api.portal")
29
28
 
30
29
  try:
@@ -26,7 +26,6 @@ from zope.lifecycleevent import modified
26
26
 
27
27
  import logging
28
28
 
29
-
30
29
  try:
31
30
  distribution("plone.app.iterate")
32
31
  except PackageNotFoundError:
@@ -257,6 +257,109 @@ api.portal.send_email(
257
257
  % 'attachment; filename="report.xml',
258
258
  % payloads[1]['Content-Disposition']
259
259
  % )
260
+ % mailhost.messages.clear()
261
+
262
+ The following code is a more complex example that constructs an email with a file attachment, HTML, plain text, and mail headers to control the mail response.
263
+
264
+ ```python
265
+ from email.encoders import encode_base64
266
+ from email.header import Header
267
+ from email.mime.base import MIMEBase
268
+ from email.mime.multipart import MIMEMultipart
269
+ from email.mime.text import MIMEText
270
+ from plone import api
271
+
272
+ # we need a message part to bundle the HTML and Plain Text
273
+ textmsgpart = MIMEMultipart("alternative")
274
+
275
+ # create plain text part of email
276
+ plaintextpart = MIMEText("Fill out your plain text", "plain", "utf-8")
277
+
278
+ # create html text of email
279
+ html = "<html><body><p>fill out your text in HTML</p></body></html>"
280
+ htmlpart = MIMEText(html, "html", "utf-8")
281
+
282
+ # bundle the parts
283
+ textmsgpart.attach(plaintextpart)
284
+ textmsgpart.attach(htmlpart)
285
+
286
+ # handle the file attachment
287
+
288
+ # Create a dummy PDF as NamedBlobFile
289
+ from plone.namedfile.file import NamedBlobFile
290
+
291
+ # Minimal PDF content (a valid but nearly empty PDF)
292
+ pdf_content = b"""%PDF-1.4
293
+ 1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
294
+ 2 0 obj<</Type/Pages/Count 1/Kids[3 0 R]>>endobj
295
+ 3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]/Contents 4 0 R>>endobj
296
+ 4 0 obj<</Length 44>>stream
297
+ BT /F1 12 Tf 100 700 Td (Hello World) Tj ET
298
+ endstream endobj
299
+ xref
300
+ 0 5
301
+ 0000000000 65535 f
302
+ 0000000009 00000 n
303
+ 0000000056 00000 n
304
+ 0000000115 00000 n
305
+ 0000000214 00000 n
306
+ trailer<</Size 5/Root 1 0 R>>
307
+ startxref
308
+ 315
309
+ %%EOF"""
310
+
311
+ attachment = NamedBlobFile(
312
+ data=pdf_content,
313
+ contentType='application/pdf',
314
+ filename='document.pdf'
315
+ )
316
+
317
+ filepart = MIMEBase("application", "pdf")
318
+ filepart.set_payload(attachment.data)
319
+ encode_base64(filepart)
320
+ filepart.add_header(
321
+ "Content-Disposition",
322
+ "attachment",
323
+ filename=(Header(attachment.filename, "utf-8").encode()),
324
+ )
325
+
326
+ # we need a mixed Multipart Message to bundle the text bundle and the attachment
327
+ msg = MIMEMultipart("mixed")
328
+ msg["From"] = "sender@abc"
329
+ msg["To"] = "recipient@zzz"
330
+ msg["Subject"] = "The Mail Subject"
331
+ msg["Reply-To"] = "community@plone.org"
332
+ msg["Return-Path"] = "error@xxx"
333
+
334
+ # add the text message bundle
335
+ msg.attach(textmsgpart)
336
+
337
+ # add the file attachment
338
+ msg.attach(filepart)
339
+
340
+ # send with plone.api
341
+ api.portal.send_email(
342
+ sender=msg["From"],
343
+ recipient=msg["To"],
344
+ subject=msg["Subject"],
345
+ body=msg.as_string()
346
+ )
347
+ ```
348
+
349
+ % invisible-code-block: python
350
+ %
351
+ % self.assertEqual(len(mailhost.messages), 1)
352
+ %
353
+ % msg = message_from_bytes(mailhost.messages[0])
354
+ % payloads = msg.get_payload()
355
+ % self.assertTrue(len(payloads) == 2)
356
+ % self.assertTrue(msg['Reply-To'] == 'community@plone.org')
357
+ % text_payloads = payloads[0].get_payload()
358
+ % self.assertEqual(len(text_payloads), 2)
359
+ % self.assertIn(
360
+ % 'attachment; filename',
361
+ % payloads[1]['Content-Disposition']
362
+ % )
260
363
  % api.portal.PRINTINGMAILHOST_ENABLED = False
261
364
  % mailhost.reset()
262
365
 
@@ -6,7 +6,6 @@ from plone.api.tests.base import INTEGRATION_TESTING
6
6
 
7
7
  import unittest
8
8
 
9
-
10
9
  ADDON = "plone.session"
11
10
 
12
11
 
@@ -15,6 +15,7 @@ from plone.base.interfaces import INavigationRoot
15
15
  from plone.indexer import indexer
16
16
  from plone.uuid.interfaces import IMutableUUID
17
17
  from plone.uuid.interfaces import IUUIDGenerator
18
+ from Products.CMFCore.indexing import processQueue
18
19
  from Products.CMFCore.interfaces import IFolderish
19
20
  from Products.CMFCore.WorkflowCore import WorkflowException
20
21
  from Products.ZCatalog.interfaces import IZCatalog
@@ -357,6 +358,11 @@ class TestPloneApiContent(unittest.TestCase):
357
358
  id="test-unicode-folder",
358
359
  container=self.portal,
359
360
  )
361
+ # The indexing queue may defer catalog operations until
362
+ # processQueue() is called (e.g. by a catalog search or
363
+ # at transaction commit time). Flush explicitly so the
364
+ # broken indexer is invoked inside the assertRaises block.
365
+ processQueue()
360
366
 
361
367
  # check that the exception is the one we raised
362
368
  self.assertEqual(ude.exception.reason, unicode_exception_message)
@@ -21,7 +21,6 @@ import os
21
21
  import re
22
22
  import unittest
23
23
 
24
-
25
24
  logger = getLogger(__name__)
26
25
 
27
26
  try: