list.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. from urllib import urlencode
  2. from django.contrib import messages
  3. from django.core.paginator import Paginator, EmptyPage
  4. from django.core.urlresolvers import reverse
  5. from django.db import transaction
  6. from django.shortcuts import redirect
  7. from django.utils.translation import ugettext_lazy as _
  8. from misago.core.exceptions import ExplicitFirstPage
  9. from misago.admin.views.generic.base import AdminView
  10. class MassActionError(Exception):
  11. pass
  12. class ListView(AdminView):
  13. """
  14. Admin items list view
  15. Uses following attributes:
  16. template = template name used to render items list
  17. items_per_page = number of items displayed on single page
  18. (enter 0 or don't define for no pagination)
  19. ordering = tuple of tuples defining allowed orderings
  20. typles should follow this format: (name, order_by)
  21. """
  22. template = 'list.html'
  23. items_per_page = 0
  24. ordering = None
  25. extra_actions = None
  26. mass_actions = None
  27. selection_label = _('Selected: 0')
  28. empty_selection_label = _('Select items')
  29. @classmethod
  30. def add_mass_action(cls, action, name, prompt=None):
  31. if not cls.mass_actions:
  32. cls.mass_actions = []
  33. cls.extra_actions.append((action, name, prompt))
  34. @classmethod
  35. def add_item_action(cls, name, icon, link, style=None):
  36. if not cls.extra_actions:
  37. cls.extra_actions = []
  38. cls.extra_actions.append({
  39. 'name': name,
  40. 'icon': icon,
  41. 'link': link,
  42. 'style': style,
  43. })
  44. def get_queryset(self):
  45. return self.get_model().objects.all()
  46. def paginate_items(self, context, page):
  47. try:
  48. page = int(page)
  49. if page == 1:
  50. raise ExplicitFirstPage()
  51. elif page == 0:
  52. page = 1
  53. except ValueError:
  54. page_no = 1
  55. context['paginator'] = Paginator(context['items'],
  56. self.items_per_page,
  57. allow_empty_first_page=True)
  58. context['page'] = context['paginator'].page(page)
  59. context['items'] = context['page'].object_list
  60. """
  61. Filter list items
  62. """
  63. SearchForm = None
  64. def get_search_form(self, request):
  65. return self.SearchForm
  66. @property
  67. def filters_session_key(self):
  68. return 'misago_admin_%s_filters' % self.root_link
  69. def get_filters_from_GET(self, SearchForm, request):
  70. form = SearchForm(request.GET)
  71. form.is_valid()
  72. return self.clean_filtering_data(form.cleaned_data)
  73. def get_filters_from_session(self, SearchForm, request):
  74. session_filters = request.session.get(self.filters_session_key, {})
  75. form = SearchForm(session_filters)
  76. form.is_valid()
  77. return self.clean_filtering_data(form.cleaned_data)
  78. def clean_filtering_data(self, data):
  79. for key, value in data.items():
  80. if not value:
  81. del data[key]
  82. return data
  83. def get_filtering_methods(self, request):
  84. SearchForm = self.get_search_form(request)
  85. methods = {
  86. 'GET': self.get_filters_from_GET(SearchForm, request),
  87. 'session': self.get_filters_from_session(SearchForm, request),
  88. }
  89. if request.GET.get('set_filters'):
  90. methods['session'] = {}
  91. return methods
  92. def get_filtering_method_to_use(self, methods):
  93. for method in ('GET', 'session'):
  94. if methods.get(method):
  95. return methods.get(method)
  96. else:
  97. return {}
  98. def apply_filtering_on_context(self, context, active_filters, SearchForm):
  99. context['active_filters'] = active_filters
  100. context['search_form'] = SearchForm(initial=context['active_filters'])
  101. if context['active_filters']:
  102. context['items'] = context['search_form'].filter_queryset(
  103. active_filters, context['items'])
  104. """
  105. Order list items
  106. """
  107. @property
  108. def ordering_session_key(self):
  109. return 'misago_admin_%s_order_by' % self.root_link
  110. def get_ordering_from_GET(self, request):
  111. sort = request.GET.get('sort')
  112. if request.GET.get('direction') == 'desc':
  113. new_ordering = '-%s' % sort
  114. elif request.GET.get('direction') == 'asc':
  115. new_ordering = sort
  116. else:
  117. new_ordering = '?nope'
  118. return self.clean_ordering(new_ordering)
  119. def get_ordering_from_session(self, request):
  120. new_ordering = request.session.get(self.ordering_session_key)
  121. return self.clean_ordering(new_ordering)
  122. def clean_ordering(self, new_ordering):
  123. for order_by, name in self.ordering:
  124. if order_by == new_ordering:
  125. return order_by
  126. else:
  127. return None
  128. def get_ordering_methods(self, request):
  129. return {
  130. 'GET': self.get_ordering_from_GET(request),
  131. 'session': self.get_ordering_from_session(request),
  132. 'default': self.clean_ordering(self.ordering[0][0]),
  133. }
  134. def get_ordering_method_to_use(self, methods):
  135. for method in ('GET', 'session', 'default'):
  136. if methods.get(method):
  137. return methods.get(method)
  138. def set_ordering_in_context(self, context, method):
  139. for order_by, name in self.ordering:
  140. order_as_dict = {
  141. 'type': 'desc' if order_by[0] == '-' else 'asc',
  142. 'order_by': order_by,
  143. 'name': name,
  144. }
  145. if order_by == method:
  146. context['order'] = order_as_dict
  147. context['items'] = context['items'].order_by(
  148. order_as_dict['order_by'])
  149. elif order_as_dict['name']:
  150. if order_as_dict['type'] == 'desc':
  151. order_as_dict['order_by'] = order_as_dict['order_by'][1:]
  152. context['order_by'].append(order_as_dict)
  153. """
  154. Mass actions
  155. """
  156. def handle_mass_action(self, request, context):
  157. limit = self.items_per_page or 64
  158. action = self.select_mass_action(request.POST.get('action'))
  159. items = [x for x in request.POST.getlist('selected_items')[:limit]]
  160. context['selected_items'] = items
  161. if not context['selected_items']:
  162. raise MassActionError(_("You have to select one or more items."))
  163. action_queryset = context['items'].filter(pk__in=items)
  164. if not action_queryset.exists():
  165. raise MassActionError(_("You have to select one or more items."))
  166. action_callable = getattr(self, 'action_%s' % action)
  167. with transaction.atomic():
  168. return action_callable(request, action_queryset)
  169. def select_mass_action(self, action):
  170. for definition in self.mass_actions:
  171. if definition[0] == action:
  172. return action
  173. else:
  174. raise MassActionError(_("Action is not allowed."))
  175. def mass_actions_as_dicts(self):
  176. dicts = []
  177. for definition in self.mass_actions or []:
  178. dicts.append({
  179. 'action': definition[0],
  180. 'name': definition[1],
  181. 'prompt': definition[2] if len(definition) == 3 else None,
  182. })
  183. return dicts
  184. """
  185. Querystrings builder
  186. """
  187. def make_querystrings(self, context):
  188. values = {}
  189. filter_values = {}
  190. order_values = {}
  191. if context['active_filters']:
  192. filter_values = context['active_filters']
  193. values.update(filter_values)
  194. if context['order_by']:
  195. order_values = {
  196. 'sort': context['order']['order_by'],
  197. 'direction': context['order']['type'],
  198. }
  199. if order_values['sort'][0] == '-':
  200. # We don't start sorting criteria with minus in querystring
  201. order_values['sort'] = order_values['sort'][1:]
  202. values.update(order_values)
  203. if values:
  204. context['querystring'] = '?%s' % urlencode(values)
  205. if order_values:
  206. context['query_order'] = order_values
  207. if filter_values:
  208. context['query_filters'] = filter_values
  209. """
  210. Dispatch response
  211. """
  212. def dispatch(self, request, *args, **kwargs):
  213. mass_actions_list = self.mass_actions_as_dicts()
  214. extra_actions_list = self.extra_actions or []
  215. refresh_querystring = False
  216. context = {
  217. 'items': self.get_queryset(),
  218. 'paginator': None,
  219. 'page': None,
  220. 'order_by': [],
  221. 'order': None,
  222. 'search_form': None,
  223. 'active_filters': {},
  224. 'querystring': '',
  225. 'query_order': {},
  226. 'query_filters': {},
  227. 'selected_items': [],
  228. 'selection_label': self.selection_label,
  229. 'empty_selection_label': self.empty_selection_label,
  230. 'mass_actions': mass_actions_list,
  231. 'extra_actions': extra_actions_list,
  232. 'extra_actions_len': len(extra_actions_list),
  233. }
  234. if request.method == 'POST' and mass_actions_list:
  235. try:
  236. response = self.handle_mass_action(request, context)
  237. if response:
  238. return response
  239. else:
  240. return redirect(request.path)
  241. except MassActionError as e:
  242. messages.error(request, e.args[0])
  243. if self.ordering:
  244. ordering_methods = self.get_ordering_methods(request)
  245. used_method = self.get_ordering_method_to_use(ordering_methods)
  246. self.set_ordering_in_context(context, used_method)
  247. if (ordering_methods['GET'] and
  248. ordering_methods['GET'] != ordering_methods['session']):
  249. # Store GET ordering in session for future requests
  250. session_key = self.ordering_session_key
  251. request.session[session_key] = ordering_methods['GET']
  252. if context['order_by'] and not ordering_methods['GET']:
  253. # Make view redirect to itself with querystring,
  254. # So address ball contains copy-friendly link
  255. refresh_querystring = True
  256. SearchForm = self.get_search_form(request)
  257. if SearchForm:
  258. filtering_methods = self.get_filtering_methods(request)
  259. active_filters = self.get_filtering_method_to_use(filtering_methods)
  260. self.apply_filtering_on_context(context, active_filters, SearchForm)
  261. if (filtering_methods['GET'] and
  262. filtering_methods['GET'] != filtering_methods['session']):
  263. # Store GET filters in session for future requests
  264. session_key = self.filters_session_key
  265. request.session[session_key] = filtering_methods['GET']
  266. if request.GET.get('clear_filters'):
  267. # Clear filters from querystring
  268. request.session.pop(self.filters_session_key, None)
  269. context['active_filters'] = {}
  270. elif request.GET.get('set_filters'):
  271. # Force store filters in session
  272. session_key = self.filters_session_key
  273. request.session[session_key] = context['active_filters']
  274. refresh_querystring = True
  275. if context['active_filters'] and not filtering_methods['GET']:
  276. # Make view redirect to itself with querystring,
  277. # So address ball contains copy-friendly link
  278. refresh_querystring = True
  279. self.make_querystrings(context)
  280. if self.items_per_page:
  281. try:
  282. self.paginate_items(context, kwargs.get('page', 0))
  283. except EmptyPage:
  284. return redirect(
  285. '%s%s' % (reverse(self.root_link), context['querystring']))
  286. if refresh_querystring:
  287. return redirect('%s%s' % (request.path, context['querystring']))
  288. return self.render(request, context)