views.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import datetime
  2. from collections import OrderedDict
  3. from django.contrib.admin.utils import quote
  4. from django.core.exceptions import PermissionDenied
  5. from django.shortcuts import get_object_or_404, redirect
  6. from django.urls import reverse
  7. from django.utils.translation import gettext, gettext_lazy, ngettext
  8. from django.views.generic import TemplateView
  9. from django_filters import DateFromToRangeFilter
  10. from wagtail.admin import messages
  11. from wagtail.admin.filters import DateRangePickerWidget, WagtailFilterSet
  12. from wagtail.admin.ui.tables import Column, TitleColumn
  13. from wagtail.admin.views import generic
  14. from wagtail.admin.views.generic.base import BaseListingView
  15. from wagtail.admin.views.mixins import SpreadsheetExportMixin
  16. from wagtail.contrib.forms.utils import get_forms_for_user
  17. from wagtail.models import Page
  18. def get_submissions_list_view(request, *args, **kwargs):
  19. """Call the form page's list submissions view class"""
  20. page_id = kwargs.get("page_id")
  21. form_page = get_object_or_404(Page, id=page_id).specific
  22. return form_page.serve_submissions_list_view(request, *args, **kwargs)
  23. class ContentTypeColumn(Column):
  24. edit_url_name = "wagtailadmin_pages:edit"
  25. cell_template_name = "wagtailforms/content_type_column.html"
  26. def get_url(self, instance):
  27. return reverse(self.edit_url_name, args=(quote(instance.pk),))
  28. def get_cell_context_data(self, instance, parent_context):
  29. context = super().get_cell_context_data(instance, parent_context)
  30. context["url"] = self.get_url(instance)
  31. return context
  32. class FormPagesListView(generic.IndexView):
  33. """Lists the available form pages for the current user"""
  34. template_name = "wagtailforms/index.html"
  35. results_template_name = "wagtailforms/index_results.html"
  36. context_object_name = "form_pages"
  37. paginate_by = 20
  38. page_kwarg = "p"
  39. index_url_name = "wagtailforms:index"
  40. index_results_url_name = "wagtailforms:index_results"
  41. page_title = gettext_lazy("Forms")
  42. header_icon = "form"
  43. _show_breadcrumbs = True
  44. columns = [
  45. TitleColumn(
  46. "title",
  47. classname="title",
  48. label=gettext_lazy("Title"),
  49. width="50%",
  50. url_name="wagtailforms:list_submissions",
  51. sort_key="title",
  52. ),
  53. ContentTypeColumn(
  54. "content_type",
  55. label=gettext_lazy("Origin"),
  56. width="50%",
  57. sort_key="content_type",
  58. ),
  59. ]
  60. model = Page
  61. is_searchable = False
  62. def get_breadcrumbs_items(self):
  63. return self.breadcrumbs_items + [
  64. {"url": "", "label": self.page_title, "sublabel": gettext("Pages")},
  65. ]
  66. def get_base_queryset(self):
  67. """Return the queryset of form pages for this view"""
  68. return get_forms_for_user(self.request.user).select_related("content_type")
  69. class DeleteSubmissionsView(TemplateView):
  70. """Delete the selected submissions"""
  71. template_name = "wagtailforms/confirm_delete.html"
  72. page = None
  73. submissions = None
  74. success_url = "wagtailforms:list_submissions"
  75. def get_queryset(self):
  76. """Returns a queryset for the selected submissions"""
  77. submission_ids = self.request.GET.getlist("selected-submissions")
  78. submission_class = self.page.get_submission_class()
  79. return submission_class._default_manager.filter(id__in=submission_ids)
  80. def handle_delete(self, submissions):
  81. """Deletes the given queryset"""
  82. count = submissions.count()
  83. submissions.delete()
  84. messages.success(
  85. self.request,
  86. ngettext(
  87. "One submission has been deleted.",
  88. "%(count)d submissions have been deleted.",
  89. count,
  90. )
  91. % {"count": count},
  92. )
  93. def get_success_url(self):
  94. """Returns the success URL to redirect to after a successful deletion"""
  95. return self.success_url
  96. def dispatch(self, request, *args, **kwargs):
  97. """Check permissions, set the page and submissions, handle delete"""
  98. page_id = kwargs.get("page_id")
  99. if not get_forms_for_user(self.request.user).filter(id=page_id).exists():
  100. raise PermissionDenied
  101. self.page = get_object_or_404(Page, id=page_id).specific
  102. self.submissions = self.get_queryset()
  103. if self.request.method == "POST":
  104. self.handle_delete(self.submissions)
  105. return redirect(self.get_success_url(), page_id)
  106. return super().dispatch(request, *args, **kwargs)
  107. def get_context_data(self, **kwargs):
  108. """Get the context for this view"""
  109. context = super().get_context_data(**kwargs)
  110. context.update(
  111. {
  112. "page": self.page,
  113. "submissions": self.submissions,
  114. }
  115. )
  116. return context
  117. class SubmissionsListFilterSet(WagtailFilterSet):
  118. date = DateFromToRangeFilter(
  119. label=gettext_lazy("Submission date"),
  120. field_name="submit_time",
  121. widget=DateRangePickerWidget,
  122. )
  123. class SubmissionsListView(SpreadsheetExportMixin, BaseListingView):
  124. """Lists submissions for the provided form page"""
  125. template_name = "wagtailforms/submissions_index.html"
  126. context_object_name = "submissions"
  127. form_page = None
  128. default_ordering = ("-submit_time",)
  129. ordering_csv = ("submit_time",) # keep legacy CSV ordering
  130. orderable_fields = (
  131. "id",
  132. "submit_time",
  133. ) # used to validate ordering in URL
  134. page_title = gettext_lazy("Form data")
  135. paginate_by = 20
  136. filterset_class = SubmissionsListFilterSet
  137. def dispatch(self, request, *args, **kwargs):
  138. """Check permissions and set the form page"""
  139. self.form_page = kwargs.get("form_page")
  140. if not get_forms_for_user(request.user).filter(pk=self.form_page.id).exists():
  141. raise PermissionDenied
  142. if self.is_export:
  143. data_fields = self.form_page.get_data_fields()
  144. # Set the export fields and the headings for spreadsheet export
  145. self.list_export = [field for field, label in data_fields]
  146. self.export_headings = dict(data_fields)
  147. return super().dispatch(request, *args, **kwargs)
  148. def get_filterset_kwargs(self):
  149. kwargs = super().get_filterset_kwargs()
  150. kwargs["queryset"] = self.get_base_queryset()
  151. return kwargs
  152. def get_base_queryset(self):
  153. """Return queryset of form submissions"""
  154. submission_class = self.form_page.get_submission_class()
  155. queryset = submission_class._default_manager.filter(page=self.form_page)
  156. return queryset
  157. def get_validated_ordering(self):
  158. """Return a dict of field names with ordering labels if ordering is valid"""
  159. orderable_fields = self.orderable_fields or ()
  160. ordering = {}
  161. if self.is_export:
  162. # Revert to CSV order_by submit_time ascending for backwards compatibility
  163. default_ordering = self.ordering_csv or ()
  164. else:
  165. default_ordering = self.default_ordering or ()
  166. if isinstance(default_ordering, str):
  167. default_ordering = (default_ordering,)
  168. ordering_strs = self.request.GET.getlist("order_by") or list(default_ordering)
  169. for order in ordering_strs:
  170. try:
  171. _, prefix, field_name = order.rpartition("-")
  172. if field_name in orderable_fields:
  173. ordering[field_name] = (
  174. prefix,
  175. "descending" if prefix == "-" else "ascending",
  176. )
  177. except (IndexError, ValueError):
  178. continue # invalid ordering specified, skip it
  179. return ordering
  180. def get_ordering(self):
  181. """Return the field or fields to use for ordering the queryset"""
  182. ordering = self.get_validated_ordering()
  183. return [values[0] + name for name, values in ordering.items()]
  184. def get_filename(self):
  185. """Returns the base filename for the generated spreadsheet data file"""
  186. return "{}-export-{}".format(
  187. self.form_page.slug, datetime.datetime.today().strftime("%Y-%m-%d")
  188. )
  189. def render_to_response(self, context, **response_kwargs):
  190. if self.is_export:
  191. return self.as_spreadsheet(
  192. context["submissions"], self.request.GET.get("export")
  193. )
  194. return super().render_to_response(context, **response_kwargs)
  195. def to_row_dict(self, item):
  196. """Orders the submission dictionary for spreadsheet writing"""
  197. row_dict = OrderedDict(
  198. (field, item.get_data().get(field)) for field in self.list_export
  199. )
  200. return row_dict
  201. def get_context_data(self, **kwargs):
  202. """Return context for view"""
  203. context = super().get_context_data(**kwargs)
  204. submissions = context[self.context_object_name]
  205. data_fields = self.form_page.get_data_fields()
  206. data_rows = []
  207. context["submissions"] = submissions
  208. if not self.is_export:
  209. # Build data_rows as list of dicts containing model_id and fields
  210. for submission in submissions:
  211. form_data = submission.get_data()
  212. data_row = []
  213. for name, label in data_fields:
  214. val = form_data.get(name)
  215. if isinstance(val, list):
  216. val = ", ".join(val)
  217. data_row.append(val)
  218. data_rows.append({"model_id": submission.id, "fields": data_row})
  219. # Build data_headings as list of dicts containing model_id and fields
  220. ordering_by_field = self.get_validated_ordering()
  221. orderable_fields = self.orderable_fields
  222. data_headings = []
  223. for name, label in data_fields:
  224. order_label = None
  225. if name in orderable_fields:
  226. order = ordering_by_field.get(name)
  227. if order:
  228. order_label = order[1] # 'ascending' or 'descending'
  229. else:
  230. order_label = "orderable" # not ordered yet but can be
  231. data_headings.append(
  232. {
  233. "name": name,
  234. "label": label,
  235. "order": order_label,
  236. }
  237. )
  238. context.update(
  239. {
  240. "form_page": self.form_page,
  241. "data_headings": data_headings,
  242. "data_rows": data_rows,
  243. }
  244. )
  245. return context