Browse Source

- Users List and Search
- Groups mechanic replaced with Roles
- Updated fixtures

Ralfp 12 years ago
parent
commit
adf609d953

+ 26 - 0
misago/acl/fixtures.py

@@ -0,0 +1,26 @@
+from misago.acl.models import Role
+from misago.utils import ugettext_lazy as _
+from misago.utils import get_msgid
+
+def load_fixture():
+    role_admin = Role(
+                      name=_("Administrator").message,
+                      token='admin'
+                      )
+    role_mod = Role(
+                    name=_("Moderator").message,
+                    token='mod'
+                    )
+    role_registered = Role(
+                           name=_("Registered").message,
+                           token='registered'
+                           )
+    role_guest = Role(
+                      name=_("Guest").message,
+                      token='guest'
+                      )
+    
+    role_admin.save(force_insert=True)
+    role_mod.save(force_insert=True)
+    role_registered.save(force_insert=True)
+    role_guest.save(force_insert=True)    

+ 3 - 0
misago/acl/middleware.py

@@ -0,0 +1,3 @@
+class ACLMiddleware(object):
+    def process_request(self, request):
+        print 'ACL MIDDLEWARE!!!'

+ 11 - 0
misago/acl/models.py

@@ -0,0 +1,11 @@
+from django.db import models
+
+class Role(models.Model):
+    """
+    Misago User Role model
+    """
+    name = models.CharField(max_length=255)
+    token = models.CharField(max_length=255,null=True,blank=True)
+    
+    def is_special(self):
+        return token

+ 0 - 6
misago/forums/models.py

@@ -148,12 +148,6 @@ class Vote(models.Model):
     ip = models.GenericIPAddressField()
     option = models.PositiveIntegerField()
     
-   
-class Moderator(models.Model):
-    forum = models.ForeignKey(Forum, related_name='+')
-    group = models.ForeignKey('users.Group', related_name='+', null=True, blank=True)
-    user = models.ForeignKey('users.User', related_name='+', null=True, blank=True)
-    
     
 class Report(models.Model):
     forum = models.ForeignKey(Forum, related_name='+')

+ 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-zA-Z0-9]|-)+)-(?P<group_id>\d+)/$', 'settings', name='admin_settings')
                     ),
                ),
 )

+ 1 - 0
misago/settings_base.py

@@ -83,6 +83,7 @@ MIDDLEWARE_CLASSES = (
     'misago.banning.middleware.BanningMiddleware',
     'misago.messages.middleware.MessagesMiddleware',
     'misago.users.middleware.UserMiddleware',
+    'misago.acl.middleware.ACLMiddleware',
     'django.middleware.common.CommonMiddleware',
 )
 

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

