Repository: carpentries/amy

Issue:
Training Request list selection not preserved between operations
A couple of observations on features/bugs of the select boxes on the Training Requests list pages:

1. Selected boxes are not preserved between list screens. This means that no more than the number of displayed records may be selected for a bulk action at one time (unless an entire filtered set is desired in which case box selection is unnecessary). This is ok, but at least for my workflow it would be preferable to have selections carry over between search screens for a single bulk action. 

2. The box at the top that selects all records behaves unpredictably. If it is selected on a freshly displayed output screen, the "download selected to CSV" feature downloads ALL applications (not just that screen, not just filtered, ALL applications). If, however you select this box, then alter selections below (say, deselect and select a box) THEN the download includes only those selected on the display screen. This is manageable when you're aware of it, but might cause trouble down the road.



Discussion:
Hello @karenword,

The issue 2. was fixed as part of #1374.

The issue 1. is quite hard for me to implement for now, but it was raised a few times before. I have some general ideas, but for v2.2 it cannot happen. I'll keep the issue concentrated on this matter, though.
Is there any fix for problem 1 on the horizon? I have a new workflow that requires iteratively selecting, emailing, and removing (via status change) applicants from a results list. Without the ability to preserve check-boxes across screens it is quite inefficient, with the first page having new (but fewer) select-able applicants with each iteration until I can finally move on to page 2. If preserving selection across results screens is unlikely in the near term, I can try to suggest a set added search/filter conditions that would avoid the need for manual selection instead.
@karenword if this has high priority for you, I can work on it for point-release v2.5.1, which should be released sometime in first two weeks of February.

Can you point me to the workflow you mentioned? I'd like to take a look and think about possible solutions.
Thank you @pbanaszkiewicz!  Keep in mind that this workflow is still under development. Link is here, procedure in question is currently Part 2, steps 3-5. You will be able to see why it's challenging to work this out entirely through filters vs manual selection.
Oops: workflow here: https://docs.google.com/document/d/1bBOxupUrHculfuG_QJJMu6JjtLUubtdjzJDU1BUVVJQ/edit?usp=sharing

Resolving patch:
diff --git a/amy/extrequests/forms.py b/amy/extrequests/forms.py
--- a/amy/extrequests/forms.py
+++ b/amy/extrequests/forms.py
@@ -56,18 +56,17 @@ class BulkChangeTrainingRequestForm(forms.Form):
         # BulkMatchTrainingRequestForm.
         FormActions(
             Div(
-                Submit('discard', 'Discard selected requests',
+                Submit('accept', 'Accept selected',
+                       formnovalidate='formnovalidate',
+                       css_class="btn-success"),
+                Submit('discard', 'Discard selected',
                        formnovalidate='formnovalidate',
                        css_class="btn-danger"),
-                Submit('accept', 'Accept selected requests',
+                Submit('unmatch', 'Unmatch selected from training',
                        formnovalidate='formnovalidate',
-                       css_class="btn-success"),
+                       css_class="btn-primary"),
                 css_class="btn-group",
             ),
-            Submit('unmatch', 'Unmatch selected trainees from training',
-                   formnovalidate='formnovalidate'),
-            HTML('<a bulk-email-on-click class="btn btn-info text-white">'
-                 'Mail selected trainees</a>&nbsp;'),
         )
     )
 
diff --git a/amy/extrequests/views.py b/amy/extrequests/views.py
--- a/amy/extrequests/views.py
+++ b/amy/extrequests/views.py
@@ -174,7 +174,7 @@ class WorkshopRequestAssign(OnlyForAdminsMixin, AssignView):
 
 @admin_required
 def all_trainingrequests(request):
