diff --git a/CHANGES.rst b/CHANGES.rst index 7efef4fb..dc6ecb9e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,7 @@ Added - If users register or authenticate during a OAuth Authorization phase, they get redirected back to that page afterwards. :issue:`168` :pr:`151` +- flask-babel and pytz are now part of the `front` extras [0.0.33] - 2023-08-26 ===================== diff --git a/canaille/app/configuration.py b/canaille/app/configuration.py index b070b158..afdc2c2a 100644 --- a/canaille/app/configuration.py +++ b/canaille/app/configuration.py @@ -44,7 +44,7 @@ def setup_config(app, config=None, validate_config=True): try: import toml - except ImportError: # pragma: no cover + except ImportError: toml = None app.config.from_mapping( @@ -64,7 +64,7 @@ def setup_config(app, config=None, validate_config=True): "Either create conf/config.toml or set the 'CONFIG' variable environment." ) - if app.debug: # pragma: no cover + if app.debug: install(app.config, debug=True) if validate_config: diff --git a/canaille/app/flask.py b/canaille/app/flask.py index 791bb337..bc5a4acf 100644 --- a/canaille/app/flask.py +++ b/canaille/app/flask.py @@ -4,13 +4,13 @@ from urllib.parse import urlsplit from urllib.parse import urlunsplit from canaille.app import models +from canaille.app.i18n import gettext as _ from canaille.app.themes import render_template from flask import abort from flask import current_app from flask import g from flask import request from flask import session -from flask_babel import gettext as _ from werkzeug.routing import BaseConverter diff --git a/canaille/app/forms.py b/canaille/app/forms.py index b827b76c..6f571d08 100644 --- a/canaille/app/forms.py +++ b/canaille/app/forms.py @@ -2,16 +2,15 @@ import datetime import math import re -import pytz import wtforms.validators from canaille.app.i18n import DEFAULT_LANGUAGE_CODE +from canaille.app.i18n import gettext as _ 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 from flask import request -from flask_babel import gettext as _ from flask_wtf import FlaskForm from wtforms.meta import DefaultMeta @@ -47,7 +46,7 @@ def phone_number(form, field): def email_validator(form, field): try: import email_validator # noqa: F401 - except ImportError: # pragma: no cover + except ImportError: pass wtforms.validators.Email()(form, field) @@ -226,7 +225,7 @@ class DateTimeUTCField(wtforms.DateTimeLocalField): try: unaware_dt = datetime.datetime.strptime(date_str, format) locale_dt = user_timezone.localize(unaware_dt) - utc_dt = locale_dt.astimezone(pytz.utc) + utc_dt = locale_dt.astimezone(datetime.timezone.utc) self.data = utc_dt return except ValueError: diff --git a/canaille/app/i18n.py b/canaille/app/i18n.py index 9b54bd55..7c1ba0d7 100644 --- a/canaille/app/i18n.py +++ b/canaille/app/i18n.py @@ -1,23 +1,32 @@ -import gettext +import datetime -import pytz -from babel.dates import LOCALTZ from flask import current_app from flask import g from flask import request -from flask_babel import Babel -from flask_babel import get_locale DEFAULT_LANGUAGE_CODE = "en" -babel = Babel() +try: + from flask_babel import Babel + from flask_babel import get_locale + from flask_babel import gettext + from flask_babel import lazy_gettext + + babel = Babel() +except ImportError: + + def identity(string, *args, **kwargs): + return string + + def get_locale(): + return "en_US" + + gettext = identity + lazy_gettext = identity + babel = None def setup_i18n(app): - babel.init_app( - app, locale_selector=locale_selector, timezone_selector=timezone_selector - ) - @app.before_request def before_request(): g.available_language_codes = available_language_codes() @@ -28,6 +37,13 @@ def setup_i18n(app): "locale": get_locale(), } + if not babel: # pragma: no cover + return + + babel.init_app( + app, locale_selector=locale_selector, timezone_selector=timezone_selector + ) + def locale_selector(): from .flask import current_user @@ -44,6 +60,12 @@ def locale_selector(): def timezone_selector(): + if not babel: # pragma: no cover + return datetime.timezone.utc + + import pytz + from babel.dates import LOCALTZ + try: return pytz.timezone(current_app.config.get("TIMEZONE")) except pytz.exceptions.UnknownTimeZoneError: @@ -53,18 +75,30 @@ def timezone_selector(): def native_language_name_from_code(code): try: import pycountry - except ImportError: # pragma: no cover + from gettext import translation + except ImportError: return code language = pycountry.languages.get(alpha_2=code[:2]) if code == DEFAULT_LANGUAGE_CODE: return language.name - translation = gettext.translation( - "iso639-3", pycountry.LOCALES_DIR, languages=[code] - ) + translation = translation("iso639-3", pycountry.LOCALES_DIR, languages=[code]) return translation.gettext(language.name) def available_language_codes(): - return [str(translation) for translation in babel.list_translations()] + return ( + [str(translation) for translation in babel.list_translations()] + if babel + else [DEFAULT_LANGUAGE_CODE] + ) + + +def reload_translations(): + if not babel: # pragma: no cover + return + + from flask_babel import refresh + + return refresh() diff --git a/canaille/app/themes.py b/canaille/app/themes.py index 6ed2d821..7894e7d5 100644 --- a/canaille/app/themes.py +++ b/canaille/app/themes.py @@ -4,7 +4,7 @@ import flask try: import flask_themer -except ImportError: # pragma: no cover +except ImportError: flask_themer = None diff --git a/canaille/backends/__init__.py b/canaille/backends/__init__.py index acb52c1c..9b79a952 100644 --- a/canaille/backends/__init__.py +++ b/canaille/backends/__init__.py @@ -102,7 +102,7 @@ def setup_backend(app, backend): g.backend = backend app.backend = backend - if app.debug: # pragma: no cover + if app.debug: backend.install(app.config, True) diff --git a/canaille/backends/ldap/backend.py b/canaille/backends/ldap/backend.py index 5e03030c..62cc6e13 100644 --- a/canaille/backends/ldap/backend.py +++ b/canaille/backends/ldap/backend.py @@ -7,11 +7,11 @@ import ldap.modlist import ldif from canaille.app import models from canaille.app.configuration import ConfigurationException +from canaille.app.i18n import gettext as _ from canaille.app.themes import render_template from canaille.backends import BaseBackend from flask import current_app from flask import request -from flask_babel import gettext as _ from .utils import listify diff --git a/canaille/core/account.py b/canaille/core/account.py index e3cbb726..7681607f 100644 --- a/canaille/core/account.py +++ b/canaille/core/account.py @@ -24,6 +24,8 @@ from canaille.app.forms import is_readonly from canaille.app.forms import set_readonly from canaille.app.forms import set_writable from canaille.app.forms import TableForm +from canaille.app.i18n import gettext as _ +from canaille.app.i18n import reload_translations from canaille.app.themes import render_template from canaille.backends import BaseBackend from flask import abort @@ -36,8 +38,6 @@ from flask import request from flask import send_file from flask import session from flask import url_for -from flask_babel import gettext as _ -from flask_babel import refresh from werkzeug.datastructures import CombinedMultiDict from werkzeug.datastructures import FileStorage @@ -526,7 +526,7 @@ def profile_edition_main_form_validation(user, edited_user, profile_form): if "preferred_language" in request.form: # Refresh the babel cache in case the lang is updated - refresh() + reload_translations() if profile_form["preferred_language"].data == "auto": edited_user.preferred_language = None diff --git a/canaille/core/admin.py b/canaille/core/admin.py index d03c2c32..53bf24f0 100644 --- a/canaille/core/admin.py +++ b/canaille/core/admin.py @@ -2,6 +2,7 @@ from canaille.app import obj_to_b64 from canaille.app.flask import permissions_needed from canaille.app.forms import email_validator from canaille.app.forms import Form +from canaille.app.i18n import gettext as _ from canaille.app.themes import render_template from canaille.core.mails import build_hash from canaille.core.mails import send_test_mail @@ -10,7 +11,6 @@ from flask import current_app from flask import flash from flask import request from flask import url_for -from flask_babel import gettext as _ from wtforms import StringField from wtforms.validators import DataRequired diff --git a/canaille/core/auth.py b/canaille/core/auth.py index 2e12d8eb..53a93402 100644 --- a/canaille/core/auth.py +++ b/canaille/core/auth.py @@ -4,6 +4,7 @@ from canaille.app.flask import current_user from canaille.app.flask import login_user from canaille.app.flask import logout_user from canaille.app.flask import smtp_needed +from canaille.app.i18n import gettext as _ from canaille.app.themes import render_template from canaille.backends import BaseBackend from flask import abort @@ -14,7 +15,6 @@ from flask import redirect from flask import request from flask import session from flask import url_for -from flask_babel import gettext as _ from .forms import FirstLoginForm from .forms import ForgottenPasswordForm diff --git a/canaille/core/commands.py b/canaille/core/commands.py index 6ef1b39f..526c9185 100644 --- a/canaille/core/commands.py +++ b/canaille/core/commands.py @@ -4,7 +4,7 @@ from flask.cli import with_appcontext try: HAS_FAKER = True -except ImportError: # pragma: no cover +except ImportError: HAS_FAKER = False diff --git a/canaille/core/forms.py b/canaille/core/forms.py index 6e76c623..8c8c3646 100644 --- a/canaille/core/forms.py +++ b/canaille/core/forms.py @@ -9,10 +9,10 @@ from canaille.app.forms import phone_number from canaille.app.forms import ReadOnly from canaille.app.forms import set_readonly from canaille.app.forms import unique_values +from canaille.app.i18n import lazy_gettext as _ from canaille.app.i18n import native_language_name_from_code from flask import current_app from flask import g -from flask_babel import lazy_gettext as _ from flask_wtf.file import FileAllowed from flask_wtf.file import FileField diff --git a/canaille/core/groups.py b/canaille/core/groups.py index 788ca47e..fe202736 100644 --- a/canaille/core/groups.py +++ b/canaille/core/groups.py @@ -2,6 +2,7 @@ from canaille.app import models from canaille.app.flask import permissions_needed from canaille.app.flask import render_htmx_template from canaille.app.forms import TableForm +from canaille.app.i18n import gettext as _ from canaille.app.themes import render_template from flask import abort from flask import Blueprint @@ -9,7 +10,6 @@ from flask import flash from flask import redirect from flask import request from flask import url_for -from flask_babel import gettext as _ from .forms import CreateGroupForm from .forms import EditGroupForm diff --git a/canaille/core/mails.py b/canaille/core/mails.py index 6f5a11e1..66495255 100644 --- a/canaille/core/mails.py +++ b/canaille/core/mails.py @@ -1,10 +1,10 @@ from canaille.app import build_hash +from canaille.app.i18n import gettext as _ from canaille.app.mails import logo from canaille.app.mails import send_email from canaille.app.themes import render_template from flask import current_app from flask import url_for -from flask_babel import gettext as _ def send_test_mail(email): diff --git a/canaille/core/templates/about.html b/canaille/core/templates/about.html index 5edab60e..381d03bc 100644 --- a/canaille/core/templates/about.html +++ b/canaille/core/templates/about.html @@ -18,7 +18,7 @@
{% trans %}Free and open-source identity provider.{% endtrans %}

