Просмотр исходного кода

#646: more work at hiding users, admin edit form

Rafał Pitoń 8 лет назад
Родитель
Сommit
aaf3dc96a9

+ 48 - 4
misago/templates/misago/admin/users/edit.html

@@ -53,7 +53,7 @@ class="form-horizontal"
     {% else %}
       <div id="div_id_is_staff" class="form-group">
         <label for="id_is_staff_0" class="control-label {{ label_class }}">
-          {% trans "Is administrator" %}:
+          {{ form.IS_STAFF_LABEL }}:
         </label>
         <div class="{{ field_class }}">
           <p class="form-control-static">
@@ -64,13 +64,13 @@ class="form-horizontal"
             {% endif %}
           </p>
           <p id="hint_id_is_staff" class="help-block">
-            {% trans "Designates whether the user can log into admin sites. If Django admin site is enabled, this user will need additional permissions assigned within it to admin Django modules." %}
+            {{ form.IS_STAFF_HELP_TEXT }}
           </p>
         </div>
       </div>
       <div id="div_id_is_superuser" class="form-group">
         <label for="id_is_superuser_0" class="control-label {{ label_class }}">
-          {% trans "Is superuser" %}:
+          {{ form.IS_SUPERUSER_LABEL }}:
         </label>
         <div class="{{ field_class }}">
           <p class="form-control-static">
@@ -81,7 +81,7 @@ class="form-horizontal"
             {% endif %}
           </p>
           <p id="hint_id_is_superuser" class="help-block">
-            {% trans "Only administrators can access admin sites. In addition to admin site access, superadmins can also change other members admin levels." %}
+            {{ form.IS_SUPERUSER_HELP_TEXT }}
           </p>
         </div>
       </div>
@@ -130,6 +130,50 @@ class="form-horizontal"
     {% form_row form.subscribe_to_replied_threads label_class field_class %}
 
   </fieldset>
+  <fieldset>
+    <legend>{% trans "Account status" %}</legend>
+
+    {% if 'is_active' in form.fields %}
+      {% form_row form.is_active label_class field_class %}
+      {% form_row form.is_active_staff_message label_class field_class %}
+    {% else %}
+      <div id="div_id_is_active" class="form-group">
+        <label for="id_is_active_0" class="control-label {{ label_class }}">
+          {{ form.IS_ACTIVE_LABEL }}:
+        </label>
+        <div class="{{ field_class }}">
+          <p class="form-control-static">
+            {% if target.is_active %}
+              <strong class="text-success">{% trans "Yes" %}</strong>
+            {% else %}
+              <strong class="text-danger">{% trans "No" %}</strong>
+            {% endif %}
+          </p>
+          <p id="hint_id_is_active" class="help-block">
+            {{ form.IS_ACTIVE_HELP_TEXT }}
+          </p>
+        </div>
+      </div>
+      <div id="div_id_is_superuser" class="form-group">
+        <label for="id_is_superuser_0" class="control-label {{ label_class }}">
+          {{ form.IS_ACTIVE_STAFF_MESSAGE_LABEL }}:
+        </label>
+        <div class="{{ field_class }}">
+          <div class="form-control-static">
+            {% if target.is_active_staff_message %}
+              {{ target.is_active_staff_message|escape|linebreaks }}
+            {% else %}
+              <em>{% trans "No staff message is available." %}</em>
+            {% endif %}
+          </div>
+          <p id="hint_id_is_superuser" class="help-block">
+            {{ form.IS_ACTIVE_STAFF_MESSAGE_HELP_TEXT }}
+          </p>
+        </div>
+      </div>
+    {% endif %}
+
+  </fieldset>
   {% endwith %}
 </div>
 {% endblock form-body %}

+ 14 - 10
misago/users/api/users.py

@@ -3,6 +3,7 @@ from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
 from django.db import transaction
 from django.db.models import F
+from django.http import Http404
 from django.utils.translation import ugettext as _
 
 from rest_framework import mixins, status, viewsets
