changeset 175:492005721817

Add list of users and groups
author Sylvain Beucler <beuc@beuc.net>
date Fri, 23 Jul 2010 23:25:39 +0200
parents d785065c4423
children 27559c1989f9
files savane/filters.py savane/svmain/models.py savane/svmain/urls.py templates/base.html templates/svmain/group_list.html templates/svmain/pagination.inc.html templates/svmain/user_list.html
diffstat 7 files changed, 246 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/savane/filters.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+# 
+# Copyright (C) 2010  Sylvain Beucler
+# Copyright ??? Django team
+#
+# This file is part of Savane.
+# 
+# Savane is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# Savane is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from django.db import models
+import operator
+from django.http import HttpResponse
+
+# Copy/paste these:
+#from django.contrib.admin.views.main import
+#ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR,
+#SEARCH_VAR, TO_FIELD_VAR, IS_POPUP_VAR, ERROR_FLAG
+ALL_VAR = 'all'
+ORDER_VAR = 'o'
+ORDER_TYPE_VAR = 'ot'
+PAGE_VAR = 'p'
+SEARCH_VAR = 'q'
+TO_FIELD_VAR = 't'
+IS_POPUP_VAR = 'pop'
+ERROR_FLAG = 'e'
+
+class ChangeList:
+    """
+    Object to pass views configuration to (e.g.: search string, ordering...)
+    -Draft-
+    """
+    def __init__(model_admin, request):
+        self.query = request.GET.get(SEARCH_VAR, '')
+        self.list_display = model_admin.list_display
+
+def search(f):
+    """
+    Inspired by Django's admin interface, filter queryset based on GET
+    parameters (contrib.admin.views.main.*_VAR):
+
+    - o=N: order by ModelAdmin.display_fields[N]
+    - ot=xxx: order type: 'asc' or 'desc'
+    - q=xxx: plain text search on ModelAdmin.search_fields (^ -> istartswith, = -> iexact, @ -> search, each word ANDed)
+    - everything else: name of a Q filter
+
+    exceptions:
+    - p=N: current page
+    - all=: disable pagination
+    - pop: popup
+    - e: error
+    - to: ? (related to making JS-friendly PK values?)
+
+    additional exclusions:
+    - page: used by django.views.generic.list_detail
+
+    We could also try and deduce filters from the Model, or avoid
+    using some declared parameters as Q filters, or find a better
+    idea.
+    """
+    def _decorator(request, *args, **kwargs):
+        qs = kwargs['queryset']
+        model_admin = kwargs['model_admin']
+
+        lookup_params = request.GET.copy()
+        for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, 'page'):
+            if lookup_params.has_key(i):
+                del lookup_params[i]
+
+        try:
+            qs = qs.filter(**lookup_params)
+        # Naked except! Because we don't have any other way of validating "params".
+        # They might be invalid if the keyword arguments are incorrect, or if the
+        # values are not in the correct type, so we might get FieldError, ValueError,
+        # ValicationError, or ? from a custom field that raises yet something else 
+        # when handed impossible data.
+        except:
+            return HttpResponse("Erreur: paramètres de recherche invalides.")
+            #raise IncorrectLookupParameters
+
+        # TODO: order - but maybe in another, separate filter?
+
+        ##
+        # Search string
+        ##
+        def construct_search(field_name):
+            if field_name.startswith('^'):
+                return "%s__istartswith" % field_name[1:]
+            elif field_name.startswith('='):
+                return "%s__iexact" % field_name[1:]
+            elif field_name.startswith('@'):
+                return "%s__search" % field_name[1:]
+            else:
+                return "%s__icontains" % field_name
+
+        query = request.GET.get(SEARCH_VAR, '')
+        search_fields = model_admin.search_fields
+        if search_fields and query:
+            for bit in query.split():
+                or_queries = [models.Q(**{construct_search(str(field_name)): bit}) for field_name in search_fields]
+                qs = qs.filter(reduce(operator.or_, or_queries))
+            for field_name in search_fields:
+                if '__' in field_name:
+                    qs = qs.distinct()
+                    break
+
+        kwargs['queryset'] = qs
+
+        # TODO: pass order params
+        if not kwargs.has_key('extra_context'):
+            kwargs['extra_context'] = {}
+        kwargs['extra_context']['q'] = query
+
+        # TODO: move in a clean-up decorator
+        del kwargs['model_admin']
+        return f(request, *args, **kwargs)
+    return _decorator
--- a/savane/svmain/models.py
+++ b/savane/svmain/models.py
@@ -28,7 +28,7 @@
 http://mirobetm.blogspot.com/2007/04/en-django-extending-user_2281.html
 
 However profiles were mainly useful in Django < 1.0 where you couldn't
