Browse Source

Admin lists are filterable now

Rafał Pitoń 11 years ago
parent
commit
ec58833381

+ 79 - 18
misago/admin/views/generic/list.py

@@ -1,5 +1,6 @@
 from urllib import urlencode
 from django.core.paginator import Paginator, EmptyPage
+from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from misago.core.exceptions import ExplicitFirstPage
 from misago.admin.views.generic.base import AdminView
@@ -58,15 +59,53 @@ class ListView(AdminView):
     """
     Filter list items
     """
+    SearchForm = None
+
+    def get_search_form(self, request):
+        return self.SearchForm
+
     @property
-    def filters_token(self):
-        return '%s:filters' % self.root_link
+    def filters_session_key(self):
+        return 'misago_admin_%s_filters' % self.root_link
+
+    def get_filters_from_GET(self, SearchForm, request):
+        form = SearchForm(request.GET)
+        form.is_valid()
+        return self.clean_filtering_data(form.cleaned_data)
+
+    def get_filters_from_session(self, SearchForm, request):
+        session_filters = request.session.get(self.filters_session_key, {})
+        form = SearchForm(session_filters)
+        form.is_valid()
+        return self.clean_filtering_data(form.cleaned_data)
+
+    def clean_filtering_data(self, data):
+        for key, value in data.items():
+            if not value:
+                del data[key]
+        return data
+
+    def get_filtering_methods(self, request):
+        SearchForm = self.get_search_form(request)
+        return {
+            'GET': self.get_filters_from_GET(SearchForm, request),
+            'session': self.get_filters_from_session(SearchForm, request),
+        }
 
-    def search_form(self, request, context):
-        pass
+    def get_filtering_method_to_use(self, methods):
+        for method in ('GET', 'session'):
+            if methods.get(method):
+                return methods.get(method)
+        else:
+            return {}
 
