cherrypy-foundation 1.0.0__py3-none-any.whl

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 (136) hide show
  1. cherrypy_foundation/__init__.py +0 -0
  2. cherrypy_foundation/components/ColorModes.jinja +70 -0
  3. cherrypy_foundation/components/Datatable.css +47 -0
  4. cherrypy_foundation/components/Datatable.jinja +63 -0
  5. cherrypy_foundation/components/Datatable.js +358 -0
  6. cherrypy_foundation/components/Field.css +10 -0
  7. cherrypy_foundation/components/Field.jinja +66 -0
  8. cherrypy_foundation/components/Field.js +56 -0
  9. cherrypy_foundation/components/Fields.jinja +4 -0
  10. cherrypy_foundation/components/Flash.jinja +13 -0
  11. cherrypy_foundation/components/Icon.jinja +3 -0
  12. cherrypy_foundation/components/LocaleSelection.jinja +13 -0
  13. cherrypy_foundation/components/LocaleSelection.js +26 -0
  14. cherrypy_foundation/components/SideBySideMultiSelect.css +25 -0
  15. cherrypy_foundation/components/SideBySideMultiSelect.jinja +9 -0
  16. cherrypy_foundation/components/SideBySideMultiSelect.js +9 -0
  17. cherrypy_foundation/components/Typeahead.css +55 -0
  18. cherrypy_foundation/components/Typeahead.jinja +106 -0
  19. cherrypy_foundation/components/Typeahead.js +8 -0
  20. cherrypy_foundation/components/__init__.py +51 -0
  21. cherrypy_foundation/components/tests/__init__.py +0 -0
  22. cherrypy_foundation/components/tests/test_static.py +90 -0
  23. cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.css +2106 -0
  24. cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.min.css +5 -0
  25. cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  26. cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  27. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css +9262 -0
  28. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css.map +95 -0
  29. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css +6 -0
  30. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css.map +7 -0
  31. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js +4846 -0
  32. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js.map +1 -0
  33. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js +7 -0
  34. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js.map +7 -0
  35. cherrypy_foundation/components/vendor/bootstrap5/js/color-modes.js +80 -0
  36. cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.css +849 -0
  37. cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.min.css +1 -0
  38. cherrypy_foundation/components/vendor/datatables/images/sort_asc.png +0 -0
  39. cherrypy_foundation/components/vendor/datatables/images/sort_asc_disabled.png +0 -0
  40. cherrypy_foundation/components/vendor/datatables/images/sort_both.png +0 -0
  41. cherrypy_foundation/components/vendor/datatables/images/sort_desc.png +0 -0
  42. cherrypy_foundation/components/vendor/datatables/images/sort_desc_disabled.png +0 -0
  43. cherrypy_foundation/components/vendor/datatables/js/dataTables.js +14073 -0
  44. cherrypy_foundation/components/vendor/datatables/js/dataTables.min.js +4 -0
  45. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.css +556 -0
  46. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.min.css +1 -0
  47. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.js +1700 -0
  48. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.min.js +8 -0
  49. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.js +2944 -0
  50. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.min.js +4 -0
  51. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.css +13 -0
  52. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.min.css +1 -0
  53. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.js +1202 -0
  54. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.min.js +4 -0
  55. cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.js +11577 -0
  56. cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.min.js +13 -0
  57. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.css +194 -0
  58. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.min.css +1 -0
  59. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.js +1861 -0
  60. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.min.js +4 -0
  61. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.js +75023 -0
  62. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.min.js +3 -0
  63. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/vfs_fonts.js +6 -0
  64. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.css +53 -0
  65. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.min.css +1 -0
  66. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.js +485 -0
  67. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.min.js +4 -0
  68. cherrypy_foundation/components/vendor/jquery/jquery.min.js +2 -0
  69. cherrypy_foundation/components/vendor/multi/LICENSE +7 -0
  70. cherrypy_foundation/components/vendor/multi/README.md +109 -0
  71. cherrypy_foundation/components/vendor/multi/multi.css +95 -0
  72. cherrypy_foundation/components/vendor/multi/multi.js +328 -0
  73. cherrypy_foundation/components/vendor/popper/popper.js +1825 -0
  74. cherrypy_foundation/components/vendor/popper/popper.min.js +6 -0
  75. cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.css +1 -0
  76. cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.js +10 -0
  77. cherrypy_foundation/error_page.py +94 -0
  78. cherrypy_foundation/flash.py +50 -0
  79. cherrypy_foundation/form.py +119 -0
  80. cherrypy_foundation/logging.py +103 -0
  81. cherrypy_foundation/passwd.py +65 -0
  82. cherrypy_foundation/plugins/__init__.py +0 -0
  83. cherrypy_foundation/plugins/db.py +286 -0
  84. cherrypy_foundation/plugins/ldap.py +257 -0
  85. cherrypy_foundation/plugins/restapi.py +74 -0
  86. cherrypy_foundation/plugins/scheduler.py +287 -0
  87. cherrypy_foundation/plugins/smtp.py +223 -0
  88. cherrypy_foundation/plugins/tests/__init__.py +0 -0
  89. cherrypy_foundation/plugins/tests/test_db.py +118 -0
  90. cherrypy_foundation/plugins/tests/test_ldap.py +451 -0
  91. cherrypy_foundation/plugins/tests/test_scheduler.py +100 -0
  92. cherrypy_foundation/plugins/tests/test_scheduler_db.py +107 -0
  93. cherrypy_foundation/plugins/tests/test_smtp.py +140 -0
  94. cherrypy_foundation/sessions.py +93 -0
  95. cherrypy_foundation/tests/__init__.py +72 -0
  96. cherrypy_foundation/tests/templates/test_flash.html +9 -0
  97. cherrypy_foundation/tests/templates/test_form.html +16 -0
  98. cherrypy_foundation/tests/templates/test_url.html +15 -0
  99. cherrypy_foundation/tests/test_error_page.py +78 -0
  100. cherrypy_foundation/tests/test_flash.py +61 -0
  101. cherrypy_foundation/tests/test_form.py +148 -0
  102. cherrypy_foundation/tests/test_logging.py +78 -0
  103. cherrypy_foundation/tests/test_passwd.py +51 -0
  104. cherrypy_foundation/tests/test_sessions.py +89 -0
  105. cherrypy_foundation/tests/test_url.py +161 -0
  106. cherrypy_foundation/tools/__init__.py +0 -0
  107. cherrypy_foundation/tools/auth.py +263 -0
  108. cherrypy_foundation/tools/auth_mfa.py +249 -0
  109. cherrypy_foundation/tools/i18n.py +529 -0
  110. cherrypy_foundation/tools/jinja2.py +158 -0
  111. cherrypy_foundation/tools/ratelimit.py +265 -0
  112. cherrypy_foundation/tools/secure_headers.py +119 -0
  113. cherrypy_foundation/tools/sessions_timeout.py +167 -0
  114. cherrypy_foundation/tools/tests/__init__.py +0 -0
  115. cherrypy_foundation/tools/tests/components/Button.jinja +2 -0
  116. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  117. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po +15 -0
  118. cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo +0 -0
  119. cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po +15 -0
  120. cherrypy_foundation/tools/tests/locales/messages.pot +2 -0
  121. cherrypy_foundation/tools/tests/templates/test_jinja2.html +11 -0
  122. cherrypy_foundation/tools/tests/templates/test_jinjax.html +9 -0
  123. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +22 -0
  124. cherrypy_foundation/tools/tests/test_auth.py +110 -0
  125. cherrypy_foundation/tools/tests/test_auth_mfa.py +369 -0
  126. cherrypy_foundation/tools/tests/test_i18n.py +247 -0
  127. cherrypy_foundation/tools/tests/test_jinja2.py +153 -0
  128. cherrypy_foundation/tools/tests/test_ratelimit.py +109 -0
  129. cherrypy_foundation/tools/tests/test_secure_headers.py +200 -0
  130. cherrypy_foundation/url.py +66 -0
  131. cherrypy_foundation/widgets.py +48 -0
  132. cherrypy_foundation-1.0.0.dist-info/METADATA +71 -0
  133. cherrypy_foundation-1.0.0.dist-info/RECORD +136 -0
  134. cherrypy_foundation-1.0.0.dist-info/WHEEL +5 -0
  135. cherrypy_foundation-1.0.0.dist-info/licenses/LICENSE.md +674 -0
  136. cherrypy_foundation-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,140 @@
