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