forked from Github-Mirrors/canaille
feat: flask-babel and pytz are now part of the front extras
This commit is contained in:
parent
c0bf10dce7
commit
a2e3fce204
26 changed files with 99 additions and 51 deletions
|
@ -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
|
||||
=====================
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -4,7 +4,7 @@ import flask
|
|||
|
||||
try:
|
||||
import flask_themer
|
||||
except ImportError: # pragma: no cover
|
||||
except ImportError:
|
||||
flask_themer = None
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,7 +4,7 @@ from flask.cli import with_appcontext
|
|||
|
||||
try:
|
||||
HAS_FAKER = True
|
||||
except ImportError: # pragma: no cover
|
||||
except ImportError:
|
||||
HAS_FAKER = False
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<div class="sub header">{% trans %}Free and open-source identity provider.{% endtrans %}</div>
|
||||
</h2>
|
||||
<p style="text-align: center">
|
||||
<a href="https://pypi.org/project/canaille/">{{ gettext("Version %(version)s", version=version) }}</a> ·
|
||||
<a href="https://pypi.org/project/canaille/">{% trans %}Version {{ version }}{% endtrans %}</a> ·
|
||||
<a href="https://canaille.yaal.coop">{% trans %}Homepage{% endtrans %}</a> ·
|
||||
<a href="https://canaille.readthedocs.io">{% trans %}Documentation{% endtrans %}</a> ·
|
||||
<a href="https://gitlab.com/yaal/canaille">{% trans %}Source code{% endtrans %}</a>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
<img class="ui centered tiny image" src="{{ client.logo_uri }}" alt="{{ client.client_name }}">
|
||||
{% endif %}
|
||||
|
||||
<h2 class="ui header">{{ gettext('The application %(name)s is requesting access to:', name=client.client_name) }}</h2>
|
||||
<h2 class="ui header">
|
||||
{% trans name=client.client_name %}The application {{ name }} is requesting access to:{% endtrans %}
|
||||
</h2>
|
||||
|
||||
<div class="ui divided items">
|
||||
{% for scope in grant.request.scope.split(" ") %}
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from flask_babel import lazy_gettext as _
|
||||
from canaille.app.i18n import lazy_gettext as _
|
||||
|
||||
SCOPE_DETAILS = {
|
||||
"profile": (
|
||||
|
|
10
poetry.lock
generated
10
poetry.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
Loading…
Reference in a new issue