From 63c9976c8bf6f9bff8ff63a50c0b21f6903010c8 Mon Sep 17 00:00:00 2001
From: sks444 <krishnasingh.ss30@gmail.com>
Date: Wed, 30 Sep 2020 12:19:19 +0530
Subject: [PATCH] Add determination apis

---
 .../apply/api/v1/determination/serializers.py |  37 +++
 hypha/apply/api/v1/determination/utils.py     |  48 ++++
 hypha/apply/api/v1/determination/views.py     | 260 ++++++++++++++++++
 hypha/apply/api/v1/stream_serializers.py      |   3 +
 hypha/apply/api/v1/urls.py                    |   2 +
 hypha/apply/api/v1/utils.py                   |   1 -
 .../determinations/determination_detail.html  |   2 +-
 7 files changed, 351 insertions(+), 2 deletions(-)
 create mode 100644 hypha/apply/api/v1/determination/serializers.py
 create mode 100644 hypha/apply/api/v1/determination/utils.py
 create mode 100644 hypha/apply/api/v1/determination/views.py

diff --git a/hypha/apply/api/v1/determination/serializers.py b/hypha/apply/api/v1/determination/serializers.py
new file mode 100644
index 000000000..ad5a124b1
--- /dev/null
+++ b/hypha/apply/api/v1/determination/serializers.py
@@ -0,0 +1,37 @@
+from rest_framework import serializers
+
+from hypha.apply.determinations.models import Determination
+
+
+class SubmissionDeterminationSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Determination
+        fields = ['id', 'is_draft', ]
+        extra_kwargs = {
+            'is_draft': {'required': False},
+        }
+
+    def validate(self, data):
+        validated_data = super().validate(data)
+        validated_data['form_data'] = {
+            key: value
+            for key, value in validated_data.items()
+        }
+        return validated_data
+
+    def update(self, instance, validated_data):
+        instance = super().update(instance, validated_data)
+        instance.send_notice = (
+            self.validated_data[instance.send_notice_field.id]
+            if instance.send_notice_field else True
+        )
+        instance.message = self.validated_data[instance.message_field.id]
+        try:
+            instance.outcome = int(self.validated_data[instance.determination_field.id])
+            # Need to catch KeyError as outcome field would not exist in case of edit.
+        except KeyError:
+            pass
+        instance.is_draft = self.validated_data.get('is_draft', False)
+        instance.form_data = self.validated_data['form_data']
+        instance.save()
+        return instance
diff --git a/hypha/apply/api/v1/determination/utils.py b/hypha/apply/api/v1/determination/utils.py
new file mode 100644
index 000000000..390fbbd8e
--- /dev/null
+++ b/hypha/apply/api/v1/determination/utils.py
@@ -0,0 +1,48 @@
+from hypha.apply.determinations.forms import (
+    BatchConceptDeterminationForm,
+    BatchProposalDeterminationForm,
+    ConceptDeterminationForm,
+    ProposalDeterminationForm,
+)
+from hypha.apply.determinations.options import (
+    DETERMINATION_CHOICES,
+    TRANSITION_DETERMINATION,
+)
+from hypha.apply.determinations.utils import determination_actions
+
+
+def get_fields_for_stage(submission):
+    forms = submission.get_from_parent('determination_forms').all()
+    index = submission.workflow.stages.index(submission.stage)
+    try:
+        return forms[index].form.form_fields
+    except IndexError:
+        return forms[0].form.form_fields
+
+
+def get_form_for_stage(submission, batch=False, edit=False):
+    if batch:
+        forms = [BatchConceptDeterminationForm, BatchProposalDeterminationForm]
+    else:
+        forms = [ConceptDeterminationForm, ProposalDeterminationForm]
+    index = submission.workflow.stages.index(submission.stage)
+    return forms[index]
+
+
+def outcome_choices_for_phase(submission, user):
+    """
+    Outcome choices correspond to Phase transitions.
+    We need to filter out non-matching choices.
+    i.e. a transition to In Review is not a determination, while Needs more info or Rejected are.
+    """
+    available_choices = set()
+    choices = dict(DETERMINATION_CHOICES)
+    for transition_name in determination_actions(user, submission):
+        try:
+            determination_type = TRANSITION_DETERMINATION[transition_name]
+        except KeyError:
+            pass
+        else:
+            available_choices.add((determination_type, choices[determination_type]))
+
+    return available_choices
diff --git a/hypha/apply/api/v1/determination/views.py b/hypha/apply/api/v1/determination/views.py
new file mode 100644
index 000000000..54f8cf1fb
--- /dev/null
+++ b/hypha/apply/api/v1/determination/views.py
@@ -0,0 +1,260 @@
+from django.conf import settings
+from django.db import transaction
+from django.shortcuts import get_object_or_404
+from django.utils import timezone
+from rest_framework import permissions, status, viewsets
+from rest_framework.decorators import action
+from rest_framework.exceptions import ValidationError
+from rest_framework.response import Response
+from rest_framework_api_key.permissions import HasAPIKey
+
+from hypha.apply.activity.messaging import MESSAGES, messenger
+from hypha.apply.activity.models import Activity
+from hypha.apply.determinations.blocks import DeterminationBlock
+from hypha.apply.determinations.models import Determination
+from hypha.apply.determinations.options import NEEDS_MORE_INFO
+from hypha.apply.determinations.utils import (
+    has_final_determination,
+    transition_from_outcome,
+)
+from hypha.apply.projects.models import Project
+from hypha.apply.stream_forms.models import BaseStreamForm
+
+from ..mixin import SubmissionNestedMixin
+from ..permissions import IsApplyStaffUser
+from ..review.serializers import FieldSerializer
+from ..stream_serializers import WagtailSerializer
+# from .permissions import (
+#     HasReviewCreatePermission,
+#     HasReviewDeletePermission,
+#     HasReviewDetialPermission,
+#     HasReviewEditPermission,
+#     HasReviewOpinionPermission,
+# )
+from .serializers import SubmissionDeterminationSerializer
+from .utils import get_fields_for_stage, outcome_choices_for_phase
+
+
+class SubmissionDeterminationViewSet(
+    BaseStreamForm,
+    WagtailSerializer,
+    SubmissionNestedMixin,
+    viewsets.GenericViewSet
+):
+    permission_classes = (
+        HasAPIKey | permissions.IsAuthenticated, HasAPIKey | IsApplyStaffUser,
+    )
+    serializer_class = SubmissionDeterminationSerializer
+
+    def get_defined_fields(self):
+        """
+        Get form fields created for determining this submission.
+
+        These form fields will be used to get respective serializer fields.
+        """
+        if self.action in ['retrieve', 'update']:
+            # For detail and edit api form fields used while submitting
+            # determination should be used.
+            determination = self.get_object()
+            return determination.form_fields
+        submission = self.get_submission_object()
+        return get_fields_for_stage(submission)
+
+    def get_serializer_class(self):
+        """
+        Override get_serializer_class to send draft parameter
+        if the request is to save as draft or the determination submitted
+        is saved as draft.
+        """
+        if self.action == 'retrieve':
+            determination = self.get_object()
+            draft = determination.is_draft
+        elif self.action == 'draft':
+            draft = True
+        else:
+            draft = self.request.data.get('is_draft', False)
+        return super().get_serializer_class(draft)
+
+    def get_queryset(self):
+        submission = self.get_submission_object()
+        return Determination.objects.filter(submission=submission, is_draft=False)
+
+    def get_object(self):
+        """
+        Get the determination object by id. If not found raise 404.
+        """
+        queryset = self.get_queryset()
+        obj = get_object_or_404(queryset, id=self.kwargs['pk'])
+        self.check_object_permissions(self.request, obj)
+        return obj
+
+    def get_determination_data(self, determination):
+        """
+        Get determination data which will be used for determination detail api.
+        """
+        determination_data = determination.form_data
+        field_blocks = determination.form_fields
+        for field_block in field_blocks:
+            if isinstance(field_block.block, DeterminationBlock):
+                determination_data[field_block.id] = determination.outcome
+        determination_data['id'] = determination.id
+        determination_data['is_draft'] = determination.is_draft
+        return determination_data
+
+    def retrieve(self, request, *args, **kwargs):
+        """
+        Get details of a determination on a submission
+        """
+        determination = self.get_object()
+        ser = self.get_serializer(
+            self.get_determination_data(determination)
+        )
+        return Response(ser.data)
+
+    @action(detail=False, methods=['get'])
+    def fields(self, request, *args, **kwargs):
+        """
+        List details of all the form fields that were created by admin for adding determinations.
+
+        These field details will be used in frontend to render the determination form.
+        """
+        submission = self.get_submission_object()
+        form_fields = self.get_form_fields()
+        field_blocks = self.get_defined_fields()
+        for field_block in field_blocks:
+            if isinstance(field_block.block, DeterminationBlock):
+                outcome_choices = outcome_choices_for_phase(
+                    submission, self.request.user
+                )
+                # Outcome field choices need to be set according to the phase.
+                form_fields[field_block.id].choices = outcome_choices
+        fields = FieldSerializer(form_fields.items(), many=True)
+        return Response(fields.data)
+
+    def get_draft_determination(self):
+        submission = self.get_submission_object()
+        try:
+            determination = Determination.objects.get(submission=submission, is_draft=True)
+        except Determination.DoesNotExist:
+            return
+        else:
+            return determination
+
+    @action(detail=False, methods=['get'])
+    def draft(self, request, *args, **kwargs):
+        """
+        Returns the draft determination submitted on a submission by current user.
+        """
+        determination = self.get_draft_determination()
+        if not determination:
+            return Response({})
+        ser = self.get_serializer(
+            self.get_determination_data(determination)
+        )
+        return Response(ser.data)
+
+    def create(self, request, *args, **kwargs):
+        """
+        Create a determination on a submission.
+
+        Accept a post data in form of `{field_id: value}`.
+        `field_id` is same id which you get from the `/fields` api.
+        `value` should be submitted with html tags, so that response can
+        be displayed with correct formatting, e.g. in case of rich text field,
+        we need to show the data with same formatting user has submitted.
+
+        Accepts optional parameter `is_draft` when a determination is to be saved as draft.
+
+        Raise ValidationError if a determination is already submitted by the user.
+        """
+        submission = self.get_submission_object()
+        ser = self.get_serializer(data=request.data)
+        ser.is_valid(raise_exception=True)
+        if has_final_determination(submission):
+            return ValidationError({
+                'detail': 'A final determination has already been submitted.'
+            })
+        determination = self.get_draft_determination()
+        if determination is None:
+            determination = Determination.objects.create(
+                submission=submission, author=request.user
+            )
+        determination.form_fields = self.get_defined_fields()
+        determination.save()
+        ser.update(determination, ser.validated_data)
+        if determination.is_draft:
+            ser = self.get_serializer(
+                self.get_determination_data(determination)
+            )
+            return Response(ser.data, status=status.HTTP_201_CREATED)
+        with transaction.atomic():
+            messenger(
+                MESSAGES.DETERMINATION_OUTCOME,
+                request=self.request,
+                user=determination.author,
+                submission=submission,
+                related=determination,
+            )
+            proposal_form = ser.validated_data.get('proposal_form')
+            transition = transition_from_outcome(int(determination.outcome), submission)
+
+            if determination.outcome == NEEDS_MORE_INFO:
+                # We keep a record of the message sent to the user in the comment
+                Activity.comments.create(
+                    message=determination.stripped_message,
+                    timestamp=timezone.now(),
+                    user=self.request.user,
+                    source=submission,
+                    related_object=determination,
+                )
+            submission.perform_transition(
+                transition,
+                self.request.user,
+                request=self.request,
+                notify=False,
+                proposal_form=proposal_form,
+            )
+
+            if submission.accepted_for_funding and settings.PROJECTS_AUTO_CREATE:
+                project = Project.create_from_submission(submission)
+                if project:
+                    messenger(
+                        MESSAGES.CREATED_PROJECT,
+                        request=self.request,
+                        user=self.request.user,
+                        source=project,
+                        related=project.submission,
+                    )
+
+        messenger(
+            MESSAGES.DETERMINATION_OUTCOME,
+            request=self.request,
+            user=determination.author,
+            source=submission,
+            related=determination,
+        )
+        ser = self.get_serializer(
+            self.get_determination_data(determination)
+        )
+        return Response(ser.data, status=status.HTTP_201_CREATED)
+
+    def update(self, request, *args, **kwargs):
+        """
+        Update a determination submitted on a submission.
+        """
+        determination = self.get_object()
+        ser = self.get_serializer(data=request.data)
+        ser.is_valid(raise_exception=True)
+        ser.update(determination, ser.validated_data)
+
+        messenger(
+            MESSAGES.DETERMINATION_OUTCOME,
+            request=self.request,
+            user=determination.author,
+            source=determination.submission,
+            related=determination,
+        )
+        ser = self.get_serializer(
+            self.get_determination_data(determination)
+        )
+        return Response(ser.data)
diff --git a/hypha/apply/api/v1/stream_serializers.py b/hypha/apply/api/v1/stream_serializers.py
index 28888c19e..69e99b953 100644
--- a/hypha/apply/api/v1/stream_serializers.py
+++ b/hypha/apply/api/v1/stream_serializers.py
@@ -1,6 +1,7 @@
 import inspect
 from collections import OrderedDict
 
