1import datetime 2 3from collections import OrderedDict 4 5from django.core.exceptions import PermissionDenied 6from django.core.paginator import InvalidPage 7from django.shortcuts import get_object_or_404, redirect 8from django.utils.translation import ngettext 9from django.views.generic import ListView, TemplateView 10 11from wagtail.admin import messages 12from wagtail.admin.views.mixins import SpreadsheetExportMixin 13from wagtail.contrib.forms.forms import SelectDateForm 14from wagtail.contrib.forms.utils import get_forms_for_user 15from wagtail.core.models import Page 16 17 18def 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 24 25class SafePaginateListView(ListView): 26 """ Listing view with safe pagination, allowing incorrect or out of range values """ 27 28 paginate_by = 20 29 page_kwarg = 'p' 30 31 def paginate_queryset(self, queryset, page_size): 32 """Paginate the queryset if needed with nice defaults on invalid param.""" 33 paginator = self.get_paginator( 34 queryset, 35 page_size, 36 orphans=self.get_paginate_orphans(), 37 allow_empty_first_page=self.get_allow_empty() 38 ) 39 page_kwarg = self.page_kwarg 40 page_request = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 0 41 try: 42 page_number = int(page_request) 43 except ValueError: 44 if page_request == 'last': 45 page_number = paginator.num_pages 46 else: 47 page_number = 0 48 try: 49 if page_number > paginator.num_pages: 50 page_number = paginator.num_pages # page out of range, show last page 51 page = paginator.page(page_number) 52 return (paginator, page, page.object_list, page.has_other_pages()) 53 except InvalidPage: 54 page = paginator.page(1) 55 return (paginator, page, page.object_list, page.has_other_pages()) 56 return super().paginage_queryset(queryset, page_size) 57 58 59class FormPagesListView(SafePaginateListView): 60 """ Lists the available form pages for the current user """ 61 template_name = 'wagtailforms/index.html' 62 context_object_name = 'form_pages' 63 64 def get_queryset(self): 65 """ Return the queryset of form pages for this view """ 66 queryset = get_forms_for_user(self.request.user) 67 ordering = self.get_ordering() 68 if ordering: 69 if isinstance(ordering, str): 70 ordering = (ordering,) 71 queryset = queryset.order_by(*ordering) 72 return queryset 73 74 75class DeleteSubmissionsView(TemplateView): 76 """ Delete the selected submissions """ 77 template_name = 'wagtailforms/confirm_delete.html' 78 page = None 79 submissions = None 80 success_url = 'wagtailforms:list_submissions' 81 82 def get_queryset(self): 83 """ Returns a queryset for the selected submissions """ 84 submission_ids = self.request.GET.getlist('selected-submissions') 85 submission_class = self.page.get_submission_class() 86 return submission_class._default_manager.filter(id__in=submission_ids) 87 88 def handle_delete(self, submissions): 89 """ Deletes the given queryset """ 90 count = submissions.count() 91 submissions.delete() 92 messages.success( 93 self.request, 94 ngettext( 95 'One submission has been deleted.', 96 '%(count)d submissions have been deleted.', 97 count 98 ) % {'count': count} 99 ) 100 101 def get_success_url(self): 102 """ Returns the success URL to redirect to after a successful deletion """ 103 return self.success_url 104 105 def dispatch(self, request, *args, **kwargs): 106 """ Check permissions, set the page and submissions, handle delete """ 107 page_id = kwargs.get('page_id') 108 109 if not get_forms_for_user(self.request.user).filter(id=page_id).exists(): 110 raise PermissionDenied 111 112 self.page = get_object_or_404(Page, id=page_id).specific 113 114 self.submissions = self.get_queryset() 115 116 if self.request.method == 'POST': 117 self.handle_delete(self.submissions) 118 return redirect(self.get_success_url(), page_id) 119 120 return super().dispatch(request, *args, **kwargs) 121 122 def get_context_data(self, **kwargs): 123 """ Get the context for this view """ 124 context = super().get_context_data(**kwargs) 125 126 context.update({ 127 'page': self.page, 128 'submissions': self.submissions, 129 }) 130 131 return context 132 133 134class SubmissionsListView(SpreadsheetExportMixin, SafePaginateListView): 135 """ Lists submissions for the provided form page """ 136 template_name = 'wagtailforms/index_submissions.html' 137 context_object_name = 'submissions' 138 form_page = None 139 ordering = ('-submit_time',) 140 ordering_csv = ('submit_time',) # keep legacy CSV ordering 141 orderable_fields = ('id', 'submit_time',) # used to validate ordering in URL 142 select_date_form = None 143 144 def dispatch(self, request, *args, **kwargs): 145 """ Check permissions and set the form page """ 146 147 self.form_page = kwargs.get('form_page') 148 149 if not get_forms_for_user(request.user).filter(pk=self.form_page.id).exists(): 150 raise PermissionDenied 151 152 self.is_export = (self.request.GET.get('export') in self.FORMATS) 153 if self.is_export: 154 self.paginate_by = None 155 data_fields = self.form_page.get_data_fields() 156 # Set the export fields and the headings for spreadsheet export 157 self.list_export = [field for field, label in data_fields] 158 self.export_headings = dict(data_fields) 159 160 return super().dispatch(request, *args, **kwargs) 161 162 def get_queryset(self): 163 """ Return queryset of form submissions with filter and order_by applied """ 164 submission_class = self.form_page.get_submission_class() 165 queryset = submission_class._default_manager.filter(page=self.form_page) 166 167 filtering = self.get_filtering() 168 if filtering and isinstance(filtering, dict): 169 queryset = queryset.filter(**filtering) 170 171 ordering = self.get_ordering() 172 if ordering: 173 if isinstance(ordering, str): 174 ordering = (ordering,) 175 queryset = queryset.order_by(*ordering) 176 177 return queryset 178 179 def get_paginate_by(self, queryset): 180 """ Get the number of items to paginate by, or ``None`` for no pagination """ 181 if self.is_export: 182 return None 183 return self.paginate_by 184 185 def get_validated_ordering(self): 186 """ Return a dict of field names with ordering labels if ordering is valid """ 187 orderable_fields = self.orderable_fields or () 188 ordering = dict() 189 if self.is_export: 190 # Revert to CSV order_by submit_time ascending for backwards compatibility 191 default_ordering = self.ordering_csv or () 192 else: 193 default_ordering = self.ordering or () 194 if isinstance(default_ordering, str): 195 default_ordering = (default_ordering,) 196 ordering_strs = self.request.GET.getlist('order_by') or list(default_ordering) 197 for order in ordering_strs: 198 try: 199 _, prefix, field_name = order.rpartition('-') 200 if field_name in orderable_fields: 201 ordering[field_name] = ( 202 prefix, 'descending' if prefix == '-' else 'ascending' 203 ) 204 except (IndexError, ValueError): 205 continue # invalid ordering specified, skip it 206 return ordering 207 208 def get_ordering(self): 209 """ Return the field or fields to use for ordering the queryset """ 210 ordering = self.get_validated_ordering() 211 return [values[0] + name for name, values in ordering.items()] 212 213 def get_filtering(self): 214 """ Return filering as a dict for submissions queryset """ 215 self.select_date_form = SelectDateForm(self.request.GET) 216 result = dict() 217 if self.select_date_form.is_valid(): 218 date_from = self.select_date_form.cleaned_data.get('date_from') 219 date_to = self.select_date_form.cleaned_data.get('date_to') 220 if date_to: 221 # careful: date_to must be increased by 1 day 222 # as submit_time is a time so will always be greater 223 date_to += datetime.timedelta(days=1) 224 if date_from: 225 result['submit_time__range'] = [date_from, date_to] 226 else: 227 result['submit_time__lte'] = date_to 228 elif date_from: 229 result['submit_time__gte'] = date_from 230 return result 231 232 def get_filename(self): 233 """ Returns the base filename for the generated spreadsheet data file """ 234 return '{}-export-{}'.format( 235 self.form_page.slug, 236 datetime.datetime.today().strftime('%Y-%m-%d') 237 ) 238 239 def render_to_response(self, context, **response_kwargs): 240 if self.is_export: 241 return self.as_spreadsheet(context['submissions'], self.request.GET.get('export')) 242 return super().render_to_response(context, **response_kwargs) 243 244 def to_row_dict(self, item): 245 """ Orders the submission dictionary for spreadsheet writing """ 246 row_dict = OrderedDict((field, item.get_data().get(field)) for field in self.list_export) 247 return row_dict 248 249 def get_context_data(self, **kwargs): 250 """ Return context for view """ 251 context = super().get_context_data(**kwargs) 252 submissions = context[self.context_object_name] 253 data_fields = self.form_page.get_data_fields() 254 data_rows = [] 255 context['submissions'] = submissions 256 if not self.is_export: 257 # Build data_rows as list of dicts containing model_id and fields 258 for submission in submissions: 259 form_data = submission.get_data() 260 data_row = [] 261 for name, label in data_fields: 262 val = form_data.get(name) 263 if isinstance(val, list): 264 val = ', '.join(val) 265 data_row.append(val) 266 data_rows.append({ 267 'model_id': submission.id, 268 'fields': data_row 269 }) 270 # Build data_headings as list of dicts containing model_id and fields 271 ordering_by_field = self.get_validated_ordering() 272 orderable_fields = self.orderable_fields 273 data_headings = [] 274 for name, label in data_fields: 275 order_label = None 276 if name in orderable_fields: 277 order = ordering_by_field.get(name) 278 if order: 279 order_label = order[1] # 'ascending' or 'descending' 280 else: 281 order_label = 'orderable' # not ordered yet but can be 282 data_headings.append({ 283 'name': name, 284 'label': label, 285 'order': order_label, 286 }) 287 288 context.update({ 289 'form_page': self.form_page, 290 'select_date_form': self.select_date_form, 291 'data_headings': data_headings, 292 'data_rows': data_rows, 293 }) 294 295 return context 296