- {{ gettext("Version %(version)s", version=version) }} · + {% trans %}Version {{ version }}{% endtrans %} · {% trans %}Homepage{% endtrans %} · {% trans %}Documentation{% endtrans %} · {% trans %}Source code{% endtrans %} diff --git a/canaille/oidc/basemodels.py b/canaille/oidc/basemodels.py index f7f7124f..1dd4cd4d 100644 --- a/canaille/oidc/basemodels.py +++ b/canaille/oidc/basemodels.py @@ -91,7 +91,7 @@ class Consent(Model): revokation_date: datetime.datetime def revoke(self): - raise NotImplementedError() # pragma: no cover + raise NotImplementedError() def restore(self): - raise NotImplementedError() # pragma: no cover + raise NotImplementedError() diff --git a/canaille/oidc/clients.py b/canaille/oidc/clients.py index 7ef82195..d58add90 100644 --- a/canaille/oidc/clients.py +++ b/canaille/oidc/clients.py @@ -4,6 +4,7 @@ from canaille.app import models from canaille.app.flask import permissions_needed from canaille.app.flask import render_htmx_template from canaille.app.forms import TableForm +from canaille.app.i18n import gettext as _ from canaille.app.themes import render_template from canaille.oidc.forms import ClientAddForm from flask import abort @@ -12,7 +13,6 @@ from flask import flash from flask import redirect from flask import request from flask import url_for -from flask_babel import gettext as _ from werkzeug.security import gen_salt diff --git a/canaille/oidc/consents.py b/canaille/oidc/consents.py index 54d71a88..60a90fe3 100644 --- a/canaille/oidc/consents.py +++ b/canaille/oidc/consents.py @@ -3,12 +3,12 @@ import uuid from canaille.app import models from canaille.app.flask import user_needed +from canaille.app.i18n import gettext as _ from canaille.app.themes import render_template from flask import Blueprint from flask import flash from flask import redirect from flask import url_for -from flask_babel import gettext as _ from .utils import SCOPE_DETAILS diff --git a/canaille/oidc/endpoints.py b/canaille/oidc/endpoints.py index 13fe31a3..00c4f861 100644 --- a/canaille/oidc/endpoints.py +++ b/canaille/oidc/endpoints.py @@ -10,6 +10,7 @@ from canaille.app import models from canaille.app.flask import current_user from canaille.app.flask import logout_user from canaille.app.flask import set_parameter_in_url_query +from canaille.app.i18n import gettext as _ from canaille.app.themes import render_template from flask import abort from flask import Blueprint @@ -20,7 +21,6 @@ from flask import redirect from flask import request from flask import session from flask import url_for -from flask_babel import gettext as _ from werkzeug.datastructures import CombinedMultiDict from .forms import AuthorizeForm diff --git a/canaille/oidc/forms.py b/canaille/oidc/forms.py index 7060ab59..c6f329a2 100644 --- a/canaille/oidc/forms.py +++ b/canaille/oidc/forms.py @@ -4,7 +4,7 @@ from canaille.app.forms import email_validator from canaille.app.forms import Form from canaille.app.forms import is_uri from canaille.app.forms import unique_values -from flask_babel import lazy_gettext as _ +from canaille.app.i18n import lazy_gettext as _ class AuthorizeForm(Form): diff --git a/canaille/oidc/templates/authorize.html b/canaille/oidc/templates/authorize.html index 49edc3ec..15f044be 100644 --- a/canaille/oidc/templates/authorize.html +++ b/canaille/oidc/templates/authorize.html @@ -7,7 +7,9 @@ {{ client.client_name }} {% endif %} -

