from urllib.parse import urlencode from django.contrib import messages from django.core.paginator import EmptyPage, Paginator from django.db import transaction from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import gettext_lazy as _ from ....core.exceptions import ExplicitFirstPage from .base import AdminView class MassActionError(Exception): pass class ListView(AdminView): """ Admin items list view Uses following attributes: template = template name used to render items list items_per_page = number of items displayed on single page (enter 0 or don't define for no pagination) ordering = tuple of tuples defining allowed orderings typles should follow this format: (name, order_by) """ template = "list.html" items_per_page = 0 ordering = None extra_actions = None mass_actions = None selection_label = _("Selected: 0") empty_selection_label = _("Select items") @classmethod def add_mass_action(cls, action, name, icon, confirmation=None): if not cls.mass_actions: cls.mass_actions = [] cls.extra_actions.append( {"action": action, "name": name, "icon": icon, "confirmation": confirmation} ) @classmethod def add_item_action(cls, name, icon, link, style=None): if not cls.extra_actions: cls.extra_actions = [] cls.extra_actions.append( {"name": name, "icon": icon, "link": link, "style": style} ) def get_queryset(self): return self.get_model().objects.all() def dispatch( self, request, *args, **kwargs ): # pylint: disable=too-many-branches, too-many-locals mass_actions_list = self.mass_actions or [] extra_actions_list = self.extra_actions or [] refresh_querystring = False context = { "items": self.get_queryset(), "paginator": None, "page": None, "order_by": [], "order": None, "search_form": None, "active_filters": {}, "querystring": "", "query_order": {}, "query_filters": {}, "selected_items": [], "selection_label": self.selection_label, "empty_selection_label": self.empty_selection_label, "mass_actions": mass_actions_list, "extra_actions": extra_actions_list, "extra_actions_len": len(extra_actions_list), } if request.method == "POST" and mass_actions_list: try: response = self.handle_mass_action(request, context) if response: return response return redirect(request.path) except MassActionError as e: messages.error(request, e.args[0]) if self.ordering: ordering_methods = self.get_ordering_methods(request) used_method = self.get_ordering_method_to_use(ordering_methods) self.set_ordering_in_context(context, used_method) if ( ordering_methods["GET"] and ordering_methods["GET"] != ordering_methods["session"] ): # Store GET ordering in session for future requests session_key = self.ordering_session_key request.session[session_key] = ordering_methods["GET"] if context["order_by"] and not ordering_methods["GET"]: # Make view redirect to itself with querystring, # So address ball contains copy-friendly link refresh_querystring = True search_form = self.get_search_form(request) if search_form: filtering_methods = self.get_filtering_methods(request, search_form) active_filters = self.get_filtering_method_to_use(filtering_methods) if request.GET.get("clear_filters"): # Clear filters from querystring request.session.pop(self.filters_session_key, None) active_filters = {} self.apply_filtering_on_context(context, active_filters, search_form) if ( filtering_methods["GET"] and filtering_methods["GET"] != filtering_methods["session"] ): # Store GET filters in session for future requests session_key = self.filters_session_key request.session[session_key] = filtering_methods["GET"] if request.GET.get("set_filters"): # Force store filters in session session_key = self.filters_session_key request.session[session_key] = context["active_filters"] refresh_querystring = True if context["active_filters"] and not filtering_methods["GET"]: # Make view redirect to itself with querystring, # so address bar contains copy-friendly link refresh_querystring = True self.make_querystring(context) if self.items_per_page: try: self.paginate_items(context, kwargs.get("page", 0)) except EmptyPage: return redirect( "%s%s" % (reverse(self.root_link), context["querystring"]) ) if refresh_querystring and "redirected" not in request.GET: return redirect("%s%s" % (request.path, context["querystring"])) return self.render(request, context) def paginate_items(self, context, page): try: page = int(page) if page == 1: raise ExplicitFirstPage() elif page == 0: page = 1 except ValueError: page = 1 context["paginator"] = Paginator( context["items"], self.items_per_page, allow_empty_first_page=True ) context["page"] = context["paginator"].page(page) context["items"] = context["page"].object_list # Filter list items search_form = None def get_search_form(self, request): return self.search_form @property def filters_session_key(self): return "misago_admin_%s_filters" % self.root_link def get_filtering_methods(self, request, search_form): methods = { "GET": self.get_filters_from_GET(request, search_form), "session": self.get_filters_from_session(request, search_form), } if request.GET.get("set_filters"): methods["session"] = {} return methods def get_filters_from_GET(self, request, search_form): form = search_form(request.GET) form.is_valid() return self.clean_filtering_data(form.cleaned_data) def get_filters_from_session(self, request, search_form): session_filters = request.session.get(self.filters_session_key, {}) form = search_form(session_filters) form.is_valid() return self.clean_filtering_data(form.cleaned_data) def clean_filtering_data(self, data): for key, value in list(data.items()): if not value: del data[key] return data def get_filtering_method_to_use(self, methods): for method in ("GET", "session"): if methods.get(method): return methods.get(method) def apply_filtering_on_context(self, context, active_filters, search_form): context["active_filters"] = active_filters context["search_form"] = search_form(initial=context["active_filters"]) if context["active_filters"]: context["items"] = context["search_form"].filter_queryset( active_filters, context["items"] ) # Order list items @property def ordering_session_key(self): return "misago_admin_%s_order_by" % self.root_link def get_ordering_from_GET(self, request): sort = request.GET.get("sort") if request.GET.get("direction") == "desc": new_ordering = "-%s" % sort elif request.GET.get("direction") == "asc": new_ordering = sort else: new_ordering = "?nope" return self.clean_ordering(new_ordering) def get_ordering_from_session(self, request): new_ordering = request.session.get(self.ordering_session_key) return self.clean_ordering(new_ordering) def clean_ordering(self, new_ordering): for order_by, _ in self.ordering: # pylint: disable=not-an-iterable if order_by == new_ordering: return order_by def get_ordering_methods(self, request): return { "GET": self.get_ordering_from_GET(request), "session": self.get_ordering_from_session(request), "default": self.clean_ordering(self.ordering[0][0]), } def get_ordering_method_to_use(self, methods): for method in ("GET", "session", "default"): if methods.get(method): return methods.get(method) def set_ordering_in_context(self, context, method): for order_by, name in self.ordering: # pylint: disable=not-an-iterable order_as_dict = { "type": "desc" if order_by[0] == "-" else "asc", "order_by": order_by, "name": name, } if order_by == method: context["order"] = order_as_dict context["items"] = context["items"].order_by(order_as_dict["order_by"]) elif order_as_dict["name"]: if order_as_dict["type"] == "desc": order_as_dict["order_by"] = order_as_dict["order_by"][1:] context["order_by"].append(order_as_dict) # Mass actions def handle_mass_action(self, request, context): limit = self.items_per_page or 64 action = self.select_mass_action(request.POST.get("action")) items = [x for x in request.POST.getlist("selected_items")[:limit]] context["selected_items"] = items if not context["selected_items"]: raise MassActionError(_("You have to select one or more items.")) action_queryset = context["items"].filter(pk__in=items) if not action_queryset.exists(): raise MassActionError(_("You have to select one or more items.")) action_callable = getattr(self, "action_%s" % action["action"]) if action.get("is_atomic", True): with transaction.atomic(): return action_callable(request, action_queryset) else: return action_callable(request, action_queryset) def select_mass_action(self, action): for definition in self.mass_actions: # pylint: disable=not-an-iterable if definition["action"] == action: return definition raise MassActionError(_("Action is not allowed.")) # Querystring builder def make_querystring(self, context): values = {} filter_values = {} order_values = {} if context["active_filters"]: filter_values = context["active_filters"] values.update(filter_values) if context["order_by"]: order_values = { "sort": context["order"]["order_by"], "direction": context["order"]["type"], } if order_values["sort"][0] == "-": # We don't start sorting criteria with minus in querystring order_values["sort"] = order_values["sort"][1:] values.update(order_values) if values: values["redirected"] = 1 context["querystring"] = "?%s" % urlencode(values, "utf-8") if order_values: context["query_order"] = order_values if filter_values: context["query_filters"] = filter_values