@@ -61,8 +62,11 @@ class UserViewSet(viewsets.GenericViewSet):
         relations = ('rank', 'online_tracker', 'ban_cache')
         return self.queryset.select_related(*relations)
 
-    def get_user(self, pk):
-        return get_object_or_404(self.get_queryset(), pk=get_int_or_404(pk))
+    def get_user(self, request, pk):
+        user = get_object_or_404(self.get_queryset(), pk=get_int_or_404(pk))
+        if not user.is_active and not request.user.is_staff:
+            raise Http404()
+        return user
 
     def list(self, request):
         allow_browse_users_list(request.user)
@@ -72,7 +76,7 @@ class UserViewSet(viewsets.GenericViewSet):
         return create_endpoint(request)
 
     def retrieve(self, request, pk=None):
-        profile = self.get_user(pk)
+        profile = self.get_user(request, pk)
 
         add_acl(request.user, profile)
         profile.status = get_user_status(request.user, profile)
@@ -131,7 +135,7 @@ class UserViewSet(viewsets.GenericViewSet):
 
     @detail_route(methods=['post'])
     def follow(self, request, pk=None):
-        profile = self.get_user(pk)
+        profile = self.get_user(request, pk)
         allow_follow_user(request.user, profile)
 
         profile_followers = profile.followers
@@ -162,7 +166,7 @@ class UserViewSet(viewsets.GenericViewSet):
 
     @detail_route()
     def ban(self, request, pk=None):
-        profile = self.get_user(pk)
+        profile = self.get_user(request, pk)
         allow_see_ban_details(request.user, profile)
 
         ban = get_user_ban(profile)
@@ -173,21 +177,21 @@ class UserViewSet(viewsets.GenericViewSet):
 
     @detail_route(methods=['get', 'post'])
     def moderate_avatar(self, request, pk=None):
-        profile = self.get_user(pk)
+        profile = self.get_user(request, pk)
         allow_moderate_avatar(request.user, profile)
 
         return moderate_avatar_endpoint(request, profile)
 
     @detail_route(methods=['get', 'post'])
     def moderate_username(self, request, pk=None):
-        profile = self.get_user(pk)
+        profile = self.get_user(request, pk)
         allow_rename_user(request.user, profile)
 
         return moderate_username_endpoint(request, profile)
 
     @detail_route(methods=['get', 'post'])
     def delete(self, request, pk=None):
-        profile = self.get_user(pk)
+        profile = self.get_user(request, pk)
         allow_delete_user(request.user, profile)
 
         if request.method == 'POST':
@@ -222,7 +226,7 @@ class UserViewSet(viewsets.GenericViewSet):
 
     @detail_route(methods=['get'])
     def threads(self, request, pk=None):
-        profile = self.get_user(pk)
+        profile = self.get_user(request, pk)
 
         page = get_int_or_404(request.query_params.get('page', 0))
         if page == 1:
@@ -234,7 +238,7 @@ class UserViewSet(viewsets.GenericViewSet):
 
     @detail_route(methods=['get'])
     def posts(self, request, pk=None):
-        profile = self.get_user(pk)
+        profile = self.get_user(request, pk)
 
         page = get_int_or_404(request.query_params.get('page', 0))
         if page == 1:

+ 5 - 3
misago/users/authbackends.py

@@ -11,18 +11,20 @@ class MisagoBackend(ModelBackend):
 
         try:
             user = UserModel.objects.get_by_username_or_email(username)
-            if user.check_password(password):
-                return user
         except UserModel.DoesNotExist:
             # Run the default password hasher once to reduce the timing
             # difference between an existing and a non-existing user (#20760).
             UserModel().set_password(password)
+        else:
+            if user.check_password(password) and self.user_can_authenticate(user):
+                return user
 
     def get_user(self, pk):
         UserModel = get_user_model()
         try:
             manager = UserModel._default_manager
             relations = ('rank', 'online_tracker', 'ban_cache')
