forked from Github-Mirrors/canaille
refactor: utilities for form field readonliness
This commit is contained in:
parent
4347fb572a
commit
1352752db8
7 changed files with 70 additions and 23 deletions
|
@ -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)]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
Loading…
Reference in a new issue