refactor: utilities for form field readonliness

This commit is contained in:
Éloi Rivard 2023-07-24 18:07:35 +02:00
parent 4347fb572a
commit 1352752db8
No known key found for this signature in database
GPG key ID: 7EDA204EA57DD184
7 changed files with 70 additions and 23 deletions

View file

@ -215,3 +215,33 @@ class DateTimeUTCField(wtforms.DateTimeLocalField):
self.data = None
raise ValueError(self.gettext("Not a valid datetime value."))
class ReadOnly:
"""
Set a field readonly.
Validation fails if the form data is different than the
field object data, or if unset, from the field default data.
"""
def __init__(self):
self.field_flags = {"readonly": True}
def __call__(self, form, field):
if field.data != field.object_data:
raise wtforms.ValidationError(field.gettext("This field cannot be edited"))
def is_readonly(field):
return field.render_kw and "readonly" in field.render_kw
def set_readonly(field):
field.render_kw = field.render_kw or {}
field.render_kw["readonly"] = True
field.validators = list(field.validators) + [ReadOnly()]
def set_writable(field):
del field.render_kw["readonly"]
field.validators = [v for v in field.validators if not isinstance(v, ReadOnly)]

View file

@ -17,6 +17,9 @@ from canaille.app.flask import render_htmx_template
from canaille.app.flask import request_is_htmx
from canaille.app.flask import smtp_needed
from canaille.app.flask import user_needed
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.backends import BaseBackend
from flask import abort
@ -321,12 +324,12 @@ def registration(data, hash):
form["groups"] = wtforms.SelectMultipleField(
_("Groups"),
choices=[(group.id, group.display_name) for group in models.Group.query()],
render_kw={"readonly": "true"},
)
set_readonly(form["groups"])
form.process(CombinedMultiDict((request.files, request.form)) or None, data=data)
if "readonly" in form["user_name"].render_kw and invitation.user_name_editable:
del form["user_name"].render_kw["readonly"]
if is_readonly(form["user_name"]) and invitation.user_name_editable:
set_writable(form["user_name"])
form["password1"].validators = [
wtforms.validators.DataRequired(),
@ -371,8 +374,8 @@ def profile_creation(user):
form.process(CombinedMultiDict((request.files, request.form)) or None)
for field in form:
if field.render_kw and "readonly" in field.render_kw:
del field.render_kw["readonly"]
if is_readonly(field):
set_writable(field)
if not request.form or form.form_control():
return render_template(

View file

@ -4,6 +4,8 @@ from canaille.app.forms import BaseForm
from canaille.app.forms import DateTimeUTCField
from canaille.app.forms import Form
from canaille.app.forms import is_uri
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 native_language_name_from_code
from flask import current_app
@ -311,8 +313,7 @@ def profile_form(write_field_names, readonly_field_names, user=None):
form.user = user
for field in form:
if field.name in readonly_field_names - write_field_names:
field.render_kw = field.render_kw or {}
field.render_kw["readonly"] = "true"
set_readonly(field)
return form
@ -334,7 +335,10 @@ class CreateGroupForm(Form):
class EditGroupForm(Form):
display_name = wtforms.StringField(
_("Name"),
validators=[wtforms.validators.DataRequired()],
validators=[
wtforms.validators.DataRequired(),
ReadOnly(),
],
render_kw={
"readonly": "true",
},

View file

@ -410,7 +410,7 @@ def test_fieldlist_add_readonly(testclient, logged_user, configuration):
configuration["ACL"]["DEFAULT"]["READ"].append("phone_numbers")
res = testclient.get("/profile/user")
assert res.form["phone_numbers-0"].attrs["readonly"]
assert "readonly" in res.form["phone_numbers-0"].attrs
assert "phone_numbers-1" not in res.form.fields
data = {
@ -429,8 +429,8 @@ def test_fieldlist_remove_readonly(testclient, logged_user, configuration):
logged_user.save()
res = testclient.get("/profile/user")
assert res.form["phone_numbers-0"].attrs["readonly"]
assert res.form["phone_numbers-1"].attrs["readonly"]
assert "readonly" in res.form["phone_numbers-0"].attrs
assert "readonly" in res.form["phone_numbers-1"].attrs
data = {
"csrf_token": res.form["csrf_token"].value,

View file

@ -165,15 +165,27 @@ def test_moderator_can_create_edit_and_delete_group(
form["display_name"] = "bar2"
form["description"] = ["yolo2"]
res = form.submit(name="action", value="edit").follow()
res = form.submit(name="action", value="edit")
assert res.flashes == [("error", "Group edition failed.")]
res.mustcontain("This field cannot be edited")
bar_group = models.Group.get(display_name="bar")
assert bar_group.display_name == "bar"
assert bar_group.description == ["yolo"]
assert models.Group.get(display_name="bar2") is None
# Group description can be edited
res = testclient.get("/groups/bar", status=200)
form = res.forms["editgroupform"]
form["description"] = ["yolo2"]
res = form.submit(name="action", value="edit")
assert res.flashes == [("success", "The group bar has been sucessfully edited.")]
res = res.follow()
bar_group = models.Group.get(display_name="bar")
assert bar_group.display_name == "bar"
assert bar_group.description == ["yolo2"]
assert models.Group.get(display_name="bar2") is None
members = bar_group.members
for member in members:
res.mustcontain(member.formatted_name[0])
# Group is deleted
res = res.forms["editgroupform"].submit(name="action", value="confirm-delete")

View file

@ -25,7 +25,7 @@ def test_invitation(testclient, logged_admin, foo_group, smtpd):
res = testclient.get(url, status=200)
assert res.form["user_name"].value == "someone"
assert res.form["user_name"].attrs["readonly"]
assert "readonly" in res.form["user_name"].attrs
assert res.form["emails-0"].value == "someone@domain.tld"
assert res.form["groups"].value == [foo_group.id]
@ -303,7 +303,7 @@ def test_groups_are_saved_even_when_user_does_not_have_read_permission(
res = testclient.get(f"/register/{b64}/{hash}", status=200)
assert res.form["groups"].value == [foo_group.id]
assert res.form["groups"].attrs["readonly"]
assert "readonly" in res.form["groups"].attrs
res.form["password1"] = "whatever"
res.form["password2"] = "whatever"

View file

@ -19,14 +19,12 @@ def test_edition(
assert logged_user.groups == [foo_group]
assert foo_group.members == [logged_user]
assert bar_group.members == [admin]
assert res.form["groups"].attrs["readonly"]
assert res.form["user_name"].attrs["readonly"]
assert "readonly" in res.form["groups"].attrs
assert "readonly" in res.form["user_name"].attrs
res.form["user_name"] = "toto"
res = res.form.submit(name="action", value="edit")
assert res.flashes == [("success", "Profile updated successfully.")]
res = res.follow()
assert res.flashes == [("error", "Profile edition failed.")]
logged_user.reload()
assert logged_user.user_name == ["user"]