Repository: wagtail/wagtail

Issue:
Form Submission View needs reordering, or newest first default order.
### Issue Summary

The current form submission page makes it quite frustrating to actually see your newest submissions. This is because it is ordered oldest first. That ordering makes no real sense as the default, or at least doesn't unless there is a way to override this.

You can filter by a time range, but that isn't really that useful because the returned order is still oldest first.

### Steps to Reproduce

1. Setup form page as per this doc: http://docs.wagtail.io/en/v1.11.1/reference/contrib/forms/index.html
2. Make submissions a few minutes apart
3. go to the form submissions page for your form

The order will be oldest first. If you keep adding submissions eventually the pagination will kick in and move the newest entries onto a second page.

### Possible Solutions

At the very least, adding support for reordering would be good, or a global config setting that overrides the existing one here: https://github.com/wagtail/wagtail/blob/master/wagtail/wagtailforms/views.py#L71

Making newest first the default would be best, but may cause confusion for any users used to the current sorting.


Discussion:
@gasman I'm more than happy to do the work on this based on your comments in that pull request.

I can quite likely start looking at making newest first the default while ensuring the csv export does the same thing. That said, reversing a csv isn't exactly too hard either, but we should maintain that export since chances are someone is likely exporting and and then importing that data automatically somewhere, and this could break things for them. The gui change at least won't break things, just confuse someone briefly.

No clue where to start on making the submission list sortable with the gui so some direction there would be great. 

I would love to start properly digging into the Wagtail codebase so this could be a small enough change to get me started. :)
Hey, I second that this is an issue (just ran into this myself). I am happy to pitch in to help out, let me know if you want me to look into something here @Adrian-Turjak 
@Adrian-Turjak @gasman 

**What if the ordering setting was defined by a field on the Form Page?**

* Add a field to the `AbstractForm` Model
* Something like `order_by_recent_first` with this appearing in the Settings tab (or just not visible at all by default)
* DB Migrations for all existing Form Pages set to False
* Defaults to True for all new Pages created, but with the user's ability to override on a form by form basis
* wagtail/wagtailforms/views.py updated to check for the Boolean on the Form Page's specific instance

Downside is that this adds complexity to the UI.

Upsides is that it solves the problem of existing expectations, sets the better UX default for future pages and the setting can be all but hidden for anyone except if they want to make it available or override the default for some reason.

**Migrations to Implement**

Setting up the migration file would be complicated and I am not sure if what this plan entails is possible as we do not necessarily know the models that inherit `AbstractEmailForm` or `AbstractForm`.

* Migration File B - Add the field to `AbstractForm`, with default to `False` (updating all existing Pages). As no forms would have this field, they will all set to `False`.
* Migration File B - Update the default to `True`, which I assume will not update any models already existing as this is not a field creation any more.


@Adrian-Turjak 

**Regarding the UI to make the Table Sortable**

`contrib/modeladmin` contains a good reference for how to implement sortable headers, there is no JS involved by the looks of it. 

