Commit | Line | Data |
---|---|---|
a17e2236 | 1 | import time, datetime |
75f0e87b DB |
2 | import operator |
3 | ||
7eb6b687 | 4 | from django.db.models import Q |
a17e2236 OL |
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 | |
dcd1b959 OL |
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 | |
7eb6b687 | 10 | from django.core.exceptions import SuspiciousOperation |
7eb6b687 | 11 | from django.db import models |
dcd1b959 | 12 | from django.utils.encoding import smart_str |
75f0e87b | 13 | |
a17e2236 OL |
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 | ||
f0f6b03e OL |
25 | |
26 | class RechercheTemporelle(object): | |
27 | model = None | |
28 | params = None | |
29 | prefixe_recherche_temporelle = "" | |
30 | lookup_recherche_temporelle = {} | |
a17e2236 OL |
31 | |
32 | internal_fields = (KEY_ANNEE, KEY_DATE_DEBUT, KEY_DATE_FIN, KEY_STATUT, ) | |
910e39e5 | 33 | STATUT_CHOICES = (STATUT_ACTIF, STATUT_INACTIF, STATUT_FUTUR, STATUT_INCONNU ) |
f0f6b03e | 34 | |
6bee05ff | 35 | def __init__(self, params=params, model=model): |
f0f6b03e | 36 | self.model = model |
6bee05ff | 37 | self.params = params |
a17e2236 OL |
38 | |
39 | def get_prefix(self): | |
f0f6b03e OL |
40 | klass = getattr(self, "model_admin", self) |
41 | return getattr(klass, 'prefixe_recherche_temporelle', "") | |
a17e2236 OL |
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 | ||
a17e2236 OL |
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 | ||
f0f6b03e | 90 | def purge_params(self, lookup_params): |
84242765 | 91 | self.lookup_recherche_temporelle = {} |
f0f6b03e OL |
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 | ||
dcd1b959 | 101 | def get_q_temporel(self, qs): |
f0f6b03e OL |
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) | |
f0f6b03e | 136 | q_range = self.get_q_range(prefix, borne_gauche, borne_droite) |
dcd1b959 | 137 | return q & q_range |
f0f6b03e OL |
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 | ||
a17e2236 OL |
147 | def get_query_set(self): |
148 | ||
241907ad | 149 | use_distinct = True |
a17e2236 OL |
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] | |
f0f6b03e OL |
156 | |
157 | lookup_params = self.purge_params(lookup_params) | |
a17e2236 OL |
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 | ||
a17e2236 OL |
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 | ) | |
dcd1b959 OL |
192 | |
193 | q_temporel = self.get_q_temporel(qs) | |
194 | q = Q(**lookup_params) & q_temporel | |
a17e2236 OL |
195 | |
196 | # Apply lookup parameters from the query string. | |
197 | try: | |
dcd1b959 | 198 | qs = qs.filter(q) |
a17e2236 OL |
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 | ||
a17e2236 OL |
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 | |
dcd1b959 | 254 | |
a17e2236 OL |
255 | if use_distinct: |
256 | return qs.distinct() | |
257 | else: | |
258 | return qs |