recherche temporelle
authorOlivier Larchevêque <olivier.larcheveque@auf.org>
Thu, 16 Feb 2012 18:52:21 +0000 (13:52 -0500)
committerOlivier Larchevêque <olivier.larcheveque@auf.org>
Thu, 16 Feb 2012 18:52:21 +0000 (13:52 -0500)
project/rh/admin.py
project/rh/change_list.py [new file with mode: 0644]
project/rh/templates/admin/rh/annee_select.html
project/rh/templates/admin/rh/dossier/change_list.html
project/rh/templatetags/change_list.py

index a6a79a1..7d5ee56 100644 (file)
@@ -7,7 +7,6 @@ from django.db import models
 from django import forms
 from django.core.urlresolvers import reverse
 from django.contrib import admin
-from django.contrib.admin.views.main import ChangeList as DjangoChangeList
 from django.conf import settings
 from django.db.models import Q
 from django.template.defaultfilters import date
@@ -15,6 +14,7 @@ from ajax_select import make_ajax_form
 from auf.django.metadata.admin import AUFMetadataAdminMixin, AUFMetadataInlineAdminMixin, AUF_METADATA_READONLY_FIELDS
 from forms import ContratForm, AyantDroitForm, EmployeAdminForm, AjaxSelect
 from dae.utils import get_employe_from_user
+from change_list import ChangeList
 from groups import grp_drh
 import models as rh
 import filters
@@ -23,66 +23,10 @@ import filters
 # FILTRAGE PAR DATE 
 ################################################################################
 class DateRangeMixin(object):
-    """
-    Mixin pour que le model admin utilise le changelist trafiqué permettant de filter par range
-    non strict.
-    Par défaut, le filtrage est configuré sur aujourd'hui, soit les actifs
-    """
-    date_borne_gauche = 'date_debut'
-    date_borne_droite = 'date_fin'
+    prefixe_recherche_temporelle = ""
     def get_changelist(self, request, **kwargs):
-        if request.META.has_key('HTTP_REFERER'):
-            referer = request.META['HTTP_REFERER']
-            referer = "/".join(referer.split('/')[3:])
-            referer = "/%s" % referer.split('?')[0]
-            change_list_view = 'admin:%s_%s_changelist' % (self.model._meta.app_label, self.model.__name__.lower())
-            if referer != reverse(change_list_view):
-                params = request.GET.copy()
-                today = datetime.date.today()
-                params.update({'%s__gte' % self.date_borne_gauche : str(today), '%s__lte' % self.date_borne_droite : str(today) })
-                request.GET = params
         return ChangeList
 
-class ChangeList(DjangoChangeList):
-
-    def __init__(self, *args, **kwargs):
-        super(ChangeList, self).__init__(*args, **kwargs)
-    
-    def get_query_set(self):
-        old = self.params.copy()
-        date_debut = None
-        date_fin = None
-        for k, v in self.params.items():
-            if 'date_debut' in k:
-                prefix_debut = "".join(k.split('date_debut')[0:-1]) + 'date_debut'
-                date_debut = v
-                del self.params[k]
-            elif 'date_fin' in k:
-                prefix_fin = "".join(k.split('date_fin')[0:-1]) + 'date_fin'
-                date_fin = v
-                del self.params[k]
-        qs = super(ChangeList, self).get_query_set()
-
-        # hack pour spécifier un range
-        if date_fin is None and date_debut is not None:
-            date_fin = '2020-01-01'
-            prefix_fin = prefix_debut.replace('debut', 'fin')
-        if date_debut is None and date_fin is not None:
-            date_debut = '1000-01-01'
-            prefix_debut = prefix_fin.replace('fin', 'debut')
-
-        if date_debut is not None and date_fin is not None:    
-            q_left = (Q(**{'%s__isnull' % prefix_debut : True}) | Q(**{'%s__lte' % prefix_debut : date_debut})) & (Q(**{'%s__gte' % prefix_fin : date_debut}) & Q(**{'%s__lte' % prefix_fin : date_fin}))
-            q_right = (Q(**{'%s__isnull' % prefix_fin : True}) | Q(**{'%s__gte' % prefix_fin : date_fin})) & (Q(**{'%s__gte' % prefix_debut : date_debut}) & Q(**{'%s__lte' % prefix_debut : date_fin}))
-            q_both = Q(**{'%s__isnull' % prefix_fin : True}) | Q(**{'%s__lte' % prefix_fin : date_fin}) & (Q(**{'%s__isnull' % prefix_debut : True}) | Q(**{'%s__gte' % prefix_debut : date_debut}))
-            q_non_supprime = Q(**{'%s__exact' % prefix_debut.replace('date_debut', 'supprime') : False})
-            q = (q_left | q_right | q_both) & q_non_supprime
-            qs = qs.filter(q).distinct()
-
-        self.params = old
-        return qs
-
-################################################################################
 
 # Override of the InlineModelAdmin to support the link in the tabular inline
 class LinkedInline(admin.options.InlineModelAdmin):
