users: Replace disabled with readonly for admin group checkbox

- Added validation logic in the backend to compensate

Signed-off-by: Joseph Nuthalapati <njoseph@thoughtworks.com>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Joseph Nuthalapati 2018-07-04 14:37:07 +05:30 committed by James Valleroy
parent a50b40ee56
commit b740a37a1e
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
2 changed files with 40 additions and 23 deletions

View File

@ -87,32 +87,34 @@ class LanguageSelectionForm(LanguageSelectionFormMixin, forms.Form):
language = LanguageSelectionFormMixin.language language = LanguageSelectionFormMixin.language
class CheckboxSelectMultipleWithDisabled(forms.widgets.CheckboxSelectMultiple): class CheckboxSelectMultipleWithReadOnly(forms.widgets.CheckboxSelectMultiple):
""" """
Subclass of Django's checkbox select multiple widget that allows disabling checkbox-options. Subclass of Django's CheckboxSelectMultiple widget that allows setting
To disable an option, pass a dict instead of a string for its label, individual fields as readonly
of the form: {'label': 'option label', 'disabled': True} To mark a feature as readonly an option, pass a dict instead of a string
for its label, of the form: {'label': 'option label', 'disabled': True}
Derived from https://djangosnippets.org/snippets/2786/ Derived from https://djangosnippets.org/snippets/2786/
""" """
def render(self, name, value, attrs=None, choices=(), renderer=None): def render(self, name, value, attrs=None, choices=(), renderer=None):
if value is None: value = [] if value is None:
value = []
final_attrs = self.build_attrs(attrs) final_attrs = self.build_attrs(attrs)
output = [u'<ul>'] output = [u'<ul>']
global_disabled = 'disabled' in final_attrs global_readonly = 'readonly' in final_attrs
str_values = set([v for v in value]) str_values = set([v for v in value])
for i, (option_value, option_label) in enumerate( for i, (option_value, option_label) in enumerate(
chain(self.choices, choices)): chain(self.choices, choices)):
if not global_disabled and 'disabled' in final_attrs: if not global_readonly and 'readonly' in final_attrs:
# If the entire group is disabled keep all options disabled # If the entire group is readonly keep all options readonly
del final_attrs['disabled'] del final_attrs['readonly']
if isinstance(option_label, dict): if isinstance(option_label, dict):
if dict.get(option_label, 'disabled'): if dict.get(option_label, 'readonly'):
final_attrs = dict(final_attrs, disabled='disabled') final_attrs = dict(final_attrs, readonly='readonly')
option_label = option_label['label'] option_label = option_label['label']
final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i)) final_attrs = dict(final_attrs, id='{}_{}'.format(attrs['id'], i))
label_for = u' for="%s"' % final_attrs['id'] label_for = u' for="{}"'.format(final_attrs['id'])
cb = CheckboxInput(final_attrs, cb = CheckboxInput(final_attrs,
check_test=lambda value: value in str_values) check_test=lambda value: value in str_values)
rendered_cb = cb.render(name, option_value) rendered_cb = cb.render(name, option_value)

View File

@ -22,7 +22,6 @@ from django.contrib import auth, messages
from django.contrib.auth.forms import SetPasswordForm, UserCreationForm from django.contrib.auth.forms import SetPasswordForm, UserCreationForm
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy from django.utils.translation import ugettext_lazy
@ -87,8 +86,8 @@ class CreateUserForm(ValidNewUsernameCheckMixin,
""" """
groups = forms.MultipleChoiceField( groups = forms.MultipleChoiceField(
choices=get_group_choices(), label=ugettext_lazy('Permissions'), choices=get_group_choices(), label=ugettext_lazy('Permissions'),
required=False, required=False, widget=forms.CheckboxSelectMultiple,
widget=forms.CheckboxSelectMultiple, help_text=ugettext_lazy( help_text=ugettext_lazy(
'Select which services should be available to the new ' 'Select which services should be available to the new '
'user. The user will be able to log in to services that ' 'user. The user will be able to log in to services that '
'support single sign-on through LDAP, if they are in the ' 'support single sign-on through LDAP, if they are in the '
@ -147,8 +146,8 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
plinth.forms.LanguageSelectionFormMixin, forms.ModelForm): plinth.forms.LanguageSelectionFormMixin, forms.ModelForm):
"""When user info is changed, also updates LDAP user.""" """When user info is changed, also updates LDAP user."""
ssh_keys = forms.CharField( ssh_keys = forms.CharField(
label=ugettext_lazy('SSH Keys'), label=ugettext_lazy('SSH Keys'), required=False, widget=forms.Textarea,
required=False, widget=forms.Textarea, help_text=ugettext_lazy( help_text=ugettext_lazy(
'Setting an SSH public key will allow this user to ' 'Setting an SSH public key will allow this user to '
'securely log in to the system without using a ' 'securely log in to the system without using a '
'password. You may enter multiple keys, one on each ' 'password. You may enter multiple keys, one on each '
@ -162,7 +161,7 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
fields = ('username', 'groups', 'ssh_keys', 'language', 'is_active') fields = ('username', 'groups', 'ssh_keys', 'language', 'is_active')
model = User model = User
widgets = { widgets = {
'groups': plinth.forms.CheckboxSelectMultipleWithDisabled(), 'groups': plinth.forms.CheckboxSelectMultipleWithReadOnly(),
} }
def __init__(self, request, username, *args, **kwargs): def __init__(self, request, username, *args, **kwargs):
@ -174,7 +173,7 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
self.request = request self.request = request
self.username = username self.username = username
super(UserUpdateForm, self).__init__(*args, **kwargs) super(UserUpdateForm, self).__init__(*args, **kwargs)
last_admin_user = get_last_admin_user() self.is_last_admin_user = get_last_admin_user() == self.username
choices = [] choices = []
@ -183,8 +182,11 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
# applications not installed yet. # applications not installed yet.
if c[1] in group_choices: if c[1] in group_choices:
# Replace group names with descriptions # Replace group names with descriptions
if c[1] == 'admin' and last_admin_user is not None: if c[1] == 'admin' and self.is_last_admin_user:
choices.append((c[0], {'label': group_choices[c[1]], 'disabled': True})) choices.append((c[0], {
'label': group_choices[c[1]],
'readonly': True
}))
else: else:
choices.append((c[0], group_choices[c[1]])) choices.append((c[0], group_choices[c[1]]))
@ -195,7 +197,7 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
self.fields['is_active'].widget = forms.HiddenInput() self.fields['is_active'].widget = forms.HiddenInput()
self.fields['groups'].disabled = True self.fields['groups'].disabled = True
if last_admin_user and last_admin_user == self.username: if self.is_last_admin_user:
self.fields['is_active'].disabled = True self.fields['is_active'].disabled = True
def save(self, commit=True): def save(self, commit=True):
@ -261,6 +263,19 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
return user return user
def validate_last_admin_user(self, groups):
group_names = [group.name for group in groups]
if 'admin' not in group_names:
raise ValidationError(
_('Cannot delete the only administrator in the system.'))
def clean(self):
"""Override clean to add form validation logic."""
cleaned_data = super().clean()
if self.is_last_admin_user:
self.validate_last_admin_user(cleaned_data.get("groups"))
return cleaned_data
class UserChangePasswordForm(SetPasswordForm): class UserChangePasswordForm(SetPasswordForm):
"""Custom form that also updates password for LDAP users.""" """Custom form that also updates password for LDAP users."""