-            return manager.select_related(*relations).get(pk=pk)
+            user = manager.select_related(*relations).get(pk=pk)
         except UserModel.DoesNotExist:
             return None
+        return user if self.user_can_authenticate(user) else None

+ 138 - 64
misago/users/forms/admin.py

@@ -72,6 +72,33 @@ class NewUserForm(UserBaseForm):
 
 
 class EditUserForm(UserBaseForm):
+    IS_STAFF_LABEL = _("Is administrator")
+    IS_STAFF_HELP_TEXT = _(
+        "Designates whether the user can log into admin sites. "
+        "If Django admin site is enabled, this user will need "
+        "additional permissions assigned within it to admin "
+        "Django modules."
+    )
+
+    IS_SUPERUSER_LABEL = _("Is superuser")
+    IS_SUPERUSER_HELP_TEXT = _(
+        "Only administrators can access admin sites. "
+        "In addition to admin site access, superadmins "
+        "can also change other members admin levels."
+    )
+
+    IS_ACTIVE_LABEL = _('Is active')
+    IS_ACTIVE_HELP_TEXT = _(
+        "Designates whether this user should be treated as active. "
+        "Turning this off is non-destructible way to remove user accounts."
+    )
+
+    IS_ACTIVE_STAFF_MESSAGE_LABEL=_("Staff message")
+    IS_ACTIVE_STAFF_MESSAGE_HELP_TEXT=_(
+        "Optional message for forum team members explaining "
+        "why user's account has been disabled."
+    )
+
     new_password = forms.CharField(
         label=_("Change password to"),
         widget=forms.PasswordInput,
@@ -80,21 +107,27 @@ class EditUserForm(UserBaseForm):
 
     is_avatar_locked = forms.YesNoSwitch(
         label=_("Lock avatar"),
-        help_text=_("Setting this to yes will stop user from changing "
-                    "his/her avatar, and will reset his/her avatar to "
-                    "procedurally generated one.")
+        help_text=_(
+            "Setting this to yes will stop user from changing "
+            "his/her avatar, and will reset his/her avatar to "
+            "procedurally generated one."
+        )
     )
     avatar_lock_user_message = forms.CharField(
         label=_("User message"),
-        help_text=_("Optional message for user explaining "
-                    "why he/she is banned form changing avatar."),
+        help_text=_(
+            "Optional message for user explaining "
+            "why he/she is banned form changing avatar."
+        ),
         widget=forms.Textarea(attrs={'rows': 3}),
         required=False
     )
     avatar_lock_staff_message = forms.CharField(
         label=_("Staff message"),
-        help_text=_("Optional message for forum team members explaining "
-                    "why user is banned form changing avatar."),
+        help_text=_(
+            "Optional message for forum team members explaining "
+            "why user is banned form changing avatar."
+        ),
         widget=forms.Textarea(attrs={'rows': 3}),
         required=False
     )
@@ -106,20 +139,24 @@ class EditUserForm(UserBaseForm):
     )
     is_signature_locked = forms.YesNoSwitch(
         label=_("Lock signature"),
-        help_text=_("Setting this to yes will stop user from "
-                    "making changes to his/her signature.")
+        help_text=_(
+            "Setting this to yes will stop user from "
+            "making changes to his/her signature."
+        )
     )
     signature_lock_user_message = forms.CharField(
         label=_("User message"),
-        help_text=_("Optional message to user explaining "
-                    "why his/hers signature is locked."),
+        help_text=_(
+            "Optional message to user explaining why his/hers signature is locked."
+        ),
         widget=forms.Textarea(attrs={'rows': 3}),
         required=False
     )
     signature_lock_staff_message = forms.CharField(
         label=_("Staff message"),
-        help_text=_("Optional message to team members explaining "
-                    "why user signature is locked."),
+        help_text=_(
+            "Optional message to team members explaining why user signature is locked."
+        ),
         widget=forms.Textarea(attrs={'rows': 3}),
         required=False
     )
