Merge branch 'dev' into test
authorEric Mc Sween <eric.mcsween@gmail.com>
Tue, 7 Dec 2010 20:21:33 +0000 (15:21 -0500)
committerEric Mc Sween <eric.mcsween@gmail.com>
Tue, 7 Dec 2010 20:21:33 +0000 (15:21 -0500)
Conflicts:
auf_savoirs_en_partage/chercheurs/forms.py
auf_savoirs_en_partage/chercheurs/models.py
auf_savoirs_en_partage/chercheurs/views.py
auf_savoirs_en_partage/media/css/global.css
auf_savoirs_en_partage/media/js/chercheur_edit.js
auf_savoirs_en_partage/savoirs/admin.py
auf_savoirs_en_partage/savoirs/forms.py
auf_savoirs_en_partage/savoirs/lib/recherche.py
auf_savoirs_en_partage/savoirs/models.py
auf_savoirs_en_partage/savoirs/views.py
auf_savoirs_en_partage/scripts/sphinx.conf.py.in
auf_savoirs_en_partage/settings.py
auf_savoirs_en_partage/sitotheque/forms.py
auf_savoirs_en_partage/sql/2010-11-26.sql
auf_savoirs_en_partage/templates/accounts/login.html
auf_savoirs_en_partage/templates/accounts/new_password.html
auf_savoirs_en_partage/templates/accounts/send_password.html
auf_savoirs_en_partage/templates/chercheurs/chercheur_form.html
auf_savoirs_en_partage/templates/chercheurs/edit.html
auf_savoirs_en_partage/templates/chercheurs/fiche.html
auf_savoirs_en_partage/templates/chercheurs/index.html
auf_savoirs_en_partage/templates/chercheurs/inscription.html
auf_savoirs_en_partage/templates/chercheurs/new_password.html
auf_savoirs_en_partage/templates/chercheurs/perso.html
auf_savoirs_en_partage/templates/chercheurs/publication_display.html
auf_savoirs_en_partage/templates/chercheurs/retrieve.html
auf_savoirs_en_partage/templates/container_base.html
auf_savoirs_en_partage/templates/savoirs/actualite_index.html
auf_savoirs_en_partage/templates/savoirs/evenement_ajout.html
auf_savoirs_en_partage/templates/savoirs/evenement_index.html
auf_savoirs_en_partage/templates/savoirs/evenement_utilisation.html
auf_savoirs_en_partage/templates/savoirs/index.html
auf_savoirs_en_partage/templates/savoirs/sites_auf.html
auf_savoirs_en_partage/templates/sites/index.html
auf_savoirs_en_partage/templates/sort_link.html
buildout.cfg

18 files changed:
1  2 
auf_savoirs_en_partage/chercheurs/forms.py
auf_savoirs_en_partage/chercheurs/models.py
auf_savoirs_en_partage/media/css/global.css
auf_savoirs_en_partage/savoirs/admin.py
auf_savoirs_en_partage/savoirs/forms.py
auf_savoirs_en_partage/savoirs/lib/recherche.py
auf_savoirs_en_partage/savoirs/models.py
auf_savoirs_en_partage/savoirs/templatetags/search.py
auf_savoirs_en_partage/savoirs/views.py
auf_savoirs_en_partage/sitotheque/views.py
auf_savoirs_en_partage/sql/2010-11-26.sql
auf_savoirs_en_partage/stopwords.txt
auf_savoirs_en_partage/templates/savoirs/actualite_resultat.html
auf_savoirs_en_partage/templates/savoirs/evenement_resultat.html
auf_savoirs_en_partage/templates/savoirs/recherche.html
auf_savoirs_en_partage/templates/savoirs/ressource_resultat.html
auf_savoirs_en_partage/templates/sites/resultat.html
buildout.cfg

@@@ -44,94 -44,78 +44,75 @@@ class Utilisateur(Personne)
      def get_new_password_code(self):
          return hashlib.md5(smart_str(self.courriel + self.encrypted_password)).hexdigest()[0:6]
  
- class ChercheurManager(models.Manager):
+ class ChercheurQuerySet(SEPQuerySet):
  
-     def get_query_set(self):
-         return ChercheurQuerySet(self.model).filter(personne__actif=True)
+     def filter_groupe(self, groupe):
+         return self.filter(groupes=groupe)
+     def filter_pays(self, pays):
+         return self.filter(Q(etablissement__pays=pays) | Q(etablissement_autre_pays=pays))
+     def filter_region(self, region):
+         return self.filter(Q(etablissement__pays__region=region) | Q(etablissement_autre_pays__region=region))
  
-     def search(self, text):
-         return self.get_query_set().search(text)
+     def filter_nord_sud(self, nord_sud):
+         return self.filter(Q(etablissement__pays__nord_sud=nord_sud) | Q(etablissement_autre_pays__nord_sud=nord_sud))
  
-     def search_nom(self, nom):
-         return self.get_query_set().search_nom(nom)
+     def filter_statut(self, statut):
+         return self.filter(statut=statut)
+     def filter_expert(self):
+         return self.exclude(expertises=None)
+ class ChercheurSphinxQuerySet(SEPSphinxQuerySet):
+     def __init__(self, model=None):
+         return SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_chercheurs',
+                                           weights=dict(nom=2, prenom=2))
  
      def filter_region(self, region):
-         return self.get_query_set().filter_region(region)
+         return self.filter(region_id=region.id)
+     def filter_groupe(self, groupe):
+         return self.filter(groupe_ids=groupe.id)
+     def filter_pays(self, pays):
+         return self.filter(pays_id=pays.id)
  
