diff --git a/hypha/apply/api/v1/determination/serializers.py b/hypha/apply/api/v1/determination/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..ad5a124b1ed69c207bf19126ed3510a8c0cd3a43 --- /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 0000000000000000000000000000000000000000..390fbbd8e0905fe701c983769d3fd6ebbe098b60 --- /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 0000000000000000000000000000000000000000..54f8cf1fbb08e09e0361fb4fffc42fbef39d6e70 --- /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 28888c19e363f8e6825335659be388346d21babc..69e99b95395c6079f66b4a04b5648421e9915308 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 8207470c8ddbdd11e4a37ab5a8660e708ebff58a..e8a7635ece414386c0555cc36f8ec4018acd74b8 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 74a58106a93c636f915ac864f731e83658a808cd..90a7b83439cc0f20aa9b979d7e970b5495612978 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 06f562be2f71e82b31c0a2427a6ab6381bfa34a1..67011bb4ed757d21be9cabc90f39e6c41758bc33 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>