Browse Source

- Admin Users List displays "protected" (team members) labels as well as inactive labels
- SignUp form has been updated to make better use of Layouts feature
- Stub of Edit User action
- Performace enchancement on Admin Users List
- Notification about inactive users on Admin Home and Users List
- User roles can be made "protected", making users with them special and uneditable for anybody but godadmins.

Ralfp 12 years ago
parent
commit
af9f662d7c

+ 6 - 4
misago/acl/fixtures.py

@@ -5,19 +5,21 @@ from misago.utils import get_msgid
 def load_fixtures():
     role_admin = Role(
                       name=_("Administrator").message,
-                      token='admin'
+                      token='admin',
+                      protected=True,
                       )
     role_mod = Role(
                     name=_("Moderator").message,
-                    token='mod'
+                    token='mod',
+                    protected=True,
                     )
     role_registered = Role(
                            name=_("Registered").message,
-                           token='registered'
+                           token='registered',
                            )
     role_guest = Role(
                       name=_("Guest").message,
-                      token='guest'
+                      token='guest',
                       )
     
     role_admin.save(force_insert=True)

+ 1 - 0
misago/acl/models.py

@@ -7,6 +7,7 @@ class Role(models.Model):
     """
     name = models.CharField(max_length=255)
     token = models.CharField(max_length=255,null=True,blank=True)
+    protected = models.BooleanField(default=False)
     
     def __unicode__(self):
         return unicode(_(self.name))

+ 14 - 3
misago/admin/widgets.py

@@ -266,13 +266,17 @@ class ListWidget(BaseWidget):
             items = self.set_filters(items, request.session.get(self.get_token('filter')))
         else:
             items = items.all()
-            
+                   
         # Sort them
         items = self.sort_items(request, items, sorting_method);
         
         # Set pagination
         if self.pagination:
             items = items[paginating_method['start']:paginating_method['stop']]
+        
+        # Prefetch related?
+        if self.prefetch_related:
+            items = self.prefetch_related(items)
             
         # Default message
         message = request.messages.get_message(self.admin.id)
@@ -383,6 +387,7 @@ class FormWidget(BaseWidget):
     form = None
     layout = None
     target_name = None
+    original_name = None
     submit_fallback = False
     
     def get_url(self, request, model):
@@ -391,6 +396,11 @@ class FormWidget(BaseWidget):
     def get_form(self, request, model):
         return self.form
     
+    def get_form_instance(self, form, request, model, initial, post=False):
+        if post:
+            return form(request.POST, request=request, initial=self.get_initial_data(request, model))
+        return form(request=request, initial=self.get_initial_data(request, model))
+    
     def get_layout(self, request, form, model):
         if self.layout:
             return self.layout
@@ -413,6 +423,7 @@ class FormWidget(BaseWidget):
         model = None
         if target:
             model = self.get_and_validate_target(request, target)
+            self.original_name = self.get_target_name(model)
             if not model:
                 return redirect(self.get_fallback_url(request))
         original_model = model
@@ -422,7 +433,7 @@ class FormWidget(BaseWidget):
         
         #Submit form
         if request.method == 'POST':
-            form = FormType(request.POST, request=request, initial=self.get_initial_data(request, model))
+            form = self.get_form_instance(FormType, request, model, self.get_initial_data(request, model), True)
             if form.is_valid():
                 model, message = self.submit_form(request, form, model)
                 if message.type != 'error':
@@ -448,7 +459,7 @@ class FormWidget(BaseWidget):
                 message = Message(request, form.non_field_errors()[0])
                 message.type = 'error'
         else:
-            form = FormType(request=request, initial=self.get_initial_data(request, model))
+            form = self.get_form_instance(FormType, request, model, self.get_initial_data(request, model))
             
         # Render form
         return request.theme.render_to_response(self.get_templates(self.template),

+ 1 - 1
misago/overview/admin/__init__.py

@@ -31,7 +31,7 @@ ADMIN_ACTIONS=(
                route='admin_overview_stats',
                urlpatterns=patterns('misago.overview.admin.views',
                         url(r'^$', 'overview_stats', name='admin_overview_stats'),
-                        url(r'^(?P<model>[a-zA-Z0-9]+)/(?P<date_start>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])/(?P<date_end>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])/(?P<precision>\w+)$', 'overview_graph', name='admin_overview_graph'),
+                        url(r'^(?P<model>[a-z0-9]+)/(?P<date_start>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])/(?P<date_end>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])/(?P<precision>\w+)$', 'overview_graph', name='admin_overview_graph'),
                     ),
                ),
    AdminAction(

+ 1 - 1
misago/settings/admin/__init__.py

@@ -22,7 +22,7 @@ ADMIN_ACTIONS=(
                urlpatterns=patterns('misago.settings.admin.views',
                         url(r'^$', 'settings', name='admin_settings'),
                         url(r'^search/$', 'settings_search', name='admin_settings_search'),
-                        url(r'^(?P<group_slug>([a-zA-Z0-9]|-)+)-(?P<group_id>\d+)/$', 'settings', name='admin_settings')
+                        url(r'^(?P<group_slug>([a-z0-9]|-)+)-(?P<group_id>\d+)/$', 'settings', name='admin_settings')
                     ),
                ),
 )

+ 5 - 4
misago/users/admin/__init__.py

@@ -40,9 +40,10 @@ ADMIN_ACTIONS=(
                route='admin_users',
                urlpatterns=patterns('misago.users.admin.users.views',
                         url(r'^$', 'List', name='admin_users'),
+                        url(r'^inactive/$', 'inactive', name='admin_users_inactive'),
                         url(r'^new/$', 'List', name='admin_users_new'),
-                        url(r'^edit/(?P<slug>([a-zA-Z0-9]|-)+)-(?P<target>\d+)/$', 'List', name='admin_users_edit'),
-                        url(r'^delete/(?P<slug>([a-zA-Z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_users_delete'),
+                        url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_users_edit'),
+                        url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_users_delete'),
                     ),
                ),
    AdminAction(
@@ -72,8 +73,8 @@ ADMIN_ACTIONS=(
                urlpatterns=patterns('misago.users.admin.ranks.views',
                         url(r'^$', 'List', name='admin_users_ranks'),
                         url(r'^new/$', 'New', name='admin_users_ranks_new'),
-                        url(r'^edit/(?P<slug>([a-zA-Z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_users_ranks_edit'),
-                        url(r'^delete/(?P<slug>([a-zA-Z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_users_ranks_delete'),
+                        url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_users_ranks_edit'),
+                        url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_users_ranks_delete'),
                     ),
                ),
    AdminAction(

+ 1 - 2
misago/users/admin/ranks/views.py

@@ -128,7 +128,6 @@ class Edit(FormWidget):
                 }
     
     def submit_form(self, request, form, target):
-        original_name = target.name
         target.name = form.cleaned_data['name']
         target.name_slug = slugify(form.cleaned_data['name'])
         target.description = form.cleaned_data['description']
@@ -138,7 +137,7 @@ class Edit(FormWidget):
         target.as_tab = form.cleaned_data['as_tab']
         target.criteria = form.cleaned_data['criteria']
         target.save(force_update=True)
-        return target, BasicMessage(_('Changes in rank "%(name)s" have been saved.' % {'name': original_name}), 'success')
+        return target, BasicMessage(_('Changes in rank "%(name)s" have been saved.' % {'name': self.original_name}), 'success')
 
 
 class Delete(ButtonWidget):

+ 79 - 2
misago/users/admin/users/forms.py

@@ -1,8 +1,85 @@
+from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext_lazy as _
 from django import forms
 from misago.acl.models import Role
-from misago.forms import Form
-from misago.users.models import Rank
+from misago.users.models import User, Rank
+from misago.users.validators import validate_password, validate_email
+from misago.forms import Form, YesNoSwitch
+
+class UserForm(Form):
+    username = forms.CharField(max_length=255)
+    title = forms.CharField(max_length=255,required=False)
+    rank = forms.ModelChoiceField(queryset=Rank.objects.order_by('order').all(),required=False,empty_label=_('No rank assigned'))
+    roles = False
+    email = forms.EmailField(max_length=255)
+    new_password = forms.CharField(max_length=255,required=False,widget=forms.PasswordInput)
+    
+    layout = [
+              [
+               _("Basic Account Settings"),
+               [
+                ('username', {'label': _("Username"), 'help_text': _("Username is name under which user is known to other users. Between 3 and 15 characters, only letters and digits are allowed.")}),
+                ('title', {'label': _("User Title"), 'help_text': _("To override user title with custom one, enter it here.")}),
+                ('rank', {'label': _("User Rank"), 'help_text': _("This users's rank.")}),
+                ('roles', {'label': _("User Roles"), 'help_text': _("This user's roles. Roles are sets of user permissions")}),
+                ],
+               ],
+              [
+               _("Sign-in Credentials"),
+               [
+                ('email', {'label': _("E-mail Address"), 'help_text': _("Username is name under which user is known to other users.")}),
+                ('new_password', {'label': _("Change User Password"), 'help_text': _("If you wish to change user's password, enter here new password. Otherwhise leave this field blank"), 'has_value': False}),
+                ],
+               ],
+              ]
+        
+    def __init__(self, user=None, *args, **kwargs):
+        self.request = kwargs['request']
+        self.user = user
+        
+        # Sort out protected roles
+        if not self.request.user.is_protected():
+            self.base_fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple,queryset=Role.objects.filter(protected__exact=False).order_by('name').all(),error_messages={'required': _("User must have at least one role assigned.")})
+        else:
+            self.base_fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple,queryset=Role.objects.order_by('name').all(),error_messages={'required': _("User must have at least one role assigned.")})
+            
+        # Keep non-gods from editing permissions and e-mail addressess of protected members
+        if user.is_protected() and not self.request.user.is_god():
+            # You can edit your own e-mail all-right
+            if user.pk != self.request.user.pk:
+                del self.base_fields['email']
+                del self.layout[1][1][0]
+            del self.base_fields['roles']
+            del self.layout[0][1][3]
+            
+        super(UserForm, self).__init__(*args, **kwargs)
+    
+    def clean_username(self):
+        self.user.set_username(self.cleaned_data['username'])
+        try:
+            self.user.full_clean()
+        except ValidationError as e:
+            self.user.is_username_valid(e)
+        return self.cleaned_data['username']
+        
+    def clean_email(self):
+        self.user.set_email(self.cleaned_data['email'])
+        try:
+            self.user.full_clean()
+        except ValidationError as e:
+            self.user.is_email_valid(e)
+        return self.cleaned_data['email']
+        
+    def clean_new_password(self):
+        if self.cleaned_data['new_password']:
+            self.user.set_password(self.cleaned_data['new_password'])
+            try:
+                self.user.full_clean()
+            except ValidationError as e:
+                self.user.is_password_valid(e)
+            validate_password(self.cleaned_data['new_password'])
+            return self.cleaned_data['new_password']
+        return ''
 
 class SearchUsersForm(Form):
     username = forms.CharField(max_length=255, required=False)

+ 57 - 7
misago/users/admin/users/views.py

@@ -3,12 +3,12 @@ from django.utils.translation import ugettext as _
 from misago.admin import site
 from misago.admin.widgets import *
 from misago.utils import slugify
-from misago.users.admin.users.forms import SearchUsersForm
+from misago.users.admin.users.forms import UserForm, SearchUsersForm
 from misago.users.models import User
 
 def reverse(route, target=None):
     if target:
-        return django_reverse(route, kwargs={'target': target.pk, 'slug': slugify(target.username)})
+        return django_reverse(route, kwargs={'target': target.pk, 'slug': target.username_slug})
     return django_reverse(route)
 
 """
