Browse Source

User ranking.

Ralfp 12 years ago
parent
commit
3998230aa0

+ 43 - 14
misago/admin/widgets.py

@@ -89,6 +89,7 @@ class ListWidget(BaseWidget):
     pagination = None
     template = 'list'
     hide_actions = False
+    table_form_button = _('Go')
     empty_message = _('There are no items to display')
     empty_search_message = _('Search has returned no items')
     nothing_checked_message = _('You have to select at least one item.')
@@ -133,6 +134,12 @@ class ListWidget(BaseWidget):
         """
         return None
     
+    def table_action(self, request, page_items, cleaned_data):
+        """
+        Handle table form submission, return tuple containing message and redirect link/false
+        """
+        return None
+    
     def get_actions_form(self, page_items):
         """
         Build a form object with list of all items actions
@@ -173,16 +180,26 @@ class ListWidget(BaseWidget):
             request.session[self.get_token('sort')] = sorting_method
             
         if not sorting_method:
-            new_sorting = self.sortables.keys()[0]
-            if self.default_sorting in self.sortables:
-                new_sorting = self.default_sorting
-            sorting_method = [
-                    new_sorting,
-                    self.sortables[new_sorting] == True,
-                    new_sorting if self.sortables[new_sorting] else '-%s' % new_sorting
-                   ]
+            if self.sortables:
+                new_sorting = self.sortables.keys()[0]
+                if self.default_sorting in self.sortables:
+                    new_sorting = self.default_sorting
+                sorting_method = [
+                        new_sorting,
+                        self.sortables[new_sorting] == True,
+                        new_sorting if self.sortables[new_sorting] else '-%s' % new_sorting
+                       ]
+            else:
+                sorting_method = [
+                        id,
+                        True,
+                        '-id'
+                       ]
         return sorting_method
     
+    def sort_items(self, request, page_items, sorting_method):
+        return page_items.order_by(sorting_method[2])
+    
     def get_pagination_url(self, page):
         return reverse(self.admin.get_action_attr(self.id, 'route'), kwargs={'page': page})
     
@@ -243,12 +260,17 @@ class ListWidget(BaseWidget):
         
         # List items
         items = self.admin.model.objects
+        
         # Filter items?
         if request.session.get(self.get_token('filter')):
             items = self.set_filters(items, request.session.get(self.get_token('filter')))
         else:
             items = items.all()
-        items = items.order_by(sorting_method[2])
+            
+        # Sort them
+        items = self.sort_items(request, items, sorting_method);
+        
+        # Set pagination
         if self.pagination:
             items = items[paginating_method['start']:paginating_method['stop']]
             
@@ -295,7 +317,15 @@ class ListWidget(BaseWidget):
         TableForm = self.get_table_form(request, items)
         if TableForm:
             if request.method == 'POST' and request.POST.get('origin') == 'table':
-                pass
+                table_form = TableForm(request.POST, request=request)
+                if table_form.is_valid():
+                    message, redirect_url = self.table_action(request, items, table_form.cleaned_data)
+                    if redirect_url:
+                        request.messages.set_flash(message, message.type, self.admin.id)
+                        return redirect(redirect_url)
+                else:
+                    message = Message(request, table_form.non_field_errors()[0])
+                    message.type = 'error'
             else:
                 table_form = TableForm(request=request)
         
@@ -309,12 +339,11 @@ class ListWidget(BaseWidget):
                     try:
                         form_action = getattr(self, 'action_' + list_form.cleaned_data['list_action'])
                         message, redirect_url = form_action(request, items, list_form.cleaned_data['list_items'])
-                        if redirect:
+                        if redirect_url:
                             request.messages.set_flash(message, message.type, self.admin.id)
                             return redirect(redirect_url)
                     except AttributeError:
                         message = BasicMessage(_("Action requested is incorrect."))
-                        message.type = 'error'
                 else:
                     if 'list_items' in list_form.errors:
                         message = BasicMessage(self.nothing_checked_message)
@@ -322,7 +351,7 @@ class ListWidget(BaseWidget):
                         message = BasicMessage(_("Action requested is incorrect."))
                     else:
                         message = Message(request, list_form.non_field_errors()[0])
-                    message.type = 'error'
+                message.type = 'error'
             else:
                 list_form = ListForm(request=request)
                 
