datetime-local input fields are transformed in UTC server-side

This commit is contained in:
Éloi Rivard 2023-05-29 20:16:59 +02:00
parent f112583b09
commit c7f23e845c
12 changed files with 316 additions and 168 deletions

View file

@ -8,6 +8,7 @@ Added
- Implemented account expiration based on OpenLDAP ppolicy overlay. Needs OpenLDAP 2.5+
:issue:`13` :pr:`118`
- Timezone configuration entry. :issue:`137` :pr:`130`
Fixed
*****

View file

@ -1,8 +1,11 @@
import datetime
import math
import pytz
import wtforms
from canaille.app.i18n import DEFAULT_LANGUAGE_CODE
from canaille.app.i18n import locale_selector
from canaille.app.i18n import timezone_selector
from flask import abort
from flask import current_app
from flask import make_response
@ -81,3 +84,31 @@ class TableForm(I18NFormMixin, FlaskForm):
def validate_page(self, field):
if field.data < 1 or field.data > self.page_max:
raise wtforms.validators.ValidationError(_("The page number is not valid"))
class DateTimeUTCField(wtforms.DateTimeLocalField):
def _value(self):
if not self.data:
return ""
user_timezone = timezone_selector()
locale_dt = self.data.astimezone(user_timezone)
return locale_dt.strftime(self.format[0])
def process_formdata(self, valuelist):
if not valuelist:
return
date_str = " ".join(valuelist)
user_timezone = timezone_selector()
for format in self.strptime_format:
try:
unaware_dt = datetime.datetime.strptime(date_str, format)
locale_dt = user_timezone.localize(unaware_dt)
utc_dt = locale_dt.astimezone(pytz.utc)
self.data = utc_dt
return
except ValueError:
self.data = None
raise ValueError(self.gettext("Not a valid datetime value."))

View file

@ -1,6 +1,8 @@
import gettext
import pycountry
import pytz
from babel.dates import LOCALTZ
from flask import current_app
from flask import g
from flask import request
@ -13,7 +15,9 @@ babel = Babel()
def setup_i18n(app):
babel.init_app(app, locale_selector=locale_selector)
babel.init_app(
app, locale_selector=locale_selector, timezone_selector=timezone_selector
)
@app.before_request
def before_request():
@ -40,6 +44,13 @@ def locale_selector():
return request.accept_languages.best_match(available_language_codes)
def timezone_selector():
try:
return pytz.timezone(current_app.config.get("TIMEZONE"))
except pytz.exceptions.UnknownTimeZoneError:
return LOCALTZ
def native_language_name_from_code(code):
language = pycountry.languages.get(alpha_2=code[:2])
if code == DEFAULT_LANGUAGE_CODE:

View file

@ -25,6 +25,10 @@ SECRET_KEY = "change me before you go in production"
# If unset, language is detected
# LANGUAGE = "en"
# The timezone in which datetimes will be displayed to the users.
# If unset, the server timezone will be used.
# TIMEZONE = UTC
# If you have a sentry instance, you can set its dsn here:
# SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"

View file

