WARNING: Django 1.0.2 to Django 1.1.1 compatibility patch... There's not going back...
[htsworkflow.git] / htsworkflow / frontend / samples / changelist.py
1 """
2 Slightly modified version of the django admin component that handles filters and searches
3 """
4 from django.contrib.admin.filterspecs import FilterSpec
5 from django.contrib.admin.options import IncorrectLookupParameters
6 from django.core.paginator import Paginator, InvalidPage, EmptyPage
7 from django.db import models
8 from django.db.models.query import QuerySet
9 from django.utils.encoding import force_unicode, smart_str
10 from django.utils.translation import ugettext
11 from django.utils.http import urlencode
12 import operator
13
14 try:
15     set
16 except NameError:
17     from sets import Set as set   # Python 2.3 fallback
18
19 # The system will display a "Show all" link on the change list only if the
20 # total result count is less than or equal to this setting.
21 MAX_SHOW_ALL_ALLOWED = 20000
22
23 # Changelist settings
24 ALL_VAR = 'all'
25 ORDER_VAR = 'o'
26 ORDER_TYPE_VAR = 'ot'
27 PAGE_VAR = 'p'
28 SEARCH_VAR = 'q'
29 TO_FIELD_VAR = 't'
30 IS_POPUP_VAR = 'pop'
31 ERROR_FLAG = 'e'
32
33 # Text to display within change-list table cells if the value is blank.
34 EMPTY_CHANGELIST_VALUE = '(None)'
35
36 class ChangeList(object):
37     
38     #def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin):
39     def __init__(self, request, model, list_filter, search_fields, list_per_page, queryset=None):
40         self.model = model
41         self.opts = model._meta
42         self.lookup_opts = self.opts
43         if queryset is None:
44             self.root_query_set = model.objects.all()
45         else:
46             self.root_query_set = queryset
47         self.list_display =  []
48         self.list_display_links = None
49         self.list_filter = list_filter
50         #self.date_hierarchy = date_hierarchy
51         self.search_fields = search_fields
52         self.list_select_related = None
53         self.list_per_page = list_per_page
54         #self.list_editable = list_editable
55         self.model_admin = None
56
57         # Get search parameters from the query string.
58         try:
59             self.page_num = int(request.GET.get(PAGE_VAR, '0'))
60         except ValueError:
61             self.page_num = 0
62         self.show_all = 'all' in request.GET
63         #self.is_popup = IS_POPUP_VAR in request.GET
64         #self.to_field = request.GET.get(TO_FIELD_VAR)
65         self.params = dict(request.GET.items())
66         if PAGE_VAR in self.params:
67             del self.params[PAGE_VAR]
68         #if TO_FIELD_VAR in self.params:
69         #    del self.params[TO_FIELD_VAR]
70         if ERROR_FLAG in self.params:
71             del self.params[ERROR_FLAG]
72             
73         self.multi_page = True
74         self.can_show_all = False
75
76         self.order_field, self.order_type = self.get_ordering()
77         self.query = request.GET.get(SEARCH_VAR, '')
78         self.query_set = self.get_query_set()
79         self.get_results(request)
80         #self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name))
81         self.filter_specs, self.has_filters = self.get_filters(request)
82         #self.pk_attname = self.lookup_opts.pk.attname
83
84     def get_filters(self, request):
85         filter_specs = []
86         if self.list_filter:
87             filter_fields = [self.lookup_opts.get_field(field_name) for field_name in self.list_filter]
88             for f in filter_fields:
89                 spec = FilterSpec.create(f, request, self.params, self.model, self.model_admin)
90                 if spec and spec.has_output():
91                     filter_specs.append(spec)
92         return filter_specs, bool(filter_specs)
93
94     def get_query_string(self, new_params=None, remove=None):
95         if new_params is None: new_params = {}
96         if remove is None: remove = []
97         p = self.params.copy()
98         for r in remove:
99             for k in p.keys():
100                 if k.startswith(r):
101                     del p[k]
102         for k, v in new_params.items():
103             if v is None:
104                 if k in p:
105                     del p[k]
106             else:
107                 p[k] = v
108         return '?%s' % urlencode(p)
109
110     def get_results(self, request):
111         paginator = Paginator(self.query_set, self.list_per_page)
112         # Get the number of objects, with admin filters applied.
113         result_count = paginator.count
114
115         # Get the total number of objects, with no admin filters applied.
116         # Perform a slight optimization: Check to see whether any filters were
117         # given. If not, use paginator.hits to calculate the number of objects,
118         # because we've already done paginator.hits and the value is cached.
119         if not self.query_set.query.where:
120             full_result_count = result_count
121         else:
122             full_result_count = self.root_query_set.count()
123
124         can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED
125         multi_page = result_count > self.list_per_page
126
127         # Get the list of objects to display on this page.
128         if (self.show_all and can_show_all) or not multi_page:
129             result_list = self.query_set._clone()
130         else:
131             try:
132                 result_list = paginator.page(self.page_num+1).object_list
133             except InvalidPage:
134                 result_list = ()
135
136         self.result_count = result_count
137         self.full_result_count = full_result_count
138         self.result_list = result_list
139         self.can_show_all = can_show_all
140         self.multi_page = multi_page
141         self.paginator = paginator
142
143     def get_ordering(self):
144         lookup_opts, params = self.lookup_opts, self.params
145         # For ordering, first check the "ordering" parameter in the admin
146         # options, then check the object's default ordering. If neither of
147         # those exist, order descending by ID by default. Finally, look for
148         # manually-specified ordering from the query string.
149         ordering = lookup_opts.ordering or ['-' + lookup_opts.pk.name]
150
151         if ordering[0].startswith('-'):
152             order_field, order_type = ordering[0][1:], 'desc'
153         else:
154             order_field, order_type = ordering[0], 'asc'
155         if ORDER_VAR in params:
156             try:
157                 field_name = self.list_display[int(params[ORDER_VAR])]
158                 try:
159                     f = lookup_opts.get_field(field_name)
160                 except models.FieldDoesNotExist:
161                     # See whether field_name is a name of a non-field
162                     # that allows sorting.
163                     try:
164                         if callable(field_name):
165                             attr = field_name
166                         elif hasattr(self.model_admin, field_name):
167                             attr = getattr(self.model_admin, field_name)
168                         else:
169                             attr = getattr(self.model, field_name)
170                         order_field = attr.admin_order_field
171                     except AttributeError:
172                         pass
173                 else:
174                     order_field = f.name
175             except (IndexError, ValueError):
176                 pass # Invalid ordering specified. Just use the default.
177         if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
178             order_type = params[ORDER_TYPE_VAR]
179         return order_field, order_type
180
181     def get_query_set(self):
182         qs = self.root_query_set
183         lookup_params = self.params.copy() # a dictionary of the query string
184         for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR):
185             if i in lookup_params:
186                 del lookup_params[i]
187         for key, value in lookup_params.items():
188             if not isinstance(key, str):
189                 # 'key' will be used as a keyword argument later, so Python
190                 # requires it to be a string.
191                 del lookup_params[key]
192                 lookup_params[smart_str(key)] = value
193
194             # if key ends with __in, split parameter into separate values
195             if key.endswith('__in'):
196                 lookup_params[key] = value.split(',')
197
198         # Apply lookup parameters from the query string.
199         try:
200             qs = qs.filter(**lookup_params)
201         # Naked except! Because we don't have any other way of validating "params".
202         # They might be invalid if the keyword arguments are incorrect, or if the
203         # values are not in the correct type, so we might get FieldError, ValueError,
204         # ValicationError, or ? from a custom field that raises yet something else 
205         # when handed impossible data.
206         except:
207             raise IncorrectLookupParameters
208
209         # Use select_related() if one of the list_display options is a field
210         # with a relationship and the provided queryset doesn't already have
211         # select_related defined.
212         if not qs.query.select_related:
213             if self.list_select_related:
214                 qs = qs.select_related()
215             else:
216                 for field_name in self.list_display:
217                     try:
218                         f = self.lookup_opts.get_field(field_name)
219                     except models.FieldDoesNotExist:
220                         pass
221                     else:
222                         if isinstance(f.rel, models.ManyToOneRel):
223                             qs = qs.select_related()
224                             break
225
226         # Set ordering.
227         if self.order_field:
228             qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
229
230         # Apply keyword searches.
231         def construct_search(field_name):
232             if field_name.startswith('^'):
233                 return "%s__istartswith" % field_name[1:]
234             elif field_name.startswith('='):
235                 return "%s__iexact" % field_name[1:]
236             elif field_name.startswith('@'):
237                 return "%s__search" % field_name[1:]
238             else:
239                 return "%s__icontains" % field_name
240
241         if self.search_fields and self.query:
242             for bit in self.query.split():
243                 or_queries = [models.Q(**{construct_search(str(field_name)): bit}) for field_name in self.search_fields]
244                 qs = qs.filter(reduce(operator.or_, or_queries))
245             for field_name in self.search_fields:
246                 if '__' in field_name:
247                     qs = qs.distinct()
248                     break
249
250         return qs
251
252     #def url_for_result(self, result):
253     #    return "%s/" % quote(getattr(result, self.pk_attname))