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.
- cherrypy_foundation/__init__.py +0 -0
- cherrypy_foundation/components/ColorModes.jinja +70 -0
- cherrypy_foundation/components/Datatable.css +47 -0
- cherrypy_foundation/components/Datatable.jinja +63 -0
- cherrypy_foundation/components/Datatable.js +358 -0
- cherrypy_foundation/components/Field.css +10 -0
- cherrypy_foundation/components/Field.jinja +66 -0
- cherrypy_foundation/components/Field.js +56 -0
- cherrypy_foundation/components/Fields.jinja +4 -0
- cherrypy_foundation/components/Flash.jinja +13 -0
- cherrypy_foundation/components/Icon.jinja +3 -0
- cherrypy_foundation/components/LocaleSelection.jinja +13 -0
- cherrypy_foundation/components/LocaleSelection.js +26 -0
- cherrypy_foundation/components/SideBySideMultiSelect.css +25 -0
- cherrypy_foundation/components/SideBySideMultiSelect.jinja +9 -0
- cherrypy_foundation/components/SideBySideMultiSelect.js +9 -0
- cherrypy_foundation/components/Typeahead.css +55 -0
- cherrypy_foundation/components/Typeahead.jinja +106 -0
- cherrypy_foundation/components/Typeahead.js +8 -0
- cherrypy_foundation/components/__init__.py +51 -0
- cherrypy_foundation/components/tests/__init__.py +0 -0
- cherrypy_foundation/components/tests/test_static.py +90 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.css +2106 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.min.css +5 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css +9262 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css.map +95 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css +6 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css.map +7 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js +4846 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js.map +1 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js +7 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js.map +7 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/color-modes.js +80 -0
- cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.css +849 -0
- cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_asc.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_asc_disabled.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_both.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_desc.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_desc_disabled.png +0 -0
- cherrypy_foundation/components/vendor/datatables/js/dataTables.js +14073 -0
- cherrypy_foundation/components/vendor/datatables/js/dataTables.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.css +556 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.js +1700 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.min.js +8 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.js +2944 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.css +13 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.js +1202 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.js +11577 -0
- cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.min.js +13 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.css +194 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.js +1861 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.js +75023 -0
- cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.min.js +3 -0
- cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/vfs_fonts.js +6 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.css +53 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.js +485 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.min.js +4 -0
- cherrypy_foundation/components/vendor/jquery/jquery.min.js +2 -0
- cherrypy_foundation/components/vendor/multi/LICENSE +7 -0
- cherrypy_foundation/components/vendor/multi/README.md +109 -0
- cherrypy_foundation/components/vendor/multi/multi.css +95 -0
- cherrypy_foundation/components/vendor/multi/multi.js +328 -0
- cherrypy_foundation/components/vendor/popper/popper.js +1825 -0
- cherrypy_foundation/components/vendor/popper/popper.min.js +6 -0
- cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.css +1 -0
- cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.js +10 -0
- cherrypy_foundation/error_page.py +94 -0
- cherrypy_foundation/flash.py +50 -0
- cherrypy_foundation/form.py +119 -0
- cherrypy_foundation/logging.py +103 -0
- cherrypy_foundation/passwd.py +65 -0
- cherrypy_foundation/plugins/__init__.py +0 -0
- cherrypy_foundation/plugins/db.py +286 -0
- cherrypy_foundation/plugins/ldap.py +257 -0
- cherrypy_foundation/plugins/restapi.py +74 -0
- cherrypy_foundation/plugins/scheduler.py +287 -0
- cherrypy_foundation/plugins/smtp.py +223 -0
- cherrypy_foundation/plugins/tests/__init__.py +0 -0
- cherrypy_foundation/plugins/tests/test_db.py +118 -0
- cherrypy_foundation/plugins/tests/test_ldap.py +451 -0
- cherrypy_foundation/plugins/tests/test_scheduler.py +100 -0
- cherrypy_foundation/plugins/tests/test_scheduler_db.py +107 -0
- cherrypy_foundation/plugins/tests/test_smtp.py +140 -0
- cherrypy_foundation/sessions.py +93 -0
- cherrypy_foundation/tests/__init__.py +72 -0
- cherrypy_foundation/tests/templates/test_flash.html +9 -0
- cherrypy_foundation/tests/templates/test_form.html +16 -0
- cherrypy_foundation/tests/templates/test_url.html +15 -0
- cherrypy_foundation/tests/test_error_page.py +78 -0
- cherrypy_foundation/tests/test_flash.py +61 -0
- cherrypy_foundation/tests/test_form.py +148 -0
- cherrypy_foundation/tests/test_logging.py +78 -0
- cherrypy_foundation/tests/test_passwd.py +51 -0
- cherrypy_foundation/tests/test_sessions.py +89 -0
- cherrypy_foundation/tests/test_url.py +161 -0
- cherrypy_foundation/tools/__init__.py +0 -0
- cherrypy_foundation/tools/auth.py +263 -0
- cherrypy_foundation/tools/auth_mfa.py +249 -0
- cherrypy_foundation/tools/i18n.py +529 -0
- cherrypy_foundation/tools/jinja2.py +158 -0
- cherrypy_foundation/tools/ratelimit.py +265 -0
- cherrypy_foundation/tools/secure_headers.py +119 -0
- cherrypy_foundation/tools/sessions_timeout.py +167 -0
- cherrypy_foundation/tools/tests/__init__.py +0 -0
- cherrypy_foundation/tools/tests/components/Button.jinja +2 -0
- cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po +15 -0
- cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po +15 -0
- cherrypy_foundation/tools/tests/locales/messages.pot +2 -0
- cherrypy_foundation/tools/tests/templates/test_jinja2.html +11 -0
- cherrypy_foundation/tools/tests/templates/test_jinjax.html +9 -0
- cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +22 -0
- cherrypy_foundation/tools/tests/test_auth.py +110 -0
- cherrypy_foundation/tools/tests/test_auth_mfa.py +369 -0
- cherrypy_foundation/tools/tests/test_i18n.py +247 -0
- cherrypy_foundation/tools/tests/test_jinja2.py +153 -0
- cherrypy_foundation/tools/tests/test_ratelimit.py +109 -0
- cherrypy_foundation/tools/tests/test_secure_headers.py +200 -0
- cherrypy_foundation/url.py +66 -0
- cherrypy_foundation/widgets.py +48 -0
- cherrypy_foundation-1.0.0.dist-info/METADATA +71 -0
- cherrypy_foundation-1.0.0.dist-info/RECORD +136 -0
- cherrypy_foundation-1.0.0.dist-info/WHEEL +5 -0
- cherrypy_foundation-1.0.0.dist-info/licenses/LICENSE.md +674 -0
- cherrypy_foundation-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,247 @@
|
|
|
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 gettext
|
|
18
|
+
import importlib.resources
|
|
19
|
+
import unittest
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
|
|
22
|
+
import cherrypy
|
|
23
|
+
from cherrypy import _cpconfig
|
|
24
|
+
from cherrypy.test import helper
|
|
25
|
+
|
|
26
|
+
from .. import i18n
|
|
27
|
+
|
|
28
|
+
TEXT_EN = 'Some text to translate'
|
|
29
|
+
TEXT_FR = 'Du texte à traduire'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestI18n(unittest.TestCase):
|
|
33
|
+
def setUp(self):
|
|
34
|
+
self.mo_dir = importlib.resources.files(__package__) / 'locales'
|
|
35
|
+
self.assertTrue(self.mo_dir.is_dir())
|
|
36
|
+
cherrypy.request.config = _cpconfig.Config()
|
|
37
|
+
|
|
38
|
+
def test_search_translation_en(self):
|
|
39
|
+
# Load default translation return translation
|
|
40
|
+
t = i18n._search_translation(self.mo_dir, 'messages', 'en')
|
|
41
|
+
self.assertEqual("en", t.locale.language)
|
|
42
|
+
# Test translation object
|
|
43
|
+
self.assertEqual(TEXT_EN, t.gettext(TEXT_EN))
|
|
44
|
+
|
|
45
|
+
def test_search_translation_fr(self):
|
|
46
|
+
# Custom lang return translation.
|
|
47
|
+
t = i18n._search_translation(self.mo_dir, 'messages', 'fr')
|
|
48
|
+
self.assertIsInstance(t, gettext.GNUTranslations)
|
|
49
|
+
self.assertEqual("fr", t.locale.language)
|
|
50
|
+
# Test translation object
|
|
51
|
+
self.assertEqual(TEXT_FR, t.gettext(TEXT_EN))
|
|
52
|
+
|
|
53
|
+
def test_search_translation_invalid(self):
|
|
54
|
+
# Load invalid translation return None
|
|
55
|
+
t = i18n._search_translation(self.mo_dir, 'messages', 'tr')
|
|
56
|
+
# Return Null translations.
|
|
57
|
+
self.assertIn('NullTranslations', str(t.__class__))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Root:
|
|
61
|
+
|
|
62
|
+
@cherrypy.expose
|
|
63
|
+
@cherrypy.tools.i18n(on=True)
|
|
64
|
+
def index(self):
|
|
65
|
+
return i18n.gettext(TEXT_EN)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AbstractI18nTest(helper.CPWebCase):
|
|
69
|
+
default_lang = None
|
|
70
|
+
interactive = False
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def setup_server(cls):
|
|
74
|
+
cherrypy.config.update(
|
|
75
|
+
{
|
|
76
|
+
'tools.i18n.default': cls.default_lang,
|
|
77
|
+
'tools.i18n.mo_dir': importlib.resources.files(__package__) / 'locales',
|
|
78
|
+
'tools.i18n.domain': 'messages',
|
|
79
|
+
'tools.i18n.cookie_name': 'locale',
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
cherrypy.tree.mount(Root(), '/')
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestI18nWebCase(AbstractI18nTest):
|
|
86
|
+
|
|
87
|
+
def test_language_with_invalid(self):
|
|
88
|
+
# Query the page without login-in
|
|
89
|
+
self.getPage("/", headers=[("Accept-Language", "invalid")])
|
|
90
|
+
self.assertStatus('200 OK')
|
|
91
|
+
self.assertHeaderItemValue("Content-Language", "en")
|
|
92
|
+
self.assertInBody(TEXT_EN)
|
|
93
|
+
|
|
94
|
+
def test_language_with_unknown(self):
|
|
95
|
+
# Query the page without login-in
|
|
96
|
+
self.getPage("/", headers=[("Accept-Language", "it")])
|
|
97
|
+
self.assertStatus('200 OK')
|
|
98
|
+
self.assertHeaderItemValue("Content-Language", "en")
|
|
99
|
+
self.assertInBody(TEXT_EN)
|
|
100
|
+
|
|
101
|
+
def test_language_en(self):
|
|
102
|
+
self.getPage("/", headers=[("Accept-Language", "en-US,en;q=0.8")])
|
|
103
|
+
self.assertStatus('200 OK')
|
|
104
|
+
self.assertHeaderItemValue("Content-Language", "en-US")
|
|
105
|
+
self.assertInBody(TEXT_EN)
|
|
106
|
+
|
|
107
|
+
def test_language_en_fr(self):
|
|
108
|
+
self.getPage("/", headers=[("Accept-Language", "en-US,en;q=0.8,fr-CA;q=0.8")])
|
|
109
|
+
self.assertStatus('200 OK')
|
|
110
|
+
self.assertHeaderItemValue("Content-Language", "en-US")
|
|
111
|
+
self.assertInBody(TEXT_EN)
|
|
112
|
+
|
|
113
|
+
def test_language_fr(self):
|
|
114
|
+
self.getPage("/")
|
|
115
|
+
self.assertInBody(TEXT_EN)
|
|
116
|
+
self.getPage("/", headers=[("Accept-Language", "fr-CA;q=0.8,fr;q=0.6")])
|
|
117
|
+
self.assertStatus('200 OK')
|
|
118
|
+
self.assertHeaderItemValue("Content-Language", "fr-CA")
|
|
119
|
+
self.assertInBody(TEXT_FR)
|
|
120
|
+
|
|
121
|
+
def test_language_en_US_POSIX(self):
|
|
122
|
+
# When calling with locale variant
|
|
123
|
+
self.getPage("/", headers=[("Accept-Language", "en-US-POSIX")])
|
|
124
|
+
self.assertStatus('200 OK')
|
|
125
|
+
# Tehn page return en-US
|
|
126
|
+
self.assertHeaderItemValue("Content-Language", "en-US")
|
|
127
|
+
|
|
128
|
+
def test_cookie_fr(self):
|
|
129
|
+
# When calling with locale variant
|
|
130
|
+
self.getPage("/", headers=[("Accept-Language", "en-US"), ("Cookie", "locale=fr")])
|
|
131
|
+
self.assertStatus('200 OK')
|
|
132
|
+
# Tehn page return en-US
|
|
133
|
+
self.assertHeaderItemValue("Content-Language", "fr")
|
|
134
|
+
|
|
135
|
+
def test_with_preferred_lang(self):
|
|
136
|
+
# Given a default lang 'en'
|
|
137
|
+
date = datetime.fromtimestamp(1680111611, timezone.utc)
|
|
138
|
+
self.assertEqual(TEXT_EN, i18n.ugettext(TEXT_EN))
|
|
139
|
+
self.assertIn('March', i18n.format_datetime(date, format='long'))
|
|
140
|
+
# When using preferred_lang with french
|
|
141
|
+
with i18n.preferred_lang('fr'):
|
|
142
|
+
# Then french translation is used
|
|
143
|
+
self.assertEqual(TEXT_FR, i18n.ugettext(TEXT_EN))
|
|
144
|
+
# Then date time formating used french locale
|
|
145
|
+
self.assertIn('mars', i18n.format_datetime(date, format='long'))
|
|
146
|
+
# Then ouside the block, settings goest back to english
|
|
147
|
+
self.assertEqual(TEXT_EN, i18n.ugettext(TEXT_EN))
|
|
148
|
+
self.assertIn('March', i18n.format_datetime(date, format='long'))
|
|
149
|
+
|
|
150
|
+
def test_format_datetime_locales(self):
|
|
151
|
+
date = datetime.fromtimestamp(1680111611, timezone.utc)
|
|
152
|
+
with i18n.preferred_timezone('utc'):
|
|
153
|
+
with i18n.preferred_lang('fr'):
|
|
154
|
+
self.assertEqual('29 mars 2023, 17:40:11 TU', i18n.format_datetime(date, format='long'))
|
|
155
|
+
with i18n.preferred_lang('en'):
|
|
156
|
+
self.assertEqual('March 29, 2023, 5:40:11\u202fPM UTC', i18n.format_datetime(date, format='long'))
|
|
157
|
+
with i18n.preferred_lang('en_US'):
|
|
158
|
+
self.assertEqual('March 29, 2023, 5:40:11\u202fPM UTC', i18n.format_datetime(date, format='long'))
|
|
159
|
+
with i18n.preferred_lang('en_GB'):
|
|
160
|
+
self.assertEqual('29 March 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
|
|
161
|
+
with i18n.preferred_lang('en_CH'):
|
|
162
|
+
self.assertEqual('29 March 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
|
|
163
|
+
with i18n.preferred_lang('de'):
|
|
164
|
+
self.assertEqual('29. März 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
|
|
165
|
+
with i18n.preferred_lang('de_CH'):
|
|
166
|
+
self.assertEqual('29. März 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
|
|
167
|
+
|
|
168
|
+
with i18n.preferred_timezone('Europe/Paris'):
|
|
169
|
+
with i18n.preferred_lang('fr'):
|
|
170
|
+
self.assertEqual('29 mars 2023, 19:40:11 +0200', i18n.format_datetime(date, format='long'))
|
|
171
|
+
with i18n.preferred_lang('fr_CH'):
|
|
172
|
+
self.assertEqual('29 mars 2023, 19:40:11 +0200', i18n.format_datetime(date, format='long'))
|
|
173
|
+
with i18n.preferred_lang('en'):
|
|
174
|
+
self.assertEqual('March 29, 2023, 7:40:11\u202fPM +0200', i18n.format_datetime(date, format='long'))
|
|
175
|
+
with i18n.preferred_lang('en_US'):
|
|
176
|
+
self.assertEqual('March 29, 2023, 7:40:11\u202fPM +0200', i18n.format_datetime(date, format='long'))
|
|
177
|
+
with i18n.preferred_lang('en_GB'):
|
|
178
|
+
self.assertEqual('29 March 2023, 19:40:11 CEST', i18n.format_datetime(date, format='long'))
|
|
179
|
+
with i18n.preferred_lang('en_CH'):
|
|
180
|
+
self.assertEqual('29 March 2023, 19:40:11 CEST', i18n.format_datetime(date, format='long'))
|
|
181
|
+
with i18n.preferred_lang('de'):
|
|
182
|
+
self.assertEqual('29. März 2023, 19:40:11 MESZ', i18n.format_datetime(date, format='long'))
|
|
183
|
+
with i18n.preferred_lang('de_CH'):
|
|
184
|
+
self.assertEqual('29. März 2023, 19:40:11 MESZ', i18n.format_datetime(date, format='long'))
|
|
185
|
+
|
|
186
|
+
def test_list_available_locales(self):
|
|
187
|
+
self.assertEqual(['de', 'en', 'fr'], sorted([str(l) for l in i18n.list_available_locales()]))
|
|
188
|
+
|
|
189
|
+
def test_list_available_timezones(self):
|
|
190
|
+
timezones = i18n.list_available_timezones()
|
|
191
|
+
self.assertIn('America/Toronto', timezones)
|
|
192
|
+
|
|
193
|
+
def test_get_timezone_name(self):
|
|
194
|
+
with i18n.preferred_lang('en'):
|
|
195
|
+
self.assertEqual('Eastern Time', i18n.get_timezone_name('America/Toronto'))
|
|
196
|
+
self.assertEqual('ET', i18n.get_timezone_name('America/Toronto', width='short'))
|
|
197
|
+
self.assertEqual('Eastern Time', i18n.get_timezone_name('America/Toronto', width='long'))
|
|
198
|
+
with i18n.preferred_lang('fr'):
|
|
199
|
+
self.assertEqual('heure de l’Est nord-américain', i18n.get_timezone_name('America/Toronto'))
|
|
200
|
+
self.assertEqual('HE', i18n.get_timezone_name('America/Toronto', width='short'))
|
|
201
|
+
self.assertEqual('heure de l’Est nord-américain', i18n.get_timezone_name('America/Toronto', width='long'))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestI18nDefaultLangWebCase(AbstractI18nTest):
|
|
205
|
+
default_lang = 'FR'
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def teardown_class(cls):
|
|
209
|
+
# Reset default-lang to avoid issue with other test
|
|
210
|
+
cherrypy.config['tools.i18n.default'] = 'en'
|
|
211
|
+
super().teardown_class()
|
|
212
|
+
|
|
213
|
+
def test_default_lang_without_accept_language(self):
|
|
214
|
+
# Given a default language
|
|
215
|
+
# When user connect to the application without Accept-Language
|
|
216
|
+
self.getPage("/")
|
|
217
|
+
self.assertStatus(200)
|
|
218
|
+
# Then page is displayed with default lang
|
|
219
|
+
self.assertInBody(TEXT_FR)
|
|
220
|
+
|
|
221
|
+
def test_default_lang_with_accept_language(self):
|
|
222
|
+
# Given a default language
|
|
223
|
+
# When user connect to the application with Accept-Language English
|
|
224
|
+
self.getPage("/", headers=[("Accept-Language", "en-US,en;q=0.8")])
|
|
225
|
+
self.assertStatus(200)
|
|
226
|
+
# Then page is displayed as english
|
|
227
|
+
self.assertInBody(TEXT_EN)
|
|
228
|
+
|
|
229
|
+
def test_default_lang_with_unknown_accept_language(self):
|
|
230
|
+
# Given a default language
|
|
231
|
+
# When user connect to the application with Accept-Language English
|
|
232
|
+
self.getPage("/", headers=[("Accept-Language", "it")])
|
|
233
|
+
self.assertStatus(200)
|
|
234
|
+
# Then page is displayed as english
|
|
235
|
+
self.assertInBody(TEXT_FR)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class TestI18nInvalidDefaultLangWebCase(AbstractI18nTest):
|
|
239
|
+
default_lang = 'invalid'
|
|
240
|
+
|
|
241
|
+
def test_default_lang_invalid(self):
|
|
242
|
+
# Given an invalid default language
|
|
243
|
+
# When user connect to the application without Accept-Language
|
|
244
|
+
self.getPage("/")
|
|
245
|
+
self.assertStatus(200)
|
|
246
|
+
# Then page is displayed with fallback to "en"
|
|
247
|
+
self.assertInBody(TEXT_EN)
|
|
@@ -0,0 +1,153 @@
|
|
|
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 datetime import datetime, timezone
|
|
18
|
+
from unittest import skipUnless
|
|
19
|
+
|
|
20
|
+
import cherrypy
|
|
21
|
+
from cherrypy.test import helper
|
|
22
|
+
from parameterized import parameterized
|
|
23
|
+
|
|
24
|
+
from cherrypy_foundation.components import StaticMiddleware
|
|
25
|
+
from cherrypy_foundation.tests import SeleniumUnitTest
|
|
26
|
+
from cherrypy_foundation.url import url_for
|
|
27
|
+
|
|
28
|
+
from .. import i18n # noqa
|
|
29
|
+
from .. import jinja2 # noqa
|
|
30
|
+
|
|
31
|
+
HAS_JINJAX = importlib.util.find_spec("jinjax") is not None
|
|
32
|
+
|
|
33
|
+
env = cherrypy.tools.jinja2.create_env(
|
|
34
|
+
package_name=__package__,
|
|
35
|
+
globals={
|
|
36
|
+
'const1': 'STATIC VALUE',
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extra_processor():
|
|
42
|
+
return {'var2': 'bar'}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@cherrypy.tools.i18n(on=False)
|
|
46
|
+
@cherrypy.tools.sessions(on=False)
|
|
47
|
+
class Static:
|
|
48
|
+
|
|
49
|
+
components = StaticMiddleware()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@cherrypy.tools.jinja2(on=False, env=env, extra_processor=extra_processor)
|
|
53
|
+
class Root:
|
|
54
|
+
|
|
55
|
+
static = Static()
|
|
56
|
+
|
|
57
|
+
@cherrypy.expose
|
|
58
|
+
@cherrypy.tools.jinja2(template='test_jinja2.html')
|
|
59
|
+
def index(self):
|
|
60
|
+
return {'var1': 'test-jinja2'}
|
|
61
|
+
|
|
62
|
+
@cherrypy.expose
|
|
63
|
+
@cherrypy.tools.jinja2(template='test_jinjax.html')
|
|
64
|
+
def jinjax(self):
|
|
65
|
+
return {'var1': 'test-jinjax'}
|
|
66
|
+
|
|
67
|
+
@cherrypy.expose
|
|
68
|
+
@cherrypy.tools.jinja2(template='test_jinjax_i18n.html')
|
|
69
|
+
@cherrypy.tools.i18n(
|
|
70
|
+
default='fr',
|
|
71
|
+
default_timezone='America/Toronto',
|
|
72
|
+
mo_dir=importlib.resources.files(__package__) / 'locales',
|
|
73
|
+
domain='messages',
|
|
74
|
+
cookie_name='locale', # For LocaleSelection
|
|
75
|
+
)
|
|
76
|
+
def localized(self):
|
|
77
|
+
return {
|
|
78
|
+
'my_datetime': datetime(year=2025, month=11, day=26, hour=11, minute=16, tzinfo=timezone.utc),
|
|
79
|
+
'my_date': datetime(year=2025, month=12, day=22, hour=14, minute=8, tzinfo=timezone.utc),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Jinja2Test(helper.CPWebCase, SeleniumUnitTest):
|
|
84
|
+
default_lang = None
|
|
85
|
+
interactive = False
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def setup_server(cls):
|
|
89
|
+
cherrypy.tree.mount(Root(), '/')
|
|
90
|
+
|
|
91
|
+
def test_get_page(self):
|
|
92
|
+
# Given a page render using jinja2
|
|
93
|
+
# When querying the page
|
|
94
|
+
self.getPage("/")
|
|
95
|
+
# Then the page return without error
|
|
96
|
+
self.assertStatus(200)
|
|
97
|
+
# Then the page is render dynamically using page context
|
|
98
|
+
self.assertInBody('test-jinja2')
|
|
99
|
+
self.assertInBody('bar')
|
|
100
|
+
self.assertInBody('STATIC VALUE')
|
|
101
|
+
|
|
102
|
+
@skipUnless(HAS_JINJAX, reason='Required jinjax')
|
|
103
|
+
def test_get_page_jinjax(self):
|
|
104
|
+
# Given a page render using jinjax
|
|
105
|
+
# When querying the page
|
|
106
|
+
self.getPage("/jinjax")
|
|
107
|
+
# Then the page return without error
|
|
108
|
+
self.assertStatus(200)
|
|
109
|
+
# Then the page is render dynamically using page context
|
|
110
|
+
self.assertInBody('<a class="btn btn-primary" href="http://example.com">foo</a>')
|
|
111
|
+
|
|
112
|
+
@skipUnless(HAS_JINJAX, reason='Required jinjax')
|
|
113
|
+
@parameterized.expand(
|
|
114
|
+
[
|
|
115
|
+
('server_default', {}, 'fr'),
|
|
116
|
+
('accept_lang_fr', {'Accept-Language': 'fr'}, 'fr'),
|
|
117
|
+
('accept_lang_en', {'Accept-Language': 'en'}, 'en'),
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
def test_get_page_i18n(self, _name, headers, expected_lang):
|
|
121
|
+
# Given a localized page render with jinja2
|
|
122
|
+
# When querying the page
|
|
123
|
+
self.getPage("/localized", headers=list(headers.items()))
|
|
124
|
+
# Then the page return without error
|
|
125
|
+
self.assertStatus(200)
|
|
126
|
+
# Then the page is render dynamically using page context
|
|
127
|
+
if expected_lang == 'fr':
|
|
128
|
+
self.assertInBody('lang="fr"')
|
|
129
|
+
self.assertInBody('français')
|
|
130
|
+
self.assertInBody('Du texte à traduire')
|
|
131
|
+
self.assertInBody('mercredi 26 novembre 2025, 06:16:00 heure normale de l’Est nord-américain')
|
|
132
|
+
self.assertInBody('lundi, décembre 22, 2025')
|
|
133
|
+
else:
|
|
134
|
+
self.assertInBody('lang="en"')
|
|
135
|
+
self.assertInBody('English')
|
|
136
|
+
self.assertInBody('Some text to translate')
|
|
137
|
+
self.assertInBody('Wednesday, November 26, 2025, 6:16:00\u202fAM Eastern Standard Time')
|
|
138
|
+
self.assertInBody('Monday, December 22, 2025')
|
|
139
|
+
|
|
140
|
+
@skipUnless(HAS_JINJAX, reason='Required jinjax')
|
|
141
|
+
def test_get_page_i18n_selenium(self):
|
|
142
|
+
# Given a localized page render with jinja2
|
|
143
|
+
with self.selenium() as driver:
|
|
144
|
+
# When querying the page
|
|
145
|
+
driver.get(url_for('localized'))
|
|
146
|
+
# Then page load without error in english (enforced chronium lang)
|
|
147
|
+
self.assertFalse(driver.get_log('browser'))
|
|
148
|
+
self.assertEqual('en_US', driver.find_element('css selector', 'html').get_attribute('lang'))
|
|
149
|
+
# When user select a language
|
|
150
|
+
btn = driver.find_element('css selector', 'button[data-locale=fr]')
|
|
151
|
+
btn.click()
|
|
152
|
+
# Then page is reloaded with in French.
|
|
153
|
+
self.assertEqual('fr', driver.find_element('css selector', 'html').get_attribute('lang'))
|
|
@@ -0,0 +1,109 @@
|
|
|
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 tempfile
|
|
18
|
+
|
|
19
|
+
import cherrypy
|
|
20
|
+
from cherrypy.test import helper
|
|
21
|
+
|
|
22
|
+
from .. import ratelimit # noqa
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Root:
|
|
26
|
+
|
|
27
|
+
@cherrypy.expose
|
|
28
|
+
def index(self):
|
|
29
|
+
return "OK"
|
|
30
|
+
|
|
31
|
+
@cherrypy.expose
|
|
32
|
+
@cherrypy.tools.ratelimit(on=True, methods=['POST'])
|
|
33
|
+
def login(self):
|
|
34
|
+
return "login"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RateLimitTest(helper.CPWebCase):
|
|
38
|
+
interactive = False
|
|
39
|
+
rate_limit = 5
|
|
40
|
+
rate_limit_dir = None
|
|
41
|
+
|
|
42
|
+
def setUp(self):
|
|
43
|
+
cherrypy.tools.ratelimit.reset()
|
|
44
|
+
return super().setUp()
|
|
45
|
+
|
|
46
|
+
def tearDown(self):
|
|
47
|
+
cherrypy.tools.ratelimit.reset()
|
|
48
|
+
return super().tearDown()
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def setup_server(cls):
|
|
52
|
+
rate_limit_storage_class = None
|
|
53
|
+
if cls.rate_limit_dir:
|
|
54
|
+
rate_limit_storage_class = ratelimit.FileRateLimit
|
|
55
|
+
cherrypy.config.update(
|
|
56
|
+
{
|
|
57
|
+
'tools.ratelimit.debug': True,
|
|
58
|
+
'tools.ratelimit.delay': 3600,
|
|
59
|
+
'tools.ratelimit.limit': cls.rate_limit,
|
|
60
|
+
'tools.ratelimit.storage_class': rate_limit_storage_class,
|
|
61
|
+
'tools.ratelimit.storage_path': cls.rate_limit_dir,
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
cherrypy.tree.mount(Root(), '/')
|
|
65
|
+
|
|
66
|
+
def test_ratelimit(self):
|
|
67
|
+
# Given a endpoint with ratelimit enabled
|
|
68
|
+
# When requesting multiple time the page
|
|
69
|
+
for i in range(0, 5):
|
|
70
|
+
self.getPage('/login', method='POST')
|
|
71
|
+
self.assertStatus(200)
|
|
72
|
+
# Then a 429 error (too many request) is return
|
|
73
|
+
self.getPage('/login', method='POST')
|
|
74
|
+
self.assertStatus(429)
|
|
75
|
+
|
|
76
|
+
def test_ratelimit_forwarded_for(self):
|
|
77
|
+
# Given a endpoint with ratelimit enabled
|
|
78
|
+
# When requesting multiple time the login page with different `X-Forwarded-For`
|
|
79
|
+
for i in range(0, 5):
|
|
80
|
+
self.getPage(
|
|
81
|
+
'/login',
|
|
82
|
+
headers=[('X-Forwarded-For', '127.0.0.%s' % i)],
|
|
83
|
+
method='POST',
|
|
84
|
+
)
|
|
85
|
+
self.assertStatus(200)
|
|
86
|
+
# Then original IP get blocked
|
|
87
|
+
self.getPage(
|
|
88
|
+
'/login',
|
|
89
|
+
headers=[('X-Forwarded-For', '127.0.0.%s' % i)],
|
|
90
|
+
method='POST',
|
|
91
|
+
)
|
|
92
|
+
self.assertHeaderItemValue('X-Ratelimit-Limit', '5')
|
|
93
|
+
self.assertHeaderItemValue('X-Ratelimit-Remaining', '0')
|
|
94
|
+
self.assertHeader('X-Ratelimit-Reset')
|
|
95
|
+
self.assertStatus(429)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class FileStorageRateLimitTest(RateLimitTest):
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def setup_class(cls):
|
|
102
|
+
cls.tempdir = tempfile.TemporaryDirectory(prefix='cherrypy-foundation-', suffix='-ratelimit-test')
|
|
103
|
+
cls.rate_limit_dir = cls.tempdir.name
|
|
104
|
+
super().setup_class()
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def teardown_class(cls):
|
|
108
|
+
cls.tempdir.cleanup()
|
|
109
|
+
super().teardown_class()
|