From c310afa59a27a68ea7e8f00812aba60dc44e4c59 Mon Sep 17 00:00:00 2001
From: Erin Mullaney <erin.mullaney@torchbox.com>
Date: Fri, 22 Feb 2019 11:34:53 -0500
Subject: [PATCH] GH-958: Update the batch reviewer form to handle role
 reviewers

---
 opentech/apply/funds/forms.py | 105 +++++++++++++++++++++-------------
 opentech/apply/funds/utils.py |  68 ++++++++++++++++++++++
 opentech/apply/funds/views.py |  27 +++++----
 3 files changed, 146 insertions(+), 54 deletions(-)
 create mode 100644 opentech/apply/funds/utils.py

diff --git a/opentech/apply/funds/forms.py b/opentech/apply/funds/forms.py
index 99f11d75b..6a24fa2c4 100644
--- a/opentech/apply/funds/forms.py
+++ b/opentech/apply/funds/forms.py
@@ -6,7 +6,8 @@ from django_select2.forms import Select2Widget
 from opentech.apply.users.models import User
 from opentech.apply.utils.image import generate_image_tag
 
-from .models import ApplicationSubmission, AssignedReviewers, ReviewerRole
+from .models import ApplicationSubmission, ReviewerRole
+from .utils import render_icon, save_reviewers_with_roles
 from .widgets import Select2MultiCheckboxesWidget
 from .workflow import get_action_mapping
 
@@ -100,23 +101,14 @@ class UpdateReviewersForm(forms.ModelForm):
             )
         }
 
-        staff_reviewers = User.objects.staff().only('full_name', 'pk')
-
         self.role_fields = {}
