# HG changeset patch # User Sylvain Beucler # Date 1279920339 -7200 # Node ID 492005721817308317f4634f9960acecf48bbf35 # Parent d785065c44230251df4a27db517dd19a84fb390b Add list of users and groups diff --git a/savane/filters.py b/savane/filters.py 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 . + +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 diff --git a/savane/svmain/models.py b/savane/svmain/models.py --- 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 diff --git a/savane/svmain/urls.py b/savane/svmain/urls.py --- a/savane/svmain/urls.py +++ b/savane/svmain/urls.py @@ -18,12 +18,16 @@ # along with this program. If not, see . 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[-\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[-\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[-\w]+)$', views.user_redir), + url(r'^users/(?P[-\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[-\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[-\w]+)$', views.group_redir), url(r'^projects/(?P[-\w]+)$', views.group_redir), - url(r'^u/(?P[-\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[-\w]+)$', views.user_redir), - url(r'^users/(?P[-\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[-\w]+)$', 'django.views.generic.list_detail.object_detail', + url(r'^license/(?P[-\w]+)$', object_detail, { 'queryset' : svmain_models.License.objects.all(), }, name='savane.svmain.license_detail'), ) diff --git a/templates/base.html b/templates/base.html --- a/templates/base.html +++ b/templates/base.html @@ -27,6 +27,10 @@ {% endif %} + + + + diff --git a/templates/svmain/group_list.html b/templates/svmain/group_list.html --- a/templates/svmain/group_list.html +++ b/templates/svmain/group_list.html @@ -2,6 +2,12 @@ {% block content %} +{% include "svmain/pagination.inc.html" %} + +
+ Rechercher: +
+ {% if object_list %}
    {% for object in object_list %} @@ -9,7 +15,7 @@ {% endfor %}
{% else %} -

No group.

+

No groups.

{% endif %} {% endblock %} diff --git a/templates/svmain/pagination.inc.html b/templates/svmain/pagination.inc.html new file mode 100644 --- /dev/null +++ b/templates/svmain/pagination.inc.html @@ -0,0 +1,33 @@ +{% if page_obj %} + +{% endif %} diff --git a/templates/svmain/user_list.html b/templates/svmain/user_list.html 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" %} + +
+ Rechercher: +
+ +{% if object_list %} + +{% else %} +

No users.

+{% endif %} + +{% endblock %} + +{% comment %} +Local Variables: ** +mode: django-html ** +tab-width: 4 ** +indent-tabs-mode: nil ** +End: ** +{% endcomment %}