forked from Github-Mirrors/canaille
Merge branch 'issue-115-multiple-fields' into 'main'
Multiple fields See merge request yaal/canaille!136
This commit is contained in:
commit
d71caf56ec
15 changed files with 686 additions and 196 deletions
|
@ -11,6 +11,7 @@ Added
|
||||||
|
|
||||||
- Configuration entries can be loaded from files if the entry key has a *_FILE* suffix
|
- Configuration entries can be loaded from files if the entry key has a *_FILE* suffix
|
||||||
and the entry value is the path to the file. :issue:`134` :pr:`134`
|
and the entry value is the path to the file. :issue:`134` :pr:`134`
|
||||||
|
- Field list support. :issue:`115` :pr:`136`
|
||||||
|
|
||||||
Removed
|
Removed
|
||||||
*******
|
*******
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
import math
|
import math
|
||||||
|
import re
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
import wtforms
|
import wtforms
|
||||||
|
@ -23,6 +24,17 @@ def is_uri(form, field):
|
||||||
raise wtforms.ValidationError(_("This is not a valid URL"))
|
raise wtforms.ValidationError(_("This is not a valid URL"))
|
||||||
|
|
||||||
|
|
||||||
|
def unique_values(form, field):
|
||||||
|
values = set()
|
||||||
|
for subfield in field:
|
||||||
|
if subfield.data in values:
|
||||||
|
subfield.errors.append(_("This value is a duplicate"))
|
||||||
|
raise wtforms.ValidationError(_("This value is a duplicate"))
|
||||||
|
|
||||||
|
if subfield.data:
|
||||||
|
values.add(subfield.data)
|
||||||
|
|
||||||
|
|
||||||
meta = DefaultMeta()
|
meta = DefaultMeta()
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,6 +48,26 @@ class I18NFormMixin:
|
||||||
|
|
||||||
|
|
||||||
class HTMXFormMixin:
|
class HTMXFormMixin:
|
||||||
|
SEPARATOR = "-"
|
||||||
|
|
||||||
|
def field_from_name(self, field_name):
|
||||||
|
"""
|
||||||
|
Returns a tuple containing a field and its rendering context
|
||||||
|
"""
|
||||||
|
if self.SEPARATOR not in field_name:
|
||||||
|
field = self[field_name] if field_name in self else None
|
||||||
|
return field, {}
|
||||||
|
|
||||||
|
parts = field_name.split(self.SEPARATOR)
|
||||||
|
fieldlist_name = self.SEPARATOR.join(parts[:-1])
|
||||||
|
try:
|
||||||
|
indice = int(parts[-1])
|
||||||
|
except ValueError:
|
||||||
|
return None, {}
|
||||||
|
fieldlist, _ = self.field_from_name(fieldlist_name)
|
||||||
|
context = {"parent_list": fieldlist, "parent_indice": indice}
|
||||||
|
return fieldlist[indice], context
|
||||||
|
|
||||||
def validate(self, *args, **kwargs):
|
def validate(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
If the request is a HTMX request, this will only render the field
|
If the request is a HTMX request, this will only render the field
|
||||||
|
@ -46,21 +78,68 @@ class HTMXFormMixin:
|
||||||
return super().validate(*args, **kwargs)
|
return super().validate(*args, **kwargs)
|
||||||
|
|
||||||
field_name = request.headers.get("HX-Trigger-Name")
|
field_name = request.headers.get("HX-Trigger-Name")
|
||||||
if field_name in self:
|
field, context = self.field_from_name(field_name)
|
||||||
self.validate_field(field_name, *args, **kwargs)
|
if field:
|
||||||
self.render_field(field_name)
|
self.validate_field(field, *args, **kwargs)
|
||||||
abort(400)
|
self.render_field(field, **context)
|
||||||
|
|
||||||
def validate_field(self, field_name, *args, **kwargs):
|
abort(400, f"{field_name} is not a valid field for inline validation")
|
||||||
self[field_name].widget.hide_value = False
|
|
||||||
|
def validate_field(self, field, *args, **kwargs):
|
||||||
|
field.widget.hide_value = False
|
||||||
self.process(request.form)
|
self.process(request.form)
|
||||||
super().validate(*args, **kwargs)
|
return field.validate(self, *args, **kwargs)
|
||||||
|
|
||||||
def render_field(self, field_name, *args, **kwargs):
|
def render_field(self, field, *args, **kwargs):
|
||||||
form_macro = current_app.jinja_env.get_template("macro/form.html")
|
form_macro = current_app.jinja_env.get_template("macro/form.html")
|
||||||
response = make_response(form_macro.module.render_field(self[field_name]))
|
response = make_response(form_macro.module.render_field(field, *args, **kwargs))
|
||||||
abort(response)
|
abort(response)
|
||||||
|
|
||||||
|
def form_control(self):
|
||||||
|
"""
|
||||||
|
Checks wether the current request is the result of the users
|
||||||
|
adding or removing a field from a FieldList.
|
||||||
|
"""
|
||||||
|
FIELDLIST_ADD_BUTTON = "fieldlist_add"
|
||||||
|
FIELDLIST_REMOVE_BUTTON = "fieldlist_remove"
|
||||||
|
|
||||||
|
fieldlist_suffix = rf"{self.SEPARATOR}(\d+)$"
|
||||||
|
if field_name := request.form.get(FIELDLIST_ADD_BUTTON):
|
||||||
|
fieldlist_name = re.sub(fieldlist_suffix, "", field_name)
|
||||||
|
fieldlist, context = self.field_from_name(fieldlist_name)
|
||||||
|
|
||||||
|
if not fieldlist or not isinstance(fieldlist, wtforms.FieldList):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
if request_is_htmx():
|
||||||
|
self.validate_field(fieldlist)
|
||||||
|
|
||||||
|
fieldlist.append_entry()
|
||||||
|
|
||||||
|
if request_is_htmx():
|
||||||
|
self.render_field(fieldlist, **context)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
if field_name := request.form.get(FIELDLIST_REMOVE_BUTTON):
|
||||||
|
fieldlist_name = re.sub(fieldlist_suffix, "", field_name)
|
||||||
|
fieldlist, context = self.field_from_name(fieldlist_name)
|
||||||
|
|
||||||
|
if not fieldlist or not isinstance(fieldlist, wtforms.FieldList):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
if request_is_htmx():
|
||||||
|
self.validate_field(fieldlist)
|
||||||
|
|
||||||
|
fieldlist.pop_entry()
|
||||||
|
|
||||||
|
if request_is_htmx():
|
||||||
|
self.render_field(fieldlist, **context)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class HTMXForm(HTMXFormMixin, I18NFormMixin, FlaskForm):
|
class HTMXForm(HTMXFormMixin, I18NFormMixin, FlaskForm):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -42,6 +42,7 @@ from .forms import MINIMUM_PASSWORD_LENGTH
|
||||||
from .forms import PasswordForm
|
from .forms import PasswordForm
|
||||||
from .forms import PasswordResetForm
|
from .forms import PasswordResetForm
|
||||||
from .forms import profile_form
|
from .forms import profile_form
|
||||||
|
from .forms import PROFILE_FORM_FIELDS
|
||||||
from .mails import send_invitation_mail
|
from .mails import send_invitation_mail
|
||||||
from .mails import send_password_initialization_mail
|
from .mails import send_password_initialization_mail
|
||||||
from .mails import send_password_reset_mail
|
from .mails import send_password_reset_mail
|
||||||
|
@ -307,7 +308,7 @@ def registration(data, hash):
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"user_name": invitation.user_name,
|
"user_name": invitation.user_name,
|
||||||
"emails": invitation.email,
|
"emails": [invitation.email],
|
||||||
"groups": invitation.groups,
|
"groups": invitation.groups,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,25 +337,29 @@ def registration(data, hash):
|
||||||
form["password1"].flags.required = True
|
form["password1"].flags.required = True
|
||||||
form["password2"].flags.required = True
|
form["password2"].flags.required = True
|
||||||
|
|
||||||
if request.form:
|
if not request.form or form.form_control():
|
||||||
if not form.validate():
|
return render_template(
|
||||||
flash(_("User account creation failed."), "error")
|
"profile_add.html",
|
||||||
|
form=form,
|
||||||
|
menuitem="users",
|
||||||
|
edited_user=None,
|
||||||
|
self_deletion=False,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
if not form.validate():
|
||||||
user = profile_create(current_app, form)
|
flash(_("User account creation failed."), "error")
|
||||||
user.login()
|
return render_template(
|
||||||
flash(_("Your account has been created successfully."), "success")
|
"profile_add.html",
|
||||||
return redirect(
|
form=form,
|
||||||
url_for("account.profile_edition", username=user.user_name[0])
|
menuitem="users",
|
||||||
)
|
edited_user=None,
|
||||||
|
self_deletion=False,
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
user = profile_create(current_app, form)
|
||||||
"profile_add.html",
|
user.login()
|
||||||
form=form,
|
flash(_("Your account has been created successfully."), "success")
|
||||||
menuitem="users",
|
return redirect(url_for("account.profile_edition", username=user.user_name[0]))
|
||||||
edited_user=None,
|
|
||||||
self_deletion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/profile", methods=("GET", "POST"))
|
@bp.route("/profile", methods=("GET", "POST"))
|
||||||
|
@ -367,23 +372,27 @@ def profile_creation(user):
|
||||||
if field.render_kw and "readonly" in field.render_kw:
|
if field.render_kw and "readonly" in field.render_kw:
|
||||||
del field.render_kw["readonly"]
|
del field.render_kw["readonly"]
|
||||||
|
|
||||||
if request.form:
|
if not request.form or form.form_control():
|
||||||
if not form.validate():
|
return render_template(
|
||||||
flash(_("User account creation failed."), "error")
|
"profile_add.html",
|
||||||
|
form=form,
|
||||||
|
menuitem="users",
|
||||||
|
edited_user=None,
|
||||||
|
self_deletion=False,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
if not form.validate():
|
||||||
user = profile_create(current_app, form)
|
flash(_("User account creation failed."), "error")
|
||||||
return redirect(
|
return render_template(
|
||||||
url_for("account.profile_edition", username=user.user_name[0])
|
"profile_add.html",
|
||||||
)
|
form=form,
|
||||||
|
menuitem="users",
|
||||||
|
edited_user=None,
|
||||||
|
self_deletion=False,
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
user = profile_create(current_app, form)
|
||||||
"profile_add.html",
|
return redirect(url_for("account.profile_edition", username=user.user_name[0]))
|
||||||
form=form,
|
|
||||||
menuitem="users",
|
|
||||||
edited_user=None,
|
|
||||||
self_deletion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def profile_create(current_app, form):
|
def profile_create(current_app, form):
|
||||||
|
@ -455,11 +464,13 @@ def profile_edition(user, username):
|
||||||
"organization",
|
"organization",
|
||||||
}
|
}
|
||||||
data = {
|
data = {
|
||||||
k: getattr(user, k)[0]
|
field: getattr(user, field)[0]
|
||||||
if getattr(user, k) and isinstance(getattr(user, k), list)
|
if getattr(user, field)
|
||||||
else getattr(user, k) or ""
|
and isinstance(getattr(user, field), list)
|
||||||
for k in fields
|
and not PROFILE_FORM_FIELDS[field].field_class == wtforms.FieldList
|
||||||
if hasattr(user, k) and k in available_fields
|
else getattr(user, field) or ""
|
||||||
|
for field in fields
|
||||||
|
if hasattr(user, field) and field in available_fields
|
||||||
}
|
}
|
||||||
|
|
||||||
form = profile_form(
|
form = profile_form(
|
||||||
|
@ -467,40 +478,45 @@ def profile_edition(user, username):
|
||||||
)
|
)
|
||||||
form.process(CombinedMultiDict((request.files, request.form)) or None, data=data)
|
form.process(CombinedMultiDict((request.files, request.form)) or None, data=data)
|
||||||
|
|
||||||
if request.form:
|
if not request.form or form.form_control():
|
||||||
if not form.validate():
|
return render_template(
|
||||||
flash(_("Profile edition failed."), "error")
|
"profile_edit.html",
|
||||||
|
form=form,
|
||||||
|
menuitem=menuitem,
|
||||||
|
edited_user=user,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
if not form.validate():
|
||||||
for attribute in form:
|
flash(_("Profile edition failed."), "error")
|
||||||
if attribute.name in user.attributes and attribute.name in editor.write:
|
return render_template(
|
||||||
if isinstance(attribute.data, FileStorage):
|
"profile_edit.html",
|
||||||
data = attribute.data.stream.read()
|
form=form,
|
||||||
else:
|
menuitem=menuitem,
|
||||||
data = attribute.data
|
edited_user=user,
|
||||||
|
)
|
||||||
|
|
||||||
setattr(user, attribute.name, data)
|
for attribute in form:
|
||||||
|
if attribute.name in user.attributes and attribute.name in editor.write:
|
||||||
|
if isinstance(attribute.data, FileStorage):
|
||||||
|
data = attribute.data.stream.read()
|
||||||
|
else:
|
||||||
|
data = attribute.data
|
||||||
|
|
||||||
if "photo" in form and form["photo_delete"].data:
|
setattr(user, attribute.name, data)
|
||||||
del user.photo
|
|
||||||
|
|
||||||
if "preferred_language" in request.form:
|
if "photo" in form and form["photo_delete"].data:
|
||||||
# Refresh the babel cache in case the lang is updated
|
del user.photo
|
||||||
refresh()
|
|
||||||
|
|
||||||
if form["preferred_language"].data == "auto":
|
if "preferred_language" in request.form:
|
||||||
user.preferred_language = None
|
# Refresh the babel cache in case the lang is updated
|
||||||
|
refresh()
|
||||||
|
|
||||||
user.save()
|
if form["preferred_language"].data == "auto":
|
||||||
flash(_("Profile updated successfully."), "success")
|
user.preferred_language = None
|
||||||
return redirect(url_for("account.profile_edition", username=username))
|
|
||||||
|
|
||||||
return render_template(
|
user.save()
|
||||||
"profile_edit.html",
|
flash(_("Profile updated successfully."), "success")
|
||||||
form=form,
|
return redirect(url_for("account.profile_edition", username=username))
|
||||||
menuitem=menuitem,
|
|
||||||
edited_user=user,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/profile/<username>/settings", methods=("GET", "POST"))
|
@bp.route("/profile/<username>/settings", methods=("GET", "POST"))
|
||||||
|
|
|
@ -4,6 +4,7 @@ from canaille.app.forms import DateTimeUTCField
|
||||||
from canaille.app.forms import HTMXBaseForm
|
from canaille.app.forms import HTMXBaseForm
|
||||||
from canaille.app.forms import HTMXForm
|
from canaille.app.forms import HTMXForm
|
||||||
from canaille.app.forms import is_uri
|
from canaille.app.forms import is_uri
|
||||||
|
from canaille.app.forms import unique_values
|
||||||
from canaille.app.i18n import native_language_name_from_code
|
from canaille.app.i18n import native_language_name_from_code
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import g
|
from flask import g
|
||||||
|
@ -155,24 +156,32 @@ PROFILE_FORM_FIELDS = dict(
|
||||||
"autocorrect": "off",
|
"autocorrect": "off",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
emails=wtforms.EmailField(
|
emails=wtforms.FieldList(
|
||||||
_("Email address"),
|
wtforms.EmailField(
|
||||||
validators=[
|
_("Email address"),
|
||||||
wtforms.validators.DataRequired(),
|
validators=[
|
||||||
wtforms.validators.Email(),
|
wtforms.validators.DataRequired(),
|
||||||
unique_email,
|
wtforms.validators.Email(),
|
||||||
],
|
unique_email,
|
||||||
description=_(
|
],
|
||||||
"This email will be used as a recovery address to reset the password if needed"
|
description=_(
|
||||||
|
"This email will be used as a recovery address to reset the password if needed"
|
||||||
|
),
|
||||||
|
render_kw={
|
||||||
|
"placeholder": _("jane@doe.com"),
|
||||||
|
"spellcheck": "false",
|
||||||
|
"autocorrect": "off",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
render_kw={
|
min_entries=1,
|
||||||
"placeholder": _("jane@doe.com"),
|
validators=[unique_values],
|
||||||
"spellcheck": "false",
|
|
||||||
"autocorrect": "off",
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
phone_numbers=wtforms.TelField(
|
phone_numbers=wtforms.FieldList(
|
||||||
_("Phone number"), render_kw={"placeholder": _("555-000-555")}
|
wtforms.TelField(
|
||||||
|
_("Phone number"), render_kw={"placeholder": _("555-000-555")}
|
||||||
|
),
|
||||||
|
min_entries=1,
|
||||||
|
validators=[unique_values],
|
||||||
),
|
),
|
||||||
formatted_address=wtforms.StringField(
|
formatted_address=wtforms.StringField(
|
||||||
_("Address"),
|
_("Address"),
|
||||||
|
|
|
@ -3,7 +3,6 @@ import datetime
|
||||||
from canaille.app import models
|
from canaille.app import models
|
||||||
from canaille.app.flask import permissions_needed
|
from canaille.app.flask import permissions_needed
|
||||||
from canaille.app.flask import render_htmx_template
|
from canaille.app.flask import render_htmx_template
|
||||||
from canaille.app.flask import request_is_htmx
|
|
||||||
from canaille.app.forms import TableForm
|
from canaille.app.forms import TableForm
|
||||||
from canaille.oidc.forms import ClientAddForm
|
from canaille.oidc.forms import ClientAddForm
|
||||||
from flask import abort
|
from flask import abort
|
||||||
|
@ -37,7 +36,7 @@ def index(user):
|
||||||
def add(user):
|
def add(user):
|
||||||
form = ClientAddForm(request.form or None)
|
form = ClientAddForm(request.form or None)
|
||||||
|
|
||||||
if not request.form:
|
if not request.form or form.form_control():
|
||||||
return render_template(
|
return render_template(
|
||||||
"oidc/admin/client_add.html", form=form, menuitem="admin"
|
"oidc/admin/client_add.html", form=form, menuitem="admin"
|
||||||
)
|
)
|
||||||
|
@ -57,11 +56,11 @@ def add(user):
|
||||||
client_id=client_id,
|
client_id=client_id,
|
||||||
client_id_issued_at=client_id_issued_at,
|
client_id_issued_at=client_id_issued_at,
|
||||||
client_name=form["client_name"].data,
|
client_name=form["client_name"].data,
|
||||||
contacts=[form["contacts"].data],
|
contacts=form["contacts"].data,
|
||||||
client_uri=form["client_uri"].data,
|
client_uri=form["client_uri"].data,
|
||||||
grant_types=form["grant_types"].data,
|
grant_types=form["grant_types"].data,
|
||||||
redirect_uris=[form["redirect_uris"].data],
|
redirect_uris=form["redirect_uris"].data,
|
||||||
post_logout_redirect_uris=[form["post_logout_redirect_uris"].data],
|
post_logout_redirect_uris=form["post_logout_redirect_uris"].data,
|
||||||
response_types=form["response_types"].data,
|
response_types=form["response_types"].data,
|
||||||
scope=form["scope"].data.split(" "),
|
scope=form["scope"].data.split(" "),
|
||||||
token_endpoint_auth_method=form["token_endpoint_auth_method"].data,
|
token_endpoint_auth_method=form["token_endpoint_auth_method"].data,
|
||||||
|
@ -90,17 +89,9 @@ def add(user):
|
||||||
@bp.route("/edit/<client_id>", methods=["GET", "POST"])
|
@bp.route("/edit/<client_id>", methods=["GET", "POST"])
|
||||||
@permissions_needed("manage_oidc")
|
@permissions_needed("manage_oidc")
|
||||||
def edit(user, client_id):
|
def edit(user, client_id):
|
||||||
if (
|
if request.form and request.form.get("action") == "delete":
|
||||||
request.method == "GET"
|
|
||||||
or request.form.get("action") == "edit"
|
|
||||||
or request_is_htmx()
|
|
||||||
):
|
|
||||||
return client_edit(client_id)
|
|
||||||
|
|
||||||
if request.form.get("action") == "delete":
|
|
||||||
return client_delete(client_id)
|
return client_delete(client_id)
|
||||||
|
return client_edit(client_id)
|
||||||
abort(400)
|
|
||||||
|
|
||||||
|
|
||||||
def client_edit(client_id):
|
def client_edit(client_id):
|
||||||
|
@ -111,17 +102,10 @@ def client_edit(client_id):
|
||||||
|
|
||||||
data = {attribute: getattr(client, attribute) for attribute in client.attributes}
|
data = {attribute: getattr(client, attribute) for attribute in client.attributes}
|
||||||
data["scope"] = " ".join(data["scope"])
|
data["scope"] = " ".join(data["scope"])
|
||||||
data["redirect_uris"] = data["redirect_uris"][0] if data["redirect_uris"] else ""
|
|
||||||
data["contacts"] = data["contacts"][0] if data["contacts"] else ""
|
|
||||||
data["post_logout_redirect_uris"] = (
|
|
||||||
data["post_logout_redirect_uris"][0]
|
|
||||||
if data["post_logout_redirect_uris"]
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
data["preconsent"] = client.preconsent
|
data["preconsent"] = client.preconsent
|
||||||
form = ClientAddForm(request.form or None, data=data, client=client)
|
form = ClientAddForm(request.form or None, data=data, client=client)
|
||||||
|
|
||||||
if not request.form:
|
if not request.form or form.form_control():
|
||||||
return render_template(
|
return render_template(
|
||||||
"oidc/admin/client_edit.html", form=form, client=client, menuitem="admin"
|
"oidc/admin/client_edit.html", form=form, client=client, menuitem="admin"
|
||||||
)
|
)
|
||||||
|
@ -137,11 +121,11 @@ def client_edit(client_id):
|
||||||
|
|
||||||
client.update(
|
client.update(
|
||||||
client_name=form["client_name"].data,
|
client_name=form["client_name"].data,
|
||||||
contacts=[form["contacts"].data],
|
contacts=form["contacts"].data,
|
||||||
client_uri=form["client_uri"].data,
|
client_uri=form["client_uri"].data,
|
||||||
grant_types=form["grant_types"].data,
|
grant_types=form["grant_types"].data,
|
||||||
redirect_uris=[form["redirect_uris"].data],
|
redirect_uris=form["redirect_uris"].data,
|
||||||
post_logout_redirect_uris=[form["post_logout_redirect_uris"].data],
|
post_logout_redirect_uris=form["post_logout_redirect_uris"].data,
|
||||||
response_types=form["response_types"].data,
|
response_types=form["response_types"].data,
|
||||||
scope=form["scope"].data.split(" "),
|
scope=form["scope"].data.split(" "),
|
||||||
token_endpoint_auth_method=form["token_endpoint_auth_method"].data,
|
token_endpoint_auth_method=form["token_endpoint_auth_method"].data,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import wtforms
|
||||||
from canaille.app import models
|
from canaille.app import models
|
||||||
from canaille.app.forms import HTMXForm
|
from canaille.app.forms import HTMXForm
|
||||||
from canaille.app.forms import is_uri
|
from canaille.app.forms import is_uri
|
||||||
|
from canaille.app.forms import unique_values
|
||||||
from flask_babel import lazy_gettext as _
|
from flask_babel import lazy_gettext as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,10 +24,14 @@ class ClientAddForm(HTMXForm):
|
||||||
validators=[wtforms.validators.DataRequired()],
|
validators=[wtforms.validators.DataRequired()],
|
||||||
render_kw={"placeholder": "Client Name"},
|
render_kw={"placeholder": "Client Name"},
|
||||||
)
|
)
|
||||||
contacts = wtforms.EmailField(
|
contacts = wtforms.FieldList(
|
||||||
_("Contact"),
|
wtforms.EmailField(
|
||||||
validators=[wtforms.validators.Optional(), wtforms.validators.Email()],
|
_("Contact"),
|
||||||
render_kw={"placeholder": "admin@mydomain.tld"},
|
validators=[wtforms.validators.Optional(), wtforms.validators.Email()],
|
||||||
|
render_kw={"placeholder": "admin@mydomain.tld"},
|
||||||
|
),
|
||||||
|
min_entries=1,
|
||||||
|
validators=[unique_values],
|
||||||
)
|
)
|
||||||
client_uri = wtforms.URLField(
|
client_uri = wtforms.URLField(
|
||||||
_("URI"),
|
_("URI"),
|
||||||
|
@ -36,21 +41,31 @@ class ClientAddForm(HTMXForm):
|
||||||
],
|
],
|
||||||
render_kw={"placeholder": "https://mydomain.tld"},
|
render_kw={"placeholder": "https://mydomain.tld"},
|
||||||
)
|
)
|
||||||
redirect_uris = wtforms.URLField(
|
redirect_uris = wtforms.FieldList(
|
||||||
_("Redirect URIs"),
|
wtforms.URLField(
|
||||||
validators=[
|
_("Redirect URIs"),
|
||||||
wtforms.validators.DataRequired(),
|
validators=[
|
||||||
is_uri,
|
wtforms.validators.DataRequired(),
|
||||||
],
|
is_uri,
|
||||||
render_kw={"placeholder": "https://mydomain.tld/callback"},
|
],
|
||||||
|
render_kw={"placeholder": "https://mydomain.tld/callback"},
|
||||||
|
),
|
||||||
|
min_entries=1,
|
||||||
|
validators=[unique_values],
|
||||||
)
|
)
|
||||||
post_logout_redirect_uris = wtforms.URLField(
|
post_logout_redirect_uris = wtforms.FieldList(
|
||||||
_("Post logout redirect URIs"),
|
wtforms.URLField(
|
||||||
validators=[
|
_("Post logout redirect URIs"),
|
||||||
wtforms.validators.Optional(),
|
validators=[
|
||||||
is_uri,
|
wtforms.validators.Optional(),
|
||||||
],
|
is_uri,
|
||||||
render_kw={"placeholder": "https://mydomain.tld/you-have-been-disconnected"},
|
],
|
||||||
|
render_kw={
|
||||||
|
"placeholder": "https://mydomain.tld/you-have-been-disconnected"
|
||||||
|
},
|
||||||
|
),
|
||||||
|
min_entries=1,
|
||||||
|
validators=[unique_values],
|
||||||
)
|
)
|
||||||
grant_types = wtforms.SelectMultipleField(
|
grant_types = wtforms.SelectMultipleField(
|
||||||
_("Grant types"),
|
_("Grant types"),
|
||||||
|
|
|
@ -100,6 +100,19 @@ i.massive.massive.massive.portrait.icon, i.massive.massive.massive.portrait.icon
|
||||||
background: rgba(0,0,0,.05) !important;
|
background: rgba(0,0,0,.05) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workaround for
|
||||||
|
* https://github.com/fomantic/Fomantic-UI/issues/2829
|
||||||
|
*/
|
||||||
|
.ui.corner.labeled.action.input .ui.corner.label {
|
||||||
|
right:40px;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.corner.labeled.action.input .ui.button {
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.logo img {
|
.logo img {
|
||||||
filter: invert(.8) !important;
|
filter: invert(.8) !important;
|
||||||
|
|
|
@ -6,7 +6,9 @@ container=true,
|
||||||
noindicator=false,
|
noindicator=false,
|
||||||
indicator_icon=none,
|
indicator_icon=none,
|
||||||
indicator_text=none,
|
indicator_text=none,
|
||||||
display=true
|
display=true,
|
||||||
|
add_button=false,
|
||||||
|
del_button=false
|
||||||
) -%}
|
) -%}
|
||||||
{% set field_visible = field.type != 'HiddenField' and field.type !='CSRFTokenField' %}
|
{% set field_visible = field.type != 'HiddenField' and field.type !='CSRFTokenField' %}
|
||||||
{% if container and field_visible %}
|
{% if container and field_visible %}
|
||||||
|
@ -33,6 +35,7 @@ display=true
|
||||||
<div class="ui
|
<div class="ui
|
||||||
{%- if corner_indicator %} corner labeled{% endif -%}
|
{%- if corner_indicator %} corner labeled{% endif -%}
|
||||||
{%- if icon or field.description %} left icon{% endif -%}
|
{%- if icon or field.description %} left icon{% endif -%}
|
||||||
|
{%- if add_button or del_button %} action{% endif -%}
|
||||||
{%- if field.type not in ("BooleanField", "RadioField") %} input{% endif -%}
|
{%- if field.type not in ("BooleanField", "RadioField") %} input{% endif -%}
|
||||||
">
|
">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -66,7 +69,38 @@ display=true
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if field_visible %}
|
{% if field_visible %}
|
||||||
|
{% if del_button %}
|
||||||
|
<button
|
||||||
|
class="ui teal icon button"
|
||||||
|
title="{{ _("Remove this field") }}"
|
||||||
|
type="submit"
|
||||||
|
name="fieldlist_remove"
|
||||||
|
value="{{ field.name }}"
|
||||||
|
hx-post=""
|
||||||
|
{# Workaround for https://github.com/bigskysoftware/htmx/issues/1506 #}
|
||||||
|
hx-vals='{"fieldlist_remove": "{{ field.name }}"}'
|
||||||
|
hx-target="closest .fieldlist"
|
||||||
|
formnovalidate>
|
||||||
|
<i class="minus icon"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if add_button %}
|
||||||
|
<button
|
||||||
|
class="ui teal icon button"
|
||||||
|
title="{{ _("Add another field") }}"
|
||||||
|
type="submit"
|
||||||
|
name="fieldlist_add"
|
||||||
|
value="{{ field.name }}"
|
||||||
|
hx-post=""
|
||||||
|
{# Workaround for https://github.com/bigskysoftware/htmx/issues/1506 #}
|
||||||
|
hx-vals='{"fieldlist_add": "{{ field.name }}"}'
|
||||||
|
hx-target="closest .fieldlist"
|
||||||
|
formnovalidate>
|
||||||
|
<i class="plus icon"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -90,14 +124,39 @@ display=true
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_field(field) -%}
|
{% macro render_field(field, parent_list=none, parent_indice=none) -%}
|
||||||
{% if field.type == "BooleanField" %}
|
{% if parent_list %}
|
||||||
|
{% set last = parent_indice >= parent_list.entries|len -1 %}
|
||||||
|
{% set ignore_me = kwargs.update({
|
||||||
|
"label_visible": false,
|
||||||
|
"add_button": (last and (not parent_list.max_entries or parent_indice < parent_list.max_entries)),
|
||||||
|
"del_button": (last and parent_list.min_entries and parent_indice >= parent_list.min_entries),
|
||||||
|
}) %}
|
||||||
|
{% endif %}
|
||||||
|
{% if field.type == "FieldList" %}
|
||||||
|
{{ render_list(field, **kwargs) }}
|
||||||
|
{% elif field.type == "BooleanField" %}
|
||||||
{{ render_checkbox(field, **kwargs) }}
|
{{ render_checkbox(field, **kwargs) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ render_input(field, **kwargs) }}
|
{{ render_input(field, **kwargs) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro render_list(field) -%}
|
||||||
|
<div class="field fieldlist" id="{{ field.name }}">
|
||||||
|
{# Strangely enough, translations are not rendered when using field.label() #}
|
||||||
|
{{ field[0].label() }}
|
||||||
|
{% for subfield in field %}
|
||||||
|
{{ render_field(
|
||||||
|
subfield,
|
||||||
|
parent_list=field,
|
||||||
|
parent_indice=loop.index0,
|
||||||
|
**kwargs
|
||||||
|
) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro render_checkbox(field, display=true) -%}
|
{% macro render_checkbox(field, display=true) -%}
|
||||||
<div class="field"
|
<div class="field"
|
||||||
{% if not display %}style="display: none"{% endif %}
|
{% if not display %}style="display: none"{% endif %}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import datetime
|
||||||
|
|
||||||
import wtforms
|
import wtforms
|
||||||
from babel.dates import LOCALTZ
|
from babel.dates import LOCALTZ
|
||||||
|
from canaille.app import models
|
||||||
from canaille.app.forms import DateTimeUTCField
|
from canaille.app.forms import DateTimeUTCField
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from werkzeug.datastructures import ImmutableMultiDict
|
from werkzeug.datastructures import ImmutableMultiDict
|
||||||
|
@ -184,3 +185,262 @@ def test_datetime_utc_field_invalid_timezone(testclient):
|
||||||
form.dt()
|
form.dt()
|
||||||
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
|
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fieldlist_add(testclient, logged_admin):
|
||||||
|
assert not models.Client.query()
|
||||||
|
|
||||||
|
res = testclient.get("/admin/client/add")
|
||||||
|
assert "redirect_uris-1" not in res.form.fields
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"client_name": "foobar",
|
||||||
|
"client_uri": "https://foo.bar",
|
||||||
|
"redirect_uris-0": "https://foo.bar/callback",
|
||||||
|
"grant_types": ["password", "authorization_code"],
|
||||||
|
"response_types": ["code", "token"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
}
|
||||||
|
for k, v in data.items():
|
||||||
|
res.form[k].force_value(v)
|
||||||
|
|
||||||
|
res = res.form.submit(status=200, name="fieldlist_add", value="redirect_uris-0")
|
||||||
|
assert not models.Client.query()
|
||||||
|
|
||||||
|
data["redirect_uris-1"] = "https://foo.bar/callback2"
|
||||||
|
for k, v in data.items():
|
||||||
|
res.form[k].force_value(v)
|
||||||
|
|
||||||
|
res = res.form.submit(status=302, name="action", value="edit")
|
||||||
|
res = res.follow(status=200)
|
||||||
|
|
||||||
|
client_id = res.forms["readonly"]["client_id"].value
|
||||||
|
client = models.Client.get(client_id=client_id)
|
||||||
|
|
||||||
|
assert client.redirect_uris == [
|
||||||
|
"https://foo.bar/callback",
|
||||||
|
"https://foo.bar/callback2",
|
||||||
|
]
|
||||||
|
|
||||||
|
client.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fieldlist_delete(testclient, logged_admin):
|
||||||
|
assert not models.Client.query()
|
||||||
|
res = testclient.get("/admin/client/add")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"client_name": "foobar",
|
||||||
|
"client_uri": "https://foo.bar",
|
||||||
|
"redirect_uris-0": "https://foo.bar/callback1",
|
||||||
|
"grant_types": ["password", "authorization_code"],
|
||||||
|
"response_types": ["code", "token"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
}
|
||||||
|
for k, v in data.items():
|
||||||
|
res.form[k].force_value(v)
|
||||||
|
res = res.form.submit(status=200, name="fieldlist_add", value="redirect_uris-0")
|
||||||
|
|
||||||
|
res.form["redirect_uris-1"] = "https://foo.bar/callback2"
|
||||||
|
res = res.form.submit(status=200, name="fieldlist_remove", value="redirect_uris-1")
|
||||||
|
assert not models.Client.query()
|
||||||
|
assert "redirect_uris-1" not in res.form.fields
|
||||||
|
|
||||||
|
res = res.form.submit(status=302, name="action", value="edit")
|
||||||
|
res = res.follow(status=200)
|
||||||
|
|
||||||
|
client_id = res.forms["readonly"]["client_id"].value
|
||||||
|
client = models.Client.get(client_id=client_id)
|
||||||
|
|
||||||
|
assert client.redirect_uris == [
|
||||||
|
"https://foo.bar/callback1",
|
||||||
|
]
|
||||||
|
|
||||||
|
client.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fieldlist_add_invalid_field(testclient, logged_admin):
|
||||||
|
res = testclient.get("/admin/client/add")
|
||||||
|
data = {
|
||||||
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
|
"client_name": "foobar",
|
||||||
|
"client_uri": "https://foo.bar",
|
||||||
|
"redirect_uris-0": "https://foo.bar/callback",
|
||||||
|
"grant_types": ["password", "authorization_code"],
|
||||||
|
"response_types": ["code", "token"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
"fieldlist_add": "invalid",
|
||||||
|
}
|
||||||
|
testclient.post("/admin/client/add", data, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fieldlist_delete_invalid_field(testclient, logged_admin):
|
||||||
|
assert not models.Client.query()
|
||||||
|
res = testclient.get("/admin/client/add")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
|
"client_name": "foobar",
|
||||||
|
"client_uri": "https://foo.bar",
|
||||||
|
"redirect_uris-0": "https://foo.bar/callback1",
|
||||||
|
"redirect_uris-1": "https://foo.bar/callback2",
|
||||||
|
"grant_types": ["password", "authorization_code"],
|
||||||
|
"response_types": ["code", "token"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
"fieldlist_remove": "invalid",
|
||||||
|
}
|
||||||
|
testclient.post("/admin/client/add", data, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fieldlist_duplicate_value(testclient, logged_admin, client):
|
||||||
|
res = testclient.get("/admin/client/add")
|
||||||
|
data = {
|
||||||
|
"client_name": "foobar",
|
||||||
|
"client_uri": "https://foo.bar",
|
||||||
|
"redirect_uris-0": "https://foo.bar/samecallback",
|
||||||
|
"grant_types": ["password", "authorization_code"],
|
||||||
|
"response_types": ["code", "token"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
}
|
||||||
|
for k, v in data.items():
|
||||||
|
res.form[k].force_value(v)
|
||||||
|
res = res.form.submit(status=200, name="fieldlist_add", value="redirect_uris-0")
|
||||||
|
res.form["redirect_uris-1"] = "https://foo.bar/samecallback"
|
||||||
|
res = res.form.submit(status=200, name="action", value="edit")
|
||||||
|
res.mustcontain("This value is a duplicate")
|
||||||
|
|
||||||
|
|
||||||
|
def test_fieldlist_empty_value(testclient, logged_admin, client):
|
||||||
|
res = testclient.get("/admin/client/add")
|
||||||
|
data = {
|
||||||
|
"client_name": "foobar",
|
||||||
|
"client_uri": "https://foo.bar",
|
||||||
|
"redirect_uris-0": "https://foo.bar/samecallback",
|
||||||
|
"post_logout_redirect_uris-0": "https://foo.bar/callback1",
|
||||||
|
"grant_types": ["password", "authorization_code"],
|
||||||
|
"response_types": ["code", "token"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
}
|
||||||
|
for k, v in data.items():
|
||||||
|
res.form[k].force_value(v)
|
||||||
|
res = res.form.submit(
|
||||||
|
status=200, name="fieldlist_add", value="post_logout_redirect_uris-0"
|
||||||
|
)
|
||||||
|
res.form.submit(status=302, name="action", value="edit")
|
||||||
|
client = models.Client.get()
|
||||||
|
client.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fieldlist_add_field_htmx(testclient, logged_admin):
|
||||||
|
res = testclient.get("/admin/client/add")
|
||||||
|
data = {
|
||||||
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
|
"client_name": "foobar",
|
||||||
|
"client_uri": "https://foo.bar",
|
||||||
|
"redirect_uris-0": "https://foo.bar/callback",
|
||||||
|
"grant_types": ["password", "authorization_code"],
|
||||||
|
"response_types": ["code", "token"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
"fieldlist_add": "redirect_uris-0",
|
||||||
|
}
|
||||||
|
response = testclient.post(
|
||||||
|
"/admin/client/add",
|
||||||
|
data,
|
||||||
|
headers={
|
||||||
|
"HX-Request": "true",
|
||||||
|
"HX-Trigger-Name": "listfield_add",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert 'name="redirect_uris-0' in response.text
|
||||||
|
assert 'name="redirect_uris-1' in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_fieldlist_add_field_htmx_validation(testclient, logged_admin):
|
||||||
|
res = testclient.get("/admin/client/add")
|
||||||
|
data = {
|
||||||
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
|
"client_name": "foobar",
|
||||||
|
"client_uri": "https://foo.bar",
|
||||||
|
"redirect_uris-0": "not-a-valid-uri",
|
||||||
|
"grant_types": ["password", "authorization_code"],
|
||||||
|
"response_types": ["code", "token"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
"fieldlist_add": "redirect_uris-0",
|
||||||
|
}
|
||||||
|
response = testclient.post(
|
||||||
|
"/admin/client/add",
|
||||||
|
data,
|
||||||
|
headers={
|
||||||
|
"HX-Request": "true",
|
||||||
|
"HX-Trigger-Name": "listfield_add",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert 'name="redirect_uris-0' in response.text
|
||||||
|
assert 'name="redirect_uris-1' in response.text
|
||||||
|
assert "This is not a valid URL" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_fieldlist_remove_field_htmx(testclient, logged_admin):
|
||||||
|
res = testclient.get("/admin/client/add")
|
||||||
|
data = {
|
||||||
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
|
"client_name": "foobar",
|
||||||
|
"client_uri": "https://foo.bar",
|
||||||
|
"redirect_uris-0": "https://foo.bar/callback1",
|
||||||
|
"redirect_uris-1": "https://foo.bar/callback2",
|
||||||
|
"grant_types": ["password", "authorization_code"],
|
||||||
|
"response_types": ["code", "token"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
"fieldlist_remove": "redirect_uris-1",
|
||||||
|
}
|
||||||
|
response = testclient.post(
|
||||||
|
"/admin/client/add",
|
||||||
|
data,
|
||||||
|
headers={
|
||||||
|
"HX-Request": "true",
|
||||||
|
"HX-Trigger-Name": "listfield_remove",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert 'name="redirect_uris-0' in response.text
|
||||||
|
assert 'name="redirect_uris-1' not in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_fieldlist_inline_validation(testclient, logged_admin):
|
||||||
|
res = testclient.get("/admin/client/add")
|
||||||
|
data = {
|
||||||
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
|
"client_name": "foobar",
|
||||||
|
"client_uri": "https://foo.bar",
|
||||||
|
"redirect_uris-0": "invalid-url",
|
||||||
|
"redirect_uris-1": "https://foo.bar/callback2",
|
||||||
|
"grant_types": ["password", "authorization_code"],
|
||||||
|
"response_types": ["code", "token"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
}
|
||||||
|
response = testclient.post(
|
||||||
|
"/admin/client/add",
|
||||||
|
data,
|
||||||
|
headers={
|
||||||
|
"HX-Request": "true",
|
||||||
|
"HX-Trigger-Name": "redirect_uris-0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert 'name="redirect_uris-0' in response.text
|
||||||
|
assert 'name="redirect_uris-1' not in response.text
|
||||||
|
assert "This is not a valid URL" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_inline_validation_invalid_field(testclient, logged_admin, user):
|
||||||
|
res = testclient.get("/profile")
|
||||||
|
testclient.post(
|
||||||
|
"/profile",
|
||||||
|
{
|
||||||
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
|
"email": "john@doe.com",
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
"HX-Request": "true",
|
||||||
|
"HX-Trigger-Name": "invalid-field",
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
|
@ -50,7 +50,7 @@ def test_form_translations(testclient, logged_user):
|
||||||
logged_user.save()
|
logged_user.save()
|
||||||
|
|
||||||
res = testclient.get("/profile/user", status=200)
|
res = testclient.get("/profile/user", status=200)
|
||||||
res.form["emails"] = "invalid"
|
res.form["emails-0"] = "invalid"
|
||||||
res = res.form.submit(name="action", value="edit")
|
res = res.form.submit(name="action", value="edit")
|
||||||
|
|
||||||
res.mustcontain(no="Invalid email address.")
|
res.mustcontain(no="Invalid email address.")
|
||||||
|
|
|
@ -26,7 +26,7 @@ def test_invitation(testclient, logged_admin, foo_group, smtpd):
|
||||||
|
|
||||||
assert res.form["user_name"].value == "someone"
|
assert res.form["user_name"].value == "someone"
|
||||||
assert res.form["user_name"].attrs["readonly"]
|
assert res.form["user_name"].attrs["readonly"]
|
||||||
assert res.form["emails"].value == "someone@domain.tld"
|
assert res.form["emails-0"].value == "someone@domain.tld"
|
||||||
assert res.form["groups"].value == [foo_group.id]
|
assert res.form["groups"].value == [foo_group.id]
|
||||||
|
|
||||||
res.form["password1"] = "whatever"
|
res.form["password1"] = "whatever"
|
||||||
|
@ -75,7 +75,7 @@ def test_invitation_editable_user_name(testclient, logged_admin, foo_group, smtp
|
||||||
|
|
||||||
assert res.form["user_name"].value == "jackyjack"
|
assert res.form["user_name"].value == "jackyjack"
|
||||||
assert "readonly" not in res.form["user_name"].attrs
|
assert "readonly" not in res.form["user_name"].attrs
|
||||||
assert res.form["emails"].value == "jackyjack@domain.tld"
|
assert res.form["emails-0"].value == "jackyjack@domain.tld"
|
||||||
assert res.form["groups"].value == [foo_group.id]
|
assert res.form["groups"].value == [foo_group.id]
|
||||||
|
|
||||||
res.form["user_name"] = "djorje"
|
res.form["user_name"] = "djorje"
|
||||||
|
@ -120,7 +120,7 @@ def test_generate_link(testclient, logged_admin, foo_group, smtpd):
|
||||||
res = testclient.get(url, status=200)
|
res = testclient.get(url, status=200)
|
||||||
|
|
||||||
assert res.form["user_name"].value == "sometwo"
|
assert res.form["user_name"].value == "sometwo"
|
||||||
assert res.form["emails"].value == "sometwo@domain.tld"
|
assert res.form["emails-0"].value == "sometwo@domain.tld"
|
||||||
assert res.form["groups"].value == [foo_group.id]
|
assert res.form["groups"].value == [foo_group.id]
|
||||||
|
|
||||||
res.form["password1"] = "whatever"
|
res.form["password1"] = "whatever"
|
||||||
|
@ -169,6 +169,24 @@ def test_registration(testclient, foo_group):
|
||||||
testclient.get(f"/register/{b64}/{hash}", status=200)
|
testclient.get(f"/register/{b64}/{hash}", status=200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_registration_formcontrol(testclient):
|
||||||
|
invitation = Invitation(
|
||||||
|
datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||||
|
"someoneelse",
|
||||||
|
False,
|
||||||
|
"someone@mydomain.tld",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
b64 = invitation.b64()
|
||||||
|
hash = invitation.profile_hash()
|
||||||
|
|
||||||
|
res = testclient.get(f"/register/{b64}/{hash}", status=200)
|
||||||
|
assert "emails-1" not in res.form.fields
|
||||||
|
|
||||||
|
res = res.form.submit(status=200, name="fieldlist_add", value="emails-0")
|
||||||
|
assert "emails-1" in res.form.fields
|
||||||
|
|
||||||
|
|
||||||
def test_registration_invalid_hash(testclient, foo_group):
|
def test_registration_invalid_hash(testclient, foo_group):
|
||||||
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
invitation = Invitation(
|
invitation = Invitation(
|
||||||
|
|
|
@ -14,8 +14,8 @@ def test_user_creation_edition_and_deletion(
|
||||||
res.form["user_name"] = "george"
|
res.form["user_name"] = "george"
|
||||||
res.form["given_name"] = "George"
|
res.form["given_name"] = "George"
|
||||||
res.form["family_name"] = "Abitbol"
|
res.form["family_name"] = "Abitbol"
|
||||||
res.form["emails"] = "george@abitbol.com"
|
res.form["emails-0"] = "george@abitbol.com"
|
||||||
res.form["phone_numbers"] = "555-666-888"
|
res.form["phone_numbers-0"] = "555-666-888"
|
||||||
res.form["groups"] = [foo_group.id]
|
res.form["groups"] = [foo_group.id]
|
||||||
res.form["password1"] = "totoyolo"
|
res.form["password1"] = "totoyolo"
|
||||||
res.form["password2"] = "totoyolo"
|
res.form["password2"] = "totoyolo"
|
||||||
|
@ -70,39 +70,21 @@ def test_profile_creation_dynamic_validation(testclient, logged_admin, user):
|
||||||
"/profile",
|
"/profile",
|
||||||
{
|
{
|
||||||
"csrf_token": res.form["csrf_token"].value,
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
"emails": "john@doe.com",
|
"emails-0": "john@doe.com",
|
||||||
},
|
},
|
||||||
headers={
|
headers={
|
||||||
"HX-Request": "true",
|
"HX-Request": "true",
|
||||||
"HX-Trigger-Name": "emails",
|
"HX-Trigger-Name": "emails-0",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
res.mustcontain("The email 'john@doe.com' is already used")
|
res.mustcontain("The email 'john@doe.com' is already used")
|
||||||
|
|
||||||
|
|
||||||
def test_profile_creation_dynamic_validation_invalid_field(
|
|
||||||
testclient, logged_admin, user
|
|
||||||
):
|
|
||||||
res = testclient.get("/profile")
|
|
||||||
testclient.post(
|
|
||||||
"/profile",
|
|
||||||
{
|
|
||||||
"csrf_token": res.form["csrf_token"].value,
|
|
||||||
"email": "john@doe.com",
|
|
||||||
},
|
|
||||||
headers={
|
|
||||||
"HX-Request": "true",
|
|
||||||
"HX-Trigger-Name": "invalid-field",
|
|
||||||
},
|
|
||||||
status=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_creation_without_password(testclient, logged_moderator):
|
def test_user_creation_without_password(testclient, logged_moderator):
|
||||||
res = testclient.get("/profile", status=200)
|
res = testclient.get("/profile", status=200)
|
||||||
res.form["user_name"] = "george"
|
res.form["user_name"] = "george"
|
||||||
res.form["family_name"] = "Abitbol"
|
res.form["family_name"] = "Abitbol"
|
||||||
res.form["emails"] = "george@abitbol.com"
|
res.form["emails-0"] = "george@abitbol.com"
|
||||||
|
|
||||||
res = res.form.submit(name="action", value="edit", status=302)
|
res = res.form.submit(name="action", value="edit", status=302)
|
||||||
assert ("success", "User account creation succeed.") in res.flashes
|
assert ("success", "User account creation succeed.") in res.flashes
|
||||||
|
@ -133,7 +115,7 @@ def test_username_already_taken(
|
||||||
res = testclient.get("/profile", status=200)
|
res = testclient.get("/profile", status=200)
|
||||||
res.form["user_name"] = "user"
|
res.form["user_name"] = "user"
|
||||||
res.form["family_name"] = "foo"
|
res.form["family_name"] = "foo"
|
||||||
res.form["emails"] = "any@thing.com"
|
res.form["emails-0"] = "any@thing.com"
|
||||||
res = res.form.submit(name="action", value="edit")
|
res = res.form.submit(name="action", value="edit")
|
||||||
assert ("error", "User account creation failed.") in res.flashes
|
assert ("error", "User account creation failed.") in res.flashes
|
||||||
res.mustcontain("The login 'user' already exists")
|
res.mustcontain("The login 'user' already exists")
|
||||||
|
@ -143,7 +125,7 @@ def test_email_already_taken(testclient, logged_moderator, user, foo_group, bar_
|
||||||
res = testclient.get("/profile", status=200)
|
res = testclient.get("/profile", status=200)
|
||||||
res.form["user_name"] = "user2"
|
res.form["user_name"] = "user2"
|
||||||
res.form["family_name"] = "foo"
|
res.form["family_name"] = "foo"
|
||||||
res.form["emails"] = "john@doe.com"
|
res.form["emails-0"] = "john@doe.com"
|
||||||
res = res.form.submit(name="action", value="edit")
|
res = res.form.submit(name="action", value="edit")
|
||||||
assert ("error", "User account creation failed.") in res.flashes
|
assert ("error", "User account creation failed.") in res.flashes
|
||||||
res.mustcontain("The email 'john@doe.com' is already used")
|
res.mustcontain("The email 'john@doe.com' is already used")
|
||||||
|
@ -154,7 +136,7 @@ def test_cn_setting_with_given_name_and_surname(testclient, logged_moderator):
|
||||||
res.form["user_name"] = "george"
|
res.form["user_name"] = "george"
|
||||||
res.form["given_name"] = "George"
|
res.form["given_name"] = "George"
|
||||||
res.form["family_name"] = "Abitbol"
|
res.form["family_name"] = "Abitbol"
|
||||||
res.form["emails"] = "george@abitbol.com"
|
res.form["emails-0"] = "george@abitbol.com"
|
||||||
|
|
||||||
res = res.form.submit(name="action", value="edit", status=302).follow(status=200)
|
res = res.form.submit(name="action", value="edit", status=302).follow(status=200)
|
||||||
|
|
||||||
|
@ -167,10 +149,38 @@ def test_cn_setting_with_surname_only(testclient, logged_moderator):
|
||||||
res = testclient.get("/profile", status=200)
|
res = testclient.get("/profile", status=200)
|
||||||
res.form["user_name"] = "george"
|
res.form["user_name"] = "george"
|
||||||
res.form["family_name"] = "Abitbol"
|
res.form["family_name"] = "Abitbol"
|
||||||
res.form["emails"] = "george@abitbol.com"
|
res.form["emails-0"] = "george@abitbol.com"
|
||||||
|
|
||||||
res = res.form.submit(name="action", value="edit", status=302).follow(status=200)
|
res = res.form.submit(name="action", value="edit", status=302).follow(status=200)
|
||||||
|
|
||||||
george = models.User.get_from_login("george")
|
george = models.User.get_from_login("george")
|
||||||
assert george.formatted_name[0] == "Abitbol"
|
assert george.formatted_name[0] == "Abitbol"
|
||||||
george.delete()
|
george.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def test_formcontrol(testclient, logged_admin):
|
||||||
|
res = testclient.get("/profile")
|
||||||
|
assert "emails-1" not in res.form.fields
|
||||||
|
|
||||||
|
res = res.form.submit(status=200, name="fieldlist_add", value="emails-0")
|
||||||
|
assert "emails-1" in res.form.fields
|
||||||
|
|
||||||
|
|
||||||
|
def test_formcontrol_htmx(testclient, logged_admin):
|
||||||
|
res = testclient.get("/profile")
|
||||||
|
data = {
|
||||||
|
field: res.form[field].value
|
||||||
|
for field in res.form.fields
|
||||||
|
if len(res.form.fields.get(field)) == 1
|
||||||
|
}
|
||||||
|
data["fieldlist_add"] = "emails-0"
|
||||||
|
response = testclient.post(
|
||||||
|
"/profile",
|
||||||
|
data,
|
||||||
|
headers={
|
||||||
|
"HX-Request": "true",
|
||||||
|
"HX-Trigger-Name": "listfield_add",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert "emails-0" in response.text
|
||||||
|
assert "emails-1" in response.text
|
||||||
|
|
|
@ -104,8 +104,8 @@ def test_edition(
|
||||||
res.form["given_name"] = "given_name"
|
res.form["given_name"] = "given_name"
|
||||||
res.form["family_name"] = "family_name"
|
res.form["family_name"] = "family_name"
|
||||||
res.form["display_name"] = "display_name"
|
res.form["display_name"] = "display_name"
|
||||||
res.form["emails"] = "email@mydomain.tld"
|
res.form["emails-0"] = "email@mydomain.tld"
|
||||||
res.form["phone_numbers"] = "555-666-777"
|
res.form["phone_numbers-0"] = "555-666-777"
|
||||||
res.form["formatted_address"] = "formatted_address"
|
res.form["formatted_address"] = "formatted_address"
|
||||||
res.form["street"] = "street"
|
res.form["street"] = "street"
|
||||||
res.form["postal_code"] = "postal_code"
|
res.form["postal_code"] = "postal_code"
|
||||||
|
@ -158,7 +158,7 @@ def test_edition_remove_fields(
|
||||||
):
|
):
|
||||||
res = testclient.get("/profile/user", status=200)
|
res = testclient.get("/profile/user", status=200)
|
||||||
res.form["display_name"] = ""
|
res.form["display_name"] = ""
|
||||||
res.form["phone_numbers"] = ""
|
res.form["phone_numbers-0"] = ""
|
||||||
|
|
||||||
res = res.form.submit(name="action", value="edit")
|
res = res.form.submit(name="action", value="edit")
|
||||||
assert res.flashes == [("success", "Profile updated successfully.")], res.text
|
assert res.flashes == [("success", "Profile updated successfully.")], res.text
|
||||||
|
@ -183,11 +183,11 @@ def test_profile_edition_dynamic_validation(testclient, logged_admin, user):
|
||||||
"/profile/admin",
|
"/profile/admin",
|
||||||
{
|
{
|
||||||
"csrf_token": res.form["csrf_token"].value,
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
"emails": "john@doe.com",
|
"emails-0": "john@doe.com",
|
||||||
},
|
},
|
||||||
headers={
|
headers={
|
||||||
"HX-Request": "true",
|
"HX-Request": "true",
|
||||||
"HX-Trigger-Name": "emails",
|
"HX-Trigger-Name": "emails-0",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
res.mustcontain("The email 'john@doe.com' is already used")
|
res.mustcontain("The email 'john@doe.com' is already used")
|
||||||
|
@ -205,13 +205,13 @@ def test_field_permissions_none(testclient, logged_user):
|
||||||
}
|
}
|
||||||
|
|
||||||
res = testclient.get("/profile/user", status=200)
|
res = testclient.get("/profile/user", status=200)
|
||||||
assert "phone_numbers" not in res.form.fields
|
assert "phone_numbers-0" not in res.form.fields
|
||||||
|
|
||||||
testclient.post(
|
testclient.post(
|
||||||
"/profile/user",
|
"/profile/user",
|
||||||
{
|
{
|
||||||
"action": "edit",
|
"action": "edit",
|
||||||
"phone_numbers": "000-000-000",
|
"phone_numbers-0": "000-000-000",
|
||||||
"csrf_token": res.form["csrf_token"].value,
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -230,13 +230,13 @@ def test_field_permissions_read(testclient, logged_user):
|
||||||
"PERMISSIONS": ["edit_self"],
|
"PERMISSIONS": ["edit_self"],
|
||||||
}
|
}
|
||||||
res = testclient.get("/profile/user", status=200)
|
res = testclient.get("/profile/user", status=200)
|
||||||
assert "phone_numbers" in res.form.fields
|
assert "phone_numbers-0" in res.form.fields
|
||||||
|
|
||||||
testclient.post(
|
testclient.post(
|
||||||
"/profile/user",
|
"/profile/user",
|
||||||
{
|
{
|
||||||
"action": "edit",
|
"action": "edit",
|
||||||
"phone_numbers": "000-000-000",
|
"phone_numbers-0": "000-000-000",
|
||||||
"csrf_token": res.form["csrf_token"].value,
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -255,13 +255,13 @@ def test_field_permissions_write(testclient, logged_user):
|
||||||
"PERMISSIONS": ["edit_self"],
|
"PERMISSIONS": ["edit_self"],
|
||||||
}
|
}
|
||||||
res = testclient.get("/profile/user", status=200)
|
res = testclient.get("/profile/user", status=200)
|
||||||
assert "phone_numbers" in res.form.fields
|
assert "phone_numbers-0" in res.form.fields
|
||||||
|
|
||||||
testclient.post(
|
testclient.post(
|
||||||
"/profile/user",
|
"/profile/user",
|
||||||
{
|
{
|
||||||
"action": "edit",
|
"action": "edit",
|
||||||
"phone_numbers": "000-000-000",
|
"phone_numbers-0": "000-000-000",
|
||||||
"csrf_token": res.form["csrf_token"].value,
|
"csrf_token": res.form["csrf_token"].value,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -292,7 +292,7 @@ def test_admin_bad_request(testclient, logged_moderator):
|
||||||
def test_bad_email(testclient, logged_user):
|
def test_bad_email(testclient, logged_user):
|
||||||
res = testclient.get("/profile/user", status=200)
|
res = testclient.get("/profile/user", status=200)
|
||||||
|
|
||||||
res.form["emails"] = "john@doe.com"
|
res.form["emails-0"] = "john@doe.com"
|
||||||
|
|
||||||
res = res.form.submit(name="action", value="edit").follow()
|
res = res.form.submit(name="action", value="edit").follow()
|
||||||
|
|
||||||
|
@ -300,7 +300,7 @@ def test_bad_email(testclient, logged_user):
|
||||||
|
|
||||||
res = testclient.get("/profile/user", status=200)
|
res = testclient.get("/profile/user", status=200)
|
||||||
|
|
||||||
res.form["emails"] = "yolo"
|
res.form["emails-0"] = "yolo"
|
||||||
|
|
||||||
res = res.form.submit(name="action", value="edit", status=200)
|
res = res.form.submit(name="action", value="edit", status=200)
|
||||||
|
|
||||||
|
@ -320,3 +320,31 @@ def test_surname_is_mandatory(testclient, logged_user):
|
||||||
logged_user.reload()
|
logged_user.reload()
|
||||||
|
|
||||||
assert ["Doe"] == logged_user.family_name
|
assert ["Doe"] == logged_user.family_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_formcontrol(testclient, logged_user):
|
||||||
|
res = testclient.get("/profile/user")
|
||||||
|
assert "emails-1" not in res.form.fields
|
||||||
|
|
||||||
|
res = res.form.submit(status=200, name="fieldlist_add", value="emails-0")
|
||||||
|
assert "emails-1" in res.form.fields
|
||||||
|
|
||||||
|
|
||||||
|
def test_formcontrol_htmx(testclient, logged_user):
|
||||||
|
res = testclient.get("/profile/user")
|
||||||
|
data = {
|
||||||
|
field: res.form[field].value
|
||||||
|
for field in res.form.fields
|
||||||
|
if len(res.form.fields.get(field)) == 1
|
||||||
|
}
|
||||||
|
data["fieldlist_add"] = "emails-0"
|
||||||
|
response = testclient.post(
|
||||||
|
"/profile/user",
|
||||||
|
data,
|
||||||
|
headers={
|
||||||
|
"HX-Request": "true",
|
||||||
|
"HX-Trigger-Name": "listfield_add",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert "emails-0" in response.text
|
||||||
|
assert "emails-1" in response.text
|
||||||
|
|
|
@ -108,7 +108,7 @@ def test_photo_on_profile_creation(testclient, jpeg_photo, logged_admin):
|
||||||
res.form["photo"] = Upload("logo.jpg", jpeg_photo)
|
res.form["photo"] = Upload("logo.jpg", jpeg_photo)
|
||||||
res.form["user_name"] = "foobar"
|
res.form["user_name"] = "foobar"
|
||||||
res.form["family_name"] = "Abitbol"
|
res.form["family_name"] = "Abitbol"
|
||||||
res.form["emails"] = "george@abitbol.com"
|
res.form["emails-0"] = "george@abitbol.com"
|
||||||
res = res.form.submit(name="action", value="edit", status=302).follow(status=200)
|
res = res.form.submit(name="action", value="edit", status=302).follow(status=200)
|
||||||
|
|
||||||
user = models.User.get_from_login("foobar")
|
user = models.User.get_from_login("foobar")
|
||||||
|
@ -126,7 +126,7 @@ def test_photo_deleted_on_profile_creation(testclient, jpeg_photo, logged_admin)
|
||||||
res.form["photo_delete"] = True
|
res.form["photo_delete"] = True
|
||||||
res.form["user_name"] = "foobar"
|
res.form["user_name"] = "foobar"
|
||||||
res.form["family_name"] = "Abitbol"
|
res.form["family_name"] = "Abitbol"
|
||||||
res.form["emails"] = "george@abitbol.com"
|
res.form["emails-0"] = "george@abitbol.com"
|
||||||
res = res.form.submit(name="action", value="edit", status=302).follow(status=200)
|
res = res.form.submit(name="action", value="edit", status=302).follow(status=200)
|
||||||
|
|
||||||
user = models.User.get_from_login("foobar")
|
user = models.User.get_from_login("foobar")
|
||||||
|
|
|
@ -87,9 +87,9 @@ def test_client_add(testclient, logged_admin):
|
||||||
res = testclient.get("/admin/client/add")
|
res = testclient.get("/admin/client/add")
|
||||||
data = {
|
data = {
|
||||||
"client_name": "foobar",
|
"client_name": "foobar",
|
||||||
"contacts": "foo@bar.com",
|
"contacts-0": "foo@bar.com",
|
||||||
"client_uri": "https://foo.bar",
|
"client_uri": "https://foo.bar",
|
||||||
"redirect_uris": ["https://foo.bar/callback"],
|
"redirect_uris-0": "https://foo.bar/callback",
|
||||||
"grant_types": ["password", "authorization_code"],
|
"grant_types": ["password", "authorization_code"],
|
||||||
"scope": "openid profile",
|
"scope": "openid profile",
|
||||||
"response_types": ["code", "token"],
|
"response_types": ["code", "token"],
|
||||||
|
@ -103,12 +103,12 @@ def test_client_add(testclient, logged_admin):
|
||||||
"jwks_uri": "https://foo.bar/jwks.json",
|
"jwks_uri": "https://foo.bar/jwks.json",
|
||||||
"audience": [],
|
"audience": [],
|
||||||
"preconsent": False,
|
"preconsent": False,
|
||||||
"post_logout_redirect_uris": ["https://foo.bar/disconnected"],
|
"post_logout_redirect_uris-0": "https://foo.bar/disconnected",
|
||||||
}
|
}
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
res.form[k].force_value(v)
|
res.form[k].force_value(v)
|
||||||
|
|
||||||
res = res.form.submit(status=302, name="action", value="edit")
|
res = res.form.submit(status=302, name="action", value="add")
|
||||||
res = res.follow(status=200)
|
res = res.follow(status=200)
|
||||||
|
|
||||||
client_id = res.forms["readonly"]["client_id"].value
|
client_id = res.forms["readonly"]["client_id"].value
|
||||||
|
@ -149,9 +149,9 @@ def test_client_edit(testclient, client, logged_admin, other_client):
|
||||||
res = testclient.get("/admin/client/edit/" + client.client_id)
|
res = testclient.get("/admin/client/edit/" + client.client_id)
|
||||||
data = {
|
data = {
|
||||||
"client_name": "foobar",
|
"client_name": "foobar",
|
||||||
"contacts": "foo@bar.com",
|
"contacts-0": "foo@bar.com",
|
||||||
"client_uri": "https://foo.bar",
|
"client_uri": "https://foo.bar",
|
||||||
"redirect_uris": ["https://foo.bar/callback"],
|
"redirect_uris-0": "https://foo.bar/callback",
|
||||||
"grant_types": ["password", "authorization_code"],
|
"grant_types": ["password", "authorization_code"],
|
||||||
"scope": "openid profile",
|
"scope": "openid profile",
|
||||||
"response_types": ["code", "token"],
|
"response_types": ["code", "token"],
|
||||||
|
@ -165,7 +165,7 @@ def test_client_edit(testclient, client, logged_admin, other_client):
|
||||||
"jwks_uri": "https://foo.bar/jwks.json",
|
"jwks_uri": "https://foo.bar/jwks.json",
|
||||||
"audience": [client.id, other_client.id],
|
"audience": [client.id, other_client.id],
|
||||||
"preconsent": True,
|
"preconsent": True,
|
||||||
"post_logout_redirect_uris": ["https://foo.bar/disconnected"],
|
"post_logout_redirect_uris-0": "https://foo.bar/disconnected",
|
||||||
}
|
}
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
res.forms["clientaddform"][k].force_value(v)
|
res.forms["clientaddform"][k].force_value(v)
|
||||||
|
@ -182,7 +182,10 @@ def test_client_edit(testclient, client, logged_admin, other_client):
|
||||||
assert client.client_name == "foobar"
|
assert client.client_name == "foobar"
|
||||||
assert client.contacts == ["foo@bar.com"]
|
assert client.contacts == ["foo@bar.com"]
|
||||||
assert client.client_uri == "https://foo.bar"
|
assert client.client_uri == "https://foo.bar"
|
||||||
assert client.redirect_uris == ["https://foo.bar/callback"]
|
assert client.redirect_uris == [
|
||||||
|
"https://foo.bar/callback",
|
||||||
|
"https://mydomain.tld/redirect2",
|
||||||
|
]
|
||||||
assert client.grant_types == ["password", "authorization_code"]
|
assert client.grant_types == ["password", "authorization_code"]
|
||||||
assert client.scope == ["openid", "profile"]
|
assert client.scope == ["openid", "profile"]
|
||||||
assert client.response_types == ["code", "token"]
|
assert client.response_types == ["code", "token"]
|
||||||
|
@ -247,11 +250,6 @@ def test_client_delete_invalid_client(testclient, logged_admin, client):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_request(testclient, logged_admin, client):
|
|
||||||
res = testclient.get("/admin/client/edit/" + client.client_id)
|
|
||||||
res = res.forms["clientaddform"].submit(name="action", value="invalid", status=400)
|
|
||||||
|
|
||||||
|
|
||||||
def test_client_edit_preauth(testclient, client, logged_admin, other_client):
|
def test_client_edit_preauth(testclient, client, logged_admin, other_client):
|
||||||
assert not client.preconsent
|
assert not client.preconsent
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue