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