cherrypy-foundation 1.0.0a1__py3-none-any.whl → 1.0.0a3__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 (33) hide show
  1. cherrypy_foundation/components/Field.jinja +14 -6
  2. cherrypy_foundation/components/Typeahead.css +6 -1
  3. cherrypy_foundation/components/Typeahead.jinja +2 -2
  4. cherrypy_foundation/components/__init__.py +1 -1
  5. cherrypy_foundation/error_page.py +18 -15
  6. cherrypy_foundation/plugins/db.py +34 -8
  7. cherrypy_foundation/plugins/ldap.py +28 -22
  8. cherrypy_foundation/plugins/scheduler.py +197 -84
  9. cherrypy_foundation/plugins/smtp.py +71 -45
  10. cherrypy_foundation/plugins/tests/test_db.py +2 -2
  11. cherrypy_foundation/plugins/tests/test_scheduler.py +58 -50
  12. cherrypy_foundation/plugins/tests/test_smtp.py +9 -6
  13. cherrypy_foundation/tests/templates/test_form.html +10 -0
  14. cherrypy_foundation/tests/test_form.py +119 -0
  15. cherrypy_foundation/tools/auth.py +28 -11
  16. cherrypy_foundation/tools/auth_mfa.py +10 -13
  17. cherrypy_foundation/tools/errors.py +1 -1
  18. cherrypy_foundation/tools/i18n.py +15 -6
  19. cherrypy_foundation/tools/jinja2.py +15 -12
  20. cherrypy_foundation/tools/ratelimit.py +5 -9
  21. cherrypy_foundation/tools/secure_headers.py +0 -4
  22. cherrypy_foundation/tools/tests/components/Button.jinja +2 -0
  23. cherrypy_foundation/tools/tests/templates/test_jinja2.html +10 -0
  24. cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +11 -0
  25. cherrypy_foundation/tools/tests/templates/test_jinjax.html +8 -0
  26. cherrypy_foundation/tools/tests/test_auth.py +1 -1
  27. cherrypy_foundation/tools/tests/test_auth_mfa.py +367 -0
  28. cherrypy_foundation/tools/tests/test_jinja2.py +123 -0
  29. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/METADATA +1 -1
  30. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/RECORD +33 -25
  31. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/WHEEL +0 -0
  32. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/licenses/LICENSE.md +0 -0
  33. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/top_level.txt +0 -0
@@ -63,12 +63,15 @@ class SmtpPluginTest(helper.CPWebCase):
63
63
 
64
64
  @skipUnless(hasattr(cherrypy, 'scheduler'), reason='Required scheduler')
65
65
  def test_queue_mail(self):
66
- # Given a paused scheduler plugin
67
- cherrypy.scheduler._scheduler.pause()
68
- # When queueing a email
69
- cherrypy.engine.publish('queue_mail', to='target@test.com', subject='subjet', message='body')
70
- # Then a new job get schedule
71
- self.assertEqual(1, len(cherrypy.scheduler.list_tasks()))
66
+ with mock.patch(smtp.__name__ + '.smtplib') as smtplib:
67
+ # Given a mail being queued.
68
+ cherrypy.engine.publish('queue_mail', to='target@test.com', subject='subjet', message='body')
69
+ # When waiting for all task to be processed
70
+ cherrypy.scheduler.wait_for_jobs()
71
+ # Then smtplib is called to send the mail.
72
+ smtplib.SMTP.assert_called_once_with('__default__', 25)
73
+ smtplib.SMTP.return_value.send_message.assert_called_once_with(mock.ANY)
74
+ smtplib.SMTP.return_value.quit.assert_called_once_with()
72
75
 
73
76
  def test_html2plaintext(self):