[Template that build the list](https://github.com/wagtail/wagtail/blob/master/wagtail/contrib/modeladmin/templates/modeladmin/includes/result_list.html)

The Results List is ordered based on the URL Parameters and built in this [Template tag](https://github.com/wagtail/wagtail/blob/master/wagtail/contrib/modeladmin/templatetags/modeladmin_tags.py#L87)

There is some [minor SCSS](https://github.com/wagtail/wagtail/blob/master/wagtail/wagtailadmin/static_src/wagtailadmin/scss/components/_listing.scss#L313) that might need to be added for the arrows.

Hope that helps.


As I said on https://github.com/wagtail/wagtail/pull/3762#issuecomment-322433203, the best UX design is UX that the user doesn't have to think about - it should just be _there_. The ordering of this index page is a detail that an end user of Wagtail should not have to care about: if they're prompted to make a decision about it, either in the admin or the config file, then we've already failed...

Let's set it to newest-first by default (when viewing as an HTML page, not CSV) - since that's clearly the more friendly option - and let users reverse the order by clicking the heading. It doesn't need any more configuration than that.
@gasman sweet, ignore my previous comment.
@lb- Do you want to make a pull request to then make the list sortable by submission date, and I'll do another to set the newest first as the default sorting (while making the csv export stay oldest first)?
@Adrian-Turjak sounds good - not sure if I will get to mine this week but will see how we go 👍 

Resolving patch:
diff --git a/wagtail/wagtailforms/models.py b/wagtail/wagtailforms/models.py
--- a/wagtail/wagtailforms/models.py
+++ b/wagtail/wagtailforms/models.py
@@ -211,6 +211,33 @@ def get_form(self, *args, **kwargs):
     def get_landing_page_template(self, request, *args, **kwargs):
         return self.landing_page_template
 
+    def get_field_ordering(self, ordering_list, default=('submit_time', 'descending')):
+        """
+            Accepts a list of strings ['-submit_time', 'id']
+            Returns a list of tuples [(field_name, 'ascending'/'descending'), ...]
+            Intented to be used to process ordering via request.GET.getlist('order_by')
+            Checks if the field options are valid, only returns valid, de-duplicated options
+            invalid options are simply ignored - no error created
+        """
+        valid_fields = ['id', 'submit_time']
+        field_ordering = []
+        if len(ordering_list) == 0:
+            return [default]
+        for ordering in ordering_list:
+            try:
+                none, prefix, field_name = ordering.rpartition('-')
+                if field_name not in valid_fields:
+                    continue  # Invalid field_name, skip it
+                # only add to ordering if the field is not already set
+                if field_name not in [order[0] for order in field_ordering]:
+                    asc_desc = 'ascending'
+                    if prefix == '-':
+                        asc_desc = 'descending'
+                    field_ordering.append((field_name, asc_desc))
+            except (IndexError, ValueError):
+                continue  # Invalid ordering specified, skip it
+        return field_ordering
+
     def get_submission_class(self):
         """
         Returns submission class.
diff --git a/wagtail/wagtailforms/views.py b/wagtail/wagtailforms/views.py
--- a/wagtail/wagtailforms/views.py
+++ b/wagtail/wagtailforms/views.py
@@ -68,7 +68,30 @@ def list_submissions(request, page_id):
 
     data_fields = form_page.get_data_fields()
 
-    submissions = form_submission_class.objects.filter(page=form_page).order_by('submit_time')
+    ordering = form_page.get_field_ordering(request.GET.getlist('order_by'))
+
+    # convert ordering tuples to a list of strings like ['-submit_time']
+    ordering_strings = [
+        '%s%s' % ('-' if order_str[1] == 'descending' else '', order_str[0])
+        for order_str in ordering]
+
+    if request.GET.get('action') == 'CSV':
+        #  Revert to CSV being sorted submit_time ascending for backwards compatibility
+        submissions = form_submission_class.objects.filter(page=form_page).order_by('submit_time')
+    else:
+        submissions = form_submission_class.objects.filter(page=form_page).order_by(*ordering_strings)
+
+    data_fields_with_ordering = []
+    for name, label in data_fields:
+        order = None
+        for order_value in [o[1] for o in ordering if o[0] == name]:
+            order = order_value
+        data_fields_with_ordering.append({
+            "name": name,
+            "label": label,
+            "order": order,
+        })
+
     data_headings = [label for name, label in data_fields]
 
     select_date_form = SelectDateForm(request.GET)
@@ -127,6 +150,6 @@ def list_submissions(request, page_id):
         'form_page': form_page,
         'select_date_form': select_date_form,
         'submissions': submissions,
-        'data_headings': data_headings,
+        'data_fields_with_ordering': data_fields_with_ordering,
         'data_rows': data_rows
     })


Test patch:
diff --git a/wagtail/wagtailforms/tests/test_views.py b/wagtail/wagtailforms/tests/test_views.py
--- a/wagtail/wagtailforms/tests/test_views.py
+++ b/wagtail/wagtailforms/tests/test_views.py
@@ -300,6 +300,33 @@ def test_list_submissions_pagination_out_of_range(self):
         # Check that we got the last page
         self.assertEqual(response.context['submissions'].number, response.context['submissions'].paginator.num_pages)
 
+    def test_list_submissions_default_order(self):
+        response = self.client.get(reverse(
+            'wagtailforms:list_submissions', args=(self.form_page.id,)))
+        # check default ordering, most recent responses first
+        first_row_values = response.context['data_rows'][0]['fields']
+        self.assertTrue('this is a fairly new message' in first_row_values)
+
+    def test_list_submissions_url_params_ordering_recent_first(self):
+        response = self.client.get(reverse(
+            'wagtailforms:list_submissions',
+            args=(self.form_page.id,)),
+            {'order_by': '-submit_time'}
+        )
+        # check ordering matches '-submit_time' (most recent first)
+        first_row_values = response.context['data_rows'][0]['fields']
+        self.assertTrue('this is a fairly new message' in first_row_values)
+
+    def test_list_submissions_url_params_ordering_oldest_first(self):
+        response = self.client.get(reverse(
+            'wagtailforms:list_submissions',
+            args=(self.form_page.id,)),
+            {'order_by': 'submit_time'}
+        )
+        # check ordering matches 'submit_time' (oldest first)
+        first_row_values = response.context['data_rows'][0]['fields']
+        self.assertTrue('this is a really old message' in first_row_values)
+
 
 class TestFormsSubmissionsExport(TestCase, WagtailTestUtils):
     def setUp(self):
@@ -673,7 +700,7 @@ def test_list_submissions(self):
         self.assertEqual(len(response.context['data_rows']), 2)
 
         # CustomFormPageSubmission have custom field. This field should appear in the listing
-        self.assertContains(response, '<th>Username</th>', html=True)
+        self.assertContains(response, '<th id="username" class="">Username</th>', html=True)
         self.assertContains(response, '<td>user-m1kola</td>', html=True)
         self.assertContains(response, '<td>user-john</td>', html=True)
 
@@ -688,7 +715,7 @@ def test_list_submissions_filtering_date_from(self):
         self.assertEqual(len(response.context['data_rows']), 1)
 
         # CustomFormPageSubmission have custom field. This field should appear in the listing
-        self.assertContains(response, '<th>Username</th>', html=True)
+        self.assertContains(response, '<th id="username" class="">Username</th>', html=True)
         self.assertContains(response, '<td>user-m1kola</td>', html=True)
 
     def test_list_submissions_filtering_date_to(self):
@@ -702,7 +729,7 @@ def test_list_submissions_filtering_date_to(self):
         self.assertEqual(len(response.context['data_rows']), 1)
 
         # CustomFormPageSubmission have custom field. This field should appear in the listing
-        self.assertContains(response, '<th>Username</th>', html=True)
+        self.assertContains(response, '<th id="username" class="">Username</th>', html=True)
         self.assertContains(response, '<td>user-john</td>', html=True)
 
     def test_list_submissions_filtering_range(self):
@@ -717,7 +744,7 @@ def test_list_submissions_filtering_range(self):
         self.assertEqual(len(response.context['data_rows']), 1)
 
         # CustomFormPageSubmission have custom field. This field should appear in the listing
-        self.assertContains(response, '<th>Username</th>', html=True)
+        self.assertContains(response, '<th id="username" class="">Username</th>', html=True)
         self.assertContains(response, '<td>user-m1kola</td>', html=True)
 
     def test_list_submissions_pagination(self):
@@ -733,7 +760,7 @@ def test_list_submissions_pagination(self):
         self.assertEqual(response.context['submissions'].number, 2)
 
         # CustomFormPageSubmission have custom field. This field should appear in the listing
-        self.assertContains(response, '<th>Username</th>', html=True)
+        self.assertContains(response, '<th id="username" class="">Username</th>', html=True)
         self.assertContains(response, 'generated-username-', count=20)
 
     def test_list_submissions_pagination_invalid(self):