+from django.forms import TypedChoiceField
 from rest_framework import serializers
 
 from hypha.apply.review.fields import ScoredAnswerField
@@ -122,6 +123,8 @@ class WagtailSerializer:
         """
         if isinstance(field, ScoredAnswerField):
             return ScoredAnswerListField
+        if isinstance(field, TypedChoiceField):
+            return getattr(serializers, 'ChoiceField')
         class_name = field.__class__.__name__
         return getattr(serializers, class_name)
 
diff --git a/hypha/apply/api/v1/urls.py b/hypha/apply/api/v1/urls.py
index 8207470c8..e8a7635ec 100644
--- a/hypha/apply/api/v1/urls.py
+++ b/hypha/apply/api/v1/urls.py
@@ -1,6 +1,7 @@
 from django.urls import path
 from rest_framework_nested import routers
 
+from hypha.apply.api.v1.determination.views import SubmissionDeterminationViewSet
 from hypha.apply.api.v1.review.views import SubmissionReviewViewSet
 
 from .views import (
@@ -24,6 +25,7 @@ submission_router = routers.NestedSimpleRouter(router, r'submissions', lookup='s
 submission_router.register(r'actions', SubmissionActionViewSet, basename='submission-actions')
 submission_router.register(r'comments', SubmissionCommentViewSet, basename='submission-comments')
 submission_router.register(r'reviews', SubmissionReviewViewSet, basename='reviews')
+submission_router.register(r'determinations', SubmissionDeterminationViewSet, basename='determinations')
 
 urlpatterns = [
     path('user/', CurrentUser.as_view(), name='user'),
diff --git a/hypha/apply/api/v1/utils.py b/hypha/apply/api/v1/utils.py
index 74a58106a..90a7b8343 100644
--- a/hypha/apply/api/v1/utils.py
+++ b/hypha/apply/api/v1/utils.py
@@ -28,7 +28,6 @@ def get_field_kwargs(form_field):
     if isinstance(form_field, forms.ChoiceField):
         kwargs['choices'] = form_field.choices
     if isinstance(form_field, forms.TypedChoiceField):
-        kwargs['coerce'] = form_field.coerce
         kwargs['empty_value'] = form_field.empty_value
     if isinstance(form_field, forms.IntegerField):
         kwargs['max_value'] = form_field.max_value
diff --git a/hypha/apply/determinations/templates/determinations/determination_detail.html b/hypha/apply/determinations/templates/determinations/determination_detail.html
index 06f562be2..67011bb4e 100644
--- a/hypha/apply/determinations/templates/determinations/determination_detail.html
+++ b/hypha/apply/determinations/templates/determinations/determination_detail.html
@@ -35,7 +35,7 @@
         {% endif %}
         {% for question, answer in group.questions %}
             <h5>{{ question }}</h5>
-            {% if answer %}{{ answer|bleach }}{% else %}-{% endif %}
+            {% if answer %}{% if answer == True %}{{ answer|yesno:"Agree,Disagree" }}{% else %}{{ answer|bleach }}{% endif %}{% else %}-{% endif %}
         {% endfor %}
     {% endfor %}
 </div>
-- 
GitLab