-    def filter_items(self, context):
-        pass
+    def apply_filtering_on_context(self, context, active_filters, SearchForm):
+        context['active_filters'] = active_filters
+        context['search_form'] = SearchForm(initial=context['active_filters'])
+
+        if context['active_filters']:
+            context['items'] = context['search_form'].filter_queryset(
+                active_filters, context['items'])
 
     """
     Order list items
@@ -154,15 +193,14 @@ class ListView(AdminView):
         if values:
             context['querystring'] = '?%s' % urlencode(values)
         if order_values:
-            context['querystring_order'] = '?%s' % urlencode(order_values)
+            context['query_order'] = order_values
         if filter_values:
-            context['querystring_filter'] = '?%s' % urlencode(filter_values)
+            context['query_filters'] = filter_values
 
     """
     Dispatch response
     """
     def dispatch(self, request, *args, **kwargs):
-        active_filters = request.session.get(self.filters_token, None)
         extra_actions_list = self.extra_actions or []
 
         refresh_querystring = False
@@ -174,10 +212,10 @@ class ListView(AdminView):
             'order_by': [],
             'order': None,
             'search_form': None,
-            'active_filters': active_filters,
+            'active_filters': {},
             'querystring': '',
-            'querystring_order': '',
-            'querystring_filter': '',
+            'query_order': {},
+            'query_filters': {},
             'extra_actions': extra_actions_list,
             'extra_actions_len': len(extra_actions_list),
         }
@@ -189,7 +227,7 @@ class ListView(AdminView):
 
             if (ordering_methods['GET'] and
                     ordering_methods['GET'] != ordering_methods['session']):
-                # Store get method in session for future requests
+                # Store GET ordering in session for future requests
                 session_key = self.ordering_session_key
                 request.session[session_key] = ordering_methods['GET']
 
@@ -198,14 +236,37 @@ class ListView(AdminView):
                 # So address ball contains copy-friendly link
                 refresh_querystring = True
 
-        self.search_form(request, context)
-        if active_filters:
-            self.filter_items(context)
+        SearchForm = self.get_search_form(request)
+        if SearchForm:
+            filtering_methods = self.get_filtering_methods(request)
+            active_filters = self.get_filtering_method_to_use(filtering_methods)
+            self.apply_filtering_on_context(context, active_filters, SearchForm)
 
-        if self.items_per_page:
-            self.paginate_items(context, kwargs.get('page', 0))
+            if (filtering_methods['GET'] and
+                    filtering_methods['GET'] != filtering_methods['session']):
+                # Store GET filters in session for future requests
+                session_key = self.filters_session_key
+                request.session[session_key] = filtering_methods['GET']
+
+            if request.GET.get('clear_filters'):
+                # Clear filters from querystring
+                request.session.pop(self.filters_session_key, None)
+                context['active_filters'] = {}
+
+            if context['active_filters'] and not filtering_methods['GET']:
+                # Make view redirect to itself with querystring,
+                # So address ball contains copy-friendly link
+                refresh_querystring = True
 
         self.make_querystrings(context)
+
+        if self.items_per_page:
+            try:
+                self.paginate_items(context, kwargs.get('page', 0))
+            except EmptyPage:
+                return redirect(
+                    '%s%s' % (reverse(self.root_link), context['querystring']))
+
         if refresh_querystring:
             return redirect('%s%s' % (request.path, context['querystring']))
 

+ 68 - 1
misago/templates/misago/admin/generic/list.html

@@ -26,9 +26,12 @@
       </li>
       {% for order in order_by %}
       <li>
-        <form action="{{ querystring_filter }}" method="get">
+        <form method="get">
           <input type="hidden" name="sort" value="{{ order.order_by }}">
           <input type="hidden" name="direction" value="{{ order.type }}">
+          {% for name, value in query_filters.items %}
+          <input type="hidden" name="{{ name }}" value="{{ value }}">
+          {% endfor %}
           <button type="submit">
             <span class="fa fa-sort-numeric-{{ order.type }}"></span>
             {{ order.name }}
@@ -41,8 +44,31 @@
   {% endif %}
 
   {% if search_form %}
+  <button class="btn btn-{% if active_filters %}success{% else %}default{% endif %} pull-left" data-toggle="modal" data-target="#filter-modal">
+    {% if active_filters %}
+    <span class="fa fa-check"></span>
+    {% trans "Change search" %}
+    {% else %}
+    <span class="fa fa-search"></span>
+    {% trans "Search list" %}
+    {% endif %}
+  </button>
+
+    {% if active_filters %}
+    <form method="get" class="pull-left">
+      <input type="hidden" name="clear_filters" value="1">
+      {% for name, value in query_order.items %}
+      <input type="hidden" name="{{ name }}" value="{{ value }}">
+      {% endfor %}
+      <button type="submit" class="btn btn-default">
+        <span class="fa fa-times"></span>
+        {% trans "Remove search" %}
+      </button>
+    </form>
+    {% endif %}
   {% endif %}
 
+
 </div><!-- /.table-actions -->
 {% endif %}
 
@@ -74,3 +100,44 @@
 </div><!-- /.table-actions -->
 {% endif %}
 {% endblock view %}
+
+
+{% block content %}
+{{ block.super }}
+{% if search_form %}
+<div class="modal fade" id="filter-modal" tabindex="-1" role="dialog" aria-labelledby="filter-modal-label" aria-hidden="true">
+  <div class="modal-dialog">
+    <div class="modal-content">
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+        <h4 class="modal-title" id="filter-modal-label">
+        {% block modal-title %}
+        {% endblock modal-title %}
+        </h4>
+      </div>
+      <form method="GET">
+        {% for name, value in query_order.items %}
+        <input type="hidden" name="{{ name }}" value="{{ value }}">
+        {% endfor %}
+
+        <div class="modal-body">
+        {% block modal-body %}
+        {% endblock modal-body %}
+        </div>
+        <div class="modal-footer">
+        {% block modal-footer %}
+        <button type="button" class="btn btn-default" data-dismiss="modal">
+          {% trans "Close" %}
+        </button>
+        <button type="submit" class="btn btn-primary">
+          {% trans "Save changes" %}
+        </button>
+        {% endblock modal-footer %}
+        </div>
+
+      </form>
+    </div>
+  </div>
+</div>
+{% endif %}
+{% endblock content%}

+ 42 - 2
misago/templates/misago/admin/users/list.html

@@ -1,5 +1,5 @@
 {% extends "misago/admin/generic/list.html" %}
-{% load i18n misago_avatars %}
+{% load crispy_forms_tags i18n misago_avatars %}
 
 
 {% block page-actions %}
@@ -71,4 +71,44 @@
     <input type="checkbox">
   </label>
 </td>
-{% endblock %}
+{% endblock table-row %}
+
+
+{% block emptylist %}
+<td colspan="{{ 3|add:extra_actions_len }}">
+  <p>{% trans "No users matching search criteria have been found." %}</p>
+</td>
+{% endblock emptylist %}
+
+
+{% block modal-title %}
+{% trans "Search users" %}
+{% endblock modal-title %}
+
+
+{% block modal-body %}
+<div class="row">
+  <div class="col-md-6">
+    {{ search_form.username|as_crispy_field }}
+  </div>
+  <div class="col-md-6">
+    {{ search_form.email|as_crispy_field }}
+  </div>
+</div>
+<div class="row">
+  <div class="col-md-6">
+    {{ search_form.rank|as_crispy_field }}
+  </div>
+  <div class="col-md-6">
+    {{ search_form.role|as_crispy_field }}
+  </div>
+</div>
+<div class="row">
+  <div class="col-md-6">
+    {{ search_form.inactive|as_crispy_field }}
+  </div>
+  <div class="col-md-6">
+    {{ search_form.is_staff|as_crispy_field }}
+  </div>
+</div>
+{% endblock modal-body %}

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

@@ -1,6 +1,6 @@
 from django.contrib.auth import get_user_model
 from django.utils.translation import ugettext_lazy as _
-from misago.core import forms
+from misago.core import forms, threadstore
 from misago.core.validators import validate_sluggable
 from misago.acl.models import Role
 from misago.users.models import Rank
@@ -8,6 +8,9 @@ from misago.users.validators import (validate_username, validate_email,
                                      validate_password)
 
 
+"""
+Users forms
+"""
 class UserBaseForm(forms.ModelForm):
     username = forms.CharField(
         label=_("Username"))
@@ -122,6 +125,87 @@ def StaffFlagUserFormFactory(FormType, instance, add_staff_field):
         return FormType
 
 
+class SearchUsersFormBase(forms.Form):
+    username = forms.CharField(label=_("Username starts with"), required=False)
+    email = forms.CharField(label=_("E-mail starts with"), required=False)
+    #rank = forms.TypedChoiceField(label=_("Rank"),
+    #                              coerce=int,
+    #                              required=False,
+    #                              choices=ranks_list)
+    #role = forms.TypedChoiceField(label=_("Role"),
+    #                              coerce=int,
+    #                              required=False,
+    #                              choices=roles_list)
+    inactive = forms.YesNoSwitch(label=_("Inactive only"))
+    is_staff = forms.YesNoSwitch(label=_("Is administrator"))
+
+    def filter_queryset(self, cleaned_data, queryset):
+        if cleaned_data.get('username'):
+            queryset = queryset.filter(
+                username_slug__startswith=cleaned_data.get('username').lower())
+
+        if cleaned_data.get('email'):
+            queryset = queryset.filter(
+                email__istartswith=cleaned_data.get('email'))
+
+        if cleaned_data.get('rank'):
+            queryset = queryset.filter(
+                rank_id=cleaned_data.get('rank'))
+
+        if cleaned_data.get('role'):
+            queryset = queryset.filter(
+                roles__id=cleaned_data.get('role'))
+
+        if cleaned_data.get('inactive'):
+            pass
+
+        if cleaned_data.get('is_staff'):
+            queryset = queryset.filter(is_staff=True)
+
+        return queryset
+
+
+def SearchUsersForm(*args, **kwargs):
+    """
+    Factory that uses cache for ranks and roles,
+    and makes those ranks and roles typed choice fields that play nice
+    with passing values via GET
+    """
+    ranks_choices = threadstore.get('misago_admin_ranks_choices', 'nada')
+    if ranks_choices == 'nada':
+        ranks_choices = [('', _("All ranks"))]
+        for rank in Rank.objects.order_by('name').iterator():
+            ranks_choices.append((rank.pk, rank.name))
+        threadstore.set('misago_admin_ranks_choices', ranks_choices)
+
+    roles_choices = threadstore.get('misago_admin_roles_choices', 'nada')
+    if roles_choices == 'nada':
+        roles_choices = [('', _("All roles"))]
+        for role in Role.objects.order_by('name').iterator():
+            roles_choices.append((role.pk, role.name))
+        threadstore.set('misago_admin_roles_choices', roles_choices)
+
+
+    extra_fields = {
+        'rank': forms.TypedChoiceField(label=_("Has rank"),
+                                       coerce=int,
+                                       required=False,
+                                       choices=ranks_choices),
+        'role': forms.TypedChoiceField(label=_("Has role"),
+                                       coerce=int,
+                                       required=False,
+                                       choices=roles_choices)
+    }
+
+    FinalForm =  type('SearchUsersFormFinal',
+                      (SearchUsersFormBase,),
+                      extra_fields)
+    return FinalForm(*args, **kwargs)
+
+
+"""
+Ranks form
+"""
 class RankForm(forms.ModelForm):
     name = forms.CharField(
         label=_("Name"),

+ 5 - 2
misago/users/views/useradmin.py

@@ -4,7 +4,7 @@ from django.shortcuts import redirect
 from django.utils.translation import ugettext_lazy as _
 from misago.admin.views import generic
 from misago.users.forms.admin import (StaffFlagUserFormFactory, NewUserForm,
-                                      EditUserForm)
+                                      EditUserForm, SearchUsersForm)
 
 
 class UserAdmin(generic.AdminBaseMixin):
@@ -31,12 +31,15 @@ class UsersList(UserAdmin, generic.ListView):
         ('id', _("From oldest")),
         ('username_slug', _("A to z")),
         ('-username_slug', _("Z to a")),
-        )
+    )
 
     def get_queryset(self):
         qs = super(UsersList, self).get_queryset()
         return qs.select_related('rank')
 
+    def get_search_form(self, request):
+        return SearchUsersForm
+
 
 class NewUser(UserAdmin, generic.ModelFormView):
     Form = NewUserForm