canaille-globuzma/canaille/core/endpoints/forms.py

480 lines
13 KiB
Python
Raw Normal View History

import wtforms.form
import wtforms.validators
from flask import current_app
from flask import g
from flask_wtf.file import FileAllowed
from flask_wtf.file import FileField
from canaille.app import models
2023-07-21 12:50:04 +00:00
from canaille.app.forms import BaseForm
from canaille.app.forms import DateTimeUTCField
2023-07-21 12:50:04 +00:00
from canaille.app.forms import Form
from canaille.app.forms import IDToModel
from canaille.app.forms import email_validator
2023-04-09 13:52:55 +00:00
from canaille.app.forms import is_uri
2023-08-03 16:48:51 +00:00
from canaille.app.forms import phone_number
from canaille.app.forms import set_readonly
from canaille.app.forms import unique_values
from canaille.app.i18n import gettext
from canaille.app.i18n import lazy_gettext as _
2023-04-09 13:52:55 +00:00
from canaille.app.i18n import native_language_name_from_code
from canaille.backends import BaseBackend
2023-03-29 18:14:28 +00:00
MINIMUM_PASSWORD_LENGTH = 8
2023-03-29 18:14:28 +00:00
def unique_user_name(form, field):
if models.User.get(user_name=field.data) and (
not getattr(form, "user", None) or form.user.user_name != field.data
):
2021-12-01 11:19:28 +00:00
raise wtforms.ValidationError(
_("The user name '{user_name}' already exists").format(user_name=field.data)
2021-12-01 11:19:28 +00:00
)
def unique_email(form, field):
2023-06-22 13:14:07 +00:00
if models.User.get(emails=field.data) and (
2023-06-22 14:41:35 +00:00
not getattr(form, "user", None) or field.data not in form.user.emails
):
2021-12-01 11:19:28 +00:00
raise wtforms.ValidationError(
2023-03-29 17:34:17 +00:00
_("The email '{email}' is already used").format(email=field.data)
2021-12-01 11:19:28 +00:00
)
2021-12-07 18:41:20 +00:00
def unique_group(form, field):
if models.Group.get(display_name=field.data):
2021-12-07 18:41:20 +00:00
raise wtforms.ValidationError(
_("The group '{group}' already exists").format(group=field.data)
)
def existing_login(form, field):
if not current_app.config["CANAILLE"][
"HIDE_INVALID_LOGINS"
] and not BaseBackend.get().get_user_from_login(field.data):
2021-12-07 18:41:20 +00:00
raise wtforms.ValidationError(
_("The login '{login}' does not exist").format(login=field.data)
)
def existing_group_member(form, field):
if field.data is None:
raise wtforms.ValidationError(
gettext("The user you are trying to remove does not exist.")
)
if field.data not in form.group.members:
raise wtforms.ValidationError(
gettext(
"The user '{user}' has already been removed from the group '{group}'"
).format(user=field.data.formatted_name, group=form.group.display_name)
)
def non_empty_groups(form, field):
"""LDAP groups cannot be empty because groupOfNames.member is a MUST
attribute.
https://www.rfc-editor.org/rfc/rfc2256.html#section-7.10
"""
if not form.user:
return
for group in form.user.groups:
if len(group.members) == 1 and group not in field.data:
raise wtforms.ValidationError(
_(
"The group '{group}' cannot be removed, because it must have at least one user left."
).format(group=group.display_name)
)
2023-07-21 12:50:04 +00:00
class LoginForm(Form):
2020-08-19 14:20:57 +00:00
login = wtforms.StringField(
2020-10-20 09:44:45 +00:00
_("Login"),
2021-12-07 18:41:20 +00:00
validators=[wtforms.validators.DataRequired(), existing_login],
render_kw={
2020-10-22 15:37:01 +00:00
"placeholder": _("jane@doe.com"),
"spellcheck": "false",
"autocorrect": "off",
"inputmode": "email",
},
2020-08-19 14:20:57 +00:00
)
2020-10-20 09:44:45 +00:00
2023-07-21 12:50:04 +00:00
class PasswordForm(Form):
2021-01-23 21:30:43 +00:00
password = wtforms.PasswordField(
2021-10-29 12:20:06 +00:00
_("Password"),
validators=[wtforms.validators.DataRequired()],
render_kw={
"autocomplete": "current-password",
},
2021-01-23 21:30:43 +00:00
)
2023-07-21 12:50:04 +00:00
class ForgottenPasswordForm(Form):
2020-10-22 15:37:01 +00:00
login = wtforms.StringField(
_("Login"),
2021-12-07 18:41:20 +00:00
validators=[wtforms.validators.DataRequired(), existing_login],
2020-10-22 15:37:01 +00:00
render_kw={
"placeholder": _("jane@doe.com"),
"spellcheck": "false",
"autocorrect": "off",
},
)
2023-07-21 12:50:04 +00:00
class PasswordResetForm(Form):
2020-10-22 15:37:01 +00:00
password = wtforms.PasswordField(
_("Password"),
validators=[wtforms.validators.DataRequired()],
render_kw={
"autocomplete": "new-password",
},
2020-10-22 15:37:01 +00:00
)
confirmation = wtforms.PasswordField(
_("Password confirmation"),
validators=[
wtforms.validators.EqualTo(
"password", _("Password and confirmation do not match.")
),
],
render_kw={
"autocomplete": "new-password",
},
2020-10-22 15:37:01 +00:00
)
2023-07-21 12:50:04 +00:00
class FirstLoginForm(Form):
2022-12-20 23:20:20 +00:00
pass
def available_language_choices():
languages = [
(lang_code, native_language_name_from_code(lang_code))
for lang_code in g.available_language_codes
]
languages.sort()
return [("auto", _("Automatic"))] + languages
PROFILE_FORM_FIELDS = dict(
user_name=wtforms.StringField(
2020-10-20 09:44:45 +00:00
_("Username"),
2020-10-30 19:22:31 +00:00
render_kw={"placeholder": _("jdoe")},
validators=[wtforms.validators.DataRequired(), unique_user_name],
),
formatted_name=wtforms.StringField(_("Name")),
2023-03-13 12:47:15 +00:00
title=wtforms.StringField(
_("Title"), render_kw={"placeholder": _("Vice president")}
),
given_name=wtforms.StringField(
_("Given name"),
render_kw={
"placeholder": _("John"),
"spellcheck": "false",
"autocorrect": "off",
},
),
family_name=wtforms.StringField(
_("Family Name"),
validators=[wtforms.validators.DataRequired()],
render_kw={
"placeholder": _("Doe"),
"spellcheck": "false",
"autocorrect": "off",
},
),
display_name=wtforms.StringField(
_("Display Name"),
validators=[wtforms.validators.Optional()],
render_kw={
"placeholder": _("Johnny"),
"spellcheck": "false",
"autocorrect": "off",
},
),
emails=wtforms.FieldList(
wtforms.EmailField(
_("Email addresses"),
validators=[
wtforms.validators.DataRequired(),
email_validator,
unique_email,
],
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",
},
2021-12-20 22:57:27 +00:00
),
min_entries=1,
validators=[unique_values],
),
phone_numbers=wtforms.FieldList(
wtforms.TelField(
2023-08-03 16:48:51 +00:00
_("Phone numbers"),
render_kw={"placeholder": _("555-000-555")},
validators=[wtforms.validators.Optional(), phone_number],
),
min_entries=1,
validators=[unique_values],
),
formatted_address=wtforms.StringField(
_("Address"),
render_kw={
"placeholder": _("132, Foobar Street, Gotham City 12401, XX"),
},
),
street=wtforms.StringField(
_("Street"),
render_kw={
"placeholder": _("132, Foobar Street"),
},
),
postal_code=wtforms.StringField(
_("Postal Code"),
render_kw={
"placeholder": "12401",
},
),
locality=wtforms.StringField(
_("Locality"),
render_kw={
"placeholder": _("Gotham City"),
},
),
region=wtforms.StringField(
_("Region"),
render_kw={
2023-03-11 21:37:53 +00:00
"placeholder": _("North Pole"),
},
),
photo=FileField(
2021-12-08 17:06:50 +00:00
_("Photo"),
validators=[FileAllowed(["jpg", "jpeg"])],
render_kw={"accept": "image/jpg, image/jpeg"},
),
photo_delete=wtforms.BooleanField(_("Delete the photo")),
password1=wtforms.PasswordField(
2020-10-21 08:26:31 +00:00
_("Password"),
validators=[
wtforms.validators.Optional(),
wtforms.validators.Length(min=MINIMUM_PASSWORD_LENGTH),
],
render_kw={
"autocomplete": "new-password",
},
),
password2=wtforms.PasswordField(
2020-10-21 08:26:31 +00:00
_("Password confirmation"),
validators=[
wtforms.validators.EqualTo(
"password1", message=_("Password and confirmation do not match.")
)
],
render_kw={
"autocomplete": "new-password",
},
),
employee_number=wtforms.StringField(
2023-03-29 21:01:02 +00:00
_("User number"),
2023-03-11 11:52:36 +00:00
render_kw={
"placeholder": _("1234"),
},
),
department=wtforms.StringField(
_("Department"),
2021-10-29 12:20:06 +00:00
render_kw={
"placeholder": _("1234"),
},
),
organization=wtforms.StringField(
2023-03-17 16:35:05 +00:00
_("Organization"),
render_kw={
"placeholder": _("Cogip LTD."),
},
),
profile_url=wtforms.URLField(
2021-12-13 22:04:34 +00:00
_("Website"),
render_kw={
"placeholder": _("https://mywebsite.tld"),
},
2023-03-29 18:14:28 +00:00
validators=[wtforms.validators.Optional(), is_uri],
2021-12-13 22:04:34 +00:00
),
preferred_language=wtforms.SelectField(
_("Preferred language"),
choices=available_language_choices,
),
2023-03-16 17:39:28 +00:00
groups=wtforms.SelectMultipleField(
_("Groups"),
default=[],
choices=lambda: [
(group, group.display_name)
for group in BaseBackend.get().query(models.Group)
],
2023-03-16 17:39:28 +00:00
render_kw={"placeholder": _("users, admins …")},
coerce=IDToModel("Group"),
validators=[non_empty_groups],
2023-03-16 17:39:28 +00:00
),
)
2023-07-20 16:43:28 +00:00
def build_profile_form(write_field_names, readonly_field_names, user=None):
if "password" in write_field_names:
2021-12-02 17:23:14 +00:00
write_field_names |= {"password1", "password2"}
if "photo" in write_field_names:
write_field_names |= {"photo_delete"}
2021-12-08 17:06:50 +00:00
fields = {
name: PROFILE_FORM_FIELDS.get(name)
2021-12-02 17:23:14 +00:00
for name in write_field_names | readonly_field_names
if PROFILE_FORM_FIELDS.get(name)
}
2021-12-02 17:23:14 +00:00
if "groups" in fields and not BaseBackend.get().query(models.Group):
2023-03-16 17:39:28 +00:00
del fields["groups"]
2021-12-02 17:23:14 +00:00
2023-05-26 15:44:15 +00:00
if current_app.backend.get().has_account_lockability(): # pragma: no branch
fields["lock_date"] = DateTimeUTCField(
2023-05-26 15:44:15 +00:00
_("Account expiration"),
validators=[wtforms.validators.Optional()],
format=[
"%Y-%m-%d %H:%M",
"%Y-%m-%dT%H:%M",
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%dT%H:%M:%S",
],
)
2022-11-01 11:25:21 +00:00
2023-07-21 12:50:04 +00:00
form = BaseForm(fields)
form.user = user
2021-12-02 17:23:14 +00:00
for field in form:
if field.name in readonly_field_names - write_field_names:
set_readonly(field)
2021-12-02 17:23:14 +00:00
return form
2021-07-01 16:21:20 +00:00
2023-07-21 12:50:04 +00:00
class CreateGroupForm(Form):
display_name = wtforms.StringField(
2021-07-01 16:21:20 +00:00
_("Name"),
2022-12-27 20:32:21 +00:00
validators=[wtforms.validators.DataRequired(), unique_group],
2021-10-29 12:20:06 +00:00
render_kw={
"placeholder": _("group"),
},
2021-07-01 16:21:20 +00:00
)
2021-12-10 16:08:43 +00:00
description = wtforms.StringField(
_("Description"),
validators=[wtforms.validators.Optional()],
)
2021-12-01 11:19:28 +00:00
2023-07-21 12:50:04 +00:00
class EditGroupForm(Form):
display_name = wtforms.StringField(
2022-12-27 20:32:21 +00:00
_("Name"),
validators=[
wtforms.validators.DataRequired(),
wtforms.validators.ReadOnly(),
],
2022-12-27 20:32:21 +00:00
render_kw={
"readonly": "true",
},
)
description = wtforms.StringField(
_("Description"),
validators=[wtforms.validators.Optional()],
)
class DeleteGroupMemberForm(Form):
member = wtforms.StringField(
filters=[IDToModel("User", raise_on_errors=False)],
validators=[existing_group_member],
)
2023-08-14 22:48:49 +00:00
class JoinForm(Form):
email = wtforms.EmailField(
_("Email address"),
validators=[
wtforms.validators.DataRequired(),
email_validator,
],
render_kw={
"placeholder": _("jane@doe.com"),
"spellcheck": "false",
"autocorrect": "off",
},
)
def validate_email(form, field):
if not current_app.config["CANAILLE"]["HIDE_INVALID_LOGINS"]:
unique_email(form, field)
2023-07-21 12:50:04 +00:00
class InvitationForm(Form):
user_name = wtforms.StringField(
_("User name"),
2021-12-01 11:19:28 +00:00
render_kw={"placeholder": _("jdoe")},
validators=[wtforms.validators.DataRequired(), unique_user_name],
2021-12-01 11:19:28 +00:00
)
user_name_editable = wtforms.BooleanField(_("Username editable by the invitee"))
email = wtforms.EmailField(
2021-12-01 11:19:28 +00:00
_("Email address"),
validators=[
wtforms.validators.DataRequired(),
email_validator,
2021-12-01 11:19:28 +00:00
unique_email,
],
render_kw={
"placeholder": _("jane@doe.com"),
"spellcheck": "false",
"autocorrect": "off",
},
)
groups = wtforms.SelectMultipleField(
_("Groups"),
choices=lambda: [
(group, group.display_name)
for group in BaseBackend.get().query(models.Group)
],
2021-12-01 11:19:28 +00:00
render_kw={},
coerce=IDToModel("Group"),
2021-12-01 11:19:28 +00:00
)
2023-07-20 16:43:28 +00:00
class EmailConfirmationForm(Form):
old_emails = wtforms.FieldList(
wtforms.EmailField(
validators=[wtforms.validators.ReadOnly()],
2023-07-20 16:43:28 +00:00
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",
"readonly": "true",
},
),
2023-08-08 12:26:46 +00:00
label=_("Email addresses"),
2023-07-20 16:43:28 +00:00
)
new_email = wtforms.EmailField(
_("New email address"),
validators=[
wtforms.validators.DataRequired(),
email_validator,
2023-07-20 16:43:28 +00:00
unique_email,
],
render_kw={
"placeholder": _("jane@doe.com"),
"spellcheck": "false",
"autocorrect": "off",
},
)