list.py 12 KB

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