plug recherche temporelle dans rapport contrats
[auf_rh_dae.git] / project / rh / change_list.py
1 import time, datetime
2 from django.db.models import Q
3 from django.conf import settings
4 from django.contrib.admin.filterspecs import FilterSpec
5 from django.contrib.admin.options import IncorrectLookupParameters
6 from django.contrib.admin.util import quote, get_fields_from_path
7 from django.contrib.admin.views.main import ChangeList as DjangoChangeList
8 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
9 from django.core.exceptions import SuspiciousOperation
10 from django.core.paginator import InvalidPage
11 from django.db import models
12 from django.utils.encoding import force_unicode, smart_str
13 from django.utils.translation import ugettext, ugettext_lazy
14 from django.utils.http import urlencode
15 import operator
16
17 KEY_ANNEE = 'annee'
18 KEY_DATE_DEBUT = 'date_debut'
19 KEY_DATE_FIN = 'date_fin'
20 KEY_STATUT = 'statut'
21
22 STATUT_ACTIF = 'Actif'
23 STATUT_INACTIF = 'Inactif'
24 STATUT_FUTUR = 'Futur'
25 STATUT_INCONNU = 'Inconnu'
26
27
28 class RechercheTemporelle(object):
29 model = None
30 params = None
31 prefixe_recherche_temporelle = ""
32 lookup_recherche_temporelle = {}
33
34 internal_fields = (KEY_ANNEE, KEY_DATE_DEBUT, KEY_DATE_FIN, KEY_STATUT, )
35 STATUT_CHOICES = (STATUT_ACTIF, STATUT_INACTIF, STATUT_FUTUR, STATUT_INCONNU )
36
37 def __init__(self, request, model):
38 self.model = model
39 self.params = dict(request.GET.items())
40
41 def get_prefix(self):
42 klass = getattr(self, "model_admin", self)
43 return getattr(klass, 'prefixe_recherche_temporelle', "")
44
45 def get_annees(self):
46 prefix = self.get_prefix()
47 date_debut = "%s%s" % (prefix, KEY_DATE_DEBUT)
48 date_fin = "%s%s" % (prefix, KEY_DATE_FIN)
49 annees = self.model.objects.all().values(date_debut, date_fin)
50 annees_debut = [d[date_debut].year for d in annees if d[date_debut] is not None]
51 annees_fin = [d[date_fin].year for d in annees if d[date_fin] is not None]
52 annees = set(annees_debut + annees_fin)
53 annees = list(annees)
54 annees.sort(reverse=True)
55 return annees
56
57 def get_q_inconnu(self, prefix):
58 date_debut_nulle = Q(**{"%s%s__isnull" % (prefix, KEY_DATE_DEBUT) : True})
59 date_fin_nulle = Q(**{"%s%s__isnull" % (prefix, KEY_DATE_FIN) : True})
60 return Q(date_debut_nulle & date_fin_nulle)
61
62 def get_q_range(self, prefix, borne_gauche=None, borne_droite=None):
63
64 date_debut_nulle = Q(**{"%s%s__isnull" % (prefix, KEY_DATE_DEBUT) : True})
65 date_fin_nulle = Q(**{"%s%s__isnull" % (prefix, KEY_DATE_FIN) : True})
66 date_debut_superieure_ou_egale_a_borne_gauche = Q(**{"%s%s__gte" % (prefix, KEY_DATE_DEBUT) : borne_gauche})
67 date_debut_strict_superieure_ou_egale_a_borne_gauche = Q(**{"%s%s__gt" % (prefix, KEY_DATE_DEBUT) : borne_gauche})
68 date_debut_inferieure_ou_egale_a_borne_gauche = Q(**{"%s%s__lte" % (prefix, KEY_DATE_DEBUT) : borne_gauche})
69 date_fin_superieure_ou_egale_a_borne_gauche = Q(**{"%s%s__gte" % (prefix, KEY_DATE_FIN) : borne_gauche})
70 date_fin_inferieure_ou_egale_a_borne_droite = Q(**{"%s%s__lte" % (prefix, KEY_DATE_FIN) : borne_droite})
71 date_fin_strict_inferieure_ou_egale_a_borne_droite = Q(**{"%s%s__lt" % (prefix, KEY_DATE_FIN) : borne_droite})
72 date_debut_inferieure_ou_egale_a_borne_droite = Q(**{"%s%s__lte" % (prefix, KEY_DATE_DEBUT) : borne_droite})
73 date_fin_superieure_ou_egale_a_borne_droite = Q(**{"%s%s__gte" % (prefix, KEY_DATE_FIN) : borne_droite})
74
75 if borne_droite is None:
76 q_range = date_debut_strict_superieure_ou_egale_a_borne_gauche
77
78 if borne_gauche is None:
79 q_range = date_fin_strict_inferieure_ou_egale_a_borne_droite
80
81 if borne_droite is not None and borne_gauche is not None:
82 q_range = (date_debut_superieure_ou_egale_a_borne_gauche & date_fin_inferieure_ou_egale_a_borne_droite) | \
83 ((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) | \
84 ((date_fin_superieure_ou_egale_a_borne_droite | date_fin_nulle) & date_debut_inferieure_ou_egale_a_borne_droite) | \
85 (date_debut_inferieure_ou_egale_a_borne_gauche & date_fin_superieure_ou_egale_a_borne_droite)
86
87 if borne_droite is None and borne_gauche is None:
88 q_range = Q()
89
90 return q_range
91
92 def purge_params(self, lookup_params):
93 params = lookup_params.copy()
94 for key, value in lookup_params.items():
95 # ignorer les param GET pour la recherche temporelle
96 if len([i for i in self.internal_fields if key.endswith(i)]) > 0:
97 del params[key]
98 self.lookup_recherche_temporelle[key] = value
99 continue
100 return params
101
102 def filter_temporel(self, qs):
103 q = Q()
104 prefix = self.get_prefix()
105 borne_gauche = None
106 borne_droite = None
107 for k, v in self.lookup_recherche_temporelle.items():
108
109 if k.endswith(KEY_ANNEE):
110 borne_gauche = "%s-01-01" % v
111 borne_droite = "%s-12-31" % v
112
113 if k.endswith(KEY_DATE_DEBUT):
114 date = time.strptime(v, settings.DATE_INPUT_FORMATS[0])
115 borne_gauche = time.strftime("%Y-%m-%d", date)
116
117 if k.endswith(KEY_DATE_FIN):
118 date = time.strptime(v, settings.DATE_INPUT_FORMATS[0])
119 borne_droite = time.strftime("%Y-%m-%d", date)
120
121 if k.endswith(KEY_STATUT):
122 aujourdhui = datetime.date.today()
123 if v == STATUT_ACTIF:
124 borne_gauche = aujourdhui
125 borne_droite = aujourdhui
126 elif v == STATUT_INACTIF:
127 # dans le cas d'une FK, on retire des inactifs ceux qui ont une FK active
128 if prefix != "":
129 q_range = self.get_q_range(prefix, aujourdhui, aujourdhui)
130 id_actifs = [o.id for o in qs.filter(q_range).distinct()]
131 qs = qs.exclude(id__in=id_actifs)
132 borne_droite = aujourdhui
133 elif v == STATUT_FUTUR:
134 borne_gauche = aujourdhui
135 elif v == STATUT_INCONNU:
136 q = q & self.get_q_inconnu(prefix)
137
138 q_range = self.get_q_range(prefix, borne_gauche, borne_droite)
139 qs = qs.filter(q & q_range).distinct()
140 return qs
141
142 RechercheTemporelle.get_query_string = DjangoChangeList.get_query_string.im_func
143
144
145 class ChangeList(DjangoChangeList, RechercheTemporelle):
146
147 def __init__(self, *args, **kwargs):
148 super(ChangeList, self).__init__(*args, **kwargs)
149
150 def get_query_set(self):
151
152 use_distinct = False
153
154 qs = self.root_query_set
155 lookup_params = self.params.copy() # a dictionary of the query string
156 for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR):
157 if i in lookup_params:
158 del lookup_params[i]
159
160 lookup_params = self.purge_params(lookup_params)
161 for key, value in lookup_params.items():
162 if not isinstance(key, str):
163 # 'key' will be used as a keyword argument later, so Python
164 # requires it to be a string.
165 del lookup_params[key]
166 lookup_params[smart_str(key)] = value
167
168 if not use_distinct:
169 # Check if it's a relationship that might return more than one
170 # instance
171 field_name = key.split('__', 1)[0]
172 try:
173 f = self.lookup_opts.get_field_by_name(field_name)[0]
174 except models.FieldDoesNotExist:
175 raise IncorrectLookupParameters
176 use_distinct = field_needs_distinct(f)
177
178 # if key ends with __in, split parameter into separate values
179 if key.endswith('__in'):
180 value = value.split(',')
181 lookup_params[key] = value
182
183 # if key ends with __isnull, special case '' and false
184 if key.endswith('__isnull'):
185 if value.lower() in ('', 'false'):
186 value = False
187 else:
188 value = True
189 lookup_params[key] = value
190
191 if not self.model_admin.lookup_allowed(key, value):
192 raise SuspiciousOperation(
193 "Filtering by %s not allowed" % key
194 )
195
196 # Apply lookup parameters from the query string.
197 try:
198 qs = qs.filter(**lookup_params)
199 # Naked except! Because we don't have any other way of validating "params".
200 # They might be invalid if the keyword arguments are incorrect, or if the
201 # values are not in the correct type, so we might get FieldError, ValueError,
202 # ValicationError, or ? from a custom field that raises yet something else
203 # when handed impossible data.
204 except:
205 raise IncorrectLookupParameters
206
207 qs = self.filter_temporel(qs)
208
209 # Use select_related() if one of the list_display options is a field
210 # with a relationship and the provided queryset doesn't already have
211 # select_related defined.
212 if not qs.query.select_related:
213 if self.list_select_related:
214 qs = qs.select_related()
215 else:
216 for field_name in self.list_display:
217 try:
218 f = self.lookup_opts.get_field(field_name)
219 except models.FieldDoesNotExist:
220 pass
221 else:
222 if isinstance(f.rel, models.ManyToOneRel):
223 qs = qs.select_related()
224 break
225
226 # Set ordering.
227 if self.order_field:
228 qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
229
230 # Apply keyword searches.
231 def construct_search(field_name):
232 if field_name.startswith('^'):
233 return "%s__istartswith" % field_name[1:]
234 elif field_name.startswith('='):
235 return "%s__iexact" % field_name[1:]
236 elif field_name.startswith('@'):
237 return "%s__search" % field_name[1:]
238 else:
239 return "%s__icontains" % field_name
240
241 if self.search_fields and self.query:
242 orm_lookups = [construct_search(str(search_field))
243 for search_field in self.search_fields]
244 for bit in self.query.split():
245 or_queries = [models.Q(**{orm_lookup: bit})
246 for orm_lookup in orm_lookups]
247 qs = qs.filter(reduce(operator.or_, or_queries))
248 if not use_distinct:
249 for search_spec in orm_lookups:
250 field_name = search_spec.split('__', 1)[0]
251 f = self.lookup_opts.get_field_by_name(field_name)[0]
252 if field_needs_distinct(f):
253 use_distinct = True
254 break
255
256 if use_distinct:
257 return qs.distinct()
258 else:
259 return qs