@@ -339,7 +368,7 @@ class ListWidget(BaseWidget):
                                                  'pagination': paginating_method,
                                                  'list_form': FormLayout(list_form) if list_form else None,
                                                  'search_form': FormLayout(search_form) if search_form else None,
-                                                 'table_form': FormFields(table_form) if table_form else None,
+                                                 'table_form': FormFields(table_form).fields if table_form else None,
                                                  'items': items,
                                                  'items_total': items_total,
                                                 },

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

@@ -2,7 +2,7 @@ from django.conf.urls import patterns, include, url
 from django.utils.translation import ugettext_lazy as _
 from misago.admin import AdminSection, AdminAction
 from misago.banning.models import Ban
-from misago.users.models import User
+from misago.users.models import User, Rank
 
 ADMIN_SECTIONS=(
     AdminSection(
@@ -49,11 +49,31 @@ ADMIN_ACTIONS=(
                section='users',
                id='ranks',
                name=_("Ranks"),
-               help=_("Administrate User Ranking system"),
+               help=_("Administrate User Ranks"),
                icon='star',
+               model=Rank,
+               actions=[
+                        {
+                         'id': 'list',
+                         'icon': 'list-alt',
+                         'name': _("Browse Ranks"),
+                         'help': _("Browse all existing ranks"),
+                         'route': 'admin_users_ranks'
+                         },
+                        {
+                         'id': 'new',
+                         'icon': 'plus',
+                         'name': _("Add Rank"),
+                         'help': _("Create new rank"),
+                         'route': 'admin_users_ranks_new'
+                         },
+                        ],
                route='admin_users_ranks',
-               urlpatterns=patterns('misago.admin.views',
-                        url(r'^$', 'todo', name='admin_users_ranks'),
+               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'),
                     ),
                ),
    AdminAction(

+ 0 - 0
misago/users/admin/newsletters/__init__.py


+ 0 - 0
misago/users/admin/prune/__init__.py


+ 0 - 0
misago/users/admin/ranks/__init__.py


+ 39 - 0
misago/users/admin/ranks/forms.py

@@ -0,0 +1,39 @@
+from django.core.validators import RegexValidator
+from django.utils.translation import ugettext_lazy as _
+from django import forms
+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)
+    style = forms.CharField(max_length=255,required=False)
+    special = forms.BooleanField(widget=YesNoSwitch,required=False)
+    as_tab = forms.BooleanField(widget=YesNoSwitch,required=False)
+    criteria = forms.CharField(max_length=255,initial='0',validators=[RegexValidator(regex='^(\d+)(%?)$',message=_('This is incorrect rank match rule.'))],required=False)
+    
+    layout = (
+              (
+               _("Basic Rank Options"),
+               (
+                ('name', {'label': _("Rank Name"), 'help_text': _("Rank Name is used to identify rank in Admin Control Panel and is used as page and tab title if you decide to make this rank act as tab on users list.")}),
+                ('description', {'label': _("Rank Description"), 'help_text': _("If this rank acts as tab on users list, here you can enter optional description that will be displayed above list of users with this rank.")}),
+                ('as_tab', {'label': _("As Tab"), 'help_text': _("Should this rank have its own page on users list, containing rank's description and list of users that have it? This is good option for rank used by forum team members or members that should be visible and easily reachable.")}),
+               )
+              ),
+              (
+               _("Rank Looks"),
+               (
+                ('title', {'label': _("Rank Title"), 'help_text': _("Short description of rank's bearer role in your community.")}),
+                ('style', {'label': _("Rank CSS Class"), 'help_text': _("Optional CSS class that will be added to different elements displaying rank's owner or his content, allowing you to make them stand out from other members.")}),
+               )
+              ),
+              (
+               _("Rank Attainability"),
+               (
+                ('special', {'label': _("Special Rank"), 'help_text': _("Special ranks are ignored during updates of user ranking, making them unattainable without admin ingerention.")}),
+                ('criteria', {'label': _("Rank Criteria"), 'help_text': _("This setting allows you to limit number of users that can attain this rank. Enter 0 to assign this rank to all members (good for default rank). To give this rank to 10% of most active members, enter \"10%\". To give this rank to 10 most active members, enter \"10\". This setting is ignored for special ranks as they don't participate in user's ranking updates.")}),
+               ),
+              ),
+             )
+    

+ 152 - 0
misago/users/admin/ranks/views.py

@@ -0,0 +1,152 @@
+from django.core.urlresolvers import reverse as django_reverse
+from django import forms
+from django.utils.translation import ugettext as _
+from misago.admin import site
+from misago.admin.widgets import *
+from misago.forms import Form
+from misago.utils import slugify
+from misago.users.admin.ranks.forms import RankForm
+from misago.users.models import Rank
+
+def reverse(route, target=None):
+    if target:
+        return django_reverse(route, kwargs={'target': target.pk, 'slug': slugify(target.name)})
+    return django_reverse(route)
+
+"""
+Views
+"""
+class List(ListWidget):
+    """
+    List Users
+    """
+    admin = site.get_action('ranks')
+    id = 'list'
+    columns=(
+             ('rank', _("Rank")),
+             )
+    table_form_button = _('Reorder Ranks')
+    nothing_checked_message = _('You have to check at least one rank.')
+    actions=(
+             ('delete', _("Delete selected"), _("Are you sure you want to delete selected ranks?")),
+             )
+    
+    
+    def get_table_form(self, request, page_items):
+        order_form = {}
+        
+        # Build choices list
+        choices = []
+        for i in range(0, len(page_items)):
+           choices.append([str(i), i + 1])
+        
+        # Build selectors list
+        position = 0
+        for item in page_items:
+            order_form['pos_' + str(item.pk)] = forms.ChoiceField(choices=choices,initial=str(position))
+            position += 1
+        
+        # Turn dict into object
+        return type('OrderRanksForm', (Form,), order_form)
+    
+    def table_action(self, request, page_items, cleaned_data):
+        for item in page_items:
+            item.order = cleaned_data['pos_' + str(item.pk)]
+            item.save(force_update=True)
+        return BasicMessage(_('Ranks order has been changed'), 'success'), reverse('admin_users_ranks')
+    
+    def sort_items(self, request, page_items, sorting_method):
+        return page_items.order_by('order')
+    
+    def get_item_actions(self, request, item):
+        return (
+                self.action('pencil', _("Edit Rank"), reverse('admin_users_ranks_edit', item)),
+                self.action('remove', _("Delete Rank"), reverse('admin_users_ranks_delete', item), post=True, prompt=_("Are you sure you want to delete this rank?")),
+                )
+
+    def action_delete(self, request, items, checked):
+        Rank.objects.filter(id__in=checked).delete()
+        return BasicMessage(_('Selected ranks have been deleted successfully.'), 'success'), reverse('admin_users_ranks')
+
+
+class New(FormWidget):
+    admin = site.get_action('ranks')
+    id = 'new'
+    fallback = 'admin_users_ranks' 
+    form = RankForm
+    submit_button = _("Save Rank")
+        
+    def get_new_url(self, request, model):
+        return reverse('admin_users_ranks')
+    
+    def get_edit_url(self, request, model):
+        return reverse('admin_users_ranks_edit', model)
+    
+    def submit_form(self, request, form, target):
+        position = 0
+        last_rank = Rank.objects.latest('order')
+        new_rank = Rank(
+                      name = form.cleaned_data['name'],
+                      name_slug = slugify(form.cleaned_data['name']),
+                      description = form.cleaned_data['description'],
+                      style = form.cleaned_data['style'],
+                      title = form.cleaned_data['title'],
+                      special = form.cleaned_data['special'],
+                      as_tab = form.cleaned_data['as_tab'],
+                      order = (last_rank.order + 1 if last_rank else 0),
+                      criteria = form.cleaned_data['criteria']
+                     )
+        new_rank.save(force_insert=True)
+        return new_rank, BasicMessage(_('New Rank has been created.'), 'success')
+    
+   
+class Edit(FormWidget):
+    admin = site.get_action('ranks')
+    id = 'edit'
+    name = _("Edit Rank")
+    fallback = 'admin_users_ranks'
+    form = RankForm
+    target_name = 'name'
+    notfound_message = _('Requested Rank could not be found.')
+    submit_fallback = True
+    
+    def get_url(self, request, model):
+        return reverse('admin_users_ranks_edit', model)
+    
+    def get_edit_url(self, request, model):
+        return self.get_url(request, model)
+    
+    def get_initial_data(self, request, model):
+        return {
+                'name': model.name,
+                'description': model.description,
+                'style': model.style,
+                'title': model.title,
+                'special': model.special,
+                'as_tab': model.as_tab,
+                'criteria': model.criteria
+                }
+    
+    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']
+        target.style = form.cleaned_data['style']
+        target.title = form.cleaned_data['title']
+        target.special = form.cleaned_data['special']
+        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')
+
+
+class Delete(ButtonWidget):
+    admin = site.get_action('ranks')
+    id = 'delete'
+    fallback = 'admin_users_ranks'
+    notfound_message = _('Requested rank could not be found.')
+    
+    def action(self, request, target):
+        target.delete()
+        return BasicMessage(_('Rank "%(name)s" has been deleted.' % {'name': target.name}), 'success'), False

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

@@ -15,9 +15,6 @@ def reverse(route, target=None):
 Views
 """
 class List(ListWidget):
-    """
-    List Users
-    """
     admin = site.get_action('users')
     id = 'list'
     columns=(
@@ -54,7 +51,6 @@ class List(ListWidget):
                 )
 
     def action_delete(self, request, items, checked):
-        print '%r' % checked
         for user in items:
             if unicode(user.pk) in checked:
                 if user.pk == request.user.id:
@@ -68,9 +64,6 @@ class List(ListWidget):
 
 
 class Delete(ButtonWidget):
-    """
-    Delete QA Test
-    """
     admin = site.get_action('users')
     id = 'delete'
     fallback = 'admin_users'

+ 12 - 22
misago/users/fixtures.py

@@ -165,37 +165,29 @@ def load_fixture():
                       style='staff',
                       title=_("Forum Staff").message,
                       special=True,
-                      order=1,
-                      )
-    rank_guest = Rank(
-                      name=_("Unregistered").message,
-                      style='guest',
-                      title=_("Guest").message,
-                      special=True,
-                      order=2,
+                      order=0,
                       )
     rank_lurker = Rank(
                       name=_("Lurker").message,
                       style='lurker',
                       title=_("Lurker").message,
-                      order=3,
-                      criteria=0
+                      order=2,
+                      criteria="100%"
                       )
     rank_member = Rank(
                       name=_("Member").message,
                       title=_("Member").message,
-                      order=4,
-                      criteria=">15"
+                      order=3,
+                      criteria="15%"
                       )
     rank_active = Rank(
                       name=_("Active Member").message,
                       title=_("Active Member").message,
-                      order=5,
-                      criteria="15%"
+                      order=4,
+                      criteria="25"
                       )
     
     rank_staff.save(force_insert=True)
-    rank_guest.save(force_insert=True)
     rank_lurker.save(force_insert=True)
     rank_member.save(force_insert=True)
     rank_active.save(force_insert=True)
@@ -204,7 +196,7 @@ def load_fixture():
                          name=_("Administrators").message,
                          name_slug='administrators',
                          tab=_("Staff").message,
-                         position=1,
+                         position=0,
                          rank=rank_staff,
                          special=True,
                          )
@@ -212,30 +204,28 @@ def load_fixture():
                        name=_("Moderators").message,
                        name_slug='moderators',
                        tab=_("Staff").message,
-                       position=2,
+                       position=1,
                        rank=rank_staff,
                        )
     group_registered = Group(
                          name=_("Registered").message,
                          name_slug='registered',
                          hidden=True,
-                         position=3,
+                         position=2,
                          special=True,
                          )
     group_guests = Group(
                          name=_("Guests").message,
                          name_slug='guests',
                          hidden=True,
-                         position=4,
-                         rank=rank_guest,
+                         position=3,
                          special=True,
                          )
     group_crawlers = Group(
                            name=_("Web Crawlers").message,
                            name_slug='web-crawlers',
                            hidden=True,
-                           position=5,
-                           rank=rank_guest,
+                           position=4,
                            special=True,
                            )
     

+ 25 - 0
misago/users/management/commands/updateranking.py

@@ -0,0 +1,25 @@
+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 User, Rank
+
+class Command(BaseCommand):
+    """
+    This command is intended to work as CRON job fired of once per day or less if you have more users to update user ranking.
+    """
+    help = 'Updates users ranking'
+    def handle(self, *args, **options):
+        # Find special ranks
+        special_ranks = []
+        for rank in Rank.objects.filter(special=1):
+            special_ranks.append(str(rank.pk))
+        
+        # Count users that are in ranking
+        users_total = User.objects.exclude(rank__in=special_ranks).count()
+        
+        # Update Ranking
+        for rank in Rank.objects.filter(special=0).order_by('order'):
+            rank.assign_rank(users_total, special_ranks)
+        
+        self.stdout.write('Users ranking for has been updated.\n')

+ 0 - 12
misago/users/management/commands/updateranks.py

@@ -1,12 +0,0 @@
-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
-
-class Command(BaseCommand):
-    """
-    This command is intended to work as CRON job fired of once per day or less if you have more users.
-    """
-    help = 'Updates users ranking'
-    def handle(self):
-        self.stdout.write('Users ranking has been updated.\n')

+ 74 - 3
misago/users/models.py

@@ -1,11 +1,12 @@
 import hashlib
+import math
 from random import choice
 from django.conf import settings
 from django.contrib.auth.hashers import (
     check_password, make_password, is_password_usable, UNUSABLE_PASSWORD)
 from django.core.exceptions import ValidationError
 from django.core.mail import EmailMultiAlternatives
-from django.db import models
+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 _
@@ -127,7 +128,6 @@ 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)
-    rank_freezed = models.BooleanField(default=False,db_index=True)
     last_post = models.DateTimeField(null=True,blank=True)
     last_search = models.DateTimeField(null=True,blank=True)
     alerts = models.PositiveIntegerField(default=0)
@@ -372,15 +372,86 @@ class Group(models.Model):
 class Rank(models.Model):
     """
     Misago User Rank
-    Ranks are ready style/title pairs that are assigned to users either per group, admin action or user activity.
+    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_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)
     title = models.CharField(max_length=255,null=True,blank=True)
     special = models.BooleanField(default=False)
+    as_tab = models.BooleanField(default=False)
     order = models.IntegerField(default=0)
     criteria = models.CharField(max_length=255,default='')
     
+    def assign_rank(self, users=0, special_ranks=None):
+        if not self.criteria or self.special or users == 0:
+            # Rank cant be rolled in
+            return False
+        
+        if self.criteria == "0":
+            # Just update all fellows
+            User.objects.exclude(rank__in=special_ranks).update(rank=self)
+        else:
+            # Count number of users to update
+            if self.criteria[-1] == '%':
+                criteria = int(self.criteria[0:-1])
+                criteria = int(math.ceil(float(users / 100.0)* criteria))
+            else:
+                criteria = int(self.criteria)
+            
+            # Join special ranks
+            if special_ranks:
+                special_ranks = ','.join(special_ranks)
+            
+            # Run raw query
+            cursor = connection.cursor()
+            try:
+                # Postgresql
+                if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2'
+                    or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'):
+                    if special_ranks:
+                        cursor.execute('''UPDATE users_user
+                            FROM (
+                                SELECT id
+                                FROM users_user
+                                WHERE rank_id NOT IN (%s)
+                                ORDER BY score DESC LIMIT %s
+                                ) AS updateable
+                            SET rank_id=%s
+                            WHERE id = updateable.id
+                            RETURNING *''' % (self.id, special_ranks, criteria))
+                    else:
+                        cursor.execute('''UPDATE users_user
+                            FROM (
+                                SELECT id
+                                FROM users_user
+                                ORDER BY score DESC LIMIT %s
+                                ) AS updateable
+                            SET rank_id=%s
+                            WHERE id = updateable.id
+                            RETURNING *''', [self.id, criteria])
+                        
+                # MySQL, SQLite and Oracle
+                if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.mysql'
+                    or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3'
+                    or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.oracle'):
+                    if special_ranks:
+                        cursor.execute('''UPDATE users_user
+                            SET rank_id=%s
+                            WHERE rank_id NOT IN (%s)
+                            ORDER BY score DESC
+                            LIMIT %s''' % (self.id, special_ranks, criteria))
+                    else:
+                        cursor.execute('''UPDATE users_user
+                        SET rank_id=%s
+                        ORDER BY score DESC
+                        LIMIT %s''', [self.id, criteria])
+            except Exception as e:
+                print 'Error updating users ranking: %s' % e
+            transaction.commit_unless_managed()
+        return True
+    
     
 class Follower(models.Model):
     """

+ 2 - 0
static/admin/css/admin.css

@@ -851,10 +851,12 @@ textarea{resize:vertical;}
 .table-footer .pager>li>a:hover{background-color:#0088cc;}.table-footer .pager>li>a:hover i{background-image:url("../img/glyphicons-halflings-white.png");}
 .table-footer .table-count{padding:11px 0px;color:#555555;}
 .table-footer .form-inline{margin:0px;padding:6px 0px;}
+.table-footer .table-actions-right{margin-right:16px;}
 td.check-cell,th.check-cell{width:32px;}
 td .checkbox,th .checkbox{margin-bottom:0px;position:relative;bottom:1px;}td .checkbox input,th .checkbox input{position:relative;left:9px;}
 td.lead-cell{font-size:120%;}
 .table td{vertical-align:middle;}
+.table input,.table select{margin:0px;}
 th.table-sort{padding:0px;}th.table-sort a:link,th.table-sort a:active,th.table-sort a:visited a:hover{display:block;padding:8px;}
 th.table-sort.sort-active-asc a:link,th.table-sort.sort-active-asc a:active,th.table-sort.sort-active-asc a:visited{border-bottom:3px solid #049cdb;padding-bottom:5px;}
 th.table-sort.sort-active-asc a:hover{border-bottom:3px solid #e4776f;padding-bottom:5px;text-decoration:none;}

+ 8 - 0
static/admin/css/admin/tables.less

@@ -45,6 +45,10 @@
     margin: 0px;
     padding: 6px 0px;
   }
+  
+  .table-actions-right {
+    margin-right: 16px;
+  }
 }
 
 // Checkbox cell
@@ -75,6 +79,10 @@ td.lead-cell {
   td {
     vertical-align: middle;
   }
+  
+  input, select {
+    margin: 0px;
+  }
 }
 
 // Table sorting styles

+ 11 - 3
templates/admin/admin/list.html

@@ -20,10 +20,10 @@
       {% if sorting_method[1] %}{{ url ~ query(sort=column[0],dir=0) }}{% else %}{{ url ~ query(sort=column[0],dir=1) }}{% endif %}
       {%- else -%}
       {{ url ~ query(sort=column[0],dir=sorting[column[0]]) }}
-      {%- endif %}">{{ column[1] }}</a>{% else %}{{ column[1] }}{% endif %}</th>{% endfor %}
+      {%- endif %}">{{ column[1] }}</a>{% else %}{{ column[1] }}{% endif %}</th>{% endfor %}{% endblock %}
       <th>{% trans %}Actions{% endtrans %}</th>
       {% if list_form %}<th class="check-cell"><label class="checkbox"><input type="checkbox" class="checkbox-master"></label></th>{% endif %}
-    {% endblock %}</tr>
+    </tr>
   </thead>
   <tbody>{% for item in items %}
     <tr>{% block table_row scoped %}{% for column in action.columns %}
@@ -52,7 +52,15 @@
     </tr>{% endfor %}
   </tbody>
 </table>
-<div class="form-actions table-footer">{% if pagination and (pagination['prev'] > 0 or pagination['next'] > 0)%}
+<div class="form-actions table-footer">
+  {% if table_form %}
+  <form id="table_form" class="form-inline table-actions-right pull-left" action="{{ url }}" method="POST">
+    <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+    <input type="hidden" name="origin" value="table">
+    <button type="submit" class="btn btn-primary">{{ action.table_form_button }}</button>
+  </form>
+  {% endif %}
+  {% if pagination and (pagination['prev'] > 0 or pagination['next'] > 0)%}
   <ul class="pager pull-left">
     {%- if pagination['prev'] > 0 %}<li><a href="{{ action.get_pagination_url(pagination['prev']) }}" class="tooltip-top" title="{% trans %}Previous Page{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
     {%- if pagination['next'] > 0 %}<li><a href="{{ action.get_pagination_url(pagination['next']) }}" class="tooltip-top" title="{% trans %}Next Page{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}

+ 19 - 0
templates/admin/users/admin_users_ranks/list.html

@@ -0,0 +1,19 @@
+{% extends "admin/admin/list.html" %}
+{% load i18n %}
+{% load l10n %}
+{% load url from future %}
+{% import "_forms.html" as form_theme with context %}
+
+{% block table_head scoped %}
+  {{ super() }}
+  <th>{% trans %}Order{% endtrans %}</th>
+{% endblock %}
+
+{% block table_row scoped %}
+  <td class="lead-cell">
+  	<strong>{{ item.name }}</strong>{% if item.special %} <span class="label label-info">{% trans %}Special{% endtrans %}</span>{% endif %}{% if item.as_tab %} <span class="label label-inverse">{% trans %}Tab{% endtrans %}</span>{% endif %}
+  </td>
+  <td class="span2">
+  	{{ form_theme.field_widget(table_form['pos_' + item.pk|string], attrs={'class': 'span2', 'form': 'table_form'}) }}
+  </td>
+{% endblock%}