-    filter = TrainingRequestFilter(
+    filter_ = TrainingRequestFilter(
         request.GET,
         queryset=TrainingRequest.objects.all().prefetch_related(
             Prefetch(
@@ -187,13 +187,12 @@ def all_trainingrequests(request):
         )
     )
 
-    emails = filter.qs.values_list('email', flat=True)
-    requests = get_pagination_items(request, filter.qs)
+    form = BulkChangeTrainingRequestForm()
+    match_form = BulkMatchTrainingRequestForm()
 
     if request.method == 'POST' and 'match' in request.POST:
         # Bulk match people associated with selected TrainingRequests to
         # trainings.
-        form = BulkChangeTrainingRequestForm()
         match_form = BulkMatchTrainingRequestForm(request.POST)
 
         if match_form.is_valid():
@@ -252,47 +251,36 @@ def all_trainingrequests(request):
             messages.success(request, 'Successfully accepted and matched '
                                       'selected people to training.')
 
-            # Raw uri contains GET parameters from django filters. We use it
-            # to preserve filter settings.
-            return redirect(request.get_raw_uri())
-
-    elif request.method == 'POST' and 'discard' in request.POST:
+    elif request.method == 'POST' and 'accept' in request.POST:
         # Bulk discard selected TrainingRequests.
         form = BulkChangeTrainingRequestForm(request.POST)
-        match_form = BulkMatchTrainingRequestForm()
 
         if form.is_valid():
             # Perform bulk discard
             for r in form.cleaned_data['requests']:
-                r.state = 'd'
+                r.state = 'a'
                 r.save()
 
-            messages.success(request, 'Successfully discarded selected '
+            messages.success(request, 'Successfully accepted selected '
                                       'requests.')
 
-            return redirect(request.get_raw_uri())
-
-    elif request.method == 'POST' and 'accept' in request.POST:
+    elif request.method == 'POST' and 'discard' in request.POST:
         # Bulk discard selected TrainingRequests.
         form = BulkChangeTrainingRequestForm(request.POST)
-        match_form = BulkMatchTrainingRequestForm()
 
         if form.is_valid():
             # Perform bulk discard
             for r in form.cleaned_data['requests']:
-                r.state = 'a'
+                r.state = 'd'
                 r.save()
 
-            messages.success(request, 'Successfully accepted selected '
+            messages.success(request, 'Successfully discarded selected '
                                       'requests.')
 
-            return redirect(request.get_raw_uri())
-
     elif request.method == 'POST' and 'unmatch' in request.POST:
         # Bulk unmatch people associated with selected TrainingRequests from
         # trainings.
         form = BulkChangeTrainingRequestForm(request.POST)
-        match_form = BulkMatchTrainingRequestForm()
 
         form.check_person_matched = True
         if form.is_valid():
@@ -303,19 +291,12 @@ def all_trainingrequests(request):
             messages.success(request, 'Successfully unmatched selected '
                                       'people from trainings.')
 
-            return redirect(request.get_raw_uri())
-
-    else:  # GET request
-        form = BulkChangeTrainingRequestForm()
-        match_form = BulkMatchTrainingRequestForm()
-
     context = {
         'title': 'Training Requests',
-        'requests': requests,
-        'filter': filter,
+        'requests': filter_.qs,
+        'filter': filter_,
         'form': form,
         'match_form': match_form,
-        'emails': emails,
     }
 
     return render(request, 'requests/all_trainingrequests.html', context)


Test patch:
diff --git a/amy/extrequests/tests/test_training_request.py b/amy/extrequests/tests/test_training_request.py
--- a/amy/extrequests/tests/test_training_request.py
+++ b/amy/extrequests/tests/test_training_request.py
@@ -357,7 +357,7 @@ def test_matching_to_training_fails_in_the_case_of_unmatched_persons(self):
                               follow=True)
         self.assertEqual(rv.status_code, 200)
         self.assertEqual(rv.resolver_match.view_name, 'all_trainingrequests')
-        msg = 'Fix errors below and try again.'
+        msg = 'Fix errors in the form below and try again.'
         self.assertContains(rv, msg)
         msg = ('Some of the requests are not matched to a trainee yet. Before '
                'matching them to a training, you need to accept them '
@@ -399,7 +399,7 @@ def test_unmatching_fails_when_no_matched_trainee(self):
 
         self.assertEqual(rv.status_code, 200)
         self.assertEqual(rv.resolver_match.view_name, 'all_trainingrequests')
-        msg = 'Fix errors below and try again.'
+        msg = 'Fix errors in the form below and try again.'
         self.assertContains(rv, msg)
 
         # Check that Spiderman is still matched to first_training even though