74
77
  """
@@ -0,0 +1,10 @@
1
+ <html>
2
+ <head>
3
+ <title>test-form</title>
4
+ </head>
5
+ <body>
6
+ <form method="post" action="/">
7
+ <Fields form={{ form }} />
8
+ </form>
9
+ </body>
10
+ </html>
@@ -0,0 +1,119 @@
1
+ # CherryPy Foundation
2
+ # Copyright (C) 2025 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
+ from wtforms.fields import BooleanField, PasswordField, StringField, SubmitField
22
+ from wtforms.validators import InputRequired, Length
23
+
24
+ import cherrypy_foundation.tools.jinja2 # noqa
25
+ from cherrypy_foundation.form import CherryForm
26
+ from cherrypy_foundation.tools.i18n import gettext_lazy as _
27
+
28
+ HAS_JINJAX = importlib.util.find_spec("jinjax") is not None
29
+
30
+ env = cherrypy.tools.jinja2.create_env(
31
+ package_name=__package__,
32
+ globals={
33
+ 'const1': 'STATIC VALUE',
34
+ },
35
+ )
36
+
37
+
38
+ class LoginForm(CherryForm):
39
+ login = StringField(
40
+ _('User'),
41
+ validators=[
42
+ InputRequired(),
43
+ Length(max=256, message=_('User too long.')),
44
+ ],
45
+ render_kw={
46
+ "placeholder": _('User'),
47
+ "autocorrect": "off",
48
+ "autocapitalize": "none",
49
+ "autocomplete": "off",
50
+ "autofocus": "autofocus",
51
+ },
52
+ )
53
+ password = PasswordField(
54
+ _('Password'),
55
+ validators=[
56
+ InputRequired(),
57
+ Length(max=256, message=_('Password too long.')),
58
+ ],
59
+ render_kw={"placeholder": _("Password")},
60
+ )
61
+ persistent = BooleanField(
62
+ _('Remember me'),
63
+ # All `label-*` are assigned to the label tag.
64
+ render_kw={'container_class': 'col-sm-6', 'label-attr': 'FOO'},
65
+ )
66
+ submit = SubmitField(
67
+ _('Login'),
68
+ # All `container-*` are assigned to the container tag.
69
+ render_kw={"class": "btn-primary float-end", 'container_class': 'col-sm-6', 'container-attr': 'BAR'},
70
+ )
71
+
72
+
73
+ @cherrypy.tools.jinja2(env=env)
74
+ class Root:
75
+
76
+ @cherrypy.expose
77
+ @cherrypy.tools.jinja2(template='test_form.html')
78
+ def index(self):
79
+ form = LoginForm()
80
+ return {'form': form}
81
+
82
+
83
+ @skipUnless(HAS_JINJAX, reason='Required jinjax')
84
+ class FormTest(helper.CPWebCase):
85
+ default_lang = None
86
+ interactive = False
87
+
88
+ @classmethod
89
+ def setup_server(cls):
90
+ cherrypy.tree.mount(Root(), '/')
91
+
92
+ def test_get_form(self):
93
+ # Given a form
94
+ # When querying the page that include this form
95
+ self.getPage("/")
96
+ self.assertStatus(200)
97
+ # Then each field is render properly.
98
+ # 1. Check title
99
+ self.assertInBody('test-form')
100
+ # 2. Check user field
101
+ self.assertInBody('<label class="form-label" for="login">User</label>')
102
+ self.assertInBody(
103
+ '<input autocapitalize="none" autocomplete="off" autocorrect="off" autofocus="autofocus" class="form-control" id="login" maxlength="256" name="login" placeholder="User" required type="text" value="">'
104
+ )
105
+ # 3. Check password
106
+ self.assertInBody('<label class="form-label" for="password">Password</label>')
107
+ self.assertInBody(
108
+ '<input class="form-control" id="password" maxlength="256" name="password" placeholder="Password" required type="password" value="">'
109
+ )
110
+ # 4 Check remember me
111
+ self.assertInBody(
112
+ '<input class="form-check-input" container-class="col-sm-6" id="persistent" label-attr="FOO" name="persistent" type="checkbox" value="y">'
113
+ )
114
+ self.assertInBody('<label attr="FOO" class="form-check-label" for="persistent">Remember me</label>')
115
+ # 5. check submit button
116
+ self.assertInBody('<div attr="BAR" class="mb-2 form-field col-sm-6">')
117
+ self.assertInBody(
118
+ '<input class="btn-primary float-end btn" container-attr="BAR" container-class="col-sm-6" id="submit" name="submit" type="submit" value="Login">'
119
+ )
@@ -29,9 +29,6 @@ AUTH_DEFAULT_SESSION_KEY = "_auth_session_key"
29
29
  AUTH_DEFAULT_REAUTH_TIMEOUT = 60 # minutes
30
30
 
31
31
 
32
- logger = logging.getLogger(__name__)
33
-
34
-
35
32
  class AuthManager(cherrypy.Tool):
36
33
  """
37
34
  CherryPy tool handling authentication.
@@ -102,7 +99,12 @@ class AuthManager(cherrypy.Tool):
102
99
  try:
103
100
  currentuser = user_from_key_func(user_key)
104
101
  except Exception:
