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