-     def filter_discipline(self, discipline):
-         return self.get_query_set().filter_discipline(discipline)
- class ChercheurQuerySet(models.query.QuerySet, RandomQuerySetMixin):
-     def search(self, text):
-         q = None
-         for word in text.split():
-             matching_pays = list(Pays.objects.filter(Q(nom__icontains=word) | Q(region__nom__icontains=word)).values_list('pk', flat=True))
-             matching_etablissements = list(Etablissement.objects.filter(Q(nom__icontains=word) | Q(pays__in=matching_pays)).values_list('pk', flat=True))
-             matching_publications = list(Publication.objects.filter(titre__icontains=word).values_list('pk', flat=True))
-             matching_groupes = list(Groupe.objects.filter(nom__icontains=word).values_list('pk', flat=True))
-             matching_disciplines = list(Discipline.objects.filter(nom__icontains=word).values_list('pk', flat=True))
-             part = (Q(personne__nom__icontains=word) |
-                     Q(personne__prenom__icontains=word) |
-                     Q(theme_recherche__icontains=word) |
-                     Q(etablissement__in=matching_etablissements) |
-                     Q(etablissement_autre_nom__icontains=word) |
-                     Q(etablissement_autre_pays__in=matching_pays) |
-                     Q(discipline__in=matching_disciplines) |
-                     Q(groupe_recherche__icontains=word) |
-                     Q(publications__in=matching_publications) |
-                     Q(these__titre__icontains=word) |
-                     Q(groupes__in=matching_groupes) |
-                     Q(expertises__nom__icontains=word) |
-                     Q(mots_cles__icontains=word) |
-                     Q(membre_association_francophone_details__icontains=word) |
-                     Q(membre_reseau_institutionnel_details__icontains=word)
-                    )
-             if q is None:
-                 q = part
-             else:
-                 q = q & part
-         return self.filter(q).distinct() if q is not None else self
-     def search_nom(self, nom):
-         q = None
-         for word in nom.split():
-             part = Q(personne__nom__icontains=word) | Q(personne__prenom__icontains=word)
-             if q is None:
-                 q = part
-             else:
-                 q = q & part
-         return self.filter(q) if q is not None else self
-     def filter_discipline(self, discipline):
-         """Ne conserve que les chercheurs dans la discipline donnée.
-            
-         Si ``disicipline`` est None, ce filtre n'a aucun effet."""
-         if discipline is None:
-             return self
-         if not isinstance(discipline, Discipline):
-             discipline = Discipline.objects.get(pk=discipline)
-         return self.filter(Q(discipline=discipline) |
-                            Q(theme_recherche__icontains=discipline.nom) |
-                            Q(groupe_recherche__icontains=discipline.nom) |
-                            Q(publications__titre__icontains=discipline.nom) |
-                            Q(these__titre__icontains=discipline.nom) |
-                            Q(groupes__nom__icontains=discipline.nom) |
-                            Q(expertises__nom__icontains=discipline.nom) |
-                            Q(mots_cles__icontains=discipline.nom) |
-                            Q(membre_instance_auf_details__icontains=discipline.nom) |
-                            Q(membre_association_francophone_details__icontains=discipline.nom) |
-                            Q(expert_oif_details__icontains=discipline.nom) |
-                            Q(membre_reseau_institutionnel_details__icontains=discipline.nom)).distinct()
+     NORD_SUD_CODES = {'Nord': 1, 'Sud': 2}
+     def filter_nord_sud(self, nord_sud):
+         return self.filter(nord_sud=self.NORD_SUD_CODES[nord_sud])
+     STATUT_CODES = {'enseignant': 1, 'etudiant': 2, 'independant': 3}
+     def filter_statut(self, statut):
+         return self.filter(statut=self.STATUT_CODES[statut])
 -    def filter_expert(self):
 -        return self.filter(expert=1)
 -
+ class ChercheurManager(SEPManager):
+     def get_query_set(self):
+         return ChercheurQuerySet(self.model)
+     def get_sphinx_query_set(self):
+         return ChercheurSphinxQuerySet(self.model).order_by('-date_modification')
  
      def filter_region(self, region):
-         """Ne conserve que les évènements dans la région donnée.
-            
-         Si ``region`` est None, ce filtre n'a aucun effet."""
-         if region is None:
-             return self
-         return self.filter(Q(etablissement__pays__region=region) |
-                            Q(etablissement_autre_pays__region=region))
+         """Le filtrage de chercheurs par région n'est pas une recherche texte."""
+         return self.get_query_set().filter_region(region)
+     def filter_groupe(self, groupe):
+         return self.get_query_set().filter_groupe(groupe)
+     def filter_pays(self, pays):
+         return self.get_query_set().filter_pays(pays)
+     def filter_nord_sud(self, nord_sud):
+         return self.get_query_set().filter_nord_sud(nord_sud)
+     def filter_statut(self, statut):
+         return self.get_query_set().filter_statut(statut)
+     def filter_expert(self):
+         return self.get_query_set().filter_expert()
  
  STATUT_CHOICES = (('enseignant', 'Enseignant-chercheur dans un établissement'), ('etudiant', 'Étudiant-chercheur doctorant'), ('independant', 'Chercheur indépendant docteur'))
  class Chercheur(models.Model):