105
- logger.exception('error while resolving user from key')
102
+ cherrypy.log(
103
+ f'unexpected error searching for user_key={user_key}',
104
+ context='AUTH',
105
+ severity=logging.ERROR,
106
+ traceback=True,
107
+ )
106
108
  currentuser = None
107
109
 
108
110
  if currentuser:
@@ -122,7 +124,7 @@ class AuthManager(cherrypy.Tool):
122
124
  Validate credentials with configured checkers; on success, call login_with_result.
123
125
  """
124
126
  if not login or not password:
125
- logger.warning('empty login or password provided')
127
+ cherrypy.log('authentication failed reason=empty_credentials', context='AUTH', severity=logging.WARNING)
126
128
  return None
127
129
  # Validate credentials using checkpassword function(s).
128
130
  conf = self._merged_args()
@@ -143,15 +145,23 @@ class AuthManager(cherrypy.Tool):
143
145
  login, user_info = login, None
144
146
  # If authentication is successful, initiate login process.
145
147
  return self.login_with_result(login=login, user_info=user_info)
146
- except Exception as e:
147
- logger.exception(
148
- 'unexpected error during authentication for [%s] with [%s]: %s', login, func.__qualname__, e
148
+ except Exception:
149
+ cherrypy.log(
150
+ f'unexpected error checking password login={login} checkpassword={func.__qualname__} - continue with next function',
151
+ context='AUTH',
152
+ severity=logging.ERROR,
153
+ traceback=True,
149
154
  )
150
155
  # If we reach here, authentication failed
151
156
  if hasattr(cherrypy.serving, 'session'):
152
157
  cherrypy.serving.session.regenerate() # Prevent session analysis
153
158
 
154
- logger.warning('Failed login attempt for user=%s from ip=%s', login, cherrypy.serving.request.remote.ip)
159
+ remote_ip = cherrypy.serving.request.remote.ip
160
+ cherrypy.log(
161
+ f'authentication failed login={login} ip={remote_ip} reason=wrong_credentials',
162
+ context='AUTH',
163
+ severity=logging.WARNING,
164
+ )
155
165
 
156
166
  return None
157
167
 
@@ -166,11 +176,18 @@ class AuthManager(cherrypy.Tool):
166
176
  try:
167
177
  user_key, userobj = user_lookup_func(login=login, user_info=user_info or {})
168
178
  except Exception:
169
- logger.exception('failed to lookup user object for login=%s', login)
179
+ cherrypy.log(
180
+ f"unexpected error searching user login={login} user_info={user_info}",
181
+ context='AUTH',
182
+ severity=logging.ERROR,
183
+ traceback=True,
184
+ )
170
185
  return None
171
186
 
172
187
  if not userobj:
173
- logger.warning('failed to lookup user object for login=%s', login)
188
+ cherrypy.log(
189
+ f"authentication failed login={login} reason=not_found", context='AUTH', severity=logging.WARNING
190
+ )
174
191
  return None
175
192
 
176
193
  # Notify plugins about user login
@@ -23,14 +23,12 @@ import cherrypy
23
23
 
24
24
  from ..passwd import check_password, hash_password
25
25
 
26
- logger = logging.getLogger(__name__)
27
-
28
26
  MFA_CODE = '_auth_mfa_code'
29
27
  MFA_CODE_TIME = '_auth_mfa_code_time'
30
28
  MFA_CODE_ATTEMPT = '_auth_mfa_code_attempt'
31
29
  MFA_REDIRECT_URL = '_auth_mfa_redirect_url'
32
30
  MFA_TRUSTED_IP_LIST = '_auth_mfa_trusted_ip_list'
33
- MFA_USERNAME = '_auth_mfa_username'
31
+ MFA_USER_KEY = '_auth_mfa_user_key'
34
32
  MFA_VERIFICATION_TIME = '_auth_mfa_time'
35
33
 
36
34
  MFA_DEFAULT_CODE_TIMEOUT = 10 # minutes
@@ -86,7 +84,7 @@ class CheckAuthMfa(cherrypy.Tool):
86
84
  length = self._code_length()
87
85
  code = ''.join(secrets.choice(string.digits) for _ in range(length))
88
86
  session = cherrypy.serving.session
89
- session[MFA_USERNAME] = cherrypy.request.login
87
+ session[MFA_USER_KEY] = cherrypy.request.login
90
88
  session[MFA_CODE] = hash_password(code)
91
89
  session[MFA_CODE_TIME] = session.now()
92
90
  session[MFA_CODE_ATTEMPT] = 0
@@ -99,7 +97,7 @@ class CheckAuthMfa(cherrypy.Tool):
99
97
 
100
98
  # Check if our username match current login user.
101
99
  session = cherrypy.serving.session
102
- if session.get(MFA_USERNAME) != cherrypy.request.login:
100
+ if session.get(MFA_USER_KEY) != cherrypy.request.login:
103
101
  return False
104
102
 
105
103
  # Check if the current IP is trusted
@@ -128,7 +126,7 @@ class CheckAuthMfa(cherrypy.Tool):
128
126
  return True
129
127
 
130
128
  session = cherrypy.serving.session
131
- if session.get(MFA_USERNAME) != cherrypy.request.login:
129
+ if session.get(MFA_USER_KEY) != cherrypy.request.login:
132
130
  return True
133
131
 
134
132
  # Check if code is defined.
@@ -208,11 +206,10 @@ class CheckAuthMfa(cherrypy.Tool):
208
206
  if is_expired or not code_valid:
209
207
  if not is_expired: # Only increment if not expired
210
208
  session[MFA_CODE_ATTEMPT] = attempts = int(session.get(MFA_CODE_ATTEMPT, 0)) + 1
211
- logger.warning(
212
- 'MFA code verification failed for user=%s from ip=%s, attempt=%d',
213
- cherrypy.request.login,
214
- cherrypy.request.remote.ip,
215
- attempts,
209
+ cherrypy.log(
210
+ f'verification failed user={cherrypy.request.login} ip={cherrypy.request.remote.ip} attempts={attempts}',
211
+ context='MFA',
212
+ severity=logging.WARNING,
216
213
  )
217
214
  return False
218
215
 
@@ -239,8 +236,8 @@ class CheckAuthMfa(cherrypy.Tool):
239
236
 
240
237
  # Rotate session id to prevent fixation
241
238
  session.regenerate()
242
- logger.info(
243
- 'Successful MFA verification for user=%s from ip=%s', cherrypy.request.login, cherrypy.request.remote.ip
239
+ cherrypy.log(
240
+ f'verification successful user={cherrypy.request.login} ip={cherrypy.request.remote.ip}', context='MFA'
244
241
  )
245
242
  return True
246
243
 
@@ -1,4 +1,4 @@
1
- # udb, A web interface to manage IT network
1
+ # Cherrypy Foundation
2
2
  # Copyright (C) 2025 IKUS Software inc.
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
@@ -105,6 +105,15 @@ from babel.support import LazyProxy, NullTranslations, Translations
105
105
  _current = threading.local()
106
106
 
107
107
 
108
+ def _get_config(key, default=None):
109
+ """
110
+ Lookup configuration from request, if available. Fallback to global config.
111
+ """
112
+ if getattr(cherrypy, 'request') and getattr(cherrypy.request, 'config') and key in cherrypy.request.config:
113
+ return cherrypy.request.config[key]
114
+ return cherrypy.config.get(key, default)
115
+
116
+
108
117
  @contextmanager
109
118
  def preferred_lang(lang):
110
119
  """
