diff --git a/canaille/__init__.py b/canaille/__init__.py index 9dbb9998..5b29c0bf 100644 --- a/canaille/__init__.py +++ b/canaille/__init__.py @@ -138,7 +138,7 @@ def setup_flask(app): @app.context_processor def global_processor(): - from .flaskutils import current_user + from .utils.flask import current_user return { "has_smtp": "SMTP" in app.config, diff --git a/canaille/account.py b/canaille/account.py index 700f122a..4d593ac5 100644 --- a/canaille/account.py +++ b/canaille/account.py @@ -21,17 +21,6 @@ from flask_themer import render_template from werkzeug.datastructures import CombinedMultiDict from werkzeug.datastructures import FileStorage -from .apputils import b64_to_obj -from .apputils import default_fields -from .apputils import login_placeholder -from .apputils import obj_to_b64 -from .apputils import profile_hash -from .flaskutils import current_user -from .flaskutils import permissions_needed -from .flaskutils import render_htmx_template -from .flaskutils import request_is_htmx -from .flaskutils import smtp_needed -from .flaskutils import user_needed from .forms import FirstLoginForm from .forms import ForgottenPasswordForm from .forms import InvitationForm @@ -39,12 +28,23 @@ from .forms import LoginForm from .forms import PasswordForm from .forms import PasswordResetForm from .forms import profile_form -from .forms import TableForm from .mails import send_invitation_mail from .mails import send_password_initialization_mail from .mails import send_password_reset_mail from .models import Group from .models import User +from .utils import b64_to_obj +from .utils import default_fields +from .utils import login_placeholder +from .utils import obj_to_b64 +from .utils import profile_hash +from .utils.flask import current_user +from .utils.flask import permissions_needed +from .utils.flask import render_htmx_template +from .utils.flask import request_is_htmx +from .utils.flask import smtp_needed +from .utils.flask import user_needed +from .utils.forms import TableForm bp = Blueprint("account", __name__) diff --git a/canaille/admin.py b/canaille/admin.py index 6b9e3462..0484f0f3 100644 --- a/canaille/admin.py +++ b/canaille/admin.py @@ -1,8 +1,8 @@ -from canaille.apputils import obj_to_b64 -from canaille.flaskutils import permissions_needed -from canaille.forms import HTMXForm from canaille.mails import profile_hash from canaille.mails import send_test_mail +from canaille.utils import obj_to_b64 +from canaille.utils.flask import permissions_needed +from canaille.utils.forms import HTMXForm from flask import Blueprint from flask import current_app from flask import flash diff --git a/canaille/forms.py b/canaille/forms.py index b2f165b7..d3498763 100644 --- a/canaille/forms.py +++ b/canaille/forms.py @@ -1,26 +1,20 @@ -import math - import wtforms.form -from canaille.flaskutils import request_is_htmx +from canaille.utils.flask import request_is_htmx from flask import abort from flask import current_app from flask import g from flask import make_response from flask import request from flask_babel import lazy_gettext as _ -from flask_wtf import FlaskForm from flask_wtf.file import FileAllowed from flask_wtf.file import FileField -from .apputils import validate_uri from .i18n import native_language_name_from_code from .models import Group from .models import User - - -def is_uri(form, field): - if not validate_uri(field.data): - raise wtforms.ValidationError(_("This is not a valid URL")) +from .utils.forms import HTMXBaseForm +from .utils.forms import HTMXForm +from .utils.forms import is_uri def unique_login(form, field): @@ -57,57 +51,6 @@ def existing_login(form, field): ) -class HTMXFormMixin: - def validate(self, *args, **kwargs): - """ - If the request is a HTMX request, this will only render the field - that triggered the request (after having validated the form). This - uses the Flask abort method to interrupt the flow with an exception. - """ - if not request_is_htmx(): - return super().validate(*args, **kwargs) - - field = self[request.headers.get("HX-Trigger-Name")] - field.widget.hide_value = False - self.process(request.form) - super().validate(*args, **kwargs) - form_macro = current_app.jinja_env.get_template("macro/form.html") - response = make_response(form_macro.module.render_field(field)) - abort(response) - - -class HTMXForm(HTMXFormMixin, FlaskForm): - pass - - -class HTMXBaseForm(HTMXFormMixin, wtforms.form.BaseForm): - pass - - -class TableForm(HTMXForm): - def __init__(self, cls=None, page_size=25, fields=None, filter=None, **kwargs): - filter = filter or {} - super().__init__(**kwargs) - if self.query.data: - self.items = cls.fuzzy(self.query.data, fields, **filter) - else: - self.items = cls.query(**filter) - - self.page_size = page_size - self.nb_items = len(self.items) - self.page_max = max(1, math.ceil(self.nb_items / self.page_size)) - first_item = (self.page.data - 1) * self.page_size - last_item = min((self.page.data) * self.page_size, self.nb_items) - self.items_slice = self.items[first_item:last_item] - - page = wtforms.IntegerField(default=1) - query = wtforms.StringField(default="") - - 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 LoginForm(HTMXForm): login = wtforms.StringField( _("Login"), diff --git a/canaille/groups.py b/canaille/groups.py index 947cb69c..d468314d 100644 --- a/canaille/groups.py +++ b/canaille/groups.py @@ -7,13 +7,13 @@ from flask import url_for from flask_babel import gettext as _ from flask_themer import render_template -from .flaskutils import permissions_needed -from .flaskutils import render_htmx_template from .forms import CreateGroupForm from .forms import EditGroupForm -from .forms import TableForm from .models import Group from .models import User +from .utils.flask import permissions_needed +from .utils.flask import render_htmx_template +from .utils.forms import TableForm bp = Blueprint("groups", __name__, url_prefix="/groups") diff --git a/canaille/i18n.py b/canaille/i18n.py index 2db8feaf..1b09e40d 100644 --- a/canaille/i18n.py +++ b/canaille/i18n.py @@ -27,7 +27,7 @@ def setup_i18n(app): def locale_selector(): - from .flaskutils import current_user + from .utils.flask import current_user user = current_user() available_language_codes = getattr(g, "available_language_codes", []) diff --git a/canaille/mails.py b/canaille/mails.py index dea881d8..93bc1fec 100644 --- a/canaille/mails.py +++ b/canaille/mails.py @@ -10,9 +10,9 @@ from flask import url_for from flask_babel import gettext as _ from flask_themer import render_template -from .apputils import get_current_domain -from .apputils import get_current_mail_domain -from .apputils import profile_hash +from .utils import get_current_domain +from .utils import get_current_mail_domain +from .utils import profile_hash DEFAULT_SMTP_HOST = "localhost" DEFAULT_SMTP_PORT = 25 diff --git a/canaille/oidc/authorizations.py b/canaille/oidc/authorizations.py index 27ecbb38..d7d06b87 100644 --- a/canaille/oidc/authorizations.py +++ b/canaille/oidc/authorizations.py @@ -1,7 +1,7 @@ -from canaille.flaskutils import permissions_needed -from canaille.flaskutils import render_htmx_template -from canaille.forms import TableForm from canaille.oidc.models import AuthorizationCode +from canaille.utils.flask import permissions_needed +from canaille.utils.flask import render_htmx_template +from canaille.utils.forms import TableForm from flask import abort from flask import Blueprint from flask import request diff --git a/canaille/oidc/clients.py b/canaille/oidc/clients.py index 75c39321..76e8b1dd 100644 --- a/canaille/oidc/clients.py +++ b/canaille/oidc/clients.py @@ -1,11 +1,11 @@ import datetime -from canaille.flaskutils import permissions_needed -from canaille.flaskutils import render_htmx_template -from canaille.flaskutils import request_is_htmx -from canaille.forms import TableForm from canaille.oidc.forms import ClientAddForm from canaille.oidc.models import Client +from canaille.utils.flask import permissions_needed +from canaille.utils.flask import render_htmx_template +from canaille.utils.flask import request_is_htmx +from canaille.utils.forms import TableForm from flask import abort from flask import Blueprint from flask import flash diff --git a/canaille/oidc/consents.py b/canaille/oidc/consents.py index a5bf421a..0c96c955 100644 --- a/canaille/oidc/consents.py +++ b/canaille/oidc/consents.py @@ -1,9 +1,9 @@ import datetime import uuid -from canaille.flaskutils import user_needed from canaille.oidc.models import Client from canaille.oidc.models import Consent +from canaille.utils.flask import user_needed from flask import Blueprint from flask import flash from flask import redirect diff --git a/canaille/oidc/endpoints.py b/canaille/oidc/endpoints.py index ab676f7c..38f395d3 100644 --- a/canaille/oidc/endpoints.py +++ b/canaille/oidc/endpoints.py @@ -19,10 +19,10 @@ from flask_babel import gettext as _ from flask_themer import render_template from werkzeug.datastructures import CombinedMultiDict -from ..flaskutils import current_user -from ..flaskutils import set_parameter_in_url_query from ..forms import FullLoginForm from ..models import User +from ..utils.flask import current_user +from ..utils.flask import set_parameter_in_url_query from .forms import AuthorizeForm from .forms import LogoutForm from .models import Client diff --git a/canaille/oidc/forms.py b/canaille/oidc/forms.py index 58525000..12897cf3 100644 --- a/canaille/oidc/forms.py +++ b/canaille/oidc/forms.py @@ -1,7 +1,7 @@ import wtforms -from canaille.forms import HTMXForm -from canaille.forms import is_uri from canaille.oidc.models import Client +from canaille.utils.forms import HTMXForm +from canaille.utils.forms import is_uri from flask_babel import lazy_gettext as _ diff --git a/canaille/oidc/tokens.py b/canaille/oidc/tokens.py index b2abd0f6..2232076a 100644 --- a/canaille/oidc/tokens.py +++ b/canaille/oidc/tokens.py @@ -1,11 +1,11 @@ import datetime -from canaille.flaskutils import permissions_needed -from canaille.flaskutils import render_htmx_template -from canaille.forms import TableForm from canaille.models import User from canaille.oidc.models import Client from canaille.oidc.models import Token +from canaille.utils.flask import permissions_needed +from canaille.utils.flask import render_htmx_template +from canaille.utils.forms import TableForm from flask import abort from flask import Blueprint from flask import flash diff --git a/canaille/apputils.py b/canaille/utils/__init__.py similarity index 100% rename from canaille/apputils.py rename to canaille/utils/__init__.py diff --git a/canaille/flaskutils.py b/canaille/utils/flask.py similarity index 100% rename from canaille/flaskutils.py rename to canaille/utils/flask.py diff --git a/canaille/utils/forms.py b/canaille/utils/forms.py new file mode 100644 index 00000000..80074956 --- /dev/null +++ b/canaille/utils/forms.py @@ -0,0 +1,68 @@ +import math + +import wtforms +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 . import validate_uri +from .flask import request_is_htmx + + +def is_uri(form, field): + if not validate_uri(field.data): + raise wtforms.ValidationError(_("This is not a valid URL")) + + +class HTMXFormMixin: + def validate(self, *args, **kwargs): + """ + If the request is a HTMX request, this will only render the field + that triggered the request (after having validated the form). This + uses the Flask abort method to interrupt the flow with an exception. + """ + if not request_is_htmx(): + return super().validate(*args, **kwargs) + + field = self[request.headers.get("HX-Trigger-Name")] + field.widget.hide_value = False + self.process(request.form) + super().validate(*args, **kwargs) + form_macro = current_app.jinja_env.get_template("macro/form.html") + response = make_response(form_macro.module.render_field(field)) + abort(response) + + +class HTMXForm(HTMXFormMixin, FlaskForm): + pass + + +class HTMXBaseForm(HTMXFormMixin, wtforms.form.BaseForm): + pass + + +class TableForm(HTMXForm): + def __init__(self, cls=None, page_size=25, fields=None, filter=None, **kwargs): + filter = filter or {} + super().__init__(**kwargs) + if self.query.data: + self.items = cls.fuzzy(self.query.data, fields, **filter) + else: + self.items = cls.query(**filter) + + self.page_size = page_size + self.nb_items = len(self.items) + self.page_max = max(1, math.ceil(self.nb_items / self.page_size)) + first_item = (self.page.data - 1) * self.page_size + last_item = min((self.page.data) * self.page_size, self.nb_items) + self.items_slice = self.items[first_item:last_item] + + page = wtforms.IntegerField(default=1) + query = wtforms.StringField(default="") + + 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")) diff --git a/tests/test_apputils.py b/tests/test_apputils.py index f4de4303..90fcec14 100644 --- a/tests/test_apputils.py +++ b/tests/test_apputils.py @@ -1,4 +1,4 @@ -from canaille.apputils import validate_uri +from canaille.utils import validate_uri def test_validate_uri(): diff --git a/tests/test_flaskutils.py b/tests/test_flaskutils.py index b56243ad..a73c80ec 100644 --- a/tests/test_flaskutils.py +++ b/tests/test_flaskutils.py @@ -4,7 +4,7 @@ import ldap import pytest import toml from canaille import create_app -from canaille.flaskutils import set_parameter_in_url_query +from canaille.utils.flask import set_parameter_in_url_query from flask import g from flask_webtest import TestApp