@@@ -61,109 -128,144 +61,109 @@@ body { padding: 0; margin: 0; font-size
  #contenu img.top, .resultats img.top { height:10px; position:relative; top:-10px; left:0;}
  #contenu img.bottom, .resultats img.bottom{ height:10px; position:relative; bottom:-10px; left:0;}
  
 -.contenu-wrapper { padding:0 25px; }
 -
 -ul a { text-decoration:none; }
 -ul a:hover { text-decoration:underline; }
 -
 -ul.liste-de-l-accueil { padding:0 2.5em 0 0;}
 -ul.liste-de-l-accueil li { margin:1em 0; padding-top:1.1em; border-top:2px solid #d5d5d5;}
 -ul.liste-de-l-accueil img { float:left; margin:0 1em 5px 0; max-width:75px;}
 -ul.liste-de-l-accueil span { display:block;}
 -ul.liste-de-l-accueil .la-date { font-size:1.1em; }
 -ul.liste-de-l-accueil a.la-date { color:black; }
 -ul.liste-de-l-accueil .le-titre {display:block; font-size:1.2em; font-weight:bold; margin:.25em 0;}
 -ul.liste-de-l-accueil .le-resume {font-size:1.1em;}
 -ul.liste-de-l-accueil .le-resume a { display:inline;}
 -
 -.resultats { width:744px; display:inline; margin:0; padding:0; float:left; margin-top:100px; margin-bottom:50px; margin-left:0; background:url(../img/contenu-bkg-middle.png) repeat-y 0 0;}
 -.resultats a:hover{text-decoration:none;}
 -.resultatRecherche {  padding:0 25px 1.5em;}
 -.resultats .typeDocument { text-transform:uppercase;}
 -.resultats .le-titre { font-size:1.2em; font-weight:bold; text-decoration:underline;}
 -.resultats .resultatResume { font-size:1.1em; color:#000;}
 -.resultats a.ligne-url { font-size:1.1em; text-decoration:none; }
 -.resultats a:hover.ligne-url { text-decoration:underline;}
 -
 -.resultatPages { padding:0;margin-top:1.5em; text-align:center; font-size:1.2em;}
 -.resultatPages span { padding:0; margin:0 auto;}
 -.resultatPages span a { text-decoration:underline; color:#3a3125; }
 -.resultatPages span span { text-decoration:none; font-weight:bold; color:#3a3125;}
 -.resultatPages span a span { text-decoration:none; font-weight:normal; color:#3a3125;}
 -.resultatPages span a:hover { text-decoration:none;}
 -.resultatPages span a:hover span { text-decoration:none;}
 -.resultatPages span a span.lien-texte { color:#97012c; text-decoration:underline; font-weight:bold;}
 -.resultatPages span a span.lien-texte span { color:#97012c; text-decoration:none; font-weight:normal;}
 -.resultatPages span a:hover span.lien-texte { text-decoration:none;}
 -
 -/* Divers */
 -
 -.clear { clear:both!important; display:block!important; width:0!important; height:0!important; margin:0!important; padding:0!important; visibility:hidden!important;}
 -.clear-droite { clear:right!important; display:block!important; width:0!important; height:0!important; margin:0!important; padding:0!important; visibility:hidden!important;}
 -
 -/* Clearfix */
 -
 -.clearfix {display:inline-block; }
 -.clearfix:after, .container:after { content:"."; display:block; height:0; clear:both; visibility:hidden;}
 -* html .clearfix { height:1%; }
 -.clearfix { display:block; }
 -
 -#edit-form td { vertical-align:top; }
 -#edit-form td:first-child { width:150px; text-align:left; }
 -#edit-form table { width:100%; }
 -#edit-form input, #edit-form textarea { width:80%; }
 -#edit-form textarea { height:100px; }
 -#edit-form p { margin-bottom:2px; }
 -#edit-form tr { border-top:1px black solid; }
 -#edit-form tr:first-child { border-top:none; }
 -
 -.odd { background:#ddd; }
 -
 -#repertoire { border:1px solid #bbb; padding:20px; margin:10px; width:95% }
 +/* Régions/Disciplines */
 +
 +#regions_disciplines { position: absolute; top: 30px; right: 0; width: 190px;
 +                       background: url(../img/col-droite-background-bottom.png) no-repeat bottom left;
 +                       font-size: 90%; padding-bottom: 8px; }
 +#regions_disciplines div { background: url(../img/col-droite-background.png) repeat-x top left; 
 +                           padding: 10px; }
 +#regions_disciplines h1 { font-weight: normal; font-size: 180%; letter-spacing: -1px; }
 +#regions_disciplines ul { margin: 0; padding: 0; list-style: none; }
 +#regions_disciplines li { margin: 0; padding: 0; }
 +#regions_disciplines a { display: block; margin-left: 5px; padding: 1px 5px; text-decoration: none; color: black; }
 +#regions_disciplines li.active a, 
 +#regions_disciplines a:hover { background: #d5d5d5; margin-left: 0; border-left: 5px solid #666; }
 +
 +/* Liens */
 +
 +a { text-decoration: none; color: #97012c; }
 +a:hover { text-decoration: underline; }
 +a img { border: none; }
 +
 +/* Titres */
 +
 +h1 { font-weight: normal; font-size: 180%; letter-spacing: -1px; }
 +h2 { font-weight: normal; font-size: 150%; letter-spacing: -1px; border-bottom: 2px solid #3a3125; clear: right; }
 +h3 { font-size: 120%; letter-spacing: -1px; }
 +h1 a, h2 a, h3 a { color: #3a3125; }
 +.sous-titre { margin: -1em 0 1.5em 0; }
 +
 +/* Formulaires */
 +
 +form { margin: 1em 0; }
 +form td, form th { vertical-align: top }
 +form th { width: 20em; text-align: left; font-weight: normal; }
 +input[type=text] { width: 20em }
 +input.date, form input.time { width: 8em; }
 +select { width: 80%; overflow: hidden }
 +fieldset { padding: 10px; margin: 0; position: relative; 
 +           border-top: 1px solid black; border-bottom: none;
 +           border-left: none; border-right: none; }
 +fieldset fieldset { border: 1px solid #ccc; background: #fafafa; margin: 10px; padding: 15px; }
 +legend { font-weight: bold; color: #000000; margin: 0; padding:0 0.5em; }
 +legend em { font-weight: normal; font-style: normal; }
 +form .help { font-size: 90%; margin: 0; }
 +form ul.errorlist { color:red; margin:0; padding: 0; }
 +form ul.errorlist li { list-style:none; }
  
 +.horizontal-radio-buttons ul { margin-left:0 }
 +.horizontal-radio-buttons li { display:inline }
  
 -.errorlist { color:red; margin:0 }
 -.errorlist li { list-style:none; }
 +.delete-row { font-size: 90%; position:absolute; top:5px; right:5px }
 +.add-row { font-size: 90%; float:right; margin-right:16px }
 +.edit-publication { font-size: 90%; position: absolute; bottom: 5px; right: 5px; cursor: pointer; }
  
 -.publications_autre {border:1px solid #CCC; background:#FAFAFA; margin:10px; padding:10px; display:none;}
 +/* Tables */
  
 -.infotip
 -{
 -width:300px;
 -margin-top:5px;
 -border:1px solid #CCC;
 -background:#EEE;
 -float:right;
 -}
 -.publication
 -{
 +table { width: 100%; }
 +td, th { vertical-align: middle; padding: 5px }
 +td ul, td ol { padding: 0; margin: 0; list-style: none; }
 +tr.odd { background:#ddd; }
  
 -}
  
 -.message
 -{
 -font-weight:bold;
 -font-size:12px;
 -color:red;
 -}
 +/* Styles inline */
  
- .label { color: grey; font-weight: bold; width: 150px; }
++.label { font-weight: bold; width: 150px; }
  
 -.ressource-retrieve * {margin-top:1em;}
 -.ressource-retrieve .fiche {margin-top:2em;}
 +/* Page d'accueil */
  
 -.fiche, .original, .provenance {font-size:80%; margin:2px 0px;}
 -.fiche {margin-top:6px;}
 -.fiche a, .original a, .provenance a{text-decoration:none;}
 -.back {position:absolute; top:20px; right:20px;}
 +.demi-gauche { width: 345px; background-color: #f5f5f5; float: left; }
 +.demi-droite { width: 345px; float: right; }
  
 -.horizontal-radio-buttons ul { margin-left:0 }
 -.horizontal-radio-buttons li { display:inline }
 +.box { position: relative; padding: 10px; font-size: 90%; }
 +.box h1 { margin: 0 0 3px 0; }
  
 -.delete-row { position:absolute; top:5px; right:5px }
 -.add-row { float:right; margin-right:16px }
 +ul.actions-accueil { margin: 0; padding: 0 0 1em 0; list-style: none; font-size: 90%; }
 +ul.actions-accueil li { display: inline; padding-right: 10px; }
  
 -.cadre { width: 60%; margin: 100px auto; padding: 20px; background: #f9f9f9; border: 1px solid #aaa }
 +ul.liste-de-l-accueil { padding: 0; margin: 1em 0; list-style: none; }
 +ul.liste-de-l-accueil li { padding: 1em 0; border-top: 2px solid #d5d5d5;}
 +ul.liste-de-l-accueil img { float:left; margin:0 1em 5px 0; max-width:75px;}
 +ul.liste-de-l-accueil .titre { font-weight:bold; margin: 0.25em 0; }
  
 -/* Boîte principale */
 +#rss-agenda, #rss-actualites { position: absolute; top: 15px; right: 20px; }
  
 -#boite-principale { position: absolute; top: 280px; width: 744px; 
 -                    background: url(../img/contenu-bkg-middle.png) repeat-y 0 0; }
 -#boite-principale .top-border { position: absolute; top: -10px; }
 -#boite-principale .bottom-border { position: absolute; bottom: -10px; }
 -                    
 -#flash-message { margin: 10px 20px; background: #f3e3e7; color: #b03d5e; padding: 1em; font-size: 120%;
 -                 border: 1px solid #b03d5e; -moz-border-radius: 5px; -webkit-border-radius: 5px; }
 -#contenu { position: relative; padding: 20px; }
 +/* Actions */
  
 -/* Contenu */
 +ul.actions { font-size: 90%; float: right; text-align: right; list-style: none; 
 +             background: #f4f4f4; border: 1px solid #e4e4e4; 
 +             padding: 10px 10px 10px 10px; 
 +             -moz-border-radius: 5px; -webkit-border-radius: 5px; }
  
 -#contenu h4 { margin-bottom: 4px; font-size: 220%; font-weight: normal; letter-spacing: -1px; }
 -#contenu h4 a { text-decoration:none; color:black; }
 -#contenu h4 a:hover { text-decoration:underline; }
 -#contenu h5 { margin-bottom: 4px; font-size: 180%; font-weight: normal; letter-spacing: -1px; }
 -#contenu h5 a { text-decoration:none; color:black; }
 -#contenu h5 a:hover { text-decoration:underline; }
 +/* Pages de recherche */
  
 -#fiche_chercheur { font-size: 120%; }
 -#fiche_chercheur h5 { font-size: 150%; border-color: #000000; border-width: 1px 0 0 0; 
 -                      border-style: solid none none none; margin-top: 20px; padding: 10px; }
 -#fiche_chercheur .label { color: grey; font-weight: bold; width: 150px; }
 -#fiche_chercheur .souligne { border-bottom: 1px solid #DDD; }
 +.pagination { text-align: center; margin: 1em 0; }
 +.resultatRecherche {  padding: 0 25px 1.5em; }
 +.details { font-size: 90%; margin-top: 6px; }
  
 -/* Actions */
 +/* Clearfix */
  
 -.actions { float: right; text-align: right }
 -.box .actions { float: none; text-align: left; margin-left: 0; list-style-type: none }
 -.box .actions li { display: inline; padding-right: 10px; font-size: 110%; }
 +.clearfix {display:inline-block; }
 +.clearfix:after, .container:after { content:"."; display:block; height:0; clear:both; visibility:hidden;}
 +* html .clearfix { height:1%; }
 +.clearfix { display:block; }
  
 -/* Page d'accueil */
 +.cadre { width: 60%; margin: 100px auto; padding: 20px; background: #f9f9f9; border: 1px solid #aaa }
 +.souligne { border-bottom: 1px solid #DDD; }
  
 -.box { position: relative; padding: 10px 20px; }
 -.demi-gauche { width: 345px; background-color: #f5f5f5; float: left; }
 -.demi-droite { width: 345px; float: right; }
 -#rss-agenda, #rss-actualites { position: absolute; top: 15px; right: 20px; }
@@@ -1,8 -1,10 +1,11 @@@
  # -*- encoding: utf-8 -*-
+ import operator
  import re
 +
  from django.core.urlresolvers import reverse as url
  from django.db import models
+ from django.db.models import Q
+ from django.db.models.query import QuerySet
  from django.contrib import admin
  from django.contrib.auth.admin import UserAdmin
  from django.contrib.auth.models import User
@@@ -2,6 -2,7 +2,8 @@@
  import urllib, httplib, time, simplejson, pprint, math, re
  from django.core.urlresolvers import reverse
  from django.conf import settings
+ from django.utils.safestring import mark_safe
++from django.utils.text import truncate_words
  from auf_savoirs_en_partage.backend_config import RESOURCES
  from sep import SEP
  from utils import smart_str
@@@ -190,3 -191,29 +192,31 @@@ def build_search_regexp(query)
  
          parts.append(part)
      return re.compile('|'.join(parts), re.I) 
+ EXACT_PHRASE_RE = re.compile(r'"([^"]*?)"')
+ def excerpt_function(manager, words):
+     """Construit une fonction qui extrait la partie pertinente d'un texte
+        suite à une recherche textuelle."""
++    if not words:
++        return lambda x: truncate_words(x, 50)
+     qs = manager.get_sphinx_query_set()
+     client = qs._get_sphinx_client()
+     index = qs._index
+     phrases = EXACT_PHRASE_RE.findall(words)
+     keywords = EXACT_PHRASE_RE.sub('', words).strip()
+     def excerpt(text):
+         # On essaie de gérer à peu près correctement les phrases exactes. La
+         # vraie solution serait d'utiliser Sphinx 1.10 beta1 et son option
+         # "query_mode", mais c'est plus de trouble. Peut-être plus tard?
+         excerpt = text
+         for phrase in phrases:
+             excerpt = client.BuildExcerpts([excerpt], index, phrase, 
+                                            opts=dict(exact_phrase=True, limit=500, single_passage=True))[0]
+         if keywords:
+             excerpt = client.BuildExcerpts([excerpt], index, keywords, 
+                                            opts=dict(limit=500, single_passage=True))[0]
+         return mark_safe(excerpt)
+     return excerpt
@@@ -6,19 -6,25 +6,23 @@@ import operato
  import os
  import pytz
  import random
 -import simplejson
 -import time
  import uuid
  import vobject
+ from backend_config import RESOURCES
  from babel.dates import get_timezone_name
+ from caldav.lib import error
 -from datamaster_modeles.models import Thematique, Pays, Region
++from babel.dates import get_timezone_name
++from datamaster_modeles.models import Region, Pays, Thematique
  from django.contrib.auth.models import User
  from django.db import models
  from django.db.models import Q, Max
  from django.db.models.signals import pre_delete
- from auf_savoirs_en_partage.backend_config import RESOURCES
+ from django.utils.encoding import smart_unicode
+ from djangosphinx.models import SphinxQuerySet
  from savoirs.globals import META
 -from savoirs.lib.calendrier import combine
  from settings import CALENDRIER_URL, SITE_ROOT_URL
- from datamaster_modeles.models import Thematique, Pays, Region
- from lib.calendrier import combine
- from caldav.lib import error
+ # Fonctionnalités communes à tous les query sets
  
  class RandomQuerySetMixin(object):
      """Mixin pour les modèles.
@@@ -66,56 -134,40 +132,32 @@@ class SourceActualite(models.Model)
                                             texte=entry.summary_detail.value,
                                             url=entry.link, date=date)
  
- class ActualiteManager(models.Manager):
-     
-     def get_query_set(self):
-         return ActualiteQuerySet(self.model).filter(visible=True)
+ class ActualiteQuerySet(SEPQuerySet):
  
-     def search(self, text):
-         return self.get_query_set().search(text)
+     def filter_date(self, min=None, max=None):
+         qs = self
+         if min:
+             qs = qs.filter(date__gte=min)
+         if max:
+             qs = qs.filter(date__lte=max)
+         return qs
  
-     def filter_region(self, region):
-         return self.get_query_set().filter_region(region)
+ class ActualiteSphinxQuerySet(SEPSphinxQuerySet):
  
-     def filter_discipline(self, discipline):
-         return self.get_query_set().filter_discipline(discipline)
+     def __init__(self, model=None):
+         SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_actualites',
+                                    weights=dict(titre=3))
  
- class ActualiteQuerySet(models.query.QuerySet, RandomQuerySetMixin):
 -    def filter_date(self, min=None, max=None):
 -        qs = self
 -        if min:
 -            qs = qs.filter(date__gte=min.toordinal()+365)
 -        if max:
 -            qs = qs.filter(date__lte=max.toordinal()+365)
 -        return qs
 -
+ class ActualiteManager(SEPManager):
+     
+     def get_query_set(self):
+         return ActualiteQuerySet(self.model).filter(visible=True)
  
-     def search(self, text):
-         q = None
-         for word in text.split():
-             part = (Q(titre__icontains=word) | Q(texte__icontains=word) |
-                     Q(regions__nom__icontains=word) | Q(disciplines__nom__icontains=word))
-             if q is None:
-                 q = part
-             else:
-                 q = q & part
-         return self.filter(q).distinct() if q is not None else self
+     def get_sphinx_query_set(self):
+         return ActualiteSphinxQuerySet(self.model).order_by('-date')
  
-     def filter_discipline(self, discipline):
-         """Ne conserve que les actualités dans la discipline donnée.
-            
-         Si ``disicipline`` est None, ce filtre n'a aucun effet."""
-         if discipline is None:
-             return self
-         if not isinstance(discipline, Discipline):
-             discipline = Discipline.objects.get(pk=discipline)
-         return self.filter(Q(disciplines=discipline) |
-                            Q(titre__icontains=discipline.nom) |
-                            Q(texte__icontains=discipline.nom)).distinct()
-     def filter_region(self, region):
-         """Ne conserve que les actualités dans la région donnée.
-            
-         Si ``region`` est None, ce filtre n'a aucun effet."""
-         if region is None:
-             return self
-         if not isinstance(region, Region):
-             region = Region.objects.get(pk=region)
-         return self.filter(Q(regions=region) |
-                            Q(titre__icontains=region.nom) |
-                            Q(texte__icontains=region.nom)).distinct()
+     def filter_date(self, min=None, max=None):
+         return self.get_query_set().filter_date(min=min, max=max)
  
  class Actualite(models.Model):
      id = models.AutoField(primary_key=True, db_column='id_actualite')
      def assigner_regions(self, regions):
          self.regions.add(*regions)
  
- class EvenementManager(models.Manager):
-     def get_query_set(self):
-         return EvenementQuerySet(self.model).filter(approuve=True)
+ # Agenda
  
-     def search(self, text):
-         return self.get_query_set().search(text)
+ class EvenementQuerySet(SEPQuerySet):
  
-     def filter_region(self, region):
-         return self.get_query_set().filter_region(region)
+     def filter_type(self, type):
+         return self.filter(type=type)
  
-     def filter_discipline(self, discipline):
-         return self.get_query_set().filter_discipline(discipline)
+     def filter_debut(self, min=None, max=None):
+         qs = self
+         if min:
+             qs = qs.filter(debut__gte=min)
+         if max:
+             qs = qs.filter(debut__lt=max+datetime.timedelta(days=1))
+         return qs
  
- class EvenementQuerySet(models.query.QuerySet, RandomQuerySetMixin):
+ class EvenementSphinxQuerySet(SEPSphinxQuerySet):
  
-     def search(self, text):
-         q = None
-         for word in text.split():
-             part = (Q(titre__icontains=word) | 
-                     Q(mots_cles__icontains=word) |
-                     Q(discipline__nom__icontains=word) | 
-                     Q(discipline_secondaire__nom__icontains=word) |
-                     Q(type__icontains=word) |
-                     Q(lieu__icontains=word) |
-                     Q(description__icontains=word) |
-                     Q(contact__icontains=word) |
-                     Q(regions__nom__icontains=word))
-             if q is None:
-                 q = part
-             else:
-                 q = q & part
-         return self.filter(q).distinct() if q is not None else self
+     def __init__(self, model=None):
+         SEPSphinxQuerySet.__init__(self, model=model, index='savoirsenpartage_evenements',
+                                    weights=dict(titre=3))
  
-     def search_titre(self, text):
+     def filter_type(self, type):
+         return self.add_to_query('@type "%s"' % type)
+     
+     def filter_debut(self, min=None, max=None):
          qs = self
-         for word in text.split():
-             qs = qs.filter(titre__icontains=word)
+         if min:
+             qs = qs.filter(debut__gte=min.toordinal()+365)
+         if max:
+             qs = qs.filter(debut__lte=max.toordinal()+365)
          return qs
  
-     def filter_discipline(self, discipline):
-         """Ne conserve que les évènements dans la discipline donnée.
-            
-         Si ``disicipline`` est None, ce filtre n'a aucun effet."""
-         if discipline is None:
-             return self
-         if not isinstance(discipline, Discipline):
-             discipline = Discipline.objects.get(pk=discipline)
-         return self.filter(Q(discipline=discipline) |
-                            Q(discipline_secondaire=discipline) |
-                            Q(titre__icontains=discipline.nom) |
-                            Q(mots_cles__icontains=discipline.nom) |
-                            Q(description__icontains=discipline.nom))
+ class EvenementManager(SEPManager):
  
-     def filter_region(self, region):
-         """Ne conserve que les évènements dans la région donnée.
-            
-         Si ``region`` est None, ce filtre n'a aucun effet."""
-         if region is None:
-             return self
-         if not isinstance(region, Region):
-             region = Region.objects.get(pk=region)
-         return self.filter(Q(regions=region) |
-                            Q(titre__icontains=region.nom) |
-                            Q(mots_cles__icontains=region.nom) |
-                            Q(description__icontains=region.nom) |
-                            Q(pays__region=region) |
-                            Q(lieu__icontains=region.nom)).distinct()
+     def get_query_set(self):
+         return EvenementQuerySet(self.model).filter(approuve=True)
+     def get_sphinx_query_set(self):
+         return EvenementSphinxQuerySet(self.model).order_by('-debut')
+     def filter_type(self, type):
+         return self.get_query_set().filter_type(type)
+     def filter_debut(self, min=None, max=None):
+         return self.get_query_set().filter_debut(min=min, max=max)
  
 -def build_time_zone_choices():
 +def build_time_zone_choices(pays=None):
      fr_names = set()
 -    tzones = []
 +    timezones = pytz.country_timezones[pays] if pays else pytz.common_timezones
 +    result = []
      now = datetime.datetime.now()
 -    for tzname in pytz.common_timezones:
 +    for tzname in timezones:
          tz = pytz.timezone(tzname)
          fr_name = get_timezone_name(tz, locale='fr_FR')
          if fr_name in fr_names:
diff --cc auf_savoirs_en_partage/savoirs/templatetags/search.py
index f973f52,f973f52..0000000
deleted file mode 100644,100644
+++ /dev/null
@@@ -1,50 -1,50 +1,0 @@@
--#coding: utf-8
--
--from django import template
--from django.template.defaultfilters import stringfilter
--from django.utils.html import conditional_escape
--from django.utils.safestring import mark_safe
--
--register = template.Library()
--
--EXCERPT_LENGTH = 200
--
--@register.filter
--@stringfilter
--def highlight(text, regexp=None, autoescape=None):
--    """Met en évidence les parties du texte qui correspondent à l'expression
--       régulière passée en argument."""
--    if autoescape:
--        text = conditional_escape(text)
--    if regexp:
--        text = regexp.sub(r'<b>\g<0></b>', text)
--    return mark_safe(text)
--
--@register.filter
--@stringfilter
--def excerpt(text, regexp=None):
--    """Tronque le texte autour de la première correspondance de l'expression
--       régulière."""
--    if len(text) <= EXCERPT_LENGTH:
--        return text
--    m = regexp is not None and regexp.search(text)
--    if m:
--        pos = m.start()
--        end_of_sentence = max(text.rfind('.', 0, pos), text.rfind('?', 0, pos), text.rfind('!', 0, pos))
--        start = end_of_sentence + 1 if end_of_sentence != -1 else 0
--        end = pos + EXCERPT_LENGTH
--    else:
--        start = 0
--        end = start + EXCERPT_LENGTH
--    if end < len(text) - 1:
--        try:
--            end = text.rindex(' ', start, end)
--        except ValueError:
--            pass
--    excerpt = text[start:end].strip()
--    if start > 0:
--        excerpt = '(...) ' + excerpt
--    if end < len(text) - 1:
--        excerpt += ' (...)'
--    return excerpt
--excerpt.is_safe = True
@@@ -12,7 -10,7 +12,7 @@@ from django.shortcuts import get_object
  from django.utils.safestring import mark_safe
  from django import forms
  from django.conf import settings
- from lib.recherche import google_search, build_search_regexp
 -from lib.recherche import google_search, build_search_regexp, excerpt_function
++from lib.recherche import google_search, excerpt_function
  from lib import sep
  from lib.calendrier import evenements, evenement_info, combine
  from savoirs.globals import configuration
@@@ -121,12 -152,15 +155,12 @@@ def ressource_index(request)
      search_form = RecordSearchForm(request.GET)
      ressources = search_form.get_query_set()
      nb_resultats = ressources.count()
-     search_regexp = search_form.get_search_regexp()
 -    if search_form.is_valid():
 -        excerpt = excerpt_function(Record.objects, search_form.cleaned_data['q'])
 -    else:
 -        excerpt = lambda x: x
++    excerpt = excerpt_function(Record.objects, search_form.cleaned_data['q'])
      return render_to_response(
          "savoirs/ressource_index.html", 
-         {'search_form': search_form, 'ressources': ressources,
-          'nb_resultats': nb_resultats, 'search_regexp': search_regexp},
-         context_instance = RequestContext(request)
+         dict(search_form=search_form, ressources=ressources,
+              nb_resultats=nb_resultats, excerpt=excerpt),
+         context_instance=RequestContext(request)
      )
  
  def ressource_retrieve(request, id):
@@@ -155,19 -189,16 +189,19 @@@ def informations (request)
  def actualite_index(request):
      search_form = ActualiteSearchForm(request.GET)
      actualites = search_form.get_query_set()
-     search_regexp = search_form.get_search_regexp()
 -    if search_form.is_valid():
 -        excerpt = excerpt_function(Actualite.objects, search_form.cleaned_data['q'])
 -    else:
 -        excerpt = lambda x: x
++    excerpt = excerpt_function(Actualite.objects, search_form.cleaned_data['q'])
      return render_to_response(
          "savoirs/actualite_index.html",
          dict(actualites=actualites, search_form=search_form,
-              search_regexp=search_regexp, nb_resultats=actualites.count()),
+              excerpt=excerpt, nb_resultats=actualites.count()),
          context_instance = RequestContext(request))
  
 +def actualite(request, id):
 +    actualite = get_object_or_404(Actualite, pk=id)
 +    return render_to_response("savoirs/actualite.html",
 +                              dict(actualite=actualite),
 +                              context_instance=RequestContext(request))
 +
  # agenda
  def evenement_index(request):
      search_form = EvenementSearchForm(request.GET)
@@@ -2,18 -2,18 +2,19 @@@
  from django.shortcuts import render_to_response
  from django.template import Context, RequestContext
  from django.db.models import Q
 -from forms import SiteSearchForm
 -from models import Site
 +
- from models import Site
- from forms import SiteSearchForm
+ from savoirs.lib.recherche import excerpt_function
++from sitotheque.models import Site
++from sitotheque.forms import SiteSearchForm
  
  def index(request):
      search_form = SiteSearchForm(request.GET)
      sites = search_form.get_query_set()
-     search_regexp = search_form.get_search_regexp()
 -    nb_sites = sites.count()
+     excerpt = excerpt_function(Site.objects, search_form.cleaned_data['q'])
 +    nb_sites = sites.count()
      return render_to_response("sites/index.html",
 -                              dict(sites=sites, search_form=search_form,
 +                              dict(sites=sites, search_form=search_form, 
-                                    search_regexp=search_regexp, nb_sites=nb_sites), 
+                                    excerpt=excerpt, nb_sites=nb_sites), 
                                context_instance = RequestContext(request))
              
  def retrieve(request, id):
@@@ -44,6 -44,8 +44,7 @@@ ALTER TABLE chercheurs_chercheu
      DROP COLUMN publication3,
      DROP COLUMN publication4;
  
+ ANALYZE TABLE chercheurs_chercheur;
 -
  -- On ne peut pas à la fois forcer une clé unique sur le courriel et conserver
  -- les comptes inactifs dans la table.
  
index 0000000,372f840..c6b6fb2
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,163 +1,163 @@@
+ au
+ aux
+ avec
+ ce
+ ces
+ dans
+ de
+ des
+ du
+ elle
+ en
+ et
+ eux
+ il
+ je
+ la
+ le
+ leur
+ lui
+ ma
+ mais
+ me
 -même
++même
+ mes
+ moi
+ mon
+ ne
+ nos
+ notre
+ nous
+ on
+ ou
+ par
+ pas
+ pour
+ qu
+ que
+ qui
+ sa
+ se
+ ses
+ son
+ sur
+ ta
+ te
+ tes
+ toi
+ ton
+ tu
+ un
+ une
+ vos
+ votre
+ vous
+ c
+ d
+ j
+ l
 -à
++à
+ m
+ n
+ s
+ t
+ y
 -été
 -étée
 -étées
 -étés
 -étant
++été
++étée
++étées
++étés
++étant
+ suis
+ es
+ est
+ sommes
 -êtes
++êtes
+ sont
+ serai
+ seras
+ sera
+ serons
+ serez
+ seront
+ serais
+ serait
+ serions
+ seriez
+ seraient
 -étais
 -était
 -étions
 -étiez
 -étaient
++étais
++était
++étions
++étiez
++étaient
+ fus
+ fut
 -fûmes
 -fûtes
++fûmes
++fûtes
+ furent
+ sois
+ soit
+ soyons
+ soyez
+ soient
+ fusse
+ fusses
 -fût
++fût
+ fussions
+ fussiez
+ fussent
+ ayant
+ eu
+ eue
+ eues
+ eus
+ ai
+ as
+ avons
+ avez
+ ont
+ aurai
+ auras
+ aura
+ aurons
+ aurez
+ auront
+ aurais
+ aurait
+ aurions
+ auriez
+ auraient
+ avais
+ avait
+ avions
+ aviez
+ avaient
+ eut
 -eûmes
 -eûtes
++eûmes
++eûtes
+ eurent
+ aie
+ aies
+ ait
+ ayons
+ ayez
+ aient
+ eusse
+ eusses
 -eût
++eût
+ eussions
+ eussiez
+ eussent
+ ceci
 -celà 
++celà 
+ cet
+ cette
+ ici
+ ils
+ les
+ leurs
+ quel
+ quels
+ quelle
+ quelles
+ sans
+ soi
@@@ -1,13 -1,11 +1,13 @@@
- {% load search %}
+ {% load sep %}
  
  <div class="resultatRecherche">
 -  <div class="la-date">{{ actualite.date|date:"d F Y" }}</div>
 -  <a class="le-titre" href="{{ actualite.url }}">{{ actualite.titre|apply:excerpt }}</a>
 -  <div class="resultatResume">{{ actualite.texte|apply:excerpt }}</div>
 +  <div>{{ actualite.date|date:"d F Y" }}</div>
 +  <div class="titre">
-       <a href="{% url savoirs.views.actualite actualite.id %}">{{ actualite.titre|highlight:search_regexp }}</a>
++      <a href="{% url savoirs.views.actualite actualite.id %}">{{ actualite.titre|apply:excerpt }}</a>
 +  </div>
-   <div>{{ actualite.texte|highlight:search_regexp }}</div>
++  <div>{{ actualite.texte|apply:excerpt }}</div>
    {% if actualite.source %}
 -  <div><span class="lbl">Source:</span> {{ actualite.source.nom }}</div>
 +  <div><span class="label">Source:</span> {{ actualite.source.nom }}</div>
    {% endif %}
  </div>
  
@@@ -1,7 -1,7 +1,7 @@@
- {% load search %}
+ {% load sep %}
  
  <div class="resultatRecherche">
 -  <div class="la-date">{{ evenement.debut|date:"d/m/Y H\hi" }}</div>
 -  <div><a href="{% url savoirs.views.evenement evenement.pk %}" class="le-titre">{{ evenement.titre|apply:excerpt }}</a></div>
 -  <div class="le-resume">{{ evenement.description|apply:excerpt }}</div>
 +  <div>{{ evenement.debut|date:"d/m/Y H\hi" }}</div>
-   <div class="titre"><a href="{% url savoirs.views.evenement evenement.pk %}">{{ evenement.titre|highlight:search_regexp }}</a></div>
-   <div>{{ evenement.description|excerpt:search_regexp|highlight:search_regexp }}</div>
++  <div class="titre"><a href="{% url savoirs.views.evenement evenement.pk %}">{{ evenement.titre|apply:excerpt }}</a></div>
++  <div>{{ evenement.description|apply:excerpt }}</div>
  </div>
@@@ -2,48 -2,66 +2,48 @@@
  {% load sep %}
  
  {% block contenu %}
 -<script>
 -  $(document).ready(function(){
 -    makePageLinks ({{ page|default:0 }}, {{ data.last_page|default:0 }}, 
 -                   '{{ data.more_link }}');
 -  });
 -
 -  {% if user.is_authenticated %}
 -  function showEditModal (uri) {
 -    $('#jsonsource').load ('{% url savoirs.views.json_get %}?uri='+uri, 
 -    function() {
 -      __jf = new JSONForm ('schema', 'mainform', 'jsonsource');
 -      __jf.setup ();
 -      $('#edit-form').dialog({height: 400, width: 650, modal: true});
 -    });
 -  }
 -  {% endif %}
 -</script>
  <div class="texte">
 -    <h4>Résultats correspondant à « {{ q }} »</h4>
 +    <h1>Résultats correspondant à «&nbsp;{{ q }}&nbsp;»</h1>
      
  {% if ressources %}
-     <h2>
-         Ressources ({{ ressources|length }} sur {{ total_ressources }})
 -    <h5>Ressources ({{ ressources|length }} sur {{ total_ressources }})</h5>
++    <h2>Ressources ({{ ressources|length }} sur {{ total_ressources }})</h2>
++    <div class="sous-titre">
 +        <a class="action" href="{% url savoirs.views.ressource_index %}{{ briques_query_string }}"
-             >(Voir toutes les ressources correspondant à cette recherche)</a>
-     </h2>
++            >Voir toutes les ressources correspondant à cette recherche</a>
++    </div>
      {% for ressource in ressources %}
          {% include "savoirs/ressource_resultat.html" %} 
      {% endfor %}
  {% endif %}
  
  {% if actualites %}
-     <h2>
-         Actualités ({{ actualites|length }} sur {{ total_actualites }})
 -    <h5>Actualités ({{ actualites|length }} sur {{ total_actualites }})</h5>
++    <h2>Actualités ({{ actualites|length }} sur {{ total_actualites }})</h2>
++    <div class="sous-titre">
 +        <a class="action" href="{% url savoirs.views.actualite_index %}{{ briques_query_string }}"
-             >(Voir toutes les actualités correspondant à cette recherche)</a>
-     </h2>
++            >Voir toutes les actualités correspondant à cette recherche</a>
++    </div>
      {% for actualite in actualites %}
          {% include "savoirs/actualite_resultat.html" %} 
      {% endfor %}
  {% endif %}
  
  {% if evenements %}
-     <h2>
-         Évènements ({{ evenements|length }} sur {{ total_evenements }})
 -    <h5>Évènements ({{ evenements|length }} sur {{ total_evenements }})</h5>
++    <h2>Évènements ({{ evenements|length }} sur {{ total_evenements }})</h2>
++    <div class="sous-titre">
 +        <a class="action" href="{% url savoirs.views.evenement_index %}{{ briques_query_string }}"
-             >(Voir tous les évènements correspondant à cette recherche)</a>
-     </h2>
++            >Voir tous les évènements correspondant à cette recherche</a>
++    </div>
      {% for evenement in evenements %}
          {% include "savoirs/evenement_resultat.html" %}
      {% endfor %}
  {% endif %}
  
  {% if chercheurs %}
-     <h2>
-         Chercheurs ({{ chercheurs|length }} sur {{ total_chercheurs }})
 -    <h5>Chercheurs ({{ chercheurs|length }} sur {{ total_chercheurs }})</h5>
++    <h2>Chercheurs ({{ chercheurs|length }} sur {{ total_chercheurs }})</h2>
++    <div class="sous-titre">
 +        <a class="action" href="{% url chercheurs.views.index %}{{ briques_query_string }}"
-             >(Voir tous les chercheurs correspondant à cette recherche)</a>
-     </h2>
++            >Voir tous les chercheurs correspondant à cette recherche</a>
++    </div>
      <ul>
      {% for chercheur in chercheurs %}
          <li><a href="{% url chercheurs.views.retrieve chercheur.id %}">{{ chercheur }}</a></li>
  {% endif %}
  
  {% if sites %}
-     <h2>
-         Sites de la sitothèque ({{ sites|length }} sur {{ total_sites }})
 -    <h5>Sites de la sitothèque ({{ sites|length }} sur {{ total_sites }})</h5>
++    <h2>Sites de la sitothèque ({{ sites|length }} sur {{ total_sites }})</h2>
++    <div class="sous-titre">
 +        <a class="action" href="{% url sitotheque.views.index %}{{ briques_query_string }}"
-             >(Voir tous les sites correspondant à cette recherche)</a>
-     </h2>
++            >Voir tous les sites correspondant à cette recherche</a>
++    </div>
      {% for site in sites %}
          {% include "sites/resultat.html" %}
      {% endfor %}
  {% endif %}
  
  {% if sites_auf %}
-     <h2>
-         Sites AUF
 -    <h5>Sites AUF</h5>
++    <h2>Sites AUF</h2>
++    <div class="sous-titre">
 +        <a class="action" href="{% url savoirs.views.sites_auf %}?q={{ q|urlencode }}"
-             >(Voir tous les sites AUF correspondant à cette recherche)</a>
-     </h2>
++            >Voir tous les sites AUF correspondant à cette recherche</a>
++    </div>
      {% for r in sites_auf %}
          {% include "savoirs/sites_auf_resultat.html" %}
      {% endfor %}
  {% endif %}
  
  {% if not ressources and not actualites and not evenements and not chercheurs and not sites %}
 -    <h5>Aucun résultat</h5>
 -    <p class="pad"><a href="{% url savoirs.views.index %}">Retour à la page d'accueil</a></p>
 +    <h2>Aucun résultat</h2>
-     <p class="pad"><a href="{% url savoirs.views.index %}">Retour à la page d'accueil</a></p>
++    <p><a href="{% url savoirs.views.index %}">Retour à la page d'accueil</a></p>
  {% endif %}
  </div>
  
@@@ -1,18 -1,14 +1,18 @@@
- {% load search %}
+ {% load sep %}
  
  <div class="resultatRecherche">
 -    <a class="le-titre" href="{% url savoirs.views.ressource_retrieve ressource.id %}">{{ ressource.title|apply:excerpt }}</a>
 +    <div class="titre">
-         <a href="{% url savoirs.views.ressource_retrieve ressource.id %}">{{ ressource.title|highlight:search_regexp }}</a>
++        <a href="{% url savoirs.views.ressource_retrieve ressource.id %}">{{ ressource.title|apply:excerpt }}</a>
 +    </div>
      {% if ressource.creator %}
-     <div><span class="label">Auteur:</span> {{ ressource.creator|highlight:search_regexp }}</div>
 -    <div><span class="lbl">Auteur:</span> {{ ressource.creator|apply:excerpt }}</div>
++    <div><span class="label">Auteur:</span> {{ ressource.creator|apply:excerpt }}</div>
      {% endif %}
      {% if ressource.description %}
-     <div><span class="label">Résumé:</span> {{ ressource.description|excerpt:search_regexp|highlight:search_regexp }}</div>
 -    <div class="resultatResume"><span class="lbl">Description:</span> {{ ressource.description|apply:excerpt }}</div>
++    <div><span class="label">Résumé:</span> {{ ressource.description|apply:excerpt }}</div>
      {% endif %}
 -    <div class="fiche"><span>Fiche: </span><a href="{% url savoirs.views.ressource_retrieve ressource.id %}">{% url savoirs.views.ressource_retrieve ressource.id %}</a></div>
 -    <div class="original"><span>Contenu original: </span><a target="_blank" href="{{ ressource.uri }}">{{ ressource.uri }}</a></div>
 -    <div class="provenance"><span>Provenance: </span><a target="_blank" href="{{ ressource.getServeurURL }}">{{ ressource.getServeurURL }}</a></div>
 +    <div class="details">
 +        <div><span>Fiche: </span><a href="{% url savoirs.views.ressource_retrieve ressource.id %}">{% url savoirs.views.ressource_retrieve ressource.id %}</a></div>
 +        <div><span>Contenu original: </span><a target="_blank" href="{{ ressource.uri }}">{{ ressource.uri }}</a></div>
 +        <div><span>Provenance: </span><a target="_blank" href="{{ ressource.getServeurURL }}">{{ ressource.getServeurURL }}</a></div>
 +    </div>
  </div>
@@@ -1,7 -1,7 +1,7 @@@
- {% load search %}
+ {% load sep %}
  
  <div class="resultatRecherche">
-   <div class="titre"><a href="{% url sitotheque.views.retrieve site.id %}">{{ site|highlight:search_regexp }}</a></div>
-   <div>{{ site.description|excerpt:search_regexp|highlight:search_regexp }}</div>
 -  <div><a class="le-titre" href="{% url sitotheque.views.retrieve site.id %}">{{ site.titre|apply:excerpt }}</a></div>
 -  <div class="resultatResume">{{ site.description|apply:excerpt }}</div>
 -  <div><span class="lbl">URL:</span> <a href="{{ site.url }}">{{ site.url }}</a></div>
++  <div class="titre"><a href="{% url sitotheque.views.retrieve site.id %}">{{ site.titre|apply:excerpt }}</a></div>
++  <div>{{ site.description|apply:excerpt }}</div>
 +  <div><span class="label">URL:</span> <a href="{{ site.url }}">{{ site.url }}</a></div>
  </div>
diff --cc buildout.cfg
@@@ -1,6 -1,6 +1,6 @@@
  [buildout]
  newest = false
- parts = django articles harvest
 -parts = django articles harvest sphinx_conf
++parts = django articles harvest sphinx_config
  find-links = http://pypi.auf.org/caldav/
      http://pypi.auf.org/auf_references_client/
      http://pypi.auf.org/auf_references_modeles/
@@@ -46,3 -47,8 +47,8 @@@ template = harvest.i
  recipe = buildout_script
  template_dir = ${buildout:directory}/auf_savoirs_en_partage/scripts/
  template = import_chercheurs.in
 -[sphinx_conf]
++[sphinx_config]
+ recipe = buildout_script
+ template_dir = ${buildout:directory}/auf_savoirs_en_partage/scripts/
+ template = sphinx.conf.py.in