@@ -199,7 +208,7 @@ def get_timezone():
199
208
  return tzinfo
200
209
  # Otherwise search for a valid timezone.
201
210
  tzinfo = None
202
- default_timezone = cherrypy.config.get('tools.i18n.default_timezone')
211
+ default_timezone = _get_config('tools.i18n.default_timezone')
203
212
  preferred_timezone = getattr(_current, 'preferred_timezone', [default_timezone])
204
213
  for timezone in preferred_timezone:
205
214
  try:
@@ -226,10 +235,10 @@ def get_translation():
226
235
  # Otherwise, we need to search the translation.
227
236
  # `preferred_lang` should always has a sane value within a cherrypy request because of hooks
228
237
  # But we also need to support calls outside cherrypy.
229
- default = cherrypy.config.get('tools.i18n.default')
238
+ default = _get_config('tools.i18n.default')
230
239
  preferred_lang = getattr(_current, 'preferred_lang', [default])
231
- mo_dir = cherrypy.config.get('tools.i18n.mo_dir')
232
- domain = cherrypy.config.get('tools.i18n.domain')
240
+ mo_dir = _get_config('tools.i18n.mo_dir')
241
+ domain = _get_config('tools.i18n.domain')
233
242
  trans = _search_translation(mo_dir, domain, *preferred_lang)
234
243
  if trans is None:
235
244
  trans = NullTranslations()