1
+ # SMTP Plugins for cherrypy
2
+ # Copyright (C) 2022-2026 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ from unittest import mock, skipUnless
18
+
19
+ import cherrypy
20
+ from cherrypy.test import helper
21
+ from parameterized import parameterized
22
+
23
+ from .. import smtp # noqa
24
+
25
+
26
+ class SmtpPluginTest(helper.CPWebCase):
27
+ @classmethod
28
+ def setup_server(cls):
29
+ cherrypy.config.update(
30
+ {
31
+ 'smtp.server': '__default__',
32
+ 'smtp.username': 'username',
33
+ 'smtp.password': 'password',
34
+ 'smtp.email_from': 'Test <email_from@test.com>',
35
+ }
36
+ )
37
+
38
+ def test_send_mail(self):
39
+ # Given a valid smtp server
40
+ with mock.patch(smtp.__name__ + '.smtplib') as smtplib:
41
+ # When publishing a send_mail
42
+ cherrypy.engine.publish('send_mail', to='target@test.com', subject='subjet', message='body')
43
+ # Then smtplib is called to send the mail.
44
+ smtplib.SMTP.assert_called_once_with('__default__', 25)
45
+ smtplib.SMTP.return_value.send_message.assert_called_once_with(mock.ANY)
46
+ smtplib.SMTP.return_value.quit.assert_called_once_with()
47
+
48
+ def test_send_mail_with_to_tuple(self):
49
+ # Given a valid smtp server
50
+ with mock.patch(smtp.__name__ + '.smtplib') as smtplib:
51
+ # When publishing a send_mail
52
+ cherrypy.engine.publish(
53
+ 'send_mail',
54
+ to=('A name', 'target@test.com'),
55
+ subject='subjet',
56
+ message='body',
57
+ bcc=('A bcc name', 'bcc@test.com'),
58
+ reply_to=('A Reply Name', 'replyto@test.com'),
59
+ )
60
+ # Then smtplib is called to send the mail.
61
+ smtplib.SMTP.assert_called_once_with('__default__', 25)
62
+ smtplib.SMTP.return_value.send_message.assert_called_once_with(mock.ANY)
63
+ smtplib.SMTP.return_value.quit.assert_called_once_with()
64
+
65
+ @skipUnless(hasattr(cherrypy, 'scheduler'), reason='Required scheduler')
66
+ def test_queue_mail(self):
67
+ with mock.patch(smtp.__name__ + '.smtplib') as smtplib:
68
+ # Given a mail being queued.
69
+ cherrypy.engine.publish('queue_mail', to='target@test.com', subject='subjet', message='body')
70
+ # When waiting for all task to be processed
71
+ cherrypy.scheduler.wait_for_jobs()
72
+ # Then smtplib is called to send the mail.
73
+ smtplib.SMTP.assert_called_once_with('__default__', 25)
74
+ smtplib.SMTP.return_value.send_message.assert_called_once_with(mock.ANY)
75
+ smtplib.SMTP.return_value.quit.assert_called_once_with()
76
+
77
+ def test_html2plaintext(self):
78
+ """
79
+ Check if this convertion is working fine.
80
+ """
81
+
82
+ html = """<html>
83
+ <head>
84
+ <style type="text/css">
85
+ body { font-family:Helvetica; }
86
+ </style>
87
+ </head>
88
+ <body>
89
+ <h1>Hi!</h1>
90
+ <p id="test"
91
+ class="mb-2 text-center">
92
+ How are you?<br/>
93
+ Here&#160;is the <a href="https://www.python.org">link</a> you wanted.
94
+ </p>
95
+ </body>
96
+ </html>
97
+ """
98
+
99
+ expected = """**Hi!**
100
+ How are you?
101
+ Here is the link [1] you wanted.
102
+
103
+ [1] https://www.python.org"""
104
+ self.assertEqual(expected, smtp._html2plaintext(html))
105
+
106
+ def test_formataddr(self):
107
+ self.assertEqual('test@test.com', smtp._formataddr('test@test.com'))
108
+ self.assertEqual('TEST <test@test.com>', smtp._formataddr(('TEST', 'test@test.com')))
109
+ self.assertEqual(
110
+ 'test2@test.com, TEST3 <test3@test.com>', smtp._formataddr(['test2@test.com', ('TEST3', 'test3@test.com')])
111
+ )
112
+
113
+ @parameterized.expand(
114
+ [
115
+ (None, None, None),
116
+ (None, 'test2@test.com', 'test2@test.com'),
117
+ ('test2@test.com', None, 'test2@test.com'),
118
+ ('test1@test.com', 'test2@test.com', 'test1@test.com, test2@test.com'),
119
+ ]
120
+ )
121
+ def test_with_bcc(self, smtp_bcc, bcc, expected_bcc):
122
+ # Given a valid smtp server
123
+ with mock.patch(smtp.__name__ + '.smtplib') as smtplib:
124
+ # Given bcc is defined at plugin level.
125
+ cherrypy.smtp.bcc = smtp_bcc
126
+ # When publishing a send_mail with Bcc
127
+ cherrypy.engine.publish(
128
+ 'send_mail',
129
+ to=('A name', 'target@test.com'),
130
+ subject='subjet',
131
+ message='body',
132
+ bcc=bcc,
133
+ )
134
+ # Then message is sent
135
+ smtplib.SMTP.assert_called_once_with('__default__', 25)
136
+ smtplib.SMTP.return_value.send_message.assert_called_once_with(mock.ANY)
137
+ msg = smtplib.SMTP.return_value.send_message.call_args.args[0]
138
+ smtplib.SMTP.return_value.quit.assert_called_once_with()
139
+ # Message include our expected Bcc
140
+ self.assertEqual(expected_bcc, msg['Bcc'])
@@ -0,0 +1,93 @@
1
+ # Cherrypy-foundation
2
+ # Copyright (C) 2025-2026 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+
18
+ import logging
19
+ import os
20
+ import time
21
+ from contextlib import contextmanager
22
+
23
+ import cherrypy
24
+ import zc.lockfile
25
+ from cherrypy.lib import locking
26
+ from cherrypy.lib.sessions import FileSession as CPFileSession
27
+
28
+
29
+ class FileSession(CPFileSession):
30
+ """
31
+ Override implementation of cherrpy file session to improve file locking.
32
+ """
33
+
34
+ def acquire_lock(self, path=None):
35
+ """Acquire an exclusive lock on the currently-loaded session data."""
36
+ # See Issue https://github.com/cherrypy/cherrypy/issues/2065
37
+
38
+ if path is None:
39
+ path = self._get_file_path()
40
+ path += self.LOCK_SUFFIX
41
+ checker = locking.LockChecker(self.id, self.lock_timeout)
42
+ while not checker.expired():
43
+ try:
44
+ self.lock = zc.lockfile.LockFile(path)
45
+ except zc.lockfile.LockError:
46
+ # Sleep for 1ms only.
47
+ time.sleep(0.001)
48
+ else:
49
+ break
50
+ self.locked = True
51
+ if self.debug:
52
+ cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
53
+
54
+ def clean_up(self):
55
+ """Also clean-up left over lock files."""
56
+ # See Issue https://github.com/cherrypy/cherrypy/issues/1855
57
+
58
+ # Clean-up session files.
59
+ CPFileSession.clean_up(self)
60
+
61
+ # Then clean-up any orphane lock files.
62
+ suffix_len = len(self.LOCK_SUFFIX)
63
+ files = os.listdir(self.storage_path)
64
+ lock_files = [
65
+ fname for fname in files if fname.startswith(self.SESSION_PREFIX) and fname.endswith(self.LOCK_SUFFIX)
66
+ ]
67
+ for fname in lock_files:
68
+ session_file = fname[:-suffix_len]
69
+ if session_file not in files:
70
+ filepath = os.path.join(self.storage_path, fname)
71
+ try:
72
+ os.unlink(filepath)
73
+ except Exception as e:
74
+ cherrypy.log(f'Error deleting {filepath}: {e}', 'TOOLS.SESSIONS', severity=logging.WARNING)
75
+
76
+
77
+ @contextmanager
78
+ def session_lock():
79
+ """
80
+ Acquire session lock as required. Support re-intrant lock.
81
+ """
82
+ s = cherrypy.serving.session
83
+ if s.locking == 'explicit' and not s.locked:
84
+ s.acquire_lock()
85
+ try:
86
+ yield s
87
+ finally:
88
+ # When explicit, we want to save the session (with also release the lock.)
89
+ if s.locking == 'explicit':
90
+ s.save()
91
+ cherrypy.serving.request._sessionsaved = True
92
+ else:
93
+ yield s
@@ -0,0 +1,72 @@
1
+ # Cherrypy-foundation
2
+ # Copyright (C) 2026 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ import os
18
+ import tempfile
19
+ import unittest
20
+ from contextlib import contextmanager
21
+
22
+ from selenium import webdriver
23
+
24
+
25
+ class SeleniumUnitTest:
26
+
27
+ @property
28
+ def _session_id(self):
29
+ if hasattr(self, 'cookies') and self.cookies:
30
+ for unused, value in self.cookies:
31
+ for part in value.split(';'):
32
+ key, unused, value = part.partition('=')
33
+ if key == 'session_id':
34
+ return value
35
+
36
+ @contextmanager
37
+ def selenium(self, headless=True, implicitly_wait=3):
38
+ """
39
+ Decorator to load selenium for a test.
40
+ """
41
+ # Skip selenium test is display is not available.
42
+ if not os.environ.get('DISPLAY', False):
43
+ raise unittest.SkipTest("selenium require a display")
44
+ # Start selenium driver
45
+ options = webdriver.ChromeOptions()
46
+ if headless:
47
+ options.add_argument('--headless')
48
+ options.add_argument('--disable-gpu')
49
+ options.add_argument('--window-size=1280,800')
50
+ options.add_argument('--no-sandbox')
51
+ options.add_argument('--disable-dev-shm-usage')
52
+ options.add_argument('--lang=en-US')
53
+ driver = webdriver.Chrome(options=options)
54
+ try:
55
+ # If logged in, reuse the same session id.
56
+ if self._session_id:
57
+ driver.get(f'{self.baseurl}/login/')
58
+ driver.add_cookie({"name": "session_id", "value": self.session_id})
59
+ # Configure download folder
60
+ download = os.path.join(os.path.expanduser('~'), 'Downloads')
61
+ os.makedirs(download, exist_ok=True)
62
+ self._selenium_download_dir = tempfile.mkdtemp(dir=download, prefix='selenium-download-')
63
+ driver.execute_cdp_cmd(
64
+ 'Page.setDownloadBehavior', {'behavior': 'allow', 'downloadPath': self._selenium_download_dir}
65
+ )
66
+ # Set default wait.
67
+ driver.implicitly_wait(implicitly_wait)
68
+ yield driver
69
+ finally:
70
+ # Code to release resource, e.g.:
71
+ driver.close()
72
+ driver = None
@@ -0,0 +1,9 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>test-flash</title>
5
+ </head>
6
+ <body>
7
+ <Flash messages={{ get_flashed_messages() }} class="mb-2" style="min-height:100px"/>
8
+ </body>
9
+ </html>
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>test-form</title>
5
+ </head>
6
+ <body>
7
+ {% if form.error_message %}
8
+ <p>
9
+ {{ form.error_message }}
10
+ </p>
11
+ {% endif %}
12
+ <form method="post" action="/">
13
+ <Fields form={{ form }} />
14
+ </form>
15
+ </body>
16
+ </html>
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>test-url</title>
5
+ </head>
6
+ <body>
7
+ Empty: {{ url_for("", **kwargs) }}<br/>
8
+ Dot: {{ url_for(".", **kwargs) }}<br/>
9
+ Dot page: {{ url_for(".", "my-page", **kwargs) }}<br/>
10
+ Slash: {{ url_for("/", **kwargs) }}<br/>
11
+ Page: {{ url_for("my-page", **kwargs) }}<br/>
12
+ Slash page: {{ url_for("/my-page", **kwargs) }}<br/>
13
+ Query: {{ url_for("my-page", foo='1', bar='test with space', **kwargs) }}<br/>
14
+ </body>
15
+ </html>
@@ -0,0 +1,78 @@
1
+ # CherryPy
2
+ # Copyright (C) 2026 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ import cherrypy
18
+ from cherrypy.test import helper
19
+ from parameterized import parameterized
20
+
21
+ from ..error_page import error_page
22
+
23
+
24
+ class Root:
25
+
26
+ @cherrypy.expose
27
+ def index(self):
28
+ return "OK"
29
+
30
+ @cherrypy.expose
31
+ def not_found(self):
32
+ raise cherrypy.NotFound()
33
+
34
+ @cherrypy.expose
35
+ def not_found_custom(self):
36
+ raise cherrypy.HTTPError(404, message='My error message')
37
+
38
+ @cherrypy.expose
39
+ def html_error(self):
40
+ raise cherrypy.HTTPError(400, message='My error message')
41
+
42
+ @cherrypy.expose
43
+ @cherrypy.tools.json_out()
44
+ def json_error(self):
45
+ raise cherrypy.HTTPError(400, message='json error message')
46
+
47
+ @cherrypy.expose
48
+ @cherrypy.tools.response_headers(headers=[('Content-Type', 'text/plain')])
49
+ def text_error(self):
50
+ raise cherrypy.HTTPError(400, message='text error message')
51
+
52
+
53
+ class ErrorPageTest(helper.CPWebCase):
54
+ interactive = False
55
+
56
+ @classmethod
57
+ def setup_server(cls):
58
+ cherrypy.config.update(
59
+ {
60
+ 'error_page.default': error_page,
61
+ }
62
+ )
63
+ cherrypy.tree.mount(Root(), '/')
64
+
65
+ @parameterized.expand(
66
+ [
67
+ ('/not_found', '<p>Nothing matches the given URI</p>'),
68
+ ('/not_found_custom', '<p>My error message</p>'),
69
+ ('/html_error', '<p>My error message</p>'),
70
+ ('/json_error', '{"message": "json error message", "status": "400 Bad Request"}'),
71
+ ('/text_error', 'text error message'),
72
+ ]
73
+ )
74
+ def test_error_page(self, page, expect_body):
75
+ # When query return an error
76
+ self.getPage(page)
77
+ # then error page adjust the content
78
+ self.assertInBody(expect_body)
@@ -0,0 +1,61 @@
1
+ # Cherrypy-foundation
2
+ # Copyright (C) 2026 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ import importlib
17
+ from unittest import skipUnless
18
+
19
+ import cherrypy
20
+ from cherrypy.test import helper
21
+
22
+ import cherrypy_foundation.tools.jinja2 # noqa
23
+ from cherrypy_foundation.flash import flash, get_flashed_messages
24
+
25
+ HAS_JINJAX = importlib.util.find_spec("jinjax") is not None
26
+
27
+ env = cherrypy.tools.jinja2.create_env(
28
+ package_name=__package__,
29
+ globals={'get_flashed_messages': get_flashed_messages},
30
+ )
31
+
32
+
33
+ @cherrypy.tools.sessions(locking='explicit')
34
+ @cherrypy.tools.jinja2(env=env)
35
+ class Root:
36
+
37
+ @cherrypy.expose
38
+ @cherrypy.tools.jinja2(template='test_flash.html')
39
+ def index(self):
40
+ flash('default flash message', level='info')
41
+ return {}
42
+
43
+
44
+ @skipUnless(HAS_JINJAX, reason='Required jinjax')
45
+ class FormTest(helper.CPWebCase):
46
+ default_lang = None
47
+ interactive = False
48
+
49
+ @classmethod
50
+ def setup_server(cls):
51
+ cherrypy.tree.mount(Root(), '/')
52
+
53
+ def test_get_flash(self):
54
+ # Given a page returning a flash message
55
+ # When querying the page that include this form
56
+ self.getPage("/")
57
+ self.assertStatus(200)
58
+ # Then page display the message.
59
+ self.assertInBody('test-flash')
60
+ self.assertInBody('<div class="alert alert-info alert-dismissible fade show"')
61
+ self.assertInBody('default flash message')
@@ -0,0 +1,148 @@
1
+ # Cherrypy-foundation
2
+ # Copyright (C) 2026 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ import importlib
17
+ from unittest import skipUnless
18
+ from urllib.parse import urlencode
19
+
20
+ import cherrypy
21
+ from cherrypy.test import helper
22
+ from parameterized import parameterized
23
+ from wtforms.fields import BooleanField, PasswordField, StringField, SubmitField
24
+ from wtforms.validators import InputRequired, Length
25
+
26
+ import cherrypy_foundation.tools.jinja2 # noqa
27
+ from cherrypy_foundation.form import CherryForm
28
+ from cherrypy_foundation.tools.i18n import gettext_lazy as _
29
+
30
+ HAS_JINJAX = importlib.util.find_spec("jinjax") is not None
31
+
32
+ env = cherrypy.tools.jinja2.create_env(
33
+ package_name=__package__,
34
+ )
35
+
36
+
37
+ class LoginForm(CherryForm):
38
+ login = StringField(
39
+ _('User'),
40
+ validators=[
41
+ InputRequired(),
42
+ Length(max=256, message=_('User too long.')),
43
+ ],
44
+ render_kw={
45
+ "placeholder": _('User'),
46
+ "autocorrect": "off",
47
+ "autocapitalize": "none",
48
+ "autocomplete": "off",
49
+ "autofocus": "autofocus",
50
+ },
51
+ )
52
+ password = PasswordField(
53
+ _('Password'),
54
+ validators=[
55
+ InputRequired(),
56
+ Length(max=256, message=_('Password too long.')),
57
+ ],
58
+ render_kw={"placeholder": _("Password")},
59
+ )
60
+ persistent = BooleanField(
61
+ _('Remember me'),
62
+ # All `label-*` are assigned to the label tag.
63
+ render_kw={'container_class': 'col-sm-6', 'label-attr': 'FOO'},
64
+ )
65
+ submit = SubmitField(
66
+ _('Login'),
67
+ # All `container-*` are assigned to the container tag.
68
+ render_kw={"class": "btn-primary float-end", 'container_class': 'col-sm-6', 'container-attr': 'BAR'},
69
+ )
70
+
71
+
72
+ @cherrypy.tools.sessions()
73
+ @cherrypy.tools.jinja2(env=env)
74
+ class Root:
75
+
76
+ @cherrypy.expose
77
+ def index(self, **kwargs):
78
+ if 'login' not in cherrypy.session:
79
+ raise cherrypy.HTTPRedirect('/login')
80
+ return 'OK'
81
+
82
+ @cherrypy.expose
83
+ @cherrypy.tools.jinja2(template='test_form.html')
84
+ def login(self, **kwargs):
85
+ form = LoginForm()
86
+ if form.validate_on_submit():
87
+ # login user with cherrypy.tools.auth
88
+ cherrypy.session['login'] = True
89
+ raise cherrypy.HTTPRedirect('/')
90
+ return {'form': form}
91
+
92
+
93
+ @skipUnless(HAS_JINJAX, reason='Required jinjax')
94
+ class FormTest(helper.CPWebCase):
95
+ default_lang = None
96
+ interactive = False
97
+
98
+ @classmethod
99
+ def setup_server(cls):
100
+ cherrypy.tree.mount(Root(), '/')
101
+
102
+ def test_get_form(self):
103
+ # Given a form
104
+ # When querying the page that include this form
105
+ self.getPage("/login")
106
+ self.assertStatus(200)
107
+ # Then each field is render properly.
108
+ # 1. Check title
109
+ self.assertInBody('test-form')
110
+ # 2. Check user field
111
+ self.assertInBody('<label class="form-label" for="login">User</label>')
112
+ self.assertInBody(
113
+ '<input autocapitalize="none" autocomplete="off" autocorrect="off" autofocus="autofocus" class="form-control" id="login" maxlength="256" name="login" placeholder="User" required type="text" value="">'
114
+ )
115
+ # 3. Check password
116
+ self.assertInBody('<label class="form-label" for="password">Password</label>')
117
+ self.assertInBody(
118
+ '<input class="form-control" id="password" maxlength="256" name="password" placeholder="Password" required type="password" value="">'
119
+ )
120
+ # 4 Check remember me
121
+ self.assertInBody(
122
+ '<input class="form-check-input" container-class="col-sm-6" id="persistent" label-attr="FOO" name="persistent" type="checkbox" value="y">'
123
+ )
124
+ self.assertInBody('<label attr="FOO" class="form-check-label" for="persistent">Remember me</label>')
125
+ # 5. check submit button (regex matches because class could have different order with jinjax<=0.57)
126
+ self.assertInBody('<div attr="BAR"')
127
+ self.assertMatchesBody(
128
+ '<input class="(btn-primary ?|float-end ?|btn ?){3}" container-attr="BAR" container-class="col-sm-6" id="submit" name="submit" type="submit" value="Login">'
129
+ )
130
+
131
+ @parameterized.expand(
132
+ [
133
+ ('myuser', 'mypassword', 0, 303, False),
134
+ ('myuser', '', 0, 200, 'Password: This field is required.'),
135
+ ('', 'mypassword', 0, 200, 'User: This field is required.'),
136
+ ]
137
+ )
138
+ def test_post_form(self, login, password, persistent, expect_status, expect_error):
139
+ # Given a page with a form.
140
+ # When data is sent to the form.
141
+ self.getPage(
142
+ "/login", method='POST', body=urlencode({'login': login, 'password': password, 'persistent': persistent})
143
+ )
144
+ # Then page return a status
145
+ self.assertStatus(expect_status)
146
+ # Then page may return an error
147
+ if expect_error:
148
+ self.assertInBody(expect_error)