@@ -170,7 +207,8 @@ class EditUserForm(UserBaseForm):
             raise forms.ValidationError(ungettext(
                 "Signature can't be longer than %(limit)s character.",
                 "Signature can't be longer than %(limit)s characters.",
-                length_limit) % {'limit': length_limit})
+                length_limit
+            ) % {'limit': length_limit})
 
         return data
 
@@ -180,8 +218,10 @@ def UserFormFactory(FormType, instance):
 
     extra_fields['rank'] = forms.ModelChoiceField(
         label=_("Rank"),
-        help_text=_("Ranks are used to group and distinguish users. They are "
-                    "also used to add permissions to groups of users."),
+        help_text=_(
+            "Ranks are used to group and distinguish users. They are "
+            "also used to add permissions to groups of users."
+        ),
         queryset=Rank.objects.order_by('name'),
         initial=instance.rank
     )
@@ -190,8 +230,9 @@ def UserFormFactory(FormType, instance):
 
     extra_fields['roles'] = forms.ModelMultipleChoiceField(
         label=_("Roles"),
-        help_text=_('Individual roles of this user. '
-                    'All users must have "member" role.'),
+        help_text=_(
+            'Individual roles of this user. All users must have "member" role.'
+        ),
         queryset=roles,
         initial=instance.roles.all() if instance.pk else None,
         widget=forms.CheckboxSelectMultiple
@@ -200,31 +241,53 @@ def UserFormFactory(FormType, instance):
     return type('UserFormFinal', (FormType,), extra_fields)
 
 
-def StaffFlagUserFormFactory(FormType, instance, add_staff_field):
+def StaffFlagUserFormFactory(FormType, instance):
+    staff_fields = {
+        'is_staff': forms.YesNoSwitch(
+            label=EditUserForm.IS_STAFF_LABEL,
+            help_text=EditUserForm.IS_STAFF_HELP_TEXT,
+            initial=instance.is_staff
+        ),
+        'is_superuser': forms.YesNoSwitch(
+            label=EditUserForm.IS_SUPERUSER_LABEL,
+            help_text=EditUserForm.IS_SUPERUSER_HELP_TEXT,
+            initial=instance.is_superuser
+        ),
+    }
+
+    return type('StaffUserForm', (FormType,), staff_fields)
+
+
+def UserIsActiveFormFactory(FormType, instance):
+    is_active_fields = {
+        'is_active': forms.YesNoSwitch(
+            label=EditUserForm.IS_ACTIVE_LABEL,
+            help_text=EditUserForm.IS_ACTIVE_HELP_TEXT,
+            initial=instance.is_active
+        ),
+        'is_active_staff_message': forms.CharField(
+            label=EditUserForm.IS_ACTIVE_STAFF_MESSAGE_LABEL,
+            help_text=EditUserForm.IS_ACTIVE_STAFF_MESSAGE_HELP_TEXT,
+            initial=instance.is_active_staff_message,
+            widget=forms.Textarea(attrs={'rows': 3}),
+            required=False
+        ),
+    }
+
+    return type('UserIsActiveForm', (FormType,), is_active_fields)
+
+
+def EditUserFormFactory(FormType, instance,
+        add_is_active_fields=False, add_admin_fields=False):
     FormType = UserFormFactory(FormType, instance)
 
-    if add_staff_field:
-        staff_fields = {
-            'is_staff': forms.YesNoSwitch(
-                label=_("Is administrator"),
-                help_text=_("Designates whether the user can log into admin sites. "
-                            "If Django admin site is enabled, this user will need "
-                            "additional permissions assigned within it to admin "
-                            "Django modules."),
-                initial=instance.is_staff
-            ),
-            'is_superuser': forms.YesNoSwitch(
-                label=_("Is superuser"),
-                help_text=_("Only administrators can access admin sites. "
-                            "In addition to admin site access, superadmins "
-                            "can also change other members admin levels."),
-                initial=instance.is_superuser
-            ),
-        }
+    if add_is_active_fields:
+        FormType = UserIsActiveFormFactory(FormType, instance)
+
+    if add_admin_fields:
+        FormType = StaffFlagUserFormFactory(FormType, instance)
 
-        return type('StaffUserForm', (FormType,), staff_fields)
-    else:
-        return FormType
+    return FormType
 
 
 class SearchUsersFormBase(forms.Form):
@@ -304,42 +367,51 @@ class RankForm(forms.ModelForm):
     name = forms.CharField(
         label=_("Name"),
         validators=[validate_sluggable()],
-        help_text=_('Short and descriptive name of all users with this rank. '
-                    '"The Team" or "Game Masters" are good examples.')
+        help_text=_(
+            'Short and descriptive name of all users with this rank. '
+            '"The Team" or "Game Masters" are good examples.'
+        )
     )
     title = forms.CharField(
         label=_("User title"),
         required=False,
-        help_text=_('Optional, singular version of rank name displayed by '
-                    'user names. For example "GM" or "Dev".')
+        help_text=_(
+            'Optional, singular version of rank name displayed by user names. '
+            'For example "GM" or "Dev".'
+        )
     )
     description = forms.CharField(
         label=_("Description"),
         max_length=2048,
         required=False,
         widget=forms.Textarea(attrs={'rows': 3}),
-        help_text=_("Optional description explaining function or status of "
-                    "members distincted with this rank.")
+        help_text=_(
+            "Optional description explaining function or status of "
+            "members distincted with this rank."
+        )
     )
     roles = forms.ModelMultipleChoiceField(
         label=_("User roles"),
         widget=forms.CheckboxSelectMultiple,
         queryset=Role.objects.order_by('name'),
         required=False,
-        help_text=_('Rank can give additional roles to users with it.')
+        help_text=_("Rank can give additional roles to users with it.")
     )
     css_class = forms.CharField(
         label=_("CSS class"),
         required=False,
-        help_text=_("Optional css class added to content belonging to this "
-                    "rank owner.")
+        help_text=_(
+            "Optional css class added to content belonging to this rank owner."
+        )
     )
     is_tab = forms.BooleanField(
         label=_("Give rank dedicated tab on users list"),
         required=False,
-        help_text=_("Selecting this option will make users with this rank "
-                    "easily discoverable by others trough dedicated page on "
-                    "forum users list.")
+        help_text=_(
+            "Selecting this option will make users with this rank "
+            "easily discoverable by others trough dedicated page on "
+            "forum users list."
+        )
     )
 
     class Meta:
@@ -388,8 +460,7 @@ class BanUsersForm(forms.Form):
         label=_("User message"),
         required=False,
         max_length=1000,
-        help_text=_("Optional message displayed to users "
-                    "instead of default one."),
+        help_text=_("Optional message displayed to users instead of default one."),
         widget=forms.Textarea(attrs={'rows': 3}),
         error_messages={
             'max_length': _("Message can't be longer than 1000 characters.")
@@ -408,7 +479,7 @@ class BanUsersForm(forms.Form):
     expires_on = forms.IsoDateTimeField(
         label=_("Expires on"),
         required=False,
-        help_text=_('Leave this field empty for set bans to never expire.')
+        help_text=_("Leave this field empty for set bans to never expire.")
     )
 
 
@@ -421,9 +492,11 @@ class BanForm(forms.ModelForm):
     banned_value = forms.CharField(
         label=_("Banned value"),
         max_length=250,
-        help_text=_('This value is case-insensitive and accepts asterisk (*) '
-                    'for rought matches. For example, making IP ban for value '
-                    '"83.*" will ban all IP addresses beginning with "83.".'),
+        help_text=_(
+            'This value is case-insensitive and accepts asterisk (*) '
+            'for rought matches. For example, making IP ban for value '
+            '"83.*" will ban all IP addresses beginning with "83.".'
+        ),
         error_messages={
             'max_length': _("Banned value can't be longer "
                             "than 250 characters.")
@@ -433,8 +506,7 @@ class BanForm(forms.ModelForm):
         label=_("User message"),
         required=False,
         max_length=1000,
-        help_text=_("Optional message displayed to user "
-                    "instead of default one."),
+        help_text=_("Optional message displayed to user instead of default one."),
         widget=forms.Textarea(attrs={'rows': 3}),
         error_messages={
             'max_length': _("Message can't be longer than 1000 characters.")
@@ -453,7 +525,7 @@ class BanForm(forms.ModelForm):
     expires_on = forms.IsoDateTimeField(
         label=_("Expires on"),
         required=False,
-        help_text=_('Leave this field empty for this ban to never expire.')
+        help_text=_("Leave this field empty for this ban to never expire.")
     )
 
     class Meta:
@@ -537,9 +609,11 @@ class WarningLevelForm(forms.ModelForm):
     length_in_minutes = forms.IntegerField(
         label=_("Length in minutes"),
         min_value=0,
-        help_text=_("Enter number of minutes since this warning level was "
-                    "imposed on member until it's reduced, or 0 to make "
-                    "this warning level permanent.")
+        help_text=_(
+            "Enter number of minutes since this warning level was "
+            "imposed on member until it's reduced, or 0 to make "
+            "this warning level permanent."
+        )
     )
     restricts_posting_replies = forms.TypedChoiceField(
         label=_("Posting replies"),

+ 1 - 1
misago/users/migrations/0001_initial.py

@@ -37,7 +37,7 @@ class Migration(migrations.Migration):
                 ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
                 ('acl_key', models.CharField(max_length=12, null=True, blank=True)),
                 ('is_active', models.BooleanField(
-                    default=True, verbose_name='active', help_text=(
+                    db_index=True, default=True, verbose_name='active', help_text=(
                         'Designates whether this user should be treated as active. Unselect this instead of deleting '
                         'accounts.'
                     )

+ 1 - 0
misago/users/models/user.py

@@ -218,6 +218,7 @@ class User(AbstractBaseUser, PermissionsMixin):
 
     is_active = models.BooleanField(
         _('active'),
+        db_index=True,
         default=True,
         help_text=_(
             "Designates whether this user should be treated as active. "

+ 168 - 0
misago/users/tests/test_useradmin_views.py

@@ -363,6 +363,174 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertFalse(updated_user.is_staff)
         self.assertFalse(updated_user.is_superuser)
 
+    def test_edit_disable_user(self):
+        """edit user view allows admin to disable non admin"""
+        self.user.is_superuser = False
+        self.user.save()
+
+        User = get_user_model()
+        test_user = User.objects.create_user('Bob', 'bob@test.com', 'pass123')
+        test_link = reverse('misago:admin:users:accounts:edit',
+                            kwargs={'pk': test_user.pk})
+
+        response = self.client.get(test_link)
+        self.assertContains(response, 'id="id_is_active_1"')
+        self.assertContains(response, 'id="id_is_active_staff_message"')
+
+        response = self.client.post(test_link, data={
+            'username': 'Bawww',
+            'rank': six.text_type(test_user.rank_id),
+            'roles': six.text_type(test_user.roles.all()[0].pk),
+            'email': 'reg@stered.com',
+            'new_password': 'pass123',
+            'is_staff': '0',
+            'is_superuser': '0',
+            'signature': 'Hello world!',
+            'is_signature_locked': '1',
+            'is_hiding_presence': '0',
+            'limits_private_thread_invites_to': '0',
+            'signature_lock_staff_message': 'Staff message',
+            'signature_lock_user_message': 'User message',
+            'subscribe_to_started_threads': '2',
+            'subscribe_to_replied_threads': '2',
+            'is_active': '0',
+            'is_active_staff_message': "Disabled in test!"
+        })
+        self.assertEqual(response.status_code, 302)
+
+        updated_user = User.objects.get(pk=test_user.pk)
+        self.assertFalse(updated_user.is_active)
+        self.assertEqual(updated_user.is_active_staff_message, "Disabled in test!")
+
+    def test_edit_superuser_disable_admin(self):
+        """edit user view allows admin to disable non admin"""
+        self.user.is_superuser = True
+        self.user.save()
+
+        User = get_user_model()
+        test_user = User.objects.create_user('Bob', 'bob@test.com', 'pass123')
+
+        test_user.is_staff = True
+        test_user.save()
+
+        test_link = reverse('misago:admin:users:accounts:edit',
+                            kwargs={'pk': test_user.pk})
+
+        response = self.client.get(test_link)
+        self.assertContains(response, 'id="id_is_active_1"')
+        self.assertContains(response, 'id="id_is_active_staff_message"')
+
+        response = self.client.post(test_link, data={
+            'username': 'Bawww',
+            'rank': six.text_type(test_user.rank_id),
+            'roles': six.text_type(test_user.roles.all()[0].pk),
+            'email': 'reg@stered.com',
+            'new_password': 'pass123',
+            'is_staff': '1',
+            'is_superuser': '0',
+            'signature': 'Hello world!',
+            'is_signature_locked': '1',
+            'is_hiding_presence': '0',
+            'limits_private_thread_invites_to': '0',
+            'signature_lock_staff_message': 'Staff message',
+            'signature_lock_user_message': 'User message',
+            'subscribe_to_started_threads': '2',
+            'subscribe_to_replied_threads': '2',
+            'is_active': '0',
+            'is_active_staff_message': "Disabled in test!"
+        })
+        self.assertEqual(response.status_code, 302)
+
+        updated_user = User.objects.get(pk=test_user.pk)
+        self.assertFalse(updated_user.is_active)
+        self.assertEqual(updated_user.is_active_staff_message, "Disabled in test!")
+
+    def test_edit_admin_cant_disable_admin(self):
+        """edit user view disallows admin to disable admin"""
+        self.user.is_superuser = False
+        self.user.save()
+
+        User = get_user_model()
+        test_user = User.objects.create_user('Bob', 'bob@test.com', 'pass123')
+
+        test_user.is_staff = True
+        test_user.save()
+
+        test_link = reverse('misago:admin:users:accounts:edit',
+                            kwargs={'pk': test_user.pk})
+
+        response = self.client.get(test_link)
+        self.assertNotContains(response, 'id="id_is_active_1"')
+        self.assertNotContains(response, 'id="id_is_active_staff_message"')
+
+        response = self.client.post(test_link, data={
+            'username': 'Bawww',
+            'rank': six.text_type(test_user.rank_id),
+            'roles': six.text_type(test_user.roles.all()[0].pk),
+            'email': 'reg@stered.com',
+            'new_password': 'pass123',
+            'is_staff': '1',
+            'is_superuser': '0',
+            'signature': 'Hello world!',
+            'is_signature_locked': '1',
+            'is_hiding_presence': '0',
+            'limits_private_thread_invites_to': '0',
+            'signature_lock_staff_message': 'Staff message',
+            'signature_lock_user_message': 'User message',
+            'subscribe_to_started_threads': '2',
+            'subscribe_to_replied_threads': '2',
+            'is_active': '0',
+            'is_active_staff_message': "Disabled in test!"
+        })
+        self.assertEqual(response.status_code, 302)
+
+        updated_user = User.objects.get(pk=test_user.pk)
+        self.assertTrue(updated_user.is_active)
+        self.assertFalse(updated_user.is_active_staff_message)
+
+    def test_edit_superuser_disable_admin(self):
+        """edit user view allows superuser to disable admin"""
+        self.user.is_superuser = True
+        self.user.save()
+
+        User = get_user_model()
+        test_user = User.objects.create_user('Bob', 'bob@test.com', 'pass123')
+
+        test_user.is_staff = True
+        test_user.save()
+
+        test_link = reverse('misago:admin:users:accounts:edit',
+                            kwargs={'pk': test_user.pk})
+
+        response = self.client.get(test_link)
+        self.assertContains(response, 'id="id_is_active_1"')
+        self.assertContains(response, 'id="id_is_active_staff_message"')
+
+        response = self.client.post(test_link, data={
+            'username': 'Bawww',
+            'rank': six.text_type(test_user.rank_id),
+            'roles': six.text_type(test_user.roles.all()[0].pk),
+            'email': 'reg@stered.com',
+            'new_password': 'pass123',
+            'is_staff': '1',
+            'is_superuser': '0',
+            'signature': 'Hello world!',
+            'is_signature_locked': '1',
+            'is_hiding_presence': '0',
+            'limits_private_thread_invites_to': '0',
+            'signature_lock_staff_message': 'Staff message',
+            'signature_lock_user_message': 'User message',
+            'subscribe_to_started_threads': '2',
+            'subscribe_to_replied_threads': '2',
+            'is_active': '0',
+            'is_active_staff_message': "Disabled in test!"
+        })
+        self.assertEqual(response.status_code, 302)
+
+        updated_user = User.objects.get(pk=test_user.pk)
+        self.assertFalse(updated_user.is_active)
+        self.assertEqual(updated_user.is_active_staff_message, "Disabled in test!")
+
     def test_delete_threads_view(self):
         """delete user threads view deletes threads"""
         User = get_user_model()

+ 22 - 6
misago/users/views/admin/users.py

@@ -14,7 +14,9 @@ from misago.core.pgutils import batch_update
 from misago.threads.models import Thread
 
 from ...avatars.dynamic import set_avatar as set_dynamic_avatar
-from ...forms.admin import BanUsersForm, EditUserForm, NewUserForm, SearchUsersForm, StaffFlagUserFormFactory
+from ...forms.admin import (
+    BanUsersForm, NewUserForm, SearchUsersForm,
+    EditUserForm, EditUserFormFactory)
 from ...models import ACTIVATION_REQUIRED_NONE, Ban, User
 from ...models.ban import BAN_EMAIL, BAN_IP, BAN_USERNAME
 from ...signatures import set_user_signature
@@ -28,13 +30,23 @@ class UserAdmin(generic.AdminBaseMixin):
         return get_user_model()
 
     def create_form_type(self, request, target):
-        if request.user.is_superuser:
-            add_staff_field = request.user.pk != target.pk
+        add_is_active_fields = False
+        add_admin_fields = False
+
+        if target.is_staff:
+            if request.user.is_superuser:
+                add_is_active_fields = request.user.pk != target.pk
         else:
-            add_staff_field = False
+            add_is_active_fields = True
+
+        if request.user.is_superuser:
+            add_admin_fields = request.user.pk != target.pk
 
-        return StaffFlagUserFormFactory(
-            self.Form, target, add_staff_field=add_staff_field)
+        return EditUserFormFactory(
+            self.Form, target,
+            add_is_active_fields=add_is_active_fields,
+            add_admin_fields=add_admin_fields,
+        )
 
 
 class UsersList(UserAdmin, generic.ListView):
@@ -287,6 +299,10 @@ class EditUser(UserAdmin, generic.ModelFormView):
             target.is_staff = form.cleaned_data.get('is_staff')
             target.is_superuser = form.cleaned_data.get('is_superuser')
 
+        if 'is_active' in form.fields and 'is_active_staff_message' in form.fields:
+            target.is_active = form.cleaned_data.get('is_active')
+            target.is_active_staff_message = form.cleaned_data.get('is_active_staff_message')
+
         target.rank = form.cleaned_data.get('rank')
 
         target.roles.clear()

+ 3 - 0
misago/users/views/profile.py

@@ -36,6 +36,9 @@ def profile_view(f):
 
         profile = get_object_or_404(queryset, pk=kwargs.pop('pk'))
 
+        if not profile.is_active and not request.user.is_staff:
+            raise Http404()
+
         validate_slug(profile, kwargs.pop('slug'))
         kwargs['profile'] = profile