@@ -242,8 +251,8 @@ def list_available_locales():
242
251
  """
243
252
  Return a list of available translations.
244
253
  """
245
- mo_dir = cherrypy.config.get('tools.i18n.mo_dir', False)
246
- domain = cherrypy.config.get('tools.i18n.domain')
254
+ mo_dir = _get_config('tools.i18n.mo_dir', False)
255
+ domain = _get_config('tools.i18n.domain')
247
256
  if not mo_dir:
248
257
  return
249
258
  for lang in os.listdir(mo_dir):
@@ -15,6 +15,7 @@
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
17
  import importlib
18
+ import logging
18
19
  import time
19
20
 
20
21
  import cherrypy
@@ -51,15 +52,15 @@ class Jinja2Tool(cherrypy.Tool):
51
52
 
52
53
  def wrap(*args, **kwargs):
53
54
  # Call original handler
54
- vars = self.oldhandler(*args, **kwargs)
55
+ context = self.oldhandler(*args, **kwargs)
55
56
  # Render template.
56
- return self.render_request(env=env, template=template, vars=vars, extra_processor=extra_processor)
57
+ return self.render_request(env=env, template=template, context=context, extra_processor=extra_processor)
57
58
 
58
59
  request = cherrypy.serving.request
59
60
  if request.handler is not None:
60
61
  # Replace request.handler with self
61
62
  if debug:
62
- cherrypy.log('Replacing request.handler', 'TOOLS.JINJA2')
63
+ cherrypy.log('replacing request handler', context='TOOLS.JINJA2', severity=logging.DEBUG)
63
64
  self.oldhandler = request.handler
64
65
  request.handler = wrap
65
66
 
@@ -79,12 +80,14 @@ class Jinja2Tool(cherrypy.Tool):
79
80
 
80
81
  # Enable translation if available
81
82
  if hasattr(cherrypy.tools, 'i18n'):
82
- from .i18n import format_datetime, get_language_name, ugettext, ungettext
83
+ from .i18n import format_date, format_datetime, get_language_name, get_translation, ugettext, ungettext
83
84
 
84
85
  env.add_extension('jinja2.ext.i18n')
85
86
  env.install_gettext_callables(ugettext, ungettext, newstyle=True)
87
+ env.filters['format_date'] = format_date
86
88
  env.filters['format_datetime'] = format_datetime
87
89
  env.globals['get_language_name'] = get_language_name
90
+ env.globals['get_translation'] = get_translation
88
91
 
89
92
  # Update globals, filters and tests
90
93
  env.globals.update(globals)
@@ -104,7 +107,7 @@ class Jinja2Tool(cherrypy.Tool):
104
107
 
105
108
  return env
106
109
 
107
- def render_request(self, template, vars={}, env=_UNDEFINED, extra_processor=_UNDEFINED):
110
+ def render_request(self, template, context={}, env=_UNDEFINED, extra_processor=_UNDEFINED):
108
111
  """
109
112
  Render template for a given cherrypy request.
110
113
  """
@@ -114,24 +117,24 @@ class Jinja2Tool(cherrypy.Tool):
114
117
  if extra_processor is _UNDEFINED:
115
118
  extra_processor = request.config.get('tools.jinja2.extra_processor')
116
119
  # Execute extra processor if defined.
117
- all_vars = {}
120
+ new_context = {}
118
121
  if extra_processor:
119
- all_vars.update(extra_processor())
120
- all_vars.update(vars)
122
+ new_context.update(extra_processor())
123
+ new_context.update(context)
121
124
  # Render templates
122
- return self.render(env=env, template=template, vars=all_vars)
125
+ return self.render(env=env, template=template, context=new_context)
123
126
 
124
- def render(self, env, template, vars={}):
127
+ def render(self, env, template, context={}):
125
128
  """
126
129
  Lower level function used to render a template using the given jinja2 environment, template(s) and variable context.
127
130
  """
128
131
  # Get the right templates
129
132
  if isinstance(template, (list, tuple)):
130
- names = [t.format(**vars) for t in template]
133
+ names = [t.format(**context) for t in template]
131
134
  tmpl = env.select_template(names)
132
135
  else:
133
136
  tmpl = env.get_template(template)
134
- out = tmpl.render(vars)
137
+ out = tmpl.render(context)
135
138
 
136
139
  # With JinjaX > 0.60 render explicitly here.
137
140
  if 'catalog' in env.globals and getattr(env.globals['catalog'], '_emit_assets_later', False):