@@ -41,8 +41,8 @@ ADMIN_ACTIONS=(
                urlpatterns=patterns('misago.users.admin.users.views',
                         url(r'^$', 'List', name='admin_users'),
                         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-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'),
                     ),
                ),
    AdminAction(
@@ -72,8 +72,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-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'),
                     ),
                ),
    AdminAction(

+ 1 - 1
misago/users/admin/ranks/forms.py

@@ -6,7 +6,7 @@ from misago.forms import Form, YesNoSwitch
 class RankForm(Form):
     name = forms.CharField(max_length=255)
     description = forms.CharField(widget=forms.Textarea,required=False)
-    title = forms.CharField(max_length=255)
+    title = forms.CharField(max_length=255,required=False)
     style = forms.CharField(max_length=255,required=False)
     special = forms.BooleanField(widget=YesNoSwitch,required=False)
     as_tab = forms.BooleanField(widget=YesNoSwitch,required=False)

+ 14 - 56
misago/users/fixtures.py

@@ -1,6 +1,6 @@
 from misago.monitor.fixtures import load_monitor_fixture
 from misago.settings.fixtures import load_settings_fixture
-from misago.users.models import Rank, Group
+from misago.users.models import Rank
 from misago.utils import ugettext_lazy as _
 from misago.utils import get_msgid
 
@@ -161,76 +161,34 @@ def load_fixture():
     load_settings_fixture(settings_fixtures)
     
     rank_staff = Rank(
-                      name=_("Forum Staff").message,
+                      name=_("Forum Team").message,
+                      title=_("Forum Team").message,
                       style='staff',
-                      title=_("Forum Staff").message,
                       special=True,
                       order=0,
+                      as_tab=True,
                       )
     rank_lurker = Rank(
                       name=_("Lurker").message,
                       style='lurker',
-                      title=_("Lurker").message,
-                      order=2,
+                      order=1,
                       criteria="100%"
                       )
     rank_member = Rank(
                       name=_("Member").message,
-                      title=_("Member").message,
-                      order=3,
-                      criteria="15%"
+                      order=2,
+                      criteria="75%"
                       )
     rank_active = Rank(
-                      name=_("Active Member").message,
-                      title=_("Active Member").message,
-                      order=4,
-                      criteria="25"
+                      name=_("Most Valueable Posters").message,
+                      title=_("MVP").message,
+                      style='active',
+                      order=3,
+                      criteria="5%",
+                      as_tab=True,
                       )
     
     rank_staff.save(force_insert=True)
     rank_lurker.save(force_insert=True)
     rank_member.save(force_insert=True)
-    rank_active.save(force_insert=True)
-    
-    group_admins = Group(
-                         name=_("Administrators").message,
-                         name_slug='administrators',
-                         tab=_("Staff").message,
-                         position=0,
-                         rank=rank_staff,
-                         special=True,
-                         )
-    group_mods = Group(
-                       name=_("Moderators").message,
-                       name_slug='moderators',
-                       tab=_("Staff").message,
-                       position=1,
-                       rank=rank_staff,
-                       )
-    group_registered = Group(
-                         name=_("Registered").message,
-                         name_slug='registered',
-                         hidden=True,
-                         position=2,
-                         special=True,
-                         )
-    group_guests = Group(
-                         name=_("Guests").message,
-                         name_slug='guests',
-                         hidden=True,
-                         position=3,
-                         special=True,
-                         )
-    group_crawlers = Group(
-                           name=_("Web Crawlers").message,
-                           name_slug='web-crawlers',
-                           hidden=True,
-                           position=4,
-                           special=True,
-                           )
-    
-    group_admins.save(force_insert=True)
-    group_mods.save(force_insert=True)
-    group_registered.save(force_insert=True)
-    group_guests.save(force_insert=True)
-    group_crawlers.save(force_insert=True)    
+    rank_active.save(force_insert=True)

+ 4 - 0
misago/users/forms.py

@@ -106,4 +106,8 @@ class UserSendSpecialMailForm(Form):
         except User.DoesNotExist:
             raise ValidationError(_("There is no user with such e-mail address."))
         return email
+    
+    
+class QuickFindUserForm(Form):
+    username = forms.CharField()
     

+ 9 - 9
misago/users/management/commands/adduser.py

@@ -2,7 +2,8 @@ from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
 from django.core.management.base import BaseCommand, CommandError
 from django.utils import timezone
 from optparse import make_option
-from misago.users.models import UserManager, Group
+from misago.acl.models import Role
+from misago.users.models import UserManager
 
 class Command(BaseCommand):
     args = 'username email password'
@@ -18,19 +19,18 @@ class Command(BaseCommand):
     def handle(self, *args, **options):
         if len(args) < 3:
             raise CommandError('adduser requires exactly three arguments: user name, e-mail addres and password')
-        
-        # Get group
-        if options['admin']:
-            group = Group.objects.get(pk=1)
-        else:
-            group = Group.objects.get(pk=3)
-           
+                
         # Set user
         try:
             manager = UserManager()
-            manager.create_user(args[0], args[1], args[2], group=group)
+            new_user = manager.create_user(args[0], args[1], args[2])
         except ValidationError as e:
             raise CommandError("New user cannot be created because of following errors:\n\n%s" % '\n'.join(e.messages))
+                
+        # Set admin role
+        if options['admin']:
+            new_user.roles.add(Role.objects.get(token='admin'))
+            new_user.save(force_update=True)
         
         if options['admin']:
             self.stdout.write('Successfully created new administrator "%s"' % args[0])

+ 8 - 1
misago/users/management/commands/updateranking.py

@@ -19,7 +19,14 @@ class Command(BaseCommand):
         users_total = User.objects.exclude(rank__in=special_ranks).count()
         
         # Update Ranking
+        defaulted_ranks = False
         for rank in Rank.objects.filter(special=0).order_by('order'):
-            rank.assign_rank(users_total, special_ranks)
+            if defaulted_ranks:
+                # Set ranks according to ranking
+                rank.assign_rank(users_total, special_ranks)
+            else:
+                # Set default rank first
+                Users.objects.exclude(rank__in=special_ranks).update(rank=rank)
+                defaulted_ranks = True
         
         self.stdout.write('Users ranking for has been updated.\n')

+ 23 - 23
misago/users/models.py

@@ -10,6 +10,7 @@ from django.db import models, connection, transaction
 from django.template import RequestContext
 from django.utils import timezone as tz_util
 from django.utils.translation import ugettext_lazy as _
+from misago.acl.models import Role
 from misago.monitor.monitor import Monitor
 from misago.security import get_random_string
 from misago.settings.settings import Settings as DBSettings
@@ -36,7 +37,7 @@ class UserManager(models.Manager):
         monitor['last_user_name'] = last_user.username
         monitor['last_user_slug'] = last_user.username_slug
     
-    def create_user(self, username, email, password, group, timezone=False, ip='127.0.0.1', activation=0, request=False):
+    def create_user(self, username, email, password, timezone=False, ip='127.0.0.1', activation=0, request=False):
         token = ''
         if activation > 0:
             token = get_random_string(12)
@@ -49,6 +50,12 @@ class UserManager(models.Manager):
                 db_settings = DBSettings()
                 timezone = db_settings['default_timezone']
         
+        # Get first rank
+        try:
+            default_rank = Rank.objects.filter(special=0).order_by('order')[0]
+        except Rank.DoesNotExist:
+            default_rank = None
+        
         # Store user in database
         new_user = User(
                         join_date=tz_util.now(),
@@ -56,7 +63,7 @@ class UserManager(models.Manager):
                         activation=activation,
                         token=token,
                         timezone=timezone,
-                        group=group,
+                        rank=default_rank,
                         )
         
         new_user.set_username(username)
@@ -66,8 +73,8 @@ class UserManager(models.Manager):
         new_user.default_avatar(db_settings)
         new_user.save(force_insert=True)
         
-        # Set second group and default avatar
-        new_user.groups.add(group)
+        # Set user roles
+        new_user.roles.add(Role.objects.get(token='registered'))
         new_user.save(force_update=True)
         
         # Load monitor
@@ -128,6 +135,7 @@ class User(models.Model):
     followers_delta = models.IntegerField(default=0)
     score = models.FloatField(default=0,db_index=True)
     rank = models.ForeignKey('Rank',null=True,blank=True,db_index=True,on_delete=models.SET_NULL)
+    title = models.CharField(max_length=255,null=True,blank=True)
     last_post = models.DateTimeField(null=True,blank=True)
     last_search = models.DateTimeField(null=True,blank=True)
     alerts = models.PositiveIntegerField(default=0)
@@ -143,9 +151,7 @@ class User(models.Model):
     signature_ban_reason_admin = models.TextField(null=True,blank=True)
     signature_ban_expires = models.DateTimeField(null=True,blank=True)
     timezone = models.CharField(max_length=255,default='utc')
-    roles = models.CommaSeparatedIntegerField(max_length=255,null=True,blank=True)
-    group = models.ForeignKey('Group',on_delete=models.PROTECT)
-    groups = models.ManyToManyField('Group',related_name='groups')
+    roles = models.ManyToManyField(Role)
     acl_cache = models.TextField(null=True,blank=True)
     
     objects = UserManager()   
@@ -158,7 +164,7 @@ class User(models.Model):
     statistics_name = _('Users Registrations')
         
     def is_admin(self):
-        return 1 == self.group.pk
+        return 1
     
     def is_anonymous(self):
         return False
@@ -300,6 +306,13 @@ class User(models.Model):
             size = 100
         return 'http://www.gravatar.com/avatar/%s?s=%s' % (hashlib.md5(self.email).hexdigest(), size)
     
+    def get_title(self):
+        if self.title:
+            return self.title
+        if self.rank:
+            return self.rank.title
+        return None
+    
     def email_user(self, request, template, subject, context={}):
         templates = request.theme.get_email_templates(template)
         context = RequestContext(request, context)
@@ -356,25 +369,12 @@ class Crawler(object):
         return True
     
     
-class Group(models.Model):
-    """
-    Misago Users Group model
-    """
-    name = models.CharField(max_length=255)
-    name_slug = models.SlugField(max_length=255)
-    hidden = models.BooleanField(default=False)
-    tab = models.CharField(max_length=255,null=True,blank=True)
-    position = models.IntegerField(default=0)
-    rank = models.ForeignKey('Rank',null=True,blank=True,db_index=True,on_delete=models.SET_NULL)
-    special = models.BooleanField(default=False)
-    
-    
 class Rank(models.Model):
     """
     Misago User Rank
     Ranks are ready style/title pairs that are assigned to users either by admin (special ranks) or as result of user activity.
     """
-    name = models.CharField(max_length=255,null=True,blank=True)
+    name = models.CharField(max_length=255)
     name_slug = models.CharField(max_length=255,null=True,blank=True)
     description = models.TextField(null=True,blank=True)
     style = models.CharField(max_length=255,null=True,blank=True)
@@ -382,7 +382,7 @@ class Rank(models.Model):
     special = models.BooleanField(default=False)
     as_tab = models.BooleanField(default=False)
     order = models.IntegerField(default=0)
-    criteria = models.CharField(max_length=255,default='')
+    criteria = models.CharField(max_length=255,null=True,blank=True)
     
     def assign_rank(self, users=0, special_ranks=None):
         if not self.criteria or self.special or users == 0:

+ 1 - 0
misago/users/urls.py

@@ -8,6 +8,7 @@ urlpatterns = patterns('misago.users.views',
     url(r'^reset-pass/(?P<username>\w+)-(?P<user>\d+)/(?P<token>[a-zA-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"),
     url(r'^usercp/$', 'usercp.options', name="usercp"),
     url(r'^usercp/credentials$', 'usercp.credentials', name="usercp_credentials"),
     url(r'^usercp/username$', 'usercp.username', name="usercp_username"),

+ 1 - 2
misago/users/views/__init__.py

@@ -9,7 +9,7 @@ from misago.messages import Message
 from misago.security.auth import sign_user_in
 from misago.security.decorators import *
 from misago.users.forms import *
-from misago.users.models import User, Group
+from misago.users.models import User
 from misago.views import error403
 
 @block_banned
@@ -31,7 +31,6 @@ def register(request):
                                                 form.cleaned_data['username'],
                                                 form.cleaned_data['email'],
                                                 form.cleaned_data['password'],
-                                                Group.objects.get(pk=3), # Registered members
                                                 ip=request.session.get_ip(request),
                                                 activation=need_activation,
                                                 request=request

+ 1 - 1
misago/users/views/activation.py

@@ -68,7 +68,7 @@ def form(request):
                             )
             return redirect(reverse('index'))
         else:
-            message = Message(request, form.non_field_errors()[0])
+            message = Message(request, form.non_field_errors()[0], 'error')
     else:
         form = UserSendSpecialMailForm(request=request)
     return request.theme.render_to_response('users/resend_activation.html',

+ 81 - 3
misago/users/views/profiles.py

@@ -1,12 +1,90 @@
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.template import RequestContext
-from misago.users.models import User, Group
+from misago.forms import FormFields
+from misago.messages import Message
+from misago.users.forms import QuickFindUserForm
+from misago.users.models import User, Rank
 from misago.views import error404
+from misago.utils import slugify
 
 
-def list(request):
-    pass
+def list(request, rank_slug=None):
+    ranks = Rank.objects.filter(as_tab=1).order_by('order')
+    
+    # Find active rank
+    active_rank = None
+    if rank_slug:
+        for rank in ranks:
+            if rank.name_slug == rank_slug:
+                active_rank = rank
+        if not active_rank:
+            return error404(request)
+    elif ranks:
+        active_rank = ranks[0]
+    
+    # Empty Defaults
+    message = None
+    users = []
+    in_search = False
+    
+    # Users search?
+    if request.method == 'POST':
+        in_search = True
+        active_rank = None
+        search_form = QuickFindUserForm(request.POST, request=request)
+        if search_form.is_valid():
+            # Direct hit?
+            username = search_form.cleaned_data['username']
+            try:
+                user = User.objects.get(username__iexact=username)
+                return redirect(reverse('user', args=(user.username_slug, user.pk)))
+            except User.DoesNotExist:
+                pass
+            
+            # Looks like well have to find near match
+            if len(username) > 6:
+                username = username[0:-3]
+            elif len(username) > 5:
+                username = username[0:-2]
+            elif len(username) > 4:
+                username = username[0:-1]
+            username = slugify(username.strip())
+            
+            # Go for rought match
+            if len(username) > 0:
+                print username
+                users = User.objects.filter(username_slug__startswith=username).order_by('username_slug')[:10]
+        elif search_form.non_field_errors()[0] == 'form_contains_errors':
+            message = Message(request, 'users/search_empty', 'error')
+        else:
+            message = Message(request, search_form.non_field_errors()[0], 'error')
+    else:
+        search_form = QuickFindUserForm(request=request)
+        if active_rank:
+            users = User.objects.filter(rank=active_rank).order_by('username_slug')
+    
+    return request.theme.render_to_response('users/list.html',
+                                        {
+                                         'message': message,
+                                         'search_form': FormFields(search_form).fields,
+                                         'in_search': in_search,
+                                         'active_rank': active_rank,
+                                         'ranks': ranks,
+                                         'users': users,
+                                        },
+                                        context_instance=RequestContext(request));
+
+
+def list_search(request):
+    ranks = Rank.objects.filter(as_tab=1).order_by('order')
+    return request.theme.render_to_response('users/list_search.html',
+                                            {
+                                             'search_form': FormFields(QuickFindUserForm(request=request)),
+                                             'ranks': ranks,
+                                             'users': users,
+                                            },
+                                            context_instance=RequestContext(request));
 
 
 def show(request, user, username):

+ 1 - 1
static/admin/css/admin.css

@@ -908,7 +908,7 @@ td>ul.list-actions>li>a,td>ul.list-actions>li>form>button{background:none;border
 .navbar-sections .brand{margin-right:-10px;color:#333333;font-size:200%;}.navbar-sections .brand span{color:#999999;font-size:50%;line-height:50%;}
 .navbar-sections .user-profile{padding:7px 10px 7px 3px;margin:15px 5px;color:#222222;font-weight:bold;}
 .navbar-sections .nav{margin-left:0px;margin-right:0px;}.navbar-sections .nav li a,.navbar-sections .nav li a:link,.navbar-sections .nav li a:active,.navbar-sections .nav li a:visited{opacity:0.5;filter:alpha(opacity=50);padding:22px 10px 22px;margin:0px 5px;font-weight:bold;text-shadow:0px 1px 0px #ffffff;}
-.navbar-sections .nav li a:hover{background:#e8e8e8;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;padding:7px 10px 7px;margin:15px 5px;opacity:1;filter:alpha(opacity=100);}
+.navbar-sections .nav li a:hover,.navbar-sections .nav li a:active{background:#e8e8e8;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;padding:7px 10px 7px;margin:15px 5px;opacity:1;filter:alpha(opacity=100);}
 .navbar-sections .nav li form{margin:0px;padding:0px;}
 .navbar-sections .nav li.active a,.navbar-sections .nav li.active a:link,.navbar-sections .nav li.active a:active,.navbar-sections .nav li.active a:visited,.navbar-sections .nav li.active a:hover{background-color:#0088cc;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;padding:7px 10px 7px;margin:15px 0px;opacity:1;filter:alpha(opacity=100);text-shadow:0px 1px 0px #0077b3;}.navbar-sections .nav li.active a i,.navbar-sections .nav li.active a:link i,.navbar-sections .nav li.active a:active i,.navbar-sections .nav li.active a:visited i,.navbar-sections .nav li.active a:hover i{background-image:url("../img/glyphicons-halflings-white.png");opacity:1;filter:alpha(opacity=100);}
 .navbar-sections .btn-link,.navbar-sections .btn-link:link,.navbar-sections .btn-link:visited{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;padding:7px 8px 7px;margin:15px 0px;opacity:0.6;filter:alpha(opacity=60);color:#333333;font-weight:bold;font-size:100%;}.navbar-sections .btn-link i,.navbar-sections .btn-link:link i,.navbar-sections .btn-link:visited i{opacity:1;filter:alpha(opacity=100);}

+ 1 - 1
static/admin/css/admin/navbar.less

@@ -40,7 +40,7 @@
         text-shadow: 0px 1px 0px @white;
       }
       
-      a:hover  {
+      a:hover, a:active {
         background: @navbarLinkBackgroundHover;
         .border-radius(4px);
         padding: ((@navbarHeight - @baseLineHeight) / 2)-15px 10px ((@navbarHeight - @baseLineHeight) / 2)-15px;

+ 6 - 1
static/sora/css/sora.css

@@ -855,6 +855,8 @@ th.table-sort.sort-desc a:hover{border-bottom:3px solid #eca09a;padding-bottom:5
 .well .form-actions{background-color:#fafafa;-webkit-border-radius:0px 0px 3px 3px;-moz-border-radius:0px 0px 3px 3px;border-radius:0px 0px 3px 3px;margin:-44px -12px;margin-top:12px;}
 .well .form-horizontal .form-actions{padding-left:192px;}
 .list-tiny{display:block;margin:0px -6px;margin-bottom:16px;padding:4px 0px;overflow:auto;}.list-tiny>li{background:#e8e8e8;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;margin:3px 6px;padding:4px 7px;padding-bottom:6px;float:left;list-style:none;font-weight:bold;}
+.table-users a:link,.table-users a:active,.table-users a:visited,.table-users a:hover{color:#333333;font-size:150%;text-decoration:none;}
+.table-users .avatar{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;width:42px;height:42px;}
 .btn{background:#dcdcdc;border:1px solid #dcdcdc;*border:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;padding:4px 10px;color:#6f6f6f;font-weight:bold;text-shadow:none;}.btn:hover,.btn:active{background:#e1e1e1;border:1px solid #e1e1e1;*border:0;box-shadow:none;color:#0088cc;}
 .btn i{opacity:0.7;filter:alpha(opacity=70);}
 .btn:hover i,.btn:active i{opacity:1;filter:alpha(opacity=100);}
@@ -889,7 +891,7 @@ th.table-sort.sort-desc a:hover{border-bottom:3px solid #eca09a;padding-bottom:5
 .page-header h2 .avatar{width:40px;height:40px;}
 .page-header h3 .avatar{width:28px;height:28px;}
 .page-header h4 .avatar{width:22px;height:22px;}
-.header-tabbed{border-bottom:none;padding-bottom:0px;margin-bottom:0px;}.header-tabbed .nav-tabs{margin-bottom:0px;}
+.header-tabbed{border-bottom:none;padding-bottom:0px;margin-bottom:0px;}
 .nav-tabs li a:link,.nav-tabs li a:active,.nav-tabs li a:visited{opacity:0.6;filter:alpha(opacity=60);color:#333333;font-weight:bold;}
 .nav-tabs li i{margin-right:4px;}
 .nav-tabs li.active a:link,.nav-tabs li.active a:active,.nav-tabs li.active a:visited,.nav-tabs li.active a:hover{background-color:#fcfcfc;}.nav-tabs li.active a:link i,.nav-tabs li.active a:active i,.nav-tabs li.active a:visited i,.nav-tabs li.active a:hover i{background-image:url("../img/glyphicons-halflings.png");}
@@ -914,4 +916,7 @@ th.table-sort.sort-desc a:hover{border-bottom:3px solid #eca09a;padding-bottom:5
 .navbar-header .search-form input{background:none;border:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
 .navbar-header .nav{float:right;margin:19px 0px;}.navbar-header .nav li a{background-color:#0088cc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;margin-left:8px;opacity:0.9;filter:alpha(opacity=90);padding:5px 8px;}
 .navbar-header .nav li a:hover{background-color:#ffffff;-webkit-box-shadow:0px 0px 3px #0077b3;-moz-box-shadow:0px 0px 3px #0077b3;box-shadow:0px 0px 3px #0077b3;opacity:1;filter:alpha(opacity=100);}.navbar-header .nav li a:hover i{background-image:url("../img/glyphicons-halflings.png");}
+.nav-tabs .tab-search form{marging:0px;margin-bottom:-4px;}
+.nav-tabs .tab-search.tab-search-no-tabs{position:relative;bottom:12px;}
+.nav-tabs button{padding-left:7px;padding-right:3px;}
 .clickable{cursor:pointer;}

+ 20 - 0
static/sora/css/sora/navbar.less

@@ -159,4 +159,24 @@
       }
     }
   }
+}
+
+// Nav-tabs tab with search
+.nav-tabs {
+  .tab-search {
+    form {
+      marging: 0px;
+      margin-bottom: -4px;
+    }
+    
+    &.tab-search-no-tabs {
+      position: relative;
+      bottom: 12px;
+    }
+  }
+     
+  button {
+    padding-left: 7px;
+    padding-right: 3px;
+  }
 }

+ 0 - 4
static/sora/css/sora/navs.less

@@ -4,10 +4,6 @@
   border-bottom: none;
   padding-bottom: 0px;
   margin-bottom: 0px;
-  
-  .nav-tabs {
-    margin-bottom: 0px;
-  }
 }
 
 .nav-tabs li {

+ 15 - 0
static/sora/css/sora/users-lists.less

@@ -19,3 +19,18 @@
     font-weight: bold;
   }
 }
+
+.table-users {
+  a:link, a:active,
+  a:visited, a:hover {
+    color: @textColor;
+    font-size: 150%;
+    text-decoration: none
+  }
+  
+  .avatar {
+    .border-radius(3px);
+    width: 42px;
+    height: 42px;
+  }
+}

+ 7 - 0
templates/_message/users/search_empty.html

@@ -0,0 +1,7 @@
+{% extends "_message/base.html" %}
+{% load i18n %}
+
+{% block content %}
+  <div class="alert-icon"><span><i class="icon-remove icon-white"></i></span></div>
+  <p>{% trans %}To search users you have to enter user name in search field.{% endtrans %}</p>
+{% endblock %}

+ 58 - 0
templates/sora/users/list.html

@@ -0,0 +1,58 @@
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% load url from future %}
+{% import "_forms.html" as form_theme with context %}
+
+{% block title %}{% if in_search %}{% trans %}Search Users{% endtrans %} | {% elif active_rank %}{{ _(active_rank.name) }} | {% endif %}{% trans %}Users List{% endtrans %} | {{ settings.board_name }}{% endblock %}
+
+{% block content %}
+<div class="page-header header-tabbed">
+  <h1>{% trans %}Users List{% endtrans %} <small>{% trans %}Browse notable user groups or find specific user{% endtrans %}</small></h1>
+  <ul class="nav nav-tabs">{% for rank in ranks %}
+  	<li{% if active_rank.id == rank.id %} class="active"{% endif %}><a href="{% if loop.first %}{% url 'users' %}{% else %}{% url 'users' rank_slug=rank.name_slug %}{% endif %}">{{ _(rank.name) }}</a></li>{% endfor %}
+  	<li class="tab-search{% if not ranks %} tab-search-no-tabs{% endif %} pull-right">
+      <form action="{% url 'users' %}" class="form-inline" method="post">
+        <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+      	{{ form_theme.field_widget(search_form.username, width=2, attrs={'placeholder': _('Find user...')}) }}
+        <button type="submit" class="btn btn-primary"><i class="icon-search icon-white"></i></button>
+      </form>
+  	</li>
+  </ul>
+</div>
+<h2>{% if in_search %}{% trans %}Search Users{% endtrans %}{% elif active_rank %}{{ _(active_rank.name) }}{% endif %}</h2>{% if message %}<div class="alert alert-form alert-error">
+  {% include message.tpl %}
+</div>{% endif %}
+
+{% if in_search and not message and users|length > 0 %}
+<p>{% trans %}We couldn't find a member with name you entered, so we present you with some other members with names similiar to one you searched for in hopes that one of them will turn out to be member you are looking for.{% endtrans %}</p>
+{% elif active_rank and active_rank.description %}
+{{ active_rank.description|markdown|safe }}
+{% endif %}
+
+{% if users|length > 0 %}
+<table class="table table-striped table-users">
+  <thead>
+  	<tr>
+      <th{% if users|length > 1 %} colspan="2"{% endif %}>{% if in_search %}{% trans %}Found Users{% endtrans %}{% else %}{% trans %}Users in this group{% endtrans %}{% endif %}</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>{% for user in users %}    	
+      <td{% if users|length > 1 %} {% if loop.last and loop.index is odd %}colspan="2"{% else %}class="span6"{% endif %}{% endif %}>
+        <a href="{% url 'user' username=user.username_slug, user=user.pk %}"><img src="{{ user.get_avatar('medium') }}" class="avatar" alt="{% trans %}Member's Avatar'{% endtrans %}" title="{% trans %}Member's Avatar'{% endtrans %}"> <strong>{{ user.username }}</strong>{% if user.title or (in_search and user.get_title()) %} <span class="muted">{% if in_search%}{{ _(user.get_title()) }}{% else %}{{ _(user.title) }}{% endif %}</span>{% endif %}</a>
+      </td>{% if not loop.last and loop.index is even %}
+    </tr>
+    <tr>{% endif %}
+    {% endfor %}</tr>
+  </tbody>
+</table>
+{% elif not message %}
+<p class="lead">
+  {%- if in_search -%}
+	{% trans %}We couldn't find any members with specified name.{% endtrans %}
+  {%- else -%}
+	{% trans %}Looks like there is nobody there.{% endtrans %}
+  {%- endif -%}
+</p>
+{% endif %}
+{% endblock %}