-subclass User as we do.
+subclass User.
 
 Profiles also have a few drawbacks, namely they are site-specific,
 which means you cannot have multiple applications have different
@@ -53,6 +53,11 @@
 Note that Scott's authentication backend has the same issue than
 profiles: only one profile class can be used on a single website, so
 we don't use it.
+
+The current solution is to use AutoOneToOneField: OneToOneField is
+similar to extending a model class (at the SQL tables level), and
+AutoOneToOneField is a trick from django-annoying to automatically
+create the extended data on first access.
 """
 
 from django.db import models
--- a/savane/svmain/urls.py
+++ b/savane/svmain/urls.py
@@ -18,12 +18,16 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from django.conf.urls.defaults import *
+from django.views.generic.list_detail import object_list, object_detail
 
 import savane.svmain.models as svmain_models
 import django.contrib.auth.models as auth_models
 import views
+from savane.filters import search
 
-urlpatterns = patterns ('',
+urlpatterns = patterns ('',)
+
+urlpatterns += patterns ('',
   url(r'^$', 'django.views.generic.simple.direct_to_template',
       { 'template' : 'index.html',
         'extra_context' : { 'has_left_menu': False } },
@@ -32,11 +36,41 @@
       { 'template' : 'svmain/text.html',
         'extra_context' : { 'title' : 'Contact', }, },
       name='contact'),
+)
 
-  # TODO: not sure about the views naming convention - all this
-  # "models in 'svmain', views in 'my'" is getting messy, probably a
-  # mistake from me (Beuc) :P
-  url(r'^p/(?P<slug>[-\w]+)$', 'django.views.generic.list_detail.object_detail',
+# TODO: not sure about the views naming convention - all this
+# "models in 'svmain', views in 'my'" is getting messy, probably a
+# mistake from me (Beuc) :P
+from django.contrib.auth.admin import UserAdmin
+urlpatterns += patterns ('',
+  url(r'^u/$',
+      search(object_list),
+      { 'queryset': auth_models.User.objects.all(),
+        'paginate_by': 20,
+        'model_admin': UserAdmin,
+        'extra_context' : { 'title' : 'Users' },
+        'template_name' : 'svmain/user_list.html' },
+      name='savane.svmain.user_list'),
+  url(r'^u/(?P<slug>[-\w]+)$', object_detail,
+      { 'queryset' : auth_models.User.objects.all(),
+        'slug_field' : 'username',
+        'template_name' : 'svmain/user_detail.html', },
+      name='savane.svmain.user_detail'),
+  url(r'^us/(?P<slug>[-\w]+)$', views.user_redir),
+  url(r'^users/(?P<slug>[-\w]+)/?$', views.user_redir),
+)
+
+from django.contrib.auth.admin import GroupAdmin
+urlpatterns += patterns ('',
+  url(r'^p/$',
+      search(object_list),
+      { 'queryset': auth_models.Group.objects.all(),
+        'paginate_by': 20,
+        'model_admin': GroupAdmin,
+        'extra_context' : { 'title' : 'Projects' },
+        'template_name' : 'svmain/group_list.html' },
+      name='savane.svmain.group_list'),
+  url(r'^p/(?P<slug>[-\w]+)$', object_detail,
       { 'queryset' : auth_models.Group.objects.all(),
         'slug_field' : 'name',
         'template_name' : 'svmain/group_detail.html', },
@@ -44,18 +78,10 @@
   url(r'^pr/(?P<slug>[-\w]+)$', views.group_redir),
   url(r'^projects/(?P<slug>[-\w]+)$', views.group_redir),
 
-  url(r'^u/(?P<slug>[-\w]+)$', 'django.views.generic.list_detail.object_detail',
-      { 'queryset' : auth_models.User.objects.all(),
-        'slug_field' : 'username',
-        'template_name' : 'svmain/user_detail.html', },
-      name='savane.svmain.user_detail'),
-  url(r'^us/(?P<slug>[-\w]+)$', views.user_redir),
-  url(r'^users/(?P<slug>[-\w]+)/?$', views.user_redir),
-
   url(r'^license/$', 'django.views.generic.list_detail.object_list',
       { 'queryset' : svmain_models.License.objects.all(), },
       name='savane.svmain.license_list'),
-  url(r'^license/(?P<slug>[-\w]+)$', 'django.views.generic.list_detail.object_detail',
+  url(r'^license/(?P<slug>[-\w]+)$', object_detail,
       { 'queryset' : svmain_models.License.objects.all(), },
       name='savane.svmain.license_detail'),
 )
--- a/templates/base.html
+++ b/templates/base.html
@@ -27,6 +27,10 @@
         {% endif %}
         <!-- /sitemenu -->
 
+	<li class="menutitle">Search</li>
+        <li class="menuitem"><a href="{% url savane.svmain.user_list %}">Users</a></li>
+        <li class="menuitem"><a href="{% url savane.svmain.group_list %}">Projects</a></li>
+
 	<li class="menutitle">Site help</li>
         <li class="menuitem"><a href="{% url contact %}">Contact us</a></li>
       </ul>
--- a/templates/svmain/group_list.html
+++ b/templates/svmain/group_list.html
@@ -2,6 +2,12 @@
 
 {% block content %}
 
+{% include "svmain/pagination.inc.html" %}
+
+<form action="." method="GET">
+  Rechercher: <input type="text" name="q" value="{{q}}" />
+</form>
+
 {% if object_list %}
     <ul>
     {% for object in object_list %}
@@ -9,7 +15,7 @@
     {% endfor %}
     </ul>
 {% else %}
-    <p>No group.</p>
+    <p>No groups.</p>
 {% endif %}
 
 {% endblock %}
new file mode 100644
--- /dev/null
+++ b/templates/svmain/pagination.inc.html
@@ -0,0 +1,33 @@
+{% if page_obj %}
+<div class="pagination">
+    <span class="step-links">
+      {% if paginator.num_pages > 1 %}
+        {% if not page_obj.has_previous %}
+	  préc.
+	{% else %}
+          <a href="?page={{ page_obj.previous_page_number }}">préc.</a>
+        {% endif %}
+	&lt;
+	{% for number in paginator.page_range %}
+	    {% ifequal number page_obj.number %}
+	      {{ number }}
+	    {% else %}
+              <a href="?page={{ number }}">{{ number }}</a>
+	    {% endifequal %}
+        {% endfor %}
+	&gt;
+        {% if not page_obj.has_next %}
+	    suiv.
+	{% else %}
+            <a href="?page={{ page_obj.next_page_number }}">suiv.</a>
+        {% endif %}
+
+        <span class="current">
+            - Page {{ page_obj.number }} sur {{ paginator.num_pages }} ({{paginator.count}})
+        </span>
+      {% else %}
+	({{paginator.count}} élément(s))
+      {% endif %}
+    </span>
+</div>
+{% endif %}
new file mode 100644
--- /dev/null
+++ b/templates/svmain/user_list.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+{% include "svmain/pagination.inc.html" %}
+
+<form action="." method="GET">
+  Rechercher: <input type="text" name="q" value="{{q}}" />
+</form>
+
+{% if object_list %}
+    <ul>
+    {% for object in object_list %}
+        <li><a href="{% url savane.svmain.user_detail object.username %}">{{ object.username }}</a></li>
+    {% endfor %}
+    </ul>
+{% else %}
+    <p>No users.</p>
+{% endif %}
+
+{% endblock %}
+
+{% comment %}
+Local Variables: **
+mode: django-html **
+tab-width: 4 **
+indent-tabs-mode: nil **
+End: **
+{% endcomment %}