@@ -146,6 +146,8 @@ class FileRateLimit(_DataStore):
146
146
 
147
147
 
148
148
  class Ratelimit(cherrypy.Tool):
149
+ CONTEXT = 'TOOLS.RATELIMIT'
150
+
149
151
  def __init__(self, priority=60):
150
152
  super().__init__('before_handler', self.check_ratelimit, 'ratelimit', priority)
151
153
 
@@ -182,10 +184,7 @@ class Ratelimit(cherrypy.Tool):
182
184
  request = cherrypy.request
183
185
  if methods is not None and request.method not in methods:
184
186
  if debug:
185
- cherrypy.log(
186
- 'skip rate limit for HTTP method %s' % (request.method,),
187
- 'TOOLS.RATELIMIT',
188
- )
187
+ cherrypy.log(f'skip rate limit for HTTP method {request.method}', context=self.CONTEXT)
189
188
  return
190
189
 
191
190
  # If datastore is not pass as configuration, create it for the first time.
@@ -213,10 +212,7 @@ class Ratelimit(cherrypy.Tool):
213
212
  # Get hits count using datastore.
214
213
  hits, timeout = datastore.get_and_increment(token, delay, hit)
215
214
  if debug:
216
- cherrypy.log(
217
- 'check and increase rate limit for scope %s, limit %s, hits %s' % (token, limit, hits),
218
- 'TOOLS.RATELIMIT',
219
- )
215
+ cherrypy.log(f'check and increase limit token={token} limit={limit} hits={hits}', context=self.CONTEXT)
220
216
 
221
217
  # Verify user has not exceeded rate limit
222
218
  remaining = max(0, limit - hits)
@@ -227,7 +223,7 @@ class Ratelimit(cherrypy.Tool):
227
223
  cherrypy.response.headers['X-RateLimit-Reset'] = str(timeout)
228
224
 
229
225
  if limit < hits: # block only after 'limit' successful requests
230
- cherrypy.log('ratelimit access to `%s`' % request.path_info, 'TOOLS.RATELIMIT')
226
+ cherrypy.log(f'block access to path_info={request.path_info}', context=self.CONTEXT)
231
227
  if logout:
232
228
  if hasattr(cherrypy.serving, 'session'):
233
229
  cherrypy.serving.session.clear()
@@ -15,13 +15,9 @@
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
17
  import http.cookies
18
- import logging
19
18
 
20
19
  import cherrypy
21
20
 
22
- # Define the logger
23
- logger = logging.getLogger(__name__)
24
-
25
21
  DEFAULT_CSP = {
26
22
  'default-src': 'self',
27
23
  'style-src': ('self', 'unsafe-inline'),
@@ -0,0 +1,2 @@
1
+ {# def label, link #}
2
+ <a {{ attrs.render(class='btn btn-primary', href=link) }}>{{ label }}</a>
@@ -0,0 +1,10 @@
1
+ <html>
2
+ <head>
3
+ <title>test-jinja2</title>
4
+ </head>
5
+ <body>
6
+ var1: {{ var1 }}<br/>
7
+ var2: {{ var2 }}<br/>
8
+ const1: {{ const1 }}<br/>
9
+ </body>
10
+ </html>
@@ -0,0 +1,11 @@
1
+ <html lang="{{ get_translation().locale }}">
2
+ <head>
3
+ <title>test-jinja2</title>
4
+ </head>
5
+ <body>
6
+ {{ get_language_name(get_translation().locale) }}<br/>
7
+ {% trans %}Some text to translate{% endtrans %}<br/>
8
+ {{ my_datetime | format_datetime(format='full') }}<br/>
9
+ {{ my_date | format_date(format='full') }}
10
+ </body>
11
+ </html>
@@ -0,0 +1,8 @@
1
+ <html>
2
+ <head>
3
+ <title>test-jinja2</title>
4
+ </head>
5
+ <body>
6
+ <Button link="http://example.com" label="foo" />
7
+ </body>
8
+ </html>
@@ -83,7 +83,7 @@ class AuthManagerTest(helper.CPWebCase):
83
83
  def test_auth_login(self):
84
84
  # Given unauthenticated user
85
85
  # When posting valid login
86
- self.getPage('/login/', method='POST', body=urlencode([('username', 'myuser'), ('password', 'changeme')]))
86
+ self.getPage('/login/', method='POST', body=urlencode({'username': 'myuser', 'password': 'changeme'}))
87
87
  # Then user is redirect to index.
88
88
  self.assertStatus(303)
89
89
  self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/')