-        for role in ReviewerRole.objects.all().order_by('order'):
-            field_name = 'role_reviewer_' + slugify(str(role))
-            self.role_fields[field_name] = role
-
-            self.fields[field_name] = forms.ModelChoiceField(
-                queryset=staff_reviewers,
-                widget=Select2Widget(attrs={
-                    'data-placeholder': 'Select a reviewer',
-                }),
-                required=False,
-                label=mark_safe(self.render_icon(role.icon) + f'{role.name} Reviewer'),
-            )
-            # Pre-populate form field
-            self.fields[field_name].initial = assigned_roles.get(role)
+        field_data = make_role_reviewer_fields()
+
+        for data in field_data:
+            field_name = data['field_name']
+            self.fields[field_name] = data['field']
+            self.role_fields[field_name] = data['role']
+            self.fields[field_name].initial = assigned_roles.get(data['role'])
 
         self.submitted_reviewers = User.objects.filter(
             id__in=self.instance.reviews.values('author'),
@@ -136,12 +128,6 @@ class UpdateReviewersForm(forms.ModelForm):
         else:
             self.fields.pop('reviewer_reviewers')
 
-    def render_icon(self, image):
-        if not image:
-            return ''
-        filter_spec = 'fill-20x20'
-        return generate_image_tag(image, filter_spec)
-
     def prepare_field(self, field_name, initial, excluded):
         field = self.fields[field_name]
         field.queryset = field.queryset.exclude(id__in=excluded)
@@ -155,7 +141,7 @@ class UpdateReviewersForm(forms.ModelForm):
         role_reviewers = [
             user
             for field, user in self.cleaned_data.items()
-            if field != 'reviewer_reviewers'
+            if field in self.role_fields
         ]
 
         # If any of the users match and are set to multiple roles, throw an error
@@ -166,6 +152,7 @@ class UpdateReviewersForm(forms.ModelForm):
 
     def save(self, *args, **kwargs):
         instance = super().save(*args, **kwargs)
+<<<<<<< variant A
         """
         1. Update role reviewers
         2. Update non-role reviewers
@@ -204,34 +191,72 @@ class UpdateReviewersForm(forms.ModelForm):
                 ) for reviewer in reviewers
                 if reviewer.id not in remaining_reviewers
             )
-
-        # 3. Add in anyone who has already reviewed but who is not selected as a reviewer on the form
-        orphaned_reviews = instance.reviews.exclude(
-            author__in=instance.assigned.values('reviewer')
-        ).select_related('author')
-
-        AssignedReviewers.objects.bulk_create(
-            AssignedReviewers(
-                submission=instance,
-                role=None,
-                reviewer=review.author
-            ) for review in orphaned_reviews
+>>>>>>> variant B
+======= end
+
+        instance = save_reviewers_with_roles(
+            submission=instance,
+            role_fields=self.role_fields,
+            cleaned_data=self.cleaned_data,
+            alter_external_reviewers=self.can_alter_external_reviewers(self.instance, self.user),
+            submitted_reviewers=self.submitted_reviewers,
         )
 
         return instance
 
 
 class BatchUpdateReviewersForm(forms.Form):
-    staff_reviewers = forms.ModelMultipleChoiceField(
-        queryset=User.objects.staff(),
-        widget=Select2MultiCheckboxesWidget(attrs={'data-placeholder': 'Staff'}),
-    )
     submissions = forms.CharField(widget=forms.HiddenInput(attrs={'class': 'js-submissions-id'}))
 
     def __init__(self, *args, user=None, round=None, **kwargs):
         super().__init__(*args, **kwargs)
 
+        self.role_fields = {}
+        field_data = make_role_reviewer_fields()
+
+        for data in field_data:
+            field_name = data['field_name']
+            self.fields[field_name] = data['field']
+            self.role_fields[field_name] = data['role']
+
     def clean_submissions(self):
         value = self.cleaned_data['submissions']
         submission_ids = [int(submission) for submission in value.split(',')]
         return ApplicationSubmission.objects.filter(id__in=submission_ids)
+
+    def clean(self):
+        cleaned_data = super().clean()
+        role_reviewers = [
+            user
+            for field, user in self.cleaned_data.items()
+            if field in self.role_fields
+        ]
+
+        # If any of the users match and are set to multiple roles, throw an error
+        if len(role_reviewers) != len(set(role_reviewers)) and any(role_reviewers):
+            self.add_error(None, _('Users cannot be assigned to multiple roles.'))
+
+        return cleaned_data
+
+
+def make_role_reviewer_fields():
+    role_fields = []
+    staff_reviewers = User.objects.staff().only('full_name', 'pk')
+
+    for role in ReviewerRole.objects.all().order_by('order'):
+        field_name = 'role_reviewer_' + slugify(str(role))
+        field = forms.ModelChoiceField(
+            queryset=staff_reviewers,
+            widget=Select2Widget(attrs={
+                'data-placeholder': 'Select a reviewer',
+            }),
+            required=False,
+            label=mark_safe(render_icon(role.icon) + f'{role.name} Reviewer'),
+        )
+        role_fields.append({
+            'role': role,
+            'field': field,
+            'field_name': field_name,
+        })
+
+    return role_fields
diff --git a/opentech/apply/funds/utils.py b/opentech/apply/funds/utils.py
new file mode 100644
index 000000000..8ce4f7170
--- /dev/null
+++ b/opentech/apply/funds/utils.py
@@ -0,0 +1,68 @@
+from opentech.apply.utils.image import generate_image_tag
+
+from .models import AssignedReviewers
+
+
+def save_reviewers_with_roles(submission, role_fields, cleaned_data, alter_external_reviewers=False, submitted_reviewers=None):
+    """
+    1. Update role reviewers
+    2. Update non-role reviewers
+        2a. Remove those not on form
+        2b. Add in any new non-role reviewers selected
+    3. Add in anyone who has already reviewed but who is not selected as a reviewer on the form
+    """
+    # 1. Update role reviewers
+    assigned_roles = {
+        role: cleaned_data[field]
+        for field, role in role_fields.items()
+    }
+    for role, reviewer in assigned_roles.items():
+        if reviewer:
+            AssignedReviewers.objects.update_or_create(
+                submission=submission,
+                role=role,
+                defaults={'reviewer': reviewer},
+            )
+
+    # 2. Update non-role reviewers
+    # 2a. Remove those not on form
+    if alter_external_reviewers:
+        reviewers = cleaned_data['reviewer_reviewers']
+        assigned_reviewers = submission.assigned.without_roles()
+        assigned_reviewers.exclude(
+            reviewer__in=reviewers | submitted_reviewers
+        ).delete()
+
+        remaining_reviewers = assigned_reviewers.values_list('reviewer_id', flat=True)
+
+        # 2b. Add in any new non-role reviewers selected
+        AssignedReviewers.objects.bulk_create(
+            AssignedReviewers(
+                submission=submission,
+                role=None,
+                reviewer=reviewer
+            ) for reviewer in reviewers
+            if reviewer.id not in remaining_reviewers
+        )
+
+    # 3. Add in anyone who has already reviewed but who is not selected as a reviewer on the form
+    orphaned_reviews = submission.reviews.exclude(
+        author__in=submission.assigned.values('reviewer')
+    ).select_related('author')
+
+    AssignedReviewers.objects.bulk_create(
+        AssignedReviewers(
+            submission=submission,
+            role=None,
+            reviewer=review.author
+        ) for review in orphaned_reviews
+    )
+
+    return submission
+
+
+def render_icon(image):
+    if not image:
+        return ''
+    filter_spec = 'fill-20x20'
+    return generate_image_tag(image, filter_spec)
diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py
index a2e4499a7..0849db79b 100644
--- a/opentech/apply/funds/views.py
+++ b/opentech/apply/funds/views.py
@@ -54,6 +54,7 @@ from .tables import (
     SubmissionReviewerFilterAndSearch,
     SummarySubmissionsTable,
 )
+from .utils import save_reviewers_with_roles
 from .workflow import STAGE_CHANGE_ACTIONS, PHASES_MAPPING, review_statuses
 
 
@@ -97,23 +98,18 @@ class BatchUpdateReviewersView(DelegatedViewMixin, FormView):
     form_class = BatchUpdateReviewersForm
     context_name = 'batch_reviewer_form'
 
-    def form_invalid(self, form):
-        messages.error(self.request, mark_safe(_('Sorry something went wrong') + form.errors.as_ul()))
-        return super().form_invalid(form)
-
     def form_valid(self, form):
-        """
-        Loop through all submissions selected on the page,
-        Add any reviewers that were selected,  only if they are not
-        currently saved to that submission.
-        Send out a message of updates.
-        """
-        reviewers = form.cleaned_data['staff_reviewers']
-
         submissions = form.cleaned_data['submissions']
 
         for submission in submissions:
-            submission.reviewers.add(*reviewers)
+            save_reviewers_with_roles(
+                submission=submission,
+                role_fields=form.role_fields,
+                cleaned_data=form.cleaned_data,
+                alter_external_reviewers=False,
+                submitted_reviewers=None,
+            )
+        reviewers = submissions.first().assigned.with_roles().all()
 
         messenger(
             MESSAGES.BATCH_REVIEWERS_UPDATED,
@@ -122,9 +118,12 @@ class BatchUpdateReviewersView(DelegatedViewMixin, FormView):
             submissions=submissions,
             added=reviewers,
         )
-
         return super().form_valid(form)
 
+    def form_invalid(self, form):
+        messages.error(self.request, mark_safe(_('Sorry something went wrong') + form.errors.as_ul()))
+        return super().form_invalid(form)
+
 
 @method_decorator(staff_required, name='dispatch')
 class BatchProgressSubmissionView(DelegatedViewMixin, FormView):
-- 
GitLab