@@ -48,6 +48,9 @@ class List(ListWidget):
             model = model.filter(activation__in=filters['activation'])
         return model
     
+    def prefetch_related(self, items):
+        return items.prefetch_related('roles')
+    
     def get_item_actions(self, request, item):
         return (
                 self.action('pencil', _("Edit User Details"), reverse('admin_users_edit', item)),
@@ -59,12 +62,53 @@ class List(ListWidget):
             if unicode(user.pk) in checked:
                 if user.pk == request.user.id:
                     return BasicMessage(_('You cannot delete yourself.'), 'error'), reverse('admin_users')
-                if user.is_god():
-                    return BasicMessage(_('You cannot delete system administrator.'), 'error'), reverse('admin_users')
+                if user.is_protected():
+                    return BasicMessage(_('You cannot delete protected member.'), 'error'), reverse('admin_users')
                 
         User.objects.filter(id__in=checked).delete()
         User.objects.resync_monitor(request.monitor)
         return BasicMessage(_('Selected users have been deleted successfully.'), 'success'), reverse('admin_users')
+    
+
+class Edit(FormWidget):
+    admin = site.get_action('users')
+    id = 'edit'
+    name = _("Edit User")
+    fallback = 'admin_users'
+    form = UserForm
+    target_name = 'username'
+    notfound_message = _('Requested User could not be found.')
+    submit_fallback = True
+    
+    def get_form_instance(self, form, request, model, initial, post=False):
+        if post:
+            return form(model, request.POST, request=request, initial=self.get_initial_data(request, model))
+        return form(model, request=request, initial=self.get_initial_data(request, model))
+        
+    def get_url(self, request, model):
+        return reverse('admin_users_edit', model)
+    
+    def get_edit_url(self, request, model):
+        return self.get_url(request, model)
+    
+    def get_initial_data(self, request, model):
+        return {
+                'username': model.username,
+                'title': model.title,
+                'email': model.email,
+                'rank': model.rank,
+                'roles': model.roles.all(),
+                }
+    
+    def submit_form(self, request, form, target):
+        target.title = form.cleaned_data['title']
+        target.rank = form.cleaned_data['rank']
+        if not target.is_protected() or request.user.is_god():
+            target.roles.clear()
+            for role in form.cleaned_data['roles']:
+                target.roles.add(role)
+        target.save(force_update=True)
+        return target, BasicMessage(_('Changes in user\'s "%(name)s" account have been saved.' % {'name': self.original_name}), 'success')
 
 
 class Delete(ButtonWidget):
@@ -76,8 +120,14 @@ class Delete(ButtonWidget):
     def action(self, request, target):
         if target.pk == request.user.id:
             return BasicMessage(_('You cannot delete yourself.'), 'error'), False
-        if target.is_god():
-            return BasicMessage(_('You cannot delete system administrator.'), 'error'), False
+        if target.is_protected():
+            return BasicMessage(_('You cannot delete protected member.'), 'error'), False
         target.delete()
         User.objects.resync_monitor(request.monitor)
-        return BasicMessage(_('User "%(name)s" has been deleted.' % {'name': target.username}), 'success'), False
+        return BasicMessage(_('User "%(name)s" has been deleted.' % {'name': target.username}), 'success'), False
+    
+
+def inactive(request):
+    token = 'list_filter_misago.users.models.User'
+    request.session[token] = {'activation': ['1', '2', '3']}
+    return redirect(reverse('admin_users'))

+ 9 - 15
misago/users/forms.py

@@ -9,14 +9,14 @@ from misago.users.validators import validate_password, validate_email
 
 
 class UserRegisterForm(Form):
-    username = forms.CharField(max_length=15,help_text=_("Between 3 and 15 characters, only letters and digits are allowed."))
-    email = forms.EmailField(max_length=255,help_text=_("Working e-mail inbox is required to maintain control over your forum account."))
+    username = forms.CharField(max_length=15)
+    email = forms.EmailField(max_length=255)
     email_rep = forms.EmailField(max_length=255)
-    password = forms.CharField(max_length=255,help_text=_("Password you will be using to sign in to your account. Make sure it's strong."))
-    password_rep = forms.CharField(max_length=255)
+    password = forms.CharField(max_length=255,widget=forms.PasswordInput)
+    password_rep = forms.CharField(max_length=255,widget=forms.PasswordInput)
     captcha_qa = captcha.QACaptchaField()
     recaptcha = captcha.ReCaptchaField()
-    accept_tos = forms.BooleanField(required=True,label=_("Forum Terms of Service"),error_messages={'required': _("Acceptation of board ToS is mandatory for membership.")})
+    accept_tos = forms.BooleanField(required=True,error_messages={'required': _("Acceptation of board ToS is mandatory for membership.")})
     
     validate_repeats = (('email', 'email_rep'), ('password', 'password_rep'))
     repeats_errors = [{
@@ -29,12 +29,12 @@ class UserRegisterForm(Form):
     layout = [
                  (
                      None,
-                     [('username', {'attrs': {'placeholder': _("Enter your desired username")}})]
+                     [('username', {'label': _('Username'), 'help_text': _("Your displayed username. Between 3 and 15 characters, only letters and digits are allowed."),'attrs': {'placeholder': _("Enter your desired username")}})]
                  ),
                  (
                      None,
-                     [('nested', [('email', {'label': _('E-mail address'), 'attrs': {'placeholder': _("Enter your e-mail")}, 'width': 50}), ('email_rep', {'attrs': {'placeholder': _("Repeat your e-mail")}, 'width': 50})]), 
-                      ('nested', [('password', {'label': _('Password'), 'has_value': False, 'attrs': {'placeholder': _("Enter your password")}, 'width': 50}), ('password_rep', {'has_value': False, 'attrs': {'placeholder': _("Repeat your password")}, 'width': 50})])]
+                     [('nested', [('email', {'label': _('E-mail address'), 'help_text': _("Working e-mail inbox is required to maintain control over your forum account."), 'attrs': {'placeholder': _("Enter your e-mail")}, 'width': 50}), ('email_rep', {'attrs': {'placeholder': _("Repeat your e-mail")}, 'width': 50})]), 
+                      ('nested', [('password', {'label': _('Password'), 'help_text': _("Password you will be using to sign in to your account. Make sure it's strong."), 'has_value': False, 'attrs': {'placeholder': _("Enter your password")}, 'width': 50}), ('password_rep', {'has_value': False, 'attrs': {'placeholder': _("Repeat your password")}, 'width': 50})])]
                  ),
                  (
                      None,
@@ -42,15 +42,9 @@ class UserRegisterForm(Form):
                  ),
                  (
                      None,
-                     [('accept_tos', {'inline': _("I have read and accept this forums Terms of Service.")})]
+                     [('accept_tos', {'label': _("Forum Terms of Service"), 'inline': _("I have read and accept this forums Terms of Service.")})]
                  ),
              ]
-    
-    class Meta:
-        widgets = {
-            'password': forms.PasswordInput(),
-            'password_rep': forms.PasswordInput(),
-        }
         
     def clean_username(self):
         new_user = User.objects.get_blank_user()

+ 18 - 1
misago/users/models.py

@@ -163,8 +163,19 @@ class User(models.Model):
     
     statistics_name = _('Users Registrations')
         
+    def acl(self):
+        pass
+        
     def is_admin(self):
-        return 1
+        if self.is_god():
+            return True
+        return False #TODO!
+    
+    def is_god(self):
+        for user in settings.ADMINS:
+            if user[1].lower() == self.email:
+                return True
+        return False
     
     def is_anonymous(self):
         return False
@@ -175,6 +186,12 @@ class User(models.Model):
     def is_crawler(self):
         return False
 
+    def is_protected(self):
+        for role in self.roles.all():
+            if role.protected:
+                return True
+        return False
+    
     def default_avatar(self, db_settings):
         if db_settings['default_avatar'] == 'gallery':
             try:

+ 2 - 2
misago/users/urls.py

@@ -2,10 +2,10 @@ from django.conf.urls import patterns, url, include
 
 urlpatterns = patterns('misago.users.views',
     url(r'^register/$', 'register', name="register"),
-    url(r'^activate/(?P<username>\w+)-(?P<user>\d+)/(?P<token>[a-zA-Z0-9]+)/$', 'activation.activate', name="activate"),
+    url(r'^activate/(?P<username>[a-z0-9]+)-(?P<user>\d+)/(?P<token>[a-z0-9]+)/$', 'activation.activate', name="activate"),
     url(r'^resend-activation/$', 'activation.form', name="send_activation"),
     url(r'^reset-pass/$', 'password.form', name="forgot_password"),
-    url(r'^reset-pass/(?P<username>\w+)-(?P<user>\d+)/(?P<token>[a-zA-Z0-9]+)/$', 'password.reset', name="reset_password"),
+    url(r'^reset-pass/(?P<username>[a-z0-9]+)-(?P<user>\d+)/(?P<token>[a-z0-9]+)/$', 'password.reset', name="reset_password"),
     url(r'^users/$', 'profiles.list', name="users"),
     url(r'^users/(?P<username>\w+)-(?P<user>\d+)/$', 'profiles.show', name="user"),
     url(r'^users/(?P<rank_slug>(\w|-)+)/$', 'profiles.list', name="users"),

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

@@ -23,10 +23,12 @@ def register(request):
         form = UserRegisterForm(request.POST, request=request)
         if form.is_valid():
             need_activation = 0
+            
             if request.settings['account_activation'] == 'user':
                 need_activation = User.ACTIVATION_USER
             if request.settings['account_activation'] == 'admin':
                 need_activation = User.ACTIVATION_ADMIN
+                
             new_user = User.objects.create_user(
                                                 form.cleaned_data['username'],
                                                 form.cleaned_data['email'],
@@ -35,6 +37,7 @@ def register(request):
                                                 activation=need_activation,
                                                 request=request
                                                 )
+                        
             if need_activation == User.ACTIVATION_NONE:
                 # No need for activation, sign in user
                 sign_user_in(request, new_user)

+ 6 - 0
misago/users/views/activation.py

@@ -22,6 +22,7 @@ def activate(request, username="", user="0", token=""):
     try:
         user = User.objects.get(pk=user)
         current_activation = user.activation
+        
         # Run checks
         user_ban = check_ban(username=user.username, email=user.email)
         if user_ban:
@@ -32,9 +33,14 @@ def activate(request, username="", user="0", token=""):
             return error403(request, Message(request, 'users/activation/only_by_admin', extra={'user': user}))
         if not token or not user.token or user.token != token:
             return error403(request, Message(request, 'users/invalid_confirmation_link', extra={'user': user}))
+        
         # Activate and sign in our member
         user.activation = User.ACTIVATION_NONE
         sign_user_in(request, user)
+        
+        # Update monitor
+        request.monitor['users_inactive'] = request.monitor['users_inactive'] - 1
+        
         if current_activation == User.ACTIVATION_CREDENTIALS:
             request.messages.set_flash(Message(request, 'users/activation/credentials', extra={'user':user}), 'success')
         else:

+ 12 - 0
templates/admin/overview/home.html

@@ -10,6 +10,18 @@
 <div class="page-header">
   <h1>{% trans %}Admin Home{% endtrans %} <small>{% trans %}Misago {{version}}{% endtrans %}</small></h1>
 </div>
+
+{% if monitor.users_inactive|int > 0 %}
+<div class="alert alert-info alert-form">
+  <div class="alert-icon"><span><i class="icon-info-sign icon-white"></i></span></div>
+  <p><a href="{% url 'admin_users_inactive' %}">{%- trans count=monitor.users_inactive|int, total=monitor.users_inactive|int|intcomma -%}
+  There is one inactive user.
+  {%- pluralize -%}
+  There are {{ total }} inactive users.
+  {%- endtrans -%}</a></p>
+</div>
+{% endif %}
+
 <div class="row">
   <div class="span8">
   	<h2>Administrators Online</h2>

+ 23 - 1
templates/admin/users/admin_users/list.html

@@ -3,13 +3,35 @@
 {% load l10n %}
 {% load url from future %}
 
+{% block action_body %}
+{% if monitor.users_inactive|int > 0 %}
+<div class="alert alert-info alert-form">
+  <div class="alert-icon"><span><i class="icon-info-sign icon-white"></i></span></div>
+  <p><a href="{% url 'admin_users_inactive' %}">{%- trans count=monitor.users_inactive|int, total=monitor.users_inactive|int|intcomma -%}
+  There is one inactive user.
+  {%- pluralize -%}
+  There are {{ total }} inactive users.
+  {%- endtrans -%}</a></p>
+</div>
+{% endif %}
+
+{{ super() }}
+{% endblock %}
+
 {% block table_head scoped %}
   <th>&nbsp;</th>
   {{ super() }}
 {% endblock %}
+
 {% block table_row scoped %}
   <td class="avatar-small"><img src="{{ item.get_avatar('small') }}" class="avatar-small" alt="{% trans %}User Avatar{% endtrans %}"></td>
   <td colspan="2" class="lead-cell">
-  	<strong>{{ item.username }}</strong> <span class="muted">{{ item.email }}</span>{% if item.activation > 0 %} <span class="label">{% trans %}Inactive{% endtrans %}</span>{% endif %}{% if item.is_admin() %} <span class="label label-info">{% trans %}Admin{% endtrans %}</span>{% endif %}
+  	<strong>{{ item.username }}</strong> <span class="muted">{{ item.email }}</span>{% if item.is_admin() %} <span class="label label-important">{% trans %}Admin{% endtrans %}</span>{% elif item.is_protected() %} <span class="label label-info">{% trans %}Team{% endtrans %}</span>{% endif %}{% if item.activation > 0 %} <span class="label tooltip-top" title="{% if item.activation == 1 -%}
+  	{% trans %}This user has not yet validated his e-mail address.{% endtrans %}
+  	{%- elif item.activation == 2 -%}
+  	{% trans %}This user is awaiting admin approval.{% endtrans %}
+  	{%- else -%}
+  	{% trans %}This user is changing his sign-in credentials.{% endtrans %}
+  	{%- endif %}">{% trans %}Inactive{% endtrans %}</span>{% endif %}
   </td>
 {% endblock%}