@ -1,5 +1,6 @@
import wtforms.form
from canaille.app import models
from canaille.app.forms import DateTimeUTCField
from canaille.app.forms import HTMXBaseForm
from canaille.app.forms import HTMXForm
from canaille.app.forms import is_uri
@ -286,7 +287,7 @@ def profile_form(write_field_names, readonly_field_names, user=None):
del fields["groups"]
if current_app.backend.get().has_account_lockability(): # pragma: no branch
fields["lock_date"] = wtforms.DateTimeLocalField(
fields["lock_date"] = DateTimeUTCField(
_("Account expiration"),
validators=[wtforms.validators.Optional()],
format=[

View file

@ -25,6 +25,10 @@ FAVICON = "/static/img/canaille-c.png"
# If unset, language is detected
# LANGUAGE = "en"
# The timezone in which datetimes will be displayed to the users.
# If unset, the server timezone will be used.
# TIMEZONE = UTC
# If you have a sentry instance, you can set its dsn here:
# SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"

View file

@ -25,6 +25,10 @@ FAVICON = "/static/img/canaille-c.png"
# If unset, language is detected
# LANGUAGE = "en"
# The timezone in which datetimes will be displayed to the users.
# If unset, the server timezone will be used.
# TIMEZONE = UTC
# If you have a sentry instance, you can set its dsn here:
# SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"

View file

@ -34,6 +34,9 @@ Canaille is based on Flask, so any `flask configuration <https://flask.palletspr
:LANGUAGE:
*Optional.* The locale code of the language to use. If not set, the language of the browser will be used.
:TIMEZONE:
*Optional.* The timezone in which datetimes will be displayed to the users. If unset, the server timezone will be used.
:SENTRY_DSN:
*Optional.* A DSN to a sentry instance.
This needs the ``sentry_sdk`` python package to be installed.

233
poetry.lock generated
View file

@ -1,10 +1,9 @@
# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.5.0 and should not be changed by hand.
[[package]]
name = "aiosmtpd"
version = "1.4.4.post2"
description = "aiosmtpd - asyncio based SMTP server"
category = "dev"
optional = false
python-versions = "~=3.7"
files = [
@ -21,7 +20,6 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
name = "alabaster"
version = "0.7.13"
description = "A configurable sidebar-enabled Sphinx theme"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -33,7 +31,6 @@ files = [
name = "atpublic"
version = "3.1.1"
description = "Keep all y'all's __all__'s in sync"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -45,7 +42,6 @@ files = [
name = "attrs"
version = "23.1.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -67,7 +63,6 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
name = "authlib"
version = "1.2.0"
description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
category = "main"
optional = false
python-versions = "*"
files = [
@ -82,7 +77,6 @@ cryptography = ">=3.2"
name = "babel"
version = "2.12.1"
description = "Internationalization utilities"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -97,7 +91,6 @@ pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
name = "beautifulsoup4"
version = "4.12.2"
description = "Screen-scraping library"
category = "dev"
optional = false
python-versions = ">=3.6.0"
files = [
@ -116,7 +109,6 @@ lxml = ["lxml"]
name = "blinker"
version = "1.6.2"
description = "Fast, simple object-to-object and broadcast signaling"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -128,7 +120,6 @@ files = [
name = "certifi"
version = "2023.5.7"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -140,7 +131,6 @@ files = [
name = "cffi"
version = "1.15.1"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
python-versions = "*"
files = [
@ -217,7 +207,6 @@ pycparser = "*"
name = "cfgv"
version = "3.3.1"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
python-versions = ">=3.6.1"
files = [
@ -229,7 +218,6 @@ files = [
name = "charset-normalizer"
version = "3.1.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "dev"
optional = false
python-versions = ">=3.7.0"
files = [
@ -314,7 +302,6 @@ files = [
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -330,7 +317,6 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@ -340,63 +326,62 @@ files = [
[[package]]
name = "coverage"
version = "7.2.5"
version = "7.2.6"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "coverage-7.2.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:883123d0bbe1c136f76b56276074b0c79b5817dd4238097ffa64ac67257f4b6c"},
{file = "coverage-7.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fbc2a127e857d2f8898aaabcc34c37771bf78a4d5e17d3e1f5c30cd0cbc62a"},
{file = "coverage-7.2.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f3671662dc4b422b15776cdca89c041a6349b4864a43aa2350b6b0b03bbcc7f"},
{file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780551e47d62095e088f251f5db428473c26db7829884323e56d9c0c3118791a"},
{file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:066b44897c493e0dcbc9e6a6d9f8bbb6607ef82367cf6810d387c09f0cd4fe9a"},
{file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9a4ee55174b04f6af539218f9f8083140f61a46eabcaa4234f3c2a452c4ed11"},
{file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:706ec567267c96717ab9363904d846ec009a48d5f832140b6ad08aad3791b1f5"},
{file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ae453f655640157d76209f42c62c64c4d4f2c7f97256d3567e3b439bd5c9b06c"},
{file = "coverage-7.2.5-cp310-cp310-win32.whl", hash = "sha256:f81c9b4bd8aa747d417407a7f6f0b1469a43b36a85748145e144ac4e8d303cb5"},
{file = "coverage-7.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:dc945064a8783b86fcce9a0a705abd7db2117d95e340df8a4333f00be5efb64c"},
{file = "coverage-7.2.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40cc0f91c6cde033da493227797be2826cbf8f388eaa36a0271a97a332bfd7ce"},
{file = "coverage-7.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a66e055254a26c82aead7ff420d9fa8dc2da10c82679ea850d8feebf11074d88"},
{file = "coverage-7.2.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c10fbc8a64aa0f3ed136b0b086b6b577bc64d67d5581acd7cc129af52654384e"},
{file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a22cbb5ede6fade0482111fa7f01115ff04039795d7092ed0db43522431b4f2"},
{file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292300f76440651529b8ceec283a9370532f4ecba9ad67d120617021bb5ef139"},
{file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7ff8f3fb38233035028dbc93715551d81eadc110199e14bbbfa01c5c4a43f8d8"},
{file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a08c7401d0b24e8c2982f4e307124b671c6736d40d1c39e09d7a8687bddf83ed"},
{file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef9659d1cda9ce9ac9585c045aaa1e59223b143f2407db0eaee0b61a4f266fb6"},
{file = "coverage-7.2.5-cp311-cp311-win32.whl", hash = "sha256:30dcaf05adfa69c2a7b9f7dfd9f60bc8e36b282d7ed25c308ef9e114de7fc23b"},
{file = "coverage-7.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:97072cc90f1009386c8a5b7de9d4fc1a9f91ba5ef2146c55c1f005e7b5c5e068"},
{file = "coverage-7.2.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bebea5f5ed41f618797ce3ffb4606c64a5de92e9c3f26d26c2e0aae292f015c1"},
{file = "coverage-7.2.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828189fcdda99aae0d6bf718ea766b2e715eabc1868670a0a07bf8404bf58c33"},
{file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e8a95f243d01ba572341c52f89f3acb98a3b6d1d5d830efba86033dd3687ade"},
{file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8834e5f17d89e05697c3c043d3e58a8b19682bf365048837383abfe39adaed5"},
{file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1f25ee9de21a39b3a8516f2c5feb8de248f17da7eead089c2e04aa097936b47"},
{file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1637253b11a18f453e34013c665d8bf15904c9e3c44fbda34c643fbdc9d452cd"},
{file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8e575a59315a91ccd00c7757127f6b2488c2f914096077c745c2f1ba5b8c0969"},
{file = "coverage-7.2.5-cp37-cp37m-win32.whl", hash = "sha256:509ecd8334c380000d259dc66feb191dd0a93b21f2453faa75f7f9cdcefc0718"},
{file = "coverage-7.2.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12580845917b1e59f8a1c2ffa6af6d0908cb39220f3019e36c110c943dc875b0"},
{file = "coverage-7.2.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5016e331b75310610c2cf955d9f58a9749943ed5f7b8cfc0bb89c6134ab0a84"},
{file = "coverage-7.2.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:373ea34dca98f2fdb3e5cb33d83b6d801007a8074f992b80311fc589d3e6b790"},
{file = "coverage-7.2.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a063aad9f7b4c9f9da7b2550eae0a582ffc7623dca1c925e50c3fbde7a579771"},
{file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38c0a497a000d50491055805313ed83ddba069353d102ece8aef5d11b5faf045"},
{file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b3b05e22a77bb0ae1a3125126a4e08535961c946b62f30985535ed40e26614"},
{file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0342a28617e63ad15d96dca0f7ae9479a37b7d8a295f749c14f3436ea59fdcb3"},
{file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf97ed82ca986e5c637ea286ba2793c85325b30f869bf64d3009ccc1a31ae3fd"},
{file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2c41c1b1866b670573657d584de413df701f482574bad7e28214a2362cb1fd1"},
{file = "coverage-7.2.5-cp38-cp38-win32.whl", hash = "sha256:10b15394c13544fce02382360cab54e51a9e0fd1bd61ae9ce012c0d1e103c813"},
{file = "coverage-7.2.5-cp38-cp38-win_amd64.whl", hash = "sha256:a0b273fe6dc655b110e8dc89b8ec7f1a778d78c9fd9b4bda7c384c8906072212"},
{file = "coverage-7.2.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c587f52c81211d4530fa6857884d37f514bcf9453bdeee0ff93eaaf906a5c1b"},
{file = "coverage-7.2.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4436cc9ba5414c2c998eaedee5343f49c02ca93b21769c5fdfa4f9d799e84200"},
{file = "coverage-7.2.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6599bf92f33ab041e36e06d25890afbdf12078aacfe1f1d08c713906e49a3fe5"},
{file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:857abe2fa6a4973f8663e039ead8d22215d31db613ace76e4a98f52ec919068e"},
{file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f5cab2d7f0c12f8187a376cc6582c477d2df91d63f75341307fcdcb5d60303"},
{file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aa387bd7489f3e1787ff82068b295bcaafbf6f79c3dad3cbc82ef88ce3f48ad3"},
{file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:156192e5fd3dbbcb11cd777cc469cf010a294f4c736a2b2c891c77618cb1379a"},
{file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd3b4b8175c1db502adf209d06136c000df4d245105c8839e9d0be71c94aefe1"},
{file = "coverage-7.2.5-cp39-cp39-win32.whl", hash = "sha256:ddc5a54edb653e9e215f75de377354e2455376f416c4378e1d43b08ec50acc31"},
{file = "coverage-7.2.5-cp39-cp39-win_amd64.whl", hash = "sha256:338aa9d9883aaaad53695cb14ccdeb36d4060485bb9388446330bef9c361c252"},
{file = "coverage-7.2.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:8877d9b437b35a85c18e3c6499b23674684bf690f5d96c1006a1ef61f9fdf0f3"},
{file = "coverage-7.2.5.tar.gz", hash = "sha256:f99ef080288f09ffc687423b8d60978cf3a465d3f404a18d1a05474bd8575a47"},
{file = "coverage-7.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:496b86f1fc9c81a1cd53d8842ef712e950a4611bba0c42d33366a7b91ba969ec"},
{file = "coverage-7.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbe6e8c0a9a7193ba10ee52977d4d5e7652957c1f56ccefed0701db8801a2a3b"},
{file = "coverage-7.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d06b721c2550c01a60e5d3093f417168658fb454e5dfd9a23570e9bffe39a1"},
{file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77a04b84d01f0e12c66f16e69e92616442dc675bbe51b90bfb074b1e5d1c7fbd"},
{file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35db06450272473eab4449e9c2ad9bc6a0a68dab8e81a0eae6b50d9c2838767e"},
{file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6727a0d929ff0028b1ed8b3e7f8701670b1d7032f219110b55476bb60c390bfb"},
{file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aac1d5fdc5378f6bac2c0c7ebe7635a6809f5b4376f6cf5d43243c1917a67087"},
{file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c9e4a5eb1bbc3675ee57bc31f8eea4cd7fb0cbcbe4912cf1cb2bf3b754f4a80"},
{file = "coverage-7.2.6-cp310-cp310-win32.whl", hash = "sha256:71f739f97f5f80627f1fee2331e63261355fd1e9a9cce0016394b6707ac3f4ec"},
{file = "coverage-7.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:fde5c7a9d9864d3e07992f66767a9817f24324f354caa3d8129735a3dc74f126"},
{file = "coverage-7.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc7b667f8654376e9353dd93e55e12ce2a59fb6d8e29fce40de682273425e044"},
{file = "coverage-7.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:697f4742aa3f26c107ddcb2b1784a74fe40180014edbd9adaa574eac0529914c"},
{file = "coverage-7.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:541280dde49ce74a4262c5e395b48ea1207e78454788887118c421cb4ffbfcac"},
{file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7f1a8328eeec34c54f1d5968a708b50fc38d31e62ca8b0560e84a968fbf9a9"},
{file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bbd58eb5a2371bf160590f4262109f66b6043b0b991930693134cb617bc0169"},
{file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae82c5f168d2a39a5d69a12a69d4dc23837a43cf2ca99be60dfe59996ea6b113"},
{file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f5440cdaf3099e7ab17a5a7065aed59aff8c8b079597b61c1f8be6f32fe60636"},
{file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6f03f87fea579d55e0b690d28f5042ec1368650466520fbc400e7aeaf09e995"},
{file = "coverage-7.2.6-cp311-cp311-win32.whl", hash = "sha256:dc4d5187ef4d53e0d4c8eaf530233685667844c5fb0b855fea71ae659017854b"},
{file = "coverage-7.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:c93d52c3dc7b9c65e39473704988602300e3cc1bad08b5ab5b03ca98bbbc68c1"},
{file = "coverage-7.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42c692b55a647a832025a4c048007034fe77b162b566ad537ce65ad824b12a84"},
{file = "coverage-7.2.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7786b2fa7809bf835f830779ad285215a04da76293164bb6745796873f0942d"},
{file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25bad4196104761bc26b1dae9b57383826542ec689ff0042f7f4f4dd7a815cba"},
{file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2692306d3d4cb32d2cceed1e47cebd6b1d2565c993d6d2eda8e6e6adf53301e6"},
{file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:392154d09bd4473b9d11351ab5d63391f3d5d24d752f27b3be7498b0ee2b5226"},
{file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fa079995432037b5e2ef5ddbb270bcd2ded9f52b8e191a5de11fe59a00ea30d8"},
{file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d712cefff15c712329113b01088ba71bbcef0f7ea58478ca0bbec63a824844cb"},
{file = "coverage-7.2.6-cp37-cp37m-win32.whl", hash = "sha256:004948e296149644d208964300cb3d98affc5211e9e490e9979af4030b0d6473"},
{file = "coverage-7.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:c1d7a31603c3483ac49c1726723b0934f88f2c011c660e6471e7bd735c2fa110"},
{file = "coverage-7.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3436927d1794fa6763b89b60c896f9e3bd53212001026ebc9080d23f0c2733c1"},
{file = "coverage-7.2.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44c9b9f1a245f3d0d202b1a8fa666a80b5ecbe4ad5d0859c0fb16a52d9763224"},
{file = "coverage-7.2.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3783a286d5a93a2921396d50ce45a909aa8f13eee964465012f110f0cbb611"},
{file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cff6980fe7100242170092bb40d2b1cdad79502cd532fd26b12a2b8a5f9aee0"},
{file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c534431153caffc7c495c3eddf7e6a6033e7f81d78385b4e41611b51e8870446"},
{file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3062fd5c62df988cea9f2972c593f77fed1182bfddc5a3b12b1e606cb7aba99e"},
{file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6284a2005e4f8061c58c814b1600ad0074ccb0289fe61ea709655c5969877b70"},
{file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:97729e6828643f168a2a3f07848e1b1b94a366b13a9f5aba5484c2215724edc8"},
{file = "coverage-7.2.6-cp38-cp38-win32.whl", hash = "sha256:dc11b42fa61ff1e788dd095726a0aed6aad9c03d5c5984b54cb9e1e67b276aa5"},
{file = "coverage-7.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:cbcc874f454ee51f158afd604a315f30c0e31dff1d5d5bf499fc529229d964dd"},
{file = "coverage-7.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d3cacc6a665221108ecdf90517a8028d07a2783df3417d12dcfef1c517e67478"},
{file = "coverage-7.2.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:272ab31228a9df857ab5df5d67936d8861464dc89c5d3fab35132626e9369379"},
{file = "coverage-7.2.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a8723ccec4e564d4b9a79923246f7b9a8de4ec55fa03ec4ec804459dade3c4f"},
{file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5906f6a84b47f995cd1bf0aca1c72d591c55ee955f98074e93660d64dfc66eb9"},
{file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c139b7ab3f0b15f9aad0a3fedef5a1f8c0b2bdc291d88639ca2c97d3682416"},
{file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a5ffd45c6b93c23a8507e2f436983015c6457aa832496b6a095505ca2f63e8f1"},
{file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4f3c7c19581d471af0e9cb49d928172cd8492cd78a2b7a4e82345d33662929bb"},
{file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e8c0e79820cdd67978e1120983786422d279e07a381dbf89d03bbb23ec670a6"},
{file = "coverage-7.2.6-cp39-cp39-win32.whl", hash = "sha256:13cde6bb0e58fb67d09e2f373de3899d1d1e866c5a9ff05d93615f2f54fbd2bb"},
{file = "coverage-7.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:6b9f64526286255735847aed0221b189486e0b9ed943446936e41b7e44b08783"},
{file = "coverage-7.2.6-pp37.pp38.pp39-none-any.whl", hash = "sha256:6babcbf1e66e46052442f10833cfc4a0d3554d8276aa37af8531a83ed3c1a01d"},
{file = "coverage-7.2.6.tar.gz", hash = "sha256:2025f913f2edb0272ef15d00b1f335ff8908c921c8eb2013536fcaf61f5a683d"},
]
[package.dependencies]
@ -409,7 +394,6 @@ toml = ["tomli"]
name = "cryptography"
version = "39.0.2"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -455,7 +439,6 @@ tox = ["tox"]
name = "cryptography"
version = "40.0.2"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -497,7 +480,6 @@ tox = ["tox"]
name = "cssselect"
version = "1.2.0"
description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -509,7 +491,6 @@ files = [
name = "distlib"
version = "0.3.6"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -521,7 +502,6 @@ files = [
name = "dnspython"
version = "2.3.0"
description = "DNS toolkit"
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
files = [
@ -542,7 +522,6 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"]
name = "docutils"
version = "0.18.1"
description = "Docutils -- Python Documentation Utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
@ -554,7 +533,6 @@ files = [
name = "email-validator"
version = "2.0.0.post2"
description = "A robust email address syntax and deliverability validation library."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -570,7 +548,6 @@ idna = ">=2.0.0"
name = "exceptiongroup"
version = "1.1.1"
description = "Backport of PEP 654 (exception groups)"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -585,7 +562,6 @@ test = ["pytest (>=6)"]
name = "faker"
version = "18.9.0"
description = "Faker is a Python package that generates fake data for you."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -601,7 +577,6 @@ typing-extensions = {version = ">=3.10.0.1", markers = "python_version < \"3.8\"
name = "fancycompleter"
version = "0.9.1"
description = "colorful TAB completion for Python prompt"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -617,7 +592,6 @@ pyrepl = ">=0.8.2"
name = "filelock"
version = "3.12.0"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -633,7 +607,6 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "p
name = "flask"
version = "2.2.5"
description = "A simple framework for building complex web applications."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -656,7 +629,6 @@ dotenv = ["python-dotenv"]
name = "flask-babel"
version = "3.1.0"
description = "Adds i18n/l10n support for Flask applications."
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
files = [
@ -674,7 +646,6 @@ pytz = ">=2022.7"
name = "flask-themer"
version = "1.4.3"
description = "Simple theme mechanism for Flask"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -689,7 +660,6 @@ flask = "*"
name = "flask-webtest"
version = "0.1.3"
description = "Utilities for testing Flask applications with WebTest."
category = "dev"
optional = false
python-versions = "*"
files = [
@ -709,7 +679,6 @@ tests = ["flask-sqlalchemy"]
name = "flask-wtf"
version = "1.1.1"
description = "Form rendering, validation, and CSRF protection for Flask with WTForms."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -729,7 +698,6 @@ email = ["email-validator"]
name = "freezegun"
version = "1.2.2"
description = "Let your Python tests travel through time"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -744,7 +712,6 @@ python-dateutil = ">=2.7"
name = "honcho"
version = "1.1.0"
description = "Honcho: a Python clone of Foreman. For managing Procfile-based applications."
category = "dev"
optional = false
python-versions = "*"
files = [
@ -762,7 +729,6 @@ export = ["jinja2 (>=2.7,<3)"]
name = "identify"
version = "2.5.24"
description = "File identification library for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -777,7 +743,6 @@ license = ["ukkonen"]
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@ -789,7 +754,6 @@ files = [
name = "imagesize"
version = "1.4.1"
description = "Getting image size from png/jpeg/jpeg2000/gif file"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@ -801,7 +765,6 @@ files = [
name = "importlib-metadata"
version = "6.6.0"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -822,7 +785,6 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -834,7 +796,6 @@ files = [
name = "itsdangerous"
version = "2.1.2"
description = "Safely pass data to untrusted environments and back."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -846,7 +807,6 @@ files = [
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -864,7 +824,6 @@ i18n = ["Babel (>=2.7)"]
name = "lxml"
version = "4.9.2"
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
files = [
@ -957,7 +916,6 @@ source = ["Cython (>=0.29.7)"]
name = "markupsafe"
version = "2.1.2"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1017,7 +975,6 @@ files = [
name = "mock"
version = "5.0.2"
description = "Rolling backport of unittest.mock for all Pythons"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -1034,7 +991,6 @@ test = ["pytest", "pytest-cov"]
name = "nodeenv"
version = "1.8.0"
description = "Node.js virtual environment builder"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
@ -1049,7 +1005,6 @@ setuptools = "*"
name = "packaging"
version = "23.1"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1061,7 +1016,6 @@ files = [
name = "pdbpp"
version = "0.10.3"
description = "pdb++, a drop-in replacement for pdb"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1082,7 +1036,6 @@ testing = ["funcsigs", "pytest"]
name = "platformdirs"
version = "3.5.1"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1101,7 +1054,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -1120,7 +1072,6 @@ testing = ["pytest", "pytest-benchmark"]
name = "portpicker"
version = "1.5.2"
description = "A library to choose unique available network ports."
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -1135,7 +1086,6 @@ psutil = "*"
name = "pre-commit"
version = "2.21.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1155,7 +1105,6 @@ virtualenv = ">=20.10.0"
name = "psutil"
version = "5.9.5"
description = "Cross-platform lib for process and system monitoring in Python."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@ -1182,7 +1131,6 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
name = "pyasn1"
version = "0.5.0"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
@ -1194,7 +1142,6 @@ files = [
name = "pyasn1-modules"
version = "0.3.0"
description = "A collection of ASN.1-based protocols modules"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
@ -1209,7 +1156,6 @@ pyasn1 = ">=0.4.6,<0.6.0"
name = "pycountry"
version = "22.3.5"
description = "ISO country, subdivision, language, currency and script definitions and their translations"
category = "main"
optional = false
python-versions = ">=3.6, <4"
files = [
@ -1223,7 +1169,6 @@ setuptools = "*"
name = "pycparser"
version = "2.21"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@ -1235,7 +1180,6 @@ files = [
name = "pygments"
version = "2.15.1"
description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1250,7 +1194,6 @@ plugins = ["importlib-metadata"]
name = "pyquery"
version = "2.0.0"
description = "A jquery-like library for python"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1269,7 +1212,6 @@ test = ["pytest", "pytest-cov", "requests", "webob", "webtest"]
name = "pyreadline"
version = "2.1"
description = "A python implmementation of GNU readline."
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1280,7 +1222,6 @@ files = [
name = "pyrepl"
version = "0.9.0"
description = "A library for building flexible command line interfaces"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1291,7 +1232,6 @@ files = [
name = "pytest"
version = "7.3.1"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1313,14 +1253,13 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
[[package]]
name = "pytest-cov"
version = "4.0.0"
version = "4.1.0"
description = "Pytest plugin for measuring coverage."
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
files = [
{file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
{file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
{file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
{file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
]
[package.dependencies]
@ -1334,7 +1273,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
name = "pytest-cover"
version = "3.0.0"
description = "Pytest plugin for measuring coverage. Forked from `pytest-cov`."
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1349,7 +1287,6 @@ pytest-cov = ">=2.0"
name = "pytest-coverage"
version = "0.0"
description = "Pytest plugin for measuring coverage. Forked from `pytest-cov`."
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1364,7 +1301,6 @@ pytest-cover = "*"
name = "pytest-flask"
version = "1.2.0"
description = "A set of py.test fixtures to test Flask applications."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -1384,7 +1320,6 @@ docs = ["Sphinx", "sphinx-rtd-theme"]
name = "pytest-httpserver"
version = "1.0.6"
description = "pytest-httpserver is a httpserver for pytest"
category = "dev"
optional = false
python-versions = ">=3.7,<4.0"
files = [
@ -1399,7 +1334,6 @@ Werkzeug = ">=2.0.0"
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
@ -1414,7 +1348,6 @@ six = ">=1.5"
name = "python-ldap"
version = "3.4.3"
description = "Python modules for implementing LDAP clients"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -1429,7 +1362,6 @@ pyasn1_modules = ">=0.1.5"
name = "pytz"
version = "2023.3"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
files = [
@ -1441,7 +1373,6 @@ files = [
name = "pyyaml"
version = "6.0"
description = "YAML parser and emitter for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -1489,14 +1420,13 @@ files = [
[[package]]
name = "requests"
version = "2.30.0"
version = "2.31.0"
description = "Python HTTP for Humans."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "requests-2.30.0-py3-none-any.whl", hash = "sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294"},
{file = "requests-2.30.0.tar.gz", hash = "sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4"},
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
]
[package.dependencies]
@ -1513,7 +1443,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
name = "sentry-sdk"
version = "1.21.1"
description = "Python client for Sentry (https://sentry.io)"
category = "main"
optional = true
python-versions = "*"
files = [
@ -1557,7 +1486,6 @@ tornado = ["tornado (>=5)"]
name = "setuptools"
version = "67.8.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1574,7 +1502,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@ -1586,7 +1513,6 @@ files = [
name = "slapd"
version = "0.1.3"
description = "Controls a slapd process in a pythonic way"
category = "dev"
optional = false
python-versions = ">=3.7,<4"
files = [
@ -1598,7 +1524,6 @@ files = [
name = "smtpdfix"
version = "0.5.0"
description = "A SMTP server for use as a pytest fixture that implements encryption and authentication for testing."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1623,7 +1548,6 @@ typing = ["mypy"]
name = "snowballstemmer"
version = "2.2.0"
description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1635,7 +1559,6 @@ files = [
name = "soupsieve"
version = "2.4.1"
description = "A modern CSS selector implementation for Beautiful Soup."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1647,7 +1570,6 @@ files = [
name = "sphinx"
version = "5.3.0"
description = "Python documentation generator"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -1683,7 +1605,6 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"]
name = "sphinx-issues"
version = "3.0.1"
description = "A Sphinx extension for linking to your project's issue tracker"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -1701,14 +1622,13 @@ tests = ["pytest (>=6.2.0)"]
[[package]]
name = "sphinx-rtd-theme"
version = "1.2.0"
version = "1.2.1"
description = "Read the Docs theme for Sphinx"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
{file = "sphinx_rtd_theme-1.2.0-py2.py3-none-any.whl", hash = "sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2"},
{file = "sphinx_rtd_theme-1.2.0.tar.gz", hash = "sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8"},
{file = "sphinx_rtd_theme-1.2.1-py2.py3-none-any.whl", hash = "sha256:2cc9351176cbf91944ce44cefd4fab6c3b76ac53aa9e15d6db45a3229ad7f866"},
{file = "sphinx_rtd_theme-1.2.1.tar.gz", hash = "sha256:cf9a7dc0352cf179c538891cb28d6fad6391117d4e21c891776ab41dd6c8ff70"},
]
[package.dependencies]
@ -1723,7 +1643,6 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"]
name = "sphinxcontrib-applehelp"
version = "1.0.2"
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -1739,7 +1658,6 @@ test = ["pytest"]
name = "sphinxcontrib-devhelp"
version = "1.0.2"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -1755,7 +1673,6 @@ test = ["pytest"]
name = "sphinxcontrib-htmlhelp"
version = "2.0.0"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -1771,7 +1688,6 @@ test = ["html5lib", "pytest"]
name = "sphinxcontrib-jquery"
version = "4.1"
description = "Extension to include jQuery on newer Sphinx releases"
category = "dev"
optional = false
python-versions = ">=2.7"
files = [
@ -1786,7 +1702,6 @@ Sphinx = ">=1.8"
name = "sphinxcontrib-jsmath"
version = "1.0.1"
description = "A sphinx extension which renders display math in HTML via JavaScript"
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -1801,7 +1716,6 @@ test = ["flake8", "mypy", "pytest"]
name = "sphinxcontrib-qthelp"
version = "1.0.3"
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -1817,7 +1731,6 @@ test = ["pytest"]
name = "sphinxcontrib-serializinghtml"
version = "1.1.5"
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -1833,7 +1746,6 @@ test = ["pytest"]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@ -1845,7 +1757,6 @@ files = [
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1855,21 +1766,19 @@ files = [
[[package]]
name = "typing-extensions"
version = "4.5.0"
version = "4.6.2"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
{file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
{file = "typing_extensions-4.6.2-py3-none-any.whl", hash = "sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"},
{file = "typing_extensions-4.6.2.tar.gz", hash = "sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c"},
]
[[package]]
name = "urllib3"
version = "2.0.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1887,7 +1796,6 @@ zstd = ["zstandard (>=0.18.0)"]
name = "virtualenv"
version = "20.23.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1909,7 +1817,6 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess
name = "waitress"
version = "2.1.2"
description = "Waitress WSGI server"
category = "dev"
optional = false
python-versions = ">=3.7.0"
files = [
@ -1925,7 +1832,6 @@ testing = ["coverage (>=5.0)", "pytest", "pytest-cover"]
name = "webob"
version = "1.8.7"
description = "WSGI request and response object"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*"
files = [
@ -1941,7 +1847,6 @@ testing = ["coverage", "pytest (>=3.1.0)", "pytest-cov", "pytest-xdist"]
name = "webtest"
version = "3.0.0"
description = "Helper to test WSGI applications"
category = "dev"
optional = false
python-versions = ">=3.6, <4"
files = [
@ -1962,7 +1867,6 @@ tests = ["PasteDeploy", "WSGIProxy2", "coverage", "pyquery", "pytest", "pytest-c
name = "werkzeug"
version = "2.2.3"
description = "The comprehensive WSGI web application library."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1980,7 +1884,6 @@ watchdog = ["watchdog"]
name = "wmctrl"
version = "0.4"
description = "A tool to programmatically control windows inside X"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1991,7 +1894,6 @@ files = [
name = "wtforms"
version = "3.0.1"
description = "Form validation and rendering for Python web development."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -2009,7 +1911,6 @@ email = ["email-validator"]
name = "zipp"
version = "3.15.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -2027,4 +1928,4 @@ sentry = ["sentry-sdk"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.7, <4"
content-hash = "ba916dfc7340b0327fba0582d2153de589525f77820e411afc10b2fd19060c22"
content-hash = "ea6edd79777591d8c6232b5cd2d712a4de68e72475aca7b7c6417328eac5ea61"

View file

@ -46,6 +46,7 @@ flask-themer = "<2"
flask-wtf = "<2"
pycountry = "^22.3.5"
python-ldap = "<4"
pytz = "^2023.3"
toml = "<1"
wtforms = "<4"

186
tests/app/test_forms.py Normal file
View file

@ -0,0 +1,186 @@
import datetime
import wtforms
from babel.dates import LOCALTZ
from canaille.app.forms import DateTimeUTCField
from flask import current_app
from werkzeug.datastructures import ImmutableMultiDict
def test_datetime_utc_field_no_timezone_is_local_timezone(testclient):
del current_app.config["TIMEZONE"]
offset = LOCALTZ.utcoffset(datetime.datetime.utcnow())
class TestForm(wtforms.Form):
dt = DateTimeUTCField()
form = TestForm()
form.validate()
assert form.dt.data is None
utc_date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc)
locale_date = datetime.datetime(2023, 6, 1, 12) + offset
rendered_locale_date = locale_date.strftime("%Y-%m-%d %H:%M:%S")
rendered_locale_date_form = locale_date.strftime("%Y-%m-%d %H:%M:%S")
request_form = ImmutableMultiDict({"dt": rendered_locale_date_form})
form = TestForm(request_form)
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date_form}">'
)
form = TestForm(data={"dt": utc_date})
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)
class Foobar:
dt = utc_date
form = TestForm(obj=Foobar())
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)
def test_datetime_utc_field_utc(testclient):
current_app.config["TIMEZONE"] = "UTC"
class TestForm(wtforms.Form):
dt = DateTimeUTCField()
form = TestForm()
form.validate()
assert form.dt.data is None
date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc)
rendered_date = date.strftime("%Y-%m-%d %H:%M:%S")
rendered_date_form = date.strftime("%Y-%m-%d %H:%M:%S")
request_form = ImmutableMultiDict({"dt": rendered_date_form})
form = TestForm(request_form)
assert form.validate()
assert form.dt.data == date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_date_form}">'
)
form = TestForm(data={"dt": date})
assert form.validate()
assert form.dt.data == date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_date}">'
)
class Foobar:
dt = date
form = TestForm(obj=Foobar())
assert form.validate()
assert form.dt.data == date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_date}">'
)
def test_datetime_utc_field_japan_timezone(testclient):
current_app.config["TIMEZONE"] = "Japan"
class TestForm(wtforms.Form):
dt = DateTimeUTCField()
form = TestForm()
form.validate()
assert form.dt.data is None
utc_date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc)
locale_date = datetime.datetime(2023, 6, 1, 21)
rendered_locale_date = locale_date.strftime("%Y-%m-%d %H:%M:%S")
rendered_locale_date_form = locale_date.strftime("%Y-%m-%d %H:%M:%S")
request_form = ImmutableMultiDict({"dt": rendered_locale_date_form})
form = TestForm(request_form)
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date_form}">'
)
form = TestForm(data={"dt": utc_date})
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)
class Foobar:
dt = utc_date
form = TestForm(obj=Foobar())
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)
def test_datetime_utc_field_invalid_timezone(testclient):
current_app.config["TIMEZONE"] = "invalid"
offset = LOCALTZ.utcoffset(datetime.datetime.utcnow())
class TestForm(wtforms.Form):
dt = DateTimeUTCField()
form = TestForm()
form.validate()
assert form.dt.data is None
utc_date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc)
locale_date = datetime.datetime(2023, 6, 1, 12) + offset
rendered_locale_date = locale_date.strftime("%Y-%m-%d %H:%M:%S")
rendered_locale_date_form = locale_date.strftime("%Y-%m-%d %H:%M:%S")
request_form = ImmutableMultiDict({"dt": rendered_locale_date_form})
form = TestForm(request_form)
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date_form}">'
)
form = TestForm(data={"dt": utc_date})
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)
class Foobar:
dt = utc_date
form = TestForm(obj=Foobar())
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)

View file

@ -76,6 +76,7 @@ def configuration(slapd_server, smtpd):
conf = {
"SECRET_KEY": gen_salt(24),
"LOGO": "/static/img/canaille-head.png",
"TIMEZONE": "UTC",
"BACKENDS": {
"LDAP": {
"ROOT_DN": slapd_server.suffix,