{{ gettext('The application %(name)s is requesting access to:', name=client.client_name) }}

+

+ {% trans name=client.client_name %}The application {{ name }} is requesting access to:{% endtrans %} +

{% for scope in grant.request.scope.split(" ") %} diff --git a/canaille/oidc/tokens.py b/canaille/oidc/tokens.py index 0732ee80..5929f484 100644 --- a/canaille/oidc/tokens.py +++ b/canaille/oidc/tokens.py @@ -4,13 +4,13 @@ from canaille.app import models from canaille.app.flask import permissions_needed from canaille.app.flask import render_htmx_template from canaille.app.forms import TableForm +from canaille.app.i18n import gettext as _ from canaille.app.themes import render_template from canaille.oidc.forms import TokenRevokationForm from flask import abort from flask import Blueprint from flask import flash from flask import request -from flask_babel import gettext as _ bp = Blueprint("tokens", __name__, url_prefix="/admin/token") diff --git a/canaille/oidc/utils.py b/canaille/oidc/utils.py index 05c400c2..a604a4f1 100644 --- a/canaille/oidc/utils.py +++ b/canaille/oidc/utils.py @@ -1,4 +1,4 @@ -from flask_babel import lazy_gettext as _ +from canaille.app.i18n import lazy_gettext as _ SCOPE_DETAILS = { "profile": ( diff --git a/poetry.lock b/poetry.lock index 82d6f3bb..0acb0f3c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aiosmtpd" @@ -582,7 +582,7 @@ dotenv = ["python-dotenv"] name = "flask-babel" version = "3.1.0" description = "Adds i18n/l10n support for Flask applications." -optional = false +optional = true python-versions = ">=3.7,<4.0" files = [ {file = "flask_babel-3.1.0-py3-none-any.whl", hash = "sha256:deb3ee272d5adf97f5974ed09ab501243d63e7fb4a047501a00de4bd4aca4830"}, @@ -1861,8 +1861,8 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [extras] -all = ["authlib", "email_validator", "flask-themer", "pycountry", "python-ldap", "sentry-sdk", "toml"] -front = ["email_validator", "flask-themer", "pycountry", "toml"] +all = ["authlib", "email_validator", "flask-babel", "flask-themer", "pycountry", "python-ldap", "pytz", "sentry-sdk", "toml"] +front = ["email_validator", "flask-babel", "flask-themer", "pycountry", "pytz", "toml"] ldap = ["python-ldap"] oidc = ["authlib"] sentry = ["sentry-sdk"] @@ -1870,4 +1870,4 @@ sentry = ["sentry-sdk"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "888a76fbcad92e1761f8000e7a1d69ac0035fc3024d259edc65e9a03acad18ac" +content-hash = "50fc7d78ae46cabdf2cd7403838c38c59412277bde3094f3fc36c221f7d3ec9f" diff --git a/pyproject.toml b/pyproject.toml index ab69abbe..d136a620 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,16 +38,16 @@ include = ["canaille/translations/*/LC_MESSAGES/*.mo"] [tool.poetry.dependencies] python = "^3.8" flask = ">=2.2.2 <2.3" -flask-babel = "^3.0.0" flask-wtf = "^1.1.1" -pytz = ">=2022.7" wtforms = "^3.0.1" werkzeug = ">=2.2.2 <2.3" # extra : front email_validator = {version = "^2.0.0", optional=true} +flask-babel = {version = "^3.0.0", optional=true} flask-themer = {version = "^2.0.0", optional=true} pycountry = {version = ">=22.1.10", optional=true} +pytz = {version = ">=2022.7", optional=true} toml = {version = "^0.10.0", optional=true} # extra : oidc @@ -97,8 +97,10 @@ requests = "*" front = [ "click", "email_validator", + "flask-babel", "flask-themer", "pycountry", + "pytz", "toml", ] ldap = [ @@ -113,8 +115,10 @@ sentry = [ all = [ "click", "email_validator", + "flask-babel", "flask-themer", "pycountry", + "pytz", "toml", "python-ldap", "authlib", @@ -144,6 +148,14 @@ source = [ omit = [".tox/*"] branch = true +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "raise NotImplementedError", + "except ImportError", + "if app.debug", +] + [tool.ruff] ignore = ["E501", "E722"]