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