@@ -307,8 +251,6 @@ class DossierAdmin(DateRangeMixin, AUFMetadataAdminMixin, ProtectRegionMixin, ad
         'poste__type_poste__famille_emploi',
         'poste__type_poste',
         'rh_contrats__type_contrat',
-        'date_debut',
-        'date_fin',
     )
     inlines = (DossierPieceInline, ContratInline,
                RemunerationInline,
@@ -342,10 +284,6 @@ class DossierAdmin(DateRangeMixin, AUFMetadataAdminMixin, ProtectRegionMixin, ad
             'poste__type_poste__id__exact',
             'poste__type_poste__famille_emploi__id__exact',
             'rh_contrats__type_contrat__id__exact',
-            'date_debut__gte',
-            'date_debut__isnull',
-            'date_fin__lte',
-            'date_fin__isnull',
             ):
             return True
 
diff --git a/project/rh/change_list.py b/project/rh/change_list.py
new file mode 100644 (file)
index 0000000..fe21c17
--- /dev/null
@@ -0,0 +1,243 @@
+import time, datetime
+from django.core.exceptions import SuspiciousOperation
+from django.conf import settings
+from django.contrib.admin.options import IncorrectLookupParameters
+from django.contrib.admin.views.main import ChangeList as DjangoChangeList
+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, field_needs_distinct
+from django.utils.encoding import force_unicode, smart_str
+from django.utils.http import urlencode
+from django.db.models import Q
+
+
+KEY_ANNEE = 'annee'
+KEY_DATE_DEBUT = 'date_debut'
+KEY_DATE_FIN = 'date_fin'
+KEY_STATUT = 'statut'
+
+STATUT_ACTIF = 'Actif'
+STATUT_INACTIF = 'Inactif'
+STATUT_FUTUR = 'Futur'
+STATUT_INCONNU = 'Inconnu'
+
+class ChangeList(DjangoChangeList):
+
+    internal_fields = (KEY_ANNEE, KEY_DATE_DEBUT, KEY_DATE_FIN, KEY_STATUT, )
+    STATUT_CHOICES = ('', STATUT_ACTIF, STATUT_INACTIF, STATUT_FUTUR, STATUT_INCONNU )
+
+    def __init__(self, *args, **kwargs):
+        super(ChangeList, self).__init__(*args, **kwargs)
+
+    def get_prefix(self):
+        return getattr(self.model_admin, 'prefixe_recherche_temporelle', "")
+
+    def get_annees(self):
+        prefix = self.get_prefix()
+        date_debut = "%s%s" % (prefix, KEY_DATE_DEBUT)
+        date_fin = "%s%s" % (prefix, KEY_DATE_FIN)
+        annees = self.model.objects.all().values(date_debut, date_fin)
+        annees_debut = [d[date_debut].year for d in annees if d[date_debut] is not None]
+        annees_fin = [d[date_fin].year for d in annees if d[date_fin] is not None]
+        annees = set(annees_debut + annees_fin)
+        annees = list(annees)
+        annees.sort(reverse=True)
+        return annees
+
+
+    def get_q_inconnu(self, prefix):
+        date_debut_nulle = Q(**{"%s%s__isnull" % (prefix, KEY_DATE_DEBUT) : True})
+        date_fin_nulle = Q(**{"%s%s__isnull" % (prefix, KEY_DATE_FIN) : True})
+        return Q(date_debut_nulle & date_fin_nulle)
+
+    def get_q_range(self, prefix, borne_gauche=None, borne_droite=None):
+
+        date_debut_nulle = Q(**{"%s%s__isnull" % (prefix, KEY_DATE_DEBUT) : True})
+        date_fin_nulle = Q(**{"%s%s__isnull" % (prefix, KEY_DATE_FIN) : True})
+        date_debut_superieure_ou_egale_a_borne_gauche = Q(**{"%s%s__gte" % (prefix, KEY_DATE_DEBUT) : borne_gauche})
+        date_debut_strict_superieure_ou_egale_a_borne_gauche = Q(**{"%s%s__gt" % (prefix, KEY_DATE_DEBUT) : borne_gauche})
+        date_debut_inferieure_ou_egale_a_borne_gauche = Q(**{"%s%s__lte" % (prefix, KEY_DATE_DEBUT) : borne_gauche})
+        date_fin_superieure_ou_egale_a_borne_gauche = Q(**{"%s%s__gte" % (prefix, KEY_DATE_FIN) : borne_gauche})
+        date_fin_inferieure_ou_egale_a_borne_droite = Q(**{"%s%s__lte" % (prefix, KEY_DATE_FIN) : borne_droite})
+        date_fin_strict_inferieure_ou_egale_a_borne_droite = Q(**{"%s%s__lt" % (prefix, KEY_DATE_FIN) : borne_droite})
+        date_debut_inferieure_ou_egale_a_borne_droite = Q(**{"%s%s__lte" % (prefix, KEY_DATE_DEBUT) : borne_droite})
+        date_fin_superieure_ou_egale_a_borne_droite = Q(**{"%s%s__gte" % (prefix, KEY_DATE_FIN) : borne_droite})
+
+        if borne_droite is None:
+            q_range = date_debut_strict_superieure_ou_egale_a_borne_gauche
+
+        if borne_gauche is None:
+            q_range = date_fin_strict_inferieure_ou_egale_a_borne_droite
+
+        if borne_droite is not None and borne_gauche is not None:
+            q_range = (date_debut_superieure_ou_egale_a_borne_gauche & date_fin_inferieure_ou_egale_a_borne_droite) |  \
+                          ((date_debut_inferieure_ou_egale_a_borne_gauche | date_debut_nulle) & date_fin_superieure_ou_egale_a_borne_gauche & date_fin_inferieure_ou_egale_a_borne_droite) | \
+                          ((date_fin_superieure_ou_egale_a_borne_droite | date_fin_nulle) & date_debut_inferieure_ou_egale_a_borne_droite) | \
+                          (date_debut_inferieure_ou_egale_a_borne_gauche & date_fin_superieure_ou_egale_a_borne_droite)
+
+        if borne_droite is None and borne_gauche is None:
+            q_range = Q()
+
+        return q_range
+
+    def get_query_set(self):
+
+        lookup_recherche_temporelle = {}
+        use_distinct = False
+
+        qs = self.root_query_set
+        lookup_params = self.params.copy() # a dictionary of the query string
+        for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR):
+            if i in lookup_params:
+                del lookup_params[i]
+        for key, value in lookup_params.items():
+            if not isinstance(key, str):
+                # 'key' will be used as a keyword argument later, so Python
+                # requires it to be a string.
+                del lookup_params[key]
+                lookup_params[smart_str(key)] = value
+
+            # ignorer les param GET pour la recherche temporelle
+            if len([i for i in self.internal_fields if key.endswith(i)]) > 0:
+                del lookup_params[key]
+                lookup_recherche_temporelle[key] = value
+                continue
+
+            if not use_distinct:
+                # Check if it's a relationship that might return more than one
+                # instance
+                field_name = key.split('__', 1)[0]
+                try:
+                    f = self.lookup_opts.get_field_by_name(field_name)[0]
+                except models.FieldDoesNotExist:
+                    raise IncorrectLookupParameters
+                use_distinct = field_needs_distinct(f)
+
+            # if key ends with __in, split parameter into separate values
+            if key.endswith('__in'):
+                value = value.split(',')
+                lookup_params[key] = value
+
+            # if key ends with __isnull, special case '' and false
+            if key.endswith('__isnull'):
+                if value.lower() in ('', 'false'):
+                    value = False
+                else:
+                    value = True
+                lookup_params[key] = value
+
+            if not self.model_admin.lookup_allowed(key, value):
+                raise SuspiciousOperation(
+                    "Filtering by %s not allowed" % key
+                )
+
+        # Apply lookup parameters from the query string.
+        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:
+            raise IncorrectLookupParameters
+
+        q = Q()
+        prefix = self.get_prefix()
+        borne_gauche = None
+        borne_droite = None
+        for k, v in lookup_recherche_temporelle.items():
+
+            if k.endswith(KEY_ANNEE):
+                borne_gauche = "%s-01-01" % v
+                borne_droite = "%s-12-31" % v
+
+            if k.endswith(KEY_DATE_DEBUT):
+                date = time.strptime(v, settings.DATE_INPUT_FORMATS[0])
+                borne_gauche = time.strftime("%Y-%m-%d", date)
+
+            if k.endswith(KEY_DATE_FIN):
+                date = time.strptime(v, settings.DATE_INPUT_FORMATS[0])
+                borne_droite = time.strftime("%Y-%m-%d", date)
+
+            if k.endswith(KEY_STATUT):
+                aujourdhui = datetime.date.today()
+                if v == STATUT_ACTIF:
+                    borne_gauche = aujourdhui
+                    borne_droite = aujourdhui
+                elif v == STATUT_INACTIF:
+                    borne_droite = aujourdhui
+                elif v == STATUT_FUTUR:
+                    borne_gauche = aujourdhui
+                elif v == STATUT_INCONNU:
+                    q = q & self.get_q_inconnu(prefix)
+                
+        q_range = self.get_q_range(prefix, borne_gauche, borne_droite)
+        qs = qs.filter(q & q_range)
+
+
+        # Use select_related() if one of the list_display options is a field
+        # with a relationship and the provided queryset doesn't already have
+        # select_related defined.
+        if not qs.query.select_related:
+            if self.list_select_related:
+                qs = qs.select_related()
+            else:
+                for field_name in self.list_display:
+                    try:
+                        f = self.lookup_opts.get_field(field_name)
+                    except models.FieldDoesNotExist:
+                        pass
+                    else:
+                        if isinstance(f.rel, models.ManyToOneRel):
+                            qs = qs.select_related()
+                            break
+
+        # Set ordering.
+        if self.order_field:
+            qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
+
+        # Apply keyword searches.
+        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
+
+        if self.search_fields and self.query:
+            orm_lookups = [construct_search(str(search_field))
+                           for search_field in self.search_fields]
+            for bit in self.query.split():
+                or_queries = [models.Q(**{orm_lookup: bit})
+                              for orm_lookup in orm_lookups]
+                qs = qs.filter(reduce(operator.or_, or_queries))
+            if not use_distinct:
+                for search_spec in orm_lookups:
+                    field_name = search_spec.split('__', 1)[0]
+                    f = self.lookup_opts.get_field_by_name(field_name)[0]
+                    if field_needs_distinct(f):
+                        use_distinct = True
+                        break
+
+        if use_distinct:
+            return qs.distinct()
+        else:
+            return qs
+
+    def get_query_string(self, new_params=None, remove=None):
+        if new_params is None: new_params = {}
+        if remove is None: remove = []
+        p = self.params.copy()
+        for r in remove:
+            for k in p.keys():
+                if k.startswith(r):
+                    del p[k]
+        for k, v in new_params.items():
+            if v is None:
+                if k in p:
+                    del p[k]
+            else:
+                p[k] = v
+        return '?%s' % urlencode(p)
index e3e6a3d..8c5472d 100644 (file)
@@ -1,3 +1,5 @@
+{{ form.media }}
+
 <script type="text/javascript">
   $(document).ready(function(){
     function updateQueryStringParameter(a, k, v) {
@@ -31,8 +33,8 @@
   });
 </script>
 <div class="actions">
-  <label>Période :
-    {{ form.periode }}
+  <label>Statut :
+    {{ form.statut }}
   </label>
   &nbsp;&nbsp;<strong>OU</strong>
   <label>Année :
@@ -42,5 +44,5 @@
   <label>Plage de dates:
     {{ form.date_debut }} au {{ form.date_fin }}
   </label>
-  <a href="{{ plage_date_querystring }}" id="plage_date_rechercher">Rechercher</a>
+  <a href="{{ plage_date_querystring }}" id="plage_date_rechercher" class="button">Rechercher</a>
 </div>
index bf82955..b142455 100644 (file)
@@ -16,7 +16,7 @@
   {% endcomment %}
 {% endblock %}
 
-{% block result_list %}
-    {% recherche_par_annees cl %}
+{% block search %}
     {{ block.super }}
+    {% recherche_par_annees cl %}
 {% endblock %}
index dc9ddf6..f4af7d6 100644 (file)
@@ -1,6 +1,8 @@
 from django.template import Library
 from django.db import connection
+from django.contrib.admin import widgets as adminwidgets
 from django import forms
+from .. import change_list as pcl
 
 register = Library()
 
@@ -14,34 +16,24 @@ def add_selected(cl, key, value):
 
 @register.inclusion_tag('admin/rh/annee_select.html')
 def recherche_par_annees(cl):
-    cursor = connection.cursor()
-    cursor.execute("SELECT year(date_debut) FROM rh_dossier WHERE year(date_debut) IS NOT NULL GROUP BY year(date_debut)")
-    set_annees = set(row[0] for row in cursor.fetchall())
-    cursor.execute("SELECT year(date_fin) FROM rh_dossier WHERE year(date_fin) IS NOT NULL GROUP BY year(date_fin)")
-    for row in cursor.fetchall():
-        set_annees.add(row[0])
-    list_annees = list(set_annees)
-    list_annees.insert(0, '')
+    list_annees = ['', ] + cl.get_annees()
+    statut_choices = ((cl.get_query_string({pcl.KEY_STATUT : p }, (pcl.KEY_ANNEE, pcl.KEY_DATE_DEBUT, pcl.KEY_DATE_FIN)), p) for p in cl.STATUT_CHOICES)
+    annee_choices = ((cl.get_query_string({pcl.KEY_ANNEE : a}, (pcl.KEY_STATUT , pcl.KEY_DATE_DEBUT, pcl.KEY_DATE_FIN)), a) for a in list_annees)
+    on_change = """window.location=window.location.pathname+this.options[this.selectedIndex].value"""
 
     class RechercheTemporelle(forms.Form):
-        periode = forms.ChoiceField(
-                choices=((cl.get_query_string({'periode': p }, ('annee', 'date_debut', 'date_fin')), p) for p in cl.PERIODE_CHOICE),
-                    widget=forms.Select(attrs = {
-                        'onchange' : """window.location=window.location.pathname+this.options[this.selectedIndex].value""",
-                        }))
-        annee = forms.ChoiceField(choices=((cl.get_query_string({'annee': a}, ('periode', 'date_debut', 'date_fin')), a) for a in list_annees),
-                    widget=forms.Select(attrs = {
-                        'onchange' : """window.location=window.location.pathname+this.options[this.selectedIndex].value""",
-                        }))
-        date_debut = forms.DateField()
-        date_fin = forms.DateField()
-    params = cl.params
-    if 'periode' in params:
-        params['periode'] = cl.get_query_string({'periode': params['periode']})
-    if 'annee' in params:
-        params['annee'] = cl.get_query_string({'annee': params['annee']})
+        statut = forms.ChoiceField(choices=statut_choices, widget=forms.Select(attrs = {'onchange' : on_change}))
+        annee = forms.ChoiceField(choices=annee_choices, widget=forms.Select(attrs = {'onchange' : on_change}))
+        date_debut = forms.DateField(widget=adminwidgets.AdminDateWidget)
+        date_fin = forms.DateField(widget=adminwidgets.AdminDateWidget)
+
+    params = cl.params.copy()
+    if pcl.KEY_STATUT in params:
+        params[pcl.KEY_STATUT] = cl.get_query_string({pcl.KEY_STATUT: params[pcl.KEY_STATUT]})
+    if pcl.KEY_ANNEE in params:
+        params[pcl.KEY_ANNEE] = cl.get_query_string({pcl.KEY_ANNEE: params[pcl.KEY_ANNEE]})
     f = RechercheTemporelle(params)
     return {
             'form': f,
-            'plage_date_querystring': cl.get_query_string(remove=('annee', 'periode'))
+            'plage_date_querystring': cl.get_query_string(remove=(pcl.KEY_ANNEE, pcl.KEY_STATUT))
             }