Commit | Line | Data |
---|---|---|
5e8b78d9 OL |
1 | # -*- coding: utf-8 -*- |
2 | ||
a17e2236 | 3 | import time, datetime |
75f0e87b DB |
4 | import operator |
5 | ||
7eb6b687 | 6 | from django.db.models import Q |
a17e2236 OL |
7 | from django.conf import settings |
8 | from django.contrib.admin.options import IncorrectLookupParameters | |
9 | from django.contrib.admin.views.main import ChangeList as DjangoChangeList | |
dcd1b959 OL |
10 | from django.contrib.admin.views.main import ALL_VAR, ORDER_VAR, \ |
11 | ORDER_TYPE_VAR, SEARCH_VAR, TO_FIELD_VAR, IS_POPUP_VAR, field_needs_distinct | |
7eb6b687 | 12 | from django.core.exceptions import SuspiciousOperation |
7eb6b687 | 13 | from django.db import models |
dcd1b959 | 14 | from django.utils.encoding import smart_str |
75f0e87b | 15 | |
a17e2236 OL |
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 | ||
f0f6b03e OL |
27 | |
28 | class RechercheTemporelle(object): | |
29 | model = None | |
30 | params = None | |
31 | prefixe_recherche_temporelle = "" | |
32 | lookup_recherche_temporelle = {} | |
a17e2236 OL |
33 | |
34 | internal_fields = (KEY_ANNEE, KEY_DATE_DEBUT, KEY_DATE_FIN, KEY_STATUT, ) | |
910e39e5 | 35 | STATUT_CHOICES = (STATUT_ACTIF, STATUT_INACTIF, STATUT_FUTUR, STATUT_INCONNU ) |
f0f6b03e | 36 | |
6bee05ff | 37 | def __init__(self, params=params, model=model): |
f0f6b03e | 38 | self.model = model |
6bee05ff | 39 | self.params = params |
a17e2236 OL |
40 | |
41 | def get_prefix(self): | |
f0f6b03e OL |
42 | klass = getattr(self, "model_admin", self) |
43 | return getattr(klass, 'prefixe_recherche_temporelle', "") | |
a17e2236 OL |
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 | ||
5e8b78d9 OL |
57 | def get_q_actifs(self): |
58 | qs = self.model.objects.get_query_set() | |
59 | q = qs.get_q_actifs() | |
60 | return q | |
a17e2236 | 61 | |
5e8b78d9 OL |
62 | def get_q_inactifs(self): |
63 | qs = self.model.objects.get_query_set() | |
64 | q = qs.get_q_inactifs() | |
65 | return q | |
a17e2236 | 66 | |
5e8b78d9 OL |
67 | def get_q_futurs(self): |
68 | qs = self.model.objects.get_query_set() | |
69 | q = qs.get_q_futurs() | |
70 | return q | |
a17e2236 | 71 | |
5e8b78d9 OL |
72 | def get_q_inconnus(self): |
73 | qs = self.model.objects.get_query_set() | |
74 | q = qs.get_q_inconnus() | |
75 | return q | |
a17e2236 | 76 | |
5e8b78d9 OL |
77 | def get_q_range(self, prefix, borne_gauche=None, borne_droite=None): |
78 | # on filtre si on a au moins une borne, car par défaut q_actifs est | |
79 | # un filtre sur actif à l'instant présent | |
80 | if borne_gauche is not None and borne_droite is not None: | |
81 | qs = self.model.objects.get_query_set() | |
82 | q = qs.get_q_actifs(borne_gauche, borne_droite) | |
83 | return q | |
84 | else: | |
85 | return Q() | |
a17e2236 | 86 | |
f0f6b03e | 87 | def purge_params(self, lookup_params): |
84242765 | 88 | self.lookup_recherche_temporelle = {} |
f0f6b03e OL |
89 | params = lookup_params.copy() |
90 | for key, value in lookup_params.items(): | |
91 | # ignorer les param GET pour la recherche temporelle | |
92 | if len([i for i in self.internal_fields if key.endswith(i)]) > 0: | |
93 | del params[key] | |
94 | self.lookup_recherche_temporelle[key] = value | |
95 | continue | |
96 | return params | |
97 | ||
dcd1b959 | 98 | def get_q_temporel(self, qs): |
f0f6b03e OL |
99 | q = Q() |
100 | prefix = self.get_prefix() | |
101 | borne_gauche = None | |
102 | borne_droite = None | |
5e8b78d9 | 103 | |
f0f6b03e OL |
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: | |
5e8b78d9 | 121 | q = q & self.get_q_actifs() |
f0f6b03e | 122 | elif v == STATUT_INACTIF: |
5e8b78d9 | 123 | q = q & self.get_q_inactifs() |
f0f6b03e OL |
124 | borne_droite = aujourdhui |
125 | elif v == STATUT_FUTUR: | |
5e8b78d9 | 126 | q = q & self.get_q_futurs() |
f0f6b03e | 127 | elif v == STATUT_INCONNU: |
5e8b78d9 | 128 | q = q & self.get_q_inconnus() |
f0f6b03e | 129 | q_range = self.get_q_range(prefix, borne_gauche, borne_droite) |
dcd1b959 | 130 | return q & q_range |
f0f6b03e OL |
131 | |
132 | RechercheTemporelle.get_query_string = DjangoChangeList.get_query_string.im_func | |
133 | ||
134 | ||
135 | class ChangeList(DjangoChangeList, RechercheTemporelle): | |
136 | ||
137 | def __init__(self, *args, **kwargs): | |
138 | super(ChangeList, self).__init__(*args, **kwargs) | |
139 | ||
a17e2236 OL |
140 | def get_query_set(self): |
141 | ||
241907ad | 142 | use_distinct = True |
a17e2236 OL |
143 | |
144 | qs = self.root_query_set | |
5e8b78d9 | 145 | |
a17e2236 OL |
146 | lookup_params = self.params.copy() # a dictionary of the query string |
147 | for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR): | |
148 | if i in lookup_params: | |
149 | del lookup_params[i] | |
f0f6b03e OL |
150 | |
151 | lookup_params = self.purge_params(lookup_params) | |
a17e2236 OL |
152 | for key, value in lookup_params.items(): |
153 | if not isinstance(key, str): | |
154 | # 'key' will be used as a keyword argument later, so Python | |
155 | # requires it to be a string. | |
156 | del lookup_params[key] | |
157 | lookup_params[smart_str(key)] = value | |
158 | ||
a17e2236 OL |
159 | if not use_distinct: |
160 | # Check if it's a relationship that might return more than one | |
161 | # instance | |
162 | field_name = key.split('__', 1)[0] | |
163 | try: | |
164 | f = self.lookup_opts.get_field_by_name(field_name)[0] | |
165 | except models.FieldDoesNotExist: | |
166 | raise IncorrectLookupParameters | |
167 | use_distinct = field_needs_distinct(f) | |
168 | ||
e8b6a20c BS |
169 | # Commenting out, not sure why we try to .split() a |
170 | # iterable / queryset, but it doesn't seem to work. Hope | |
171 | # i'm not breaking something. | |
172 | # if key ends with __in, split parameter into separate | |
173 | # values | |
174 | # if key.endswith('__in'): | |
175 | # lookup_params[key] = value | |
a17e2236 OL |
176 | |
177 | # if key ends with __isnull, special case '' and false | |
178 | if key.endswith('__isnull'): | |
179 | if value.lower() in ('', 'false'): | |
180 | value = False | |
181 | else: | |
182 | value = True | |
183 | lookup_params[key] = value | |
184 | ||
185 | if not self.model_admin.lookup_allowed(key, value): | |
186 | raise SuspiciousOperation( | |
187 | "Filtering by %s not allowed" % key | |
188 | ) | |
dcd1b959 OL |
189 | |
190 | q_temporel = self.get_q_temporel(qs) | |
191 | q = Q(**lookup_params) & q_temporel | |
5e8b78d9 | 192 | |
a17e2236 OL |
193 | # Apply lookup parameters from the query string. |
194 | try: | |
dcd1b959 | 195 | qs = qs.filter(q) |
a17e2236 OL |
196 | # Naked except! Because we don't have any other way of validating "params". |
197 | # They might be invalid if the keyword arguments are incorrect, or if the | |
198 | # values are not in the correct type, so we might get FieldError, ValueError, | |
199 | # ValicationError, or ? from a custom field that raises yet something else | |
200 | # when handed impossible data. | |
201 | except: | |
202 | raise IncorrectLookupParameters | |
203 | ||
a17e2236 OL |
204 | |
205 | # Use select_related() if one of the list_display options is a field | |
206 | # with a relationship and the provided queryset doesn't already have | |
207 | # select_related defined. | |
208 | if not qs.query.select_related: | |
209 | if self.list_select_related: | |
210 | qs = qs.select_related() | |
211 | else: | |
212 | for field_name in self.list_display: | |
213 | try: | |
214 | f = self.lookup_opts.get_field(field_name) | |
215 | except models.FieldDoesNotExist: | |
216 | pass | |
217 | else: | |
218 | if isinstance(f.rel, models.ManyToOneRel): | |
219 | qs = qs.select_related() | |
220 | break | |
221 | ||
222 | # Set ordering. | |
223 | if self.order_field: | |
224 | qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field)) | |
225 | ||
226 | # Apply keyword searches. | |
227 | def construct_search(field_name): | |
228 | if field_name.startswith('^'): | |
229 | return "%s__istartswith" % field_name[1:] | |
230 | elif field_name.startswith('='): | |
231 | return "%s__iexact" % field_name[1:] | |
232 | elif field_name.startswith('@'): | |
233 | return "%s__search" % field_name[1:] | |
234 | else: | |
235 | return "%s__icontains" % field_name | |
236 | ||
237 | if self.search_fields and self.query: | |
238 | orm_lookups = [construct_search(str(search_field)) | |
239 | for search_field in self.search_fields] | |
240 | for bit in self.query.split(): | |
241 | or_queries = [models.Q(**{orm_lookup: bit}) | |
242 | for orm_lookup in orm_lookups] | |
243 | qs = qs.filter(reduce(operator.or_, or_queries)) | |
244 | if not use_distinct: | |
245 | for search_spec in orm_lookups: | |
246 | field_name = search_spec.split('__', 1)[0] | |
247 | f = self.lookup_opts.get_field_by_name(field_name)[0] | |
248 | if field_needs_distinct(f): | |
249 | use_distinct = True | |
250 | break | |
dcd1b959 | 251 | |
a17e2236 OL |
252 | if use_distinct: |
253 | return qs.distinct() | |
254 | else: | |
255 | return qs |