diff --git a/hypha/apply/api/v1/determination/permissions.py b/hypha/apply/api/v1/determination/permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..8d689abe461f88f42a965852b4e94118c3bb7d71 --- /dev/null +++ b/hypha/apply/api/v1/determination/permissions.py @@ -0,0 +1,27 @@ +from rest_framework import permissions + +from hypha.apply.determinations.utils import ( + can_create_determination, + can_edit_determination, +) + + +class HasDeterminationCreatePermission(permissions.BasePermission): + """ + Custom permission that user should have for creating determination. + """ + def has_permission(self, request, view): + try: + submission = view.get_submission_object() + except KeyError: + return True + return can_create_determination(request.user, submission) + + +class HasDeterminationDraftPermission(permissions.BasePermission): + """ + Custom permission that user should have for editing determination. + """ + def has_object_permission(self, request, view, obj): + submission = view.get_submission_object() + return can_edit_determination(request.user, obj, submission) diff --git a/hypha/apply/api/v1/determination/serializers.py b/hypha/apply/api/v1/determination/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..26cdaf958eee69812807d4bdeaefd0d6dd33fe86 --- /dev/null +++ b/hypha/apply/api/v1/determination/serializers.py @@ -0,0 +1,38 @@ +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 + ) + message = self.validated_data[instance.message_field.id] + instance.message = '' if message is None else message + 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..a3cd8035fe1ff4a5ce6b71e630bebc38561cd305 --- /dev/null +++ b/hypha/apply/api/v1/determination/utils.py @@ -0,0 +1,33 @@ +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 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..32385e00a3abe291a29709b95ea6592510868a2d --- /dev/null +++ b/hypha/apply/api/v1/determination/views.py @@ -0,0 +1,283 @@ +from django import forms +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 wagtail.core.blocks.field_block import RichTextBlock + +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 ( + HasDeterminationCreatePermission, + HasDeterminationDraftPermission, +) +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, + ) + permission_classes_by_action = { + 'create': [permissions.IsAuthenticated, HasDeterminationCreatePermission, IsApplyStaffUser, ], + 'draft': [permissions.IsAuthenticated, HasDeterminationDraftPermission, IsApplyStaffUser, ], + } + serializer_class = SubmissionDeterminationSerializer + + def get_permissions(self): + try: + # return permission_classes depending on `action` + return [permission() for permission in self.permission_classes_by_action[self.action]] + except KeyError: + # action is not set return default permission_classes + return [permission() for permission in self.permission_classes] + + 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 + if isinstance(field_block.block, RichTextBlock): + determination_data[field_block.id] = field_block.value.source + 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) + + def get_form_fields(self): + form_fields = super(SubmissionDeterminationViewSet, self).get_form_fields() + submission = self.get_submission_object() + 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 + ) + if self.action == 'update': + # Outcome can not be edited after being set once, so we do not + # need to render this field. + # form_fields.pop(field_block.id) + form_fields[field_block.id].widget = forms.TextInput(attrs={'readonly': 'readonly'}) + else: + # Outcome field choices need to be set according to the phase. + form_fields[field_block.id].choices = outcome_choices + return form_fields + + @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. + """ + form_fields = self.get_form_fields() + 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/review/views.py b/hypha/apply/api/v1/review/views.py index b7d189961c04cbd5640f43b811ac150edc27844c..13898f3dd9bbe4adff3460aab212e509e73e3843 100644 --- a/hypha/apply/api/v1/review/views.py +++ b/hypha/apply/api/v1/review/views.py @@ -4,6 +4,7 @@ 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 wagtail.core.blocks.field_block import RichTextBlock from hypha.apply.activity.messaging import MESSAGES, messenger from hypha.apply.funds.models import AssignedReviewers @@ -161,6 +162,9 @@ class SubmissionReviewViewSet( review_data['score'] = review.score review_data['opinions'] = review.opinions review_data['is_draft'] = review.is_draft + for field_block in review.form_fields: + if isinstance(field_block.block, RichTextBlock): + review_data[field_block.id] = field_block.value.source return review_data def retrieve(self, request, *args, **kwargs): diff --git a/hypha/apply/api/v1/serializers.py b/hypha/apply/api/v1/serializers.py index 7c7fe23a5f540070b0a9b3e17d4c2b00bd76e7df..75b4b48b9dda7a9d9d03f5c45f215ad99776a65f 100644 --- a/hypha/apply/api/v1/serializers.py +++ b/hypha/apply/api/v1/serializers.py @@ -4,6 +4,10 @@ from django_bleach.templatetags.bleach_tags import bleach_value from rest_framework import serializers from hypha.apply.activity.models import Activity +from hypha.apply.determinations.models import Determination +from hypha.apply.determinations.templatetags.determination_tags import ( + show_determination_button, +) from hypha.apply.determinations.views import DeterminationCreateOrUpdateView from hypha.apply.funds.models import ApplicationSubmission, RoundsAndLabs from hypha.apply.review.models import Review, ReviewOpinion @@ -113,6 +117,23 @@ class TimestampField(serializers.Field): return value.timestamp() * 1000 +class DeterminationSerializer(serializers.ModelSerializer): + outcome = serializers.ReadOnlyField(source='get_outcome_display') + author = serializers.CharField(read_only=True) + url = serializers.ReadOnlyField(source='get_absolute_url') + updated_at = serializers.DateTimeField(read_only=True) + is_draft = serializers.BooleanField(read_only=True) + + class Meta: + model = Determination + fields = ('id', 'outcome', 'author', 'url', 'updated_at', 'is_draft') + + +class DeterminationSummarySerializer(serializers.Serializer): + determinations = DeterminationSerializer(many=True, read_only=True) + count = serializers.ReadOnlyField(source='determinations.count') + + class SubmissionListSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='api:v1:submissions-detail') round = serializers.SerializerMethodField() @@ -135,13 +156,15 @@ class SubmissionDetailSerializer(serializers.ModelSerializer): stage = serializers.CharField(source='stage.name') actions = ActionSerializer(source='*') review = ReviewSummarySerializer(source='*') + determination = DeterminationSummarySerializer(source='*') phase = serializers.CharField() screening = serializers.ReadOnlyField(source='screening_status.title') action_buttons = serializers.SerializerMethodField() + is_determination_form_attached = serializers.BooleanField(read_only=True) class Meta: model = ApplicationSubmission - fields = ('id', 'title', 'stage', 'status', 'phase', 'meta_questions', 'questions', 'actions', 'review', 'screening', 'action_buttons') + fields = ('id', 'title', 'stage', 'status', 'phase', 'meta_questions', 'questions', 'actions', 'review', 'screening', 'action_buttons', 'determination', 'is_determination_form_attached') def serialize_questions(self, obj, fields): for field_id in fields: @@ -176,7 +199,13 @@ class SubmissionDetailSerializer(serializers.ModelSerializer): obj.can_review(request.user) and not obj.assigned.draft_reviewed().filter(reviewer=request.user).exists() ) - return {'add_review': add_review} + show_determination = ( + show_determination_button(request.user, obj) + ) + return { + 'add_review': add_review, + 'show_determination_button': show_determination + } class SubmissionActionSerializer(serializers.ModelSerializer): diff --git a/hypha/apply/api/v1/stream_serializers.py b/hypha/apply/api/v1/stream_serializers.py index 28888c19e363f8e6825335659be388346d21babc..6ae01d56c5ca29937a91fde112a86df52173fa85 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 @@ -20,8 +21,6 @@ class WagtailSerializer: serializer_fields = OrderedDict() form_fields = self.get_form_fields() for field_id, field in form_fields.items(): - if isinstance(field, BlockFieldWrapper): - continue serializer_fields[field_id] = self._get_field( field, self.get_serializer_field_class(field), @@ -109,6 +108,14 @@ class WagtailSerializer: if attrs.get('required') and 'default' in attrs: del attrs['required'] + if isinstance(form_field, BlockFieldWrapper): + attrs['read_only'] = True + return attrs + + # avoid "May not set both `read_only` and `required`" + if form_field.widget.attrs.get('readonly', False) == 'readonly': + attrs['read_only'] = True + del attrs['required'] return attrs def get_serializer_field_class(self, field): @@ -120,8 +127,12 @@ class WagtailSerializer: have to create mapping b/w form fields and serializer fields to get the respective classes. But for now this works. """ + if isinstance(field, BlockFieldWrapper): + return getattr(serializers, 'CharField') 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> diff --git a/hypha/static_src/src/app/src/api/index.js b/hypha/static_src/src/app/src/api/index.js index 332d88cb23c2e505b4f1a91410ba36f4a470d194..320423098b29ecb12c49a30805931b95d280bdb9 100644 --- a/hypha/static_src/src/app/src/api/index.js +++ b/hypha/static_src/src/app/src/api/index.js @@ -3,7 +3,8 @@ import { fetchSubmission, fetchSubmissionsByRound, fetchSubmissionsByStatuses, - fetchReviewDraft + fetchReviewDraft, + fetchDeterminationDraft } from '@api/submissions'; import { fetchRound, fetchRounds } from '@api/rounds'; import { createNoteForSubmission, fetchNotesForSubmission, fetchNewNotesForSubmission, editNoteForSubmission } from '@api/notes'; @@ -15,6 +16,7 @@ export default { fetchSubmissionsByStatuses, fetchSubmission, fetchReviewDraft, + fetchDeterminationDraft, fetchRound, fetchRounds, diff --git a/hypha/static_src/src/app/src/api/submissions.js b/hypha/static_src/src/app/src/api/submissions.js index 6c7b3d9d773f70176bb7a22bd8cfecd5e7622f5b..fc51f4d013f44c3122d097c90fe9214b902b031a 100644 --- a/hypha/static_src/src/app/src/api/submissions.js +++ b/hypha/static_src/src/app/src/api/submissions.js @@ -20,6 +20,12 @@ export function fetchReviewDraft(id) { }; } +export function fetchDeterminationDraft(id) { + return { + path: `/v1/submissions/${id}/determinations/draft/`, + }; +} + export function fetchSubmissionsByStatuses(statuses) { const params = new URLSearchParams params.append('page_size', 1000) diff --git a/hypha/static_src/src/app/src/common/components/PageDownWidget/index.js b/hypha/static_src/src/app/src/common/components/PageDownWidget/index.js new file mode 100644 index 0000000000000000000000000000000000000000..915b56293e5ebec99641ea44854bbdde1faee595 --- /dev/null +++ b/hypha/static_src/src/app/src/common/components/PageDownWidget/index.js @@ -0,0 +1,41 @@ +import * as React from "react"; +import PropTypes from 'prop-types'; +import TinyMCE from '../TinyMCE'; +import "./index.scss"; + +const PageDownWidget = props => { + let tmp = document.createElement("DIV"); + tmp.innerHTML = props.value; + return <div > + <TinyMCE + label={props.label} + name={props.name} + onChange={props.onChange} + value={props.value} + id={props.id} + init={props.init} + required={props.required} + helperProps={props.helperProps} + /> + + { tmp.textContent.length !== 0 && + <div className="preview" dangerouslySetInnerHTML={{__html: props.value}}> + + </div>} + </div> + +} +PageDownWidget.propTypes = { + id: PropTypes.string, + init: PropTypes.object, + label: PropTypes.string, + required: PropTypes.bool, + onChange: PropTypes.func, + value: PropTypes.node, + helperProps: PropTypes.object, + name: PropTypes.string, + +} + +PageDownWidget.displayName = 'PageDownWidget'; +export default PageDownWidget; diff --git a/hypha/static_src/src/app/src/common/components/PageDownWidget/index.scss b/hypha/static_src/src/app/src/common/components/PageDownWidget/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..acafb37090b945cc9249ef714e716625fb38e10c --- /dev/null +++ b/hypha/static_src/src/app/src/common/components/PageDownWidget/index.scss @@ -0,0 +1,5 @@ +.preview{ + width : '100%'; + background-color : #e7f2f6; + padding-left: 3px; +} diff --git a/hypha/static_src/src/app/src/common/components/TinyMCE/index.js b/hypha/static_src/src/app/src/common/components/TinyMCE/index.js index 8456d29a39a304c292cb080cfeebe5caafaf847c..b92a65b6dc3b9a447563da6fdc64e9f9fdaea735 100644 --- a/hypha/static_src/src/app/src/common/components/TinyMCE/index.js +++ b/hypha/static_src/src/app/src/common/components/TinyMCE/index.js @@ -18,8 +18,8 @@ const TinyMCE = props => { ...(props.init), menubar: false }} - onChange={e => props.onChange(props.name, e.level.content)} id={props.name} + onEditorChange = {content => props.onChange(props.name, content)} /> </div> } diff --git a/hypha/static_src/src/app/src/common/containers/FormContainer/components/FormField/index.js b/hypha/static_src/src/app/src/common/containers/FormContainer/components/FormField/index.js index 4443421acaaeb6af0fcfbc15f0a1a99e15c42b9a..7bb7cd1520fe4991f628a0e14c5b5e0896f0d827 100644 --- a/hypha/static_src/src/app/src/common/containers/FormContainer/components/FormField/index.js +++ b/hypha/static_src/src/app/src/common/containers/FormContainer/components/FormField/index.js @@ -8,6 +8,7 @@ import ScoredAnswerWidget from "@common/components/ScoredAnswerWidget"; import LoadHTML from "@common/components/LoadHTML"; import Textarea from "@common/components/Textarea"; import CheckBox from "@common/components/CheckBox"; +import PageDownWidget from "@common/components/PageDownWidget"; import PropTypes from 'prop-types'; @@ -139,6 +140,18 @@ class FormField extends React.Component { return <LoadHTML text={kwargs.text} />; + + case "PagedownWidget": + return <PageDownWidget + label={kwargs.label} + name={fieldProps.name} + onChange={this.onChange} + value={value} + id={fieldProps.name} + init={widget.mce_attrs} + required={kwargs.required} + helperProps={this.getHelperprops()} + /> default: return <div>Unknown field type {this.getType()}</div> diff --git a/hypha/static_src/src/app/src/common/containers/FormContainer/helpers.js b/hypha/static_src/src/app/src/common/containers/FormContainer/helpers.js index a1781b78993e8a4c15a6cf7723471d0ec21d96fc..ae782eecd5e021fa4bc8f73cb5321d269f7dfe4b 100644 --- a/hypha/static_src/src/app/src/common/containers/FormContainer/helpers.js +++ b/hypha/static_src/src/app/src/common/containers/FormContainer/helpers.js @@ -28,7 +28,7 @@ export const initializer = (fields, initialValues = false) => { { deep: true } ) } - if (field.type === "ChoiceField" && !formState.values[field.kwargs.label]) { + if ((field.type === "ChoiceField" && !formState.values[field.kwargs.label]) || (field.type === "TypedChoiceField" && field.kwargs.initial === null)) { formState = formState.merge( { values: { diff --git a/hypha/static_src/src/app/src/containers/Determination.js b/hypha/static_src/src/app/src/containers/Determination.js new file mode 100644 index 0000000000000000000000000000000000000000..8df05322c34881e9f01bbd8c0d965f8bb28c54ee --- /dev/null +++ b/hypha/static_src/src/app/src/containers/Determination.js @@ -0,0 +1,74 @@ +import React from 'react' +import SidebarBlock from '@components/SidebarBlock' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { getDeterminationButtonStatus, getDeterminationDraftStatus } from '@selectors/submissions' +import { toggleDeterminationFormAction, setCurrentDeterminationAction } from '@actions/submissions' +import './Determination.scss'; + +class DeterminationContainer extends React.PureComponent { + + render(){ + const determination = this.props.submission ? this.props.submission.determination : null + return <div className="determination-container"> + {determination ? + <SidebarBlock title="Determination"> + {!determination.count ? + <p>Awaiting determination</p> + : + <> + {determination.determinations.map((d, index) => { + return ( + <p key={index}> + {d.isDraft && "[Draft]"} + {d.outcome} - {d.updatedAt.slice(0,10)} by {d.author} + {(!this.props.determinationDraftStatus || (this.props.determinationDraftStatus && !d.isDraft)) + && + <a onClick={() => { this.props.setCurrentDetermination(d.id); this.props.toggleDeterminationForm(true) }} title="Edit" > + <svg className="icon icon--pen"><use href="#pen"></use></svg> + </a> + } + </p> + ) + })} + + </> + } + {this.props.submission.actionButtons.showDeterminationButton && this.props.determinationDraftStatus && + <div className="status-actions"> + <button onClick = {() => this.props.toggleDeterminationForm(true)} className="button button--primary button--half-width"> + Update draft + </button> + </div>} + { this.props.submission.actionButtons.showDeterminationButton && !this.props.determinationDraftStatus && + <div className="status-actions"> + <button onClick = {() => this.props.toggleDeterminationForm(true)} className="button button--primary button--full-width"> + Add determination + </button> + </div>} + </SidebarBlock> + : null + } + </div> + } +} + +DeterminationContainer.propTypes = { + submission: PropTypes.object, + showDeterminationForm: PropTypes.bool, + determinationDraftStatus: PropTypes.bool, + toggleDeterminationForm: PropTypes.func, + setCurrentDetermination: PropTypes.func, +} + +const mapStateToProps = (state) => ({ + showDeterminationForm: getDeterminationButtonStatus(state), + determinationDraftStatus: getDeterminationDraftStatus(state), +}) + +const mapDispatchToProps = (dispatch) => ({ + toggleDeterminationForm: (status) => dispatch(toggleDeterminationFormAction(status)), + setCurrentDetermination: (reviewId) => dispatch(setCurrentDeterminationAction(reviewId)), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(DeterminationContainer) diff --git a/hypha/static_src/src/app/src/containers/Determination.scss b/hypha/static_src/src/app/src/containers/Determination.scss new file mode 100644 index 0000000000000000000000000000000000000000..c1f75cc38c6a7b2e69526487eb49c3803ac9704f --- /dev/null +++ b/hypha/static_src/src/app/src/containers/Determination.scss @@ -0,0 +1,3 @@ +.determination-container{ + padding-right: 15px +} diff --git a/hypha/static_src/src/app/src/containers/DeterminationForm/actions.js b/hypha/static_src/src/app/src/containers/DeterminationForm/actions.js new file mode 100644 index 0000000000000000000000000000000000000000..507001f8fbb5bc0c47a18866273c04de57163672 --- /dev/null +++ b/hypha/static_src/src/app/src/containers/DeterminationForm/actions.js @@ -0,0 +1,54 @@ +import * as ActionTypes from './constants'; + +export const initializeAction = (id, determinationId = null) => ({ + type: ActionTypes.INITIALIZE, + id, + determinationId +}); + + +export const getDeterminationFieldsSuccessAction = (data) => ({ + type: ActionTypes.GET_DETERMINATION_FIELDS_SUCCESS, + data +}); + +export const getDeterminationValuesSuccessAction = (data) => ({ + type: ActionTypes.GET_DETERMINATION_VALUES_SUCCESS, + data +}); + +export const submitDeterminationAction = (determinationData, id) => ({ + type: ActionTypes.SUBMIT_DETERMINATION_DATA, + id, + determinationData, +}); + +export const updateDeterminationAction = (determinationData, id, determinationId) => ({ + type: ActionTypes.UPDATE_DETERMINATION_DATA, + id, + determinationData, + determinationId +}); + +export const deleteDeterminationAction = (determinationId, id) => ({ + type: ActionTypes.DELETE_DETERMINATION_DATA, + id, + determinationId +}); + +export const showLoadingAction = () => ({ + type: ActionTypes.SHOW_LOADING, +}) + +export const hideLoadingAction = () => ({ + type: ActionTypes.HIDE_LOADING, +}) + +export const clearInitialValues = () => ({ + type: ActionTypes.CLEAR_INITIAL_VALUES +}) + +export const toggleSaveDraftAction = (status) => ({ + type: ActionTypes.TOGGLE_SAVE_DRAFT, + status +}) diff --git a/hypha/static_src/src/app/src/containers/DeterminationForm/constants.js b/hypha/static_src/src/app/src/containers/DeterminationForm/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..40d49b2dd6a0ad8ecac0c7f3c173960ed3b40e54 --- /dev/null +++ b/hypha/static_src/src/app/src/containers/DeterminationForm/constants.js @@ -0,0 +1,11 @@ + +export const INITIALIZE = 'DeterminationForm/constants/INITIALIZE'; +export const GET_DETERMINATION_FIELDS_SUCCESS = 'DeterminationForm/constants/GET_DETERMINATION_FIELDS_SUCCESS'; +export const SHOW_LOADING = 'DeterminationForm/constants/SHOW_LOADING' +export const HIDE_LOADING = 'DeterminationForm/constants/HIDE_LOADING' +export const SUBMIT_DETERMINATION_DATA = 'DeterminationForm/constants/SUBMIT_DETERMINATION_DATA' +export const UPDATE_DETERMINATION_DATA = 'DeterminationForm/constants/UPDATE_DETERMINATION_DATA' +export const DELETE_DETERMINATION_DATA = 'DeterminationForm/constants/DELETE_DETERMINATION_DATA' +export const GET_DETERMINATION_VALUES_SUCCESS = 'DeterminationForm/constants/GET_DETERMINATION_VALUES_SUCCESS' +export const CLEAR_INITIAL_VALUES = 'DeterminationForm/constants/CLEAR_INITIAL_VALUES' +export const TOGGLE_SAVE_DRAFT = 'DeterminationForm/constants/TOGGLE_SAVE_DRAFT' diff --git a/hypha/static_src/src/app/src/containers/DeterminationForm/index.js b/hypha/static_src/src/app/src/containers/DeterminationForm/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b9aff91aa4bb1a8fc59bd8be6bad7360c861880e --- /dev/null +++ b/hypha/static_src/src/app/src/containers/DeterminationForm/index.js @@ -0,0 +1,138 @@ +import React from 'react'; +import injectReducer from '@utils/injectReducer' +import injectSaga from '@utils/injectSaga' +import { withRouter } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { bindActionCreators, compose } from 'redux'; +import PropTypes from 'prop-types'; +import * as Actions from './actions'; +import reducer from './reducer'; +import saga from './sagas'; +import { getCurrentDetermination } from '@selectors/submissions' +import * as Selectors from './selectors'; +import "./styles.scss"; +import FormContainer from '@common/containers/FormContainer'; +import LoadingPanel from '@components/LoadingPanel' +import { toggleDeterminationFormAction, clearCurrentDeterminationAction } from '../../redux/actions/submissions' + + +class DeterminationFormContainer extends React.PureComponent { + + componentDidMount(){ + this.props.initializeAction(this.props.submissionID, this.props.determinationId ? this.props.determinationId : null) + } + + getMetaFields(){ + + let metaFieldsActions = [{ + text: "back", + type: "secondary", + callback: () => {this.props.toggleDeterminationForm(false); this.props.clearCurrentDetermination()} + }] ; + + if(this.props.formData.saveAsDraft){ + metaFieldsActions.push({ + text: "Update Draft", + type: "secondary", + callback: values => { + let newValues = {...values, is_draft: true} + this.props.submitDetermination(newValues, this.props.submissionID) + } }) + }else if(!this.props.determinationId){ + metaFieldsActions.push({ + text: "Save Draft", + type: "secondary", + callback: values => { + let newValues = {...values, is_draft: true} + this.props.submitDetermination(newValues, this.props.submissionID) + } + }) + } + + if(this.props.determinationId){ + metaFieldsActions.push({ + text: "Update", + type: "primary", + callback: values => this.props.updateDetermination(values,this.props.submissionID, this.props.determinationId) + }) + } + else{ + metaFieldsActions.push({ + text: "Create", + type: "primary", + callback: values => this.props.submitDetermination(values, this.props.submissionID) + }) + } + + + return { + fields: this.props.currentDetermination ? this.props.formData.metaStructure.filter(field => field.type !== "TypedChoiceField") : this.props.formData.metaStructure, + actions: metaFieldsActions, + initialValues: this.props.formData.initialValues + } + } + + render() { + return <div + className={"container"}> + {this.props.determinationId ? <h3>Update Determination</h3> : <h3>Create Determination</h3> } + {this.props.formData.loading ? <LoadingPanel /> : <> + <FormContainer metadata={this.getMetaFields()} formId={"myIntialForm"} /> + </>} + </div> + } +} + +DeterminationFormContainer.propTypes = { + formData: PropTypes.object, + initializeAction: PropTypes.func, + submitDetermination: PropTypes.func, + deleteDetermination: PropTypes.func, + updateDetermination: PropTypes.func, + toggleDeterminationForm: PropTypes.func, + toggleSaveDraft: PropTypes.func, + submissionID: PropTypes.number, + determinationId: PropTypes.number, + clearCurrentDetermination: PropTypes.func, + currentDetermination: PropTypes.number +} + + +const mapStateToProps = state => ({ + formData: Selectors.selectFieldsInfo(state), + currentDetermination: getCurrentDetermination(state) + }); + + +function mapDispatchToProps(dispatch) { + return bindActionCreators( + { + initializeAction: Actions.initializeAction, + submitDetermination: Actions.submitDeterminationAction, + deleteDetermination: Actions.deleteDeterminationAction, + updateDetermination: Actions.updateDeterminationAction, + toggleDeterminationForm: toggleDeterminationFormAction, + toggleSaveDraft: Actions.toggleSaveDraftAction, + clearCurrentDetermination: clearCurrentDeterminationAction + + }, + dispatch, + ); +} + +const withConnect = connect( + mapStateToProps, + mapDispatchToProps, +); + +const withReducer = injectReducer({ key: 'DeterminationFormContainer', reducer }); +const withSaga = injectSaga({ key: 'DeterminationFormContainer', saga }); + + + +export default compose( + withSaga, + withReducer, + withConnect, + withRouter, +)(DeterminationFormContainer); diff --git a/hypha/static_src/src/app/src/containers/DeterminationForm/models.js b/hypha/static_src/src/app/src/containers/DeterminationForm/models.js new file mode 100644 index 0000000000000000000000000000000000000000..33debd72641962b5c6b1dbb7ce3fc95d10dd9202 --- /dev/null +++ b/hypha/static_src/src/app/src/containers/DeterminationForm/models.js @@ -0,0 +1,10 @@ +import * as Immutable from 'seamless-immutable'; + +const initialState = Immutable.from({ + metaStructure : null, + loading : true, + initialValues : null, + saveAsDraft: false +}); + +export default initialState; diff --git a/hypha/static_src/src/app/src/containers/DeterminationForm/reducer.js b/hypha/static_src/src/app/src/containers/DeterminationForm/reducer.js new file mode 100644 index 0000000000000000000000000000000000000000..56dfa70fa5499e9be49b3f780f74184061af0814 --- /dev/null +++ b/hypha/static_src/src/app/src/containers/DeterminationForm/reducer.js @@ -0,0 +1,24 @@ +import * as ActionTypes from './constants'; +import initialState from './models'; + +/* eslint-disable default-case, no-param-reassign */ +const determinationFormReducer = (state = initialState, action) => { + switch (action.type) { + case ActionTypes.GET_DETERMINATION_FIELDS_SUCCESS: + return state.set("metaStructure", action.data).set("initialValues", null); + case ActionTypes.SHOW_LOADING: + return state.set("loading", true); + case ActionTypes.HIDE_LOADING: + return state.set("loading", false); + case ActionTypes.GET_DETERMINATION_VALUES_SUCCESS: + return state.set("initialValues", action.data); + case ActionTypes.CLEAR_INITIAL_VALUES: + return state.set("initialValues", null) + case ActionTypes.TOGGLE_SAVE_DRAFT: + return state.set("saveAsDraft", action.status) + default: + return state; + } +}; + +export default determinationFormReducer; diff --git a/hypha/static_src/src/app/src/containers/DeterminationForm/sagas.js b/hypha/static_src/src/app/src/containers/DeterminationForm/sagas.js new file mode 100644 index 0000000000000000000000000000000000000000..f4f234b68c0b8a3de3b89608c4640700ed7ae87a --- /dev/null +++ b/hypha/static_src/src/app/src/containers/DeterminationForm/sagas.js @@ -0,0 +1,88 @@ +import { + call, + put, + takeLatest, +} from 'redux-saga/effects'; +import * as ActionTypes from './constants'; +import * as Actions from './actions'; +import { toggleDeterminationFormAction } from '../../redux/actions/submissions' +import { apiFetch } from '@api/utils' + +function* initialFetch(action) { + + try { + yield put(Actions.showLoadingAction()) + let response = yield call(apiFetch, {path : `/v1/submissions/${action.id}/determinations/fields/`}); + let data = yield response.json() + yield put( + Actions.getDeterminationFieldsSuccessAction(data), + ); + let url = `/v1/submissions/${action.id}/determinations/draft/` + + if(action.determinationId !== null){ + url = `/v1/submissions/${action.id}/determinations/${action.determinationId}` + } + + response = yield call(apiFetch, {path : url}) + data = yield response.json() + if(data.length) + { + yield put(Actions.toggleSaveDraftAction(data.is_draft)) + } + yield put(Actions.getDeterminationValuesSuccessAction(data)) + yield put(Actions.hideLoadingAction()) + + } catch (e) { + yield put(Actions.hideLoadingAction()) + } +} + +function* submitDetermination(action){ + const url = `/v1/submissions/${action.id}/determinations/` + try{ + yield put(Actions.showLoadingAction()) + yield call( + apiFetch, + { + path : url, + method : "POST", + options : { + body : JSON.stringify(action.determinationData), + } + } + ) + yield put(toggleDeterminationFormAction(false)) + yield put(Actions.hideLoadingAction()) + }catch(e){ + yield put(Actions.hideLoadingAction()) + } +} + +function* updateDetermination(action){ + const url = `/v1/submissions/${action.id}/determinations/${action.determinationId}/` + try{ + yield put(Actions.showLoadingAction()) + yield call( + apiFetch, + { + path : url, + method : "PUT", + options : { + body : JSON.stringify(action.determinationData), + } + } + ) + yield put(toggleDeterminationFormAction(false)) + yield put(Actions.hideLoadingAction()) +} + catch(e){ + yield put(Actions.hideLoadingAction()) + } + +} + +export default function* homePageSaga() { + yield takeLatest(ActionTypes.INITIALIZE, initialFetch); + yield takeLatest(ActionTypes.SUBMIT_DETERMINATION_DATA, submitDetermination) + yield takeLatest(ActionTypes.UPDATE_DETERMINATION_DATA, updateDetermination) +} diff --git a/hypha/static_src/src/app/src/containers/DeterminationForm/selectors.js b/hypha/static_src/src/app/src/containers/DeterminationForm/selectors.js new file mode 100644 index 0000000000000000000000000000000000000000..6cc62d1d9e3d6b8c38964badef5f73aa8f8cc7fb --- /dev/null +++ b/hypha/static_src/src/app/src/containers/DeterminationForm/selectors.js @@ -0,0 +1,7 @@ +import { createSelector } from 'reselect'; +import initialState from './models'; + +export const selectFieldsRenderer = state => + state.DeterminationFormContainer ? state.DeterminationFormContainer : initialState; + +export const selectFieldsInfo = createSelector(selectFieldsRenderer, domain => domain); diff --git a/hypha/static_src/src/app/src/containers/DeterminationForm/styles.scss b/hypha/static_src/src/app/src/containers/DeterminationForm/styles.scss new file mode 100644 index 0000000000000000000000000000000000000000..92c6d81510c209707eb70b06dfcef3e5eed48f10 --- /dev/null +++ b/hypha/static_src/src/app/src/containers/DeterminationForm/styles.scss @@ -0,0 +1,6 @@ +.container { +background-color: white; +margin: 30px ; +padding: 20px; +width: 80% +} diff --git a/hypha/static_src/src/app/src/containers/DisplayPanel/index.js b/hypha/static_src/src/app/src/containers/DisplayPanel/index.js index 190ed93b1ad385f43da1920cf362468545734589..139337f2bce1a16b53fcc1b700cc5ed51785ac52 100644 --- a/hypha/static_src/src/app/src/containers/DisplayPanel/index.js +++ b/hypha/static_src/src/app/src/containers/DisplayPanel/index.js @@ -9,7 +9,9 @@ import { getCurrentSubmission, getCurrentSubmissionID, getReviewButtonStatus, - getCurrentReview + getCurrentReview, + getDeterminationButtonStatus, + getCurrentDetermination } from '@selectors/submissions' import { getDraftNoteForSubmission } from '@selectors/notes'; @@ -22,6 +24,8 @@ import StatusActions from '@containers/StatusActions' import Tabber, {Tab} from '@components/Tabber' import SubmissionLink from '@components/SubmissionLink'; import ReviewFormContainer from '@containers/ReviewForm'; +import Determination from '../Determination'; +import DeterminationFormContainer from '@containers/DeterminationForm' import './style.scss' @@ -29,7 +33,7 @@ import './style.scss' const DisplayPanel = props => { - const { submissionID, submission, addMessage, showReviewForm, currentReview} = props + const { submissionID, submission, addMessage, showReviewForm, currentReview, showDeterminationForm, currentDetermination} = props const [ currentStatus, setCurrentStatus ] = useState(undefined) const [ localSubmissionID, setLocalSubmissionID ] = useState(submissionID) @@ -66,6 +70,8 @@ const DisplayPanel = props => { let tabs = [ <Tab button="Status" key="status"> + { submission ? submission.isDeterminationFormAttached && + <Determination submissionID={submissionID} submission={submission}/> : null} <ScreeningOutcome submissionID={submissionID} /> <StatusActions submissionID={submissionID} /> <ReviewInformation submissionID={submissionID} /> @@ -83,14 +89,14 @@ const DisplayPanel = props => { tabs = [ <Tab button="Back" key="back" handleClick={ clearSubmission } />, <Tab button="Application" key="application"> - {showReviewForm ? <CurrentSubmissionDisplay /> : <ReviewFormContainer submissionID={submissionID} reviewId={currentReview}/>} + {!showReviewForm ? showDeterminationForm ? <DeterminationFormContainer submissionID={submissionID} determinationId={currentDetermination}/> :<CurrentSubmissionDisplay /> : <ReviewFormContainer submissionID={submissionID} reviewId={currentReview}/>} </Tab>, ...tabs ] } - return ( showReviewForm ? <ReviewFormContainer submissionID={submissionID} reviewId={currentReview}/> : + showDeterminationForm ? <DeterminationFormContainer submissionID={submissionID} determinationId={currentDetermination}/> : <div className="display-panel"> { !isMobile && ( @@ -127,14 +133,18 @@ DisplayPanel.propTypes = { addMessage: PropTypes.func, draftNote: PropTypes.object, showReviewForm: PropTypes.bool, - currentReview: PropTypes.number + currentReview: PropTypes.number, + currentDetermination: PropTypes.number, + showDeterminationForm: PropTypes.bool } const mapStateToProps = (state, ownProps) => ({ submissionID: getCurrentSubmissionID(state), submission: getCurrentSubmission(state), showReviewForm: getReviewButtonStatus(state), + showDeterminationForm: getDeterminationButtonStatus(state), currentReview: getCurrentReview(state), + currentDetermination: getCurrentDetermination(state), draftNote: getDraftNoteForSubmission(getCurrentSubmissionID(state))(state), }) diff --git a/hypha/static_src/src/app/src/redux/actions/submissions.js b/hypha/static_src/src/app/src/redux/actions/submissions.js index 9b2f2d4acb5b74bace118e62f78d8fb09c8e88cb..919c442f348b11297b23536001e5a351deaa38bb 100644 --- a/hypha/static_src/src/app/src/redux/actions/submissions.js +++ b/hypha/static_src/src/app/src/redux/actions/submissions.js @@ -67,7 +67,41 @@ export const TOGGLE_REVIEW_FORM = 'TOGGLE_REVIEW_FORM'; export const SET_CURRENT_REVIEW = 'SET_CURRENT_REVIEW'; export const CLEAR_CURRENT_REVIEW = 'CLEAR_CURRENT_REVIEW'; export const FETCH_REVIEW_DRAFT = 'FETCH_REVIEW_DRAFT'; -export const CLEAR_REVIEW_DRAFT = 'CLEAR_REVIEW_DRAFT' +export const CLEAR_REVIEW_DRAFT = 'CLEAR_REVIEW_DRAFT'; + +// Determination +export const TOGGLE_DETERMINATION_FORM = 'TOGGLE_DETERMINATION_FORM'; +export const SET_CURRENT_DETERMINATION = 'SET_CURRENT_DETERMINATION'; +export const CLEAR_CURRENT_DETERMINATION = 'CLEAR_CURRENT_DETERMINATION'; +export const FETCH_DETERMINATION_DRAFT = 'FETCH_DETERMINATION_DRAFT'; +export const CLEAR_DETERMINATION_DRAFT = 'CLEAR_DETERMINATION_DRAFT'; + + +export const fetchDeterminationDraft = (submissionID) => ({ + [CALL_API]: { + types: [ START_LOADING_SUBMISSION, FETCH_DETERMINATION_DRAFT, FAIL_LOADING_SUBMISSION], + endpoint: api.fetchDeterminationDraft(submissionID), + }, + submissionID, +}) + +export const clearDeterminationDraftAction = () => ({ + type: CLEAR_DETERMINATION_DRAFT, +}); + +export const toggleDeterminationFormAction = (status) =>({ + type : TOGGLE_DETERMINATION_FORM, + status +}); + +export const setCurrentDeterminationAction = (determinationId) =>({ + type : SET_CURRENT_DETERMINATION, + determinationId +}); + +export const clearCurrentDeterminationAction = () => ({ + type: CLEAR_CURRENT_DETERMINATION, +}); export const fetchReviewDraft = (submissionID) => ({ [CALL_API]: { @@ -159,6 +193,9 @@ export const setCurrentSubmission = id => (dispatch, getState) => { dispatch(toggleReviewFormAction(false)) dispatch(clearCurrentReviewAction()) dispatch(clearReviewDraftAction()) + dispatch(toggleDeterminationFormAction(false)) + dispatch(clearCurrentDeterminationAction()) + dispatch(clearDeterminationDraftAction()) dispatch(setSubmissionParam(id)); return dispatch({ @@ -298,7 +335,8 @@ export const loadCurrentSubmission = (requiredFields=[], { bypassCache = false } return null } dispatch(fetchSubmission(submissionID)) - return dispatch(fetchReviewDraft(submissionID)) + dispatch(fetchReviewDraft(submissionID)) + return dispatch(fetchDeterminationDraft(submissionID)) } diff --git a/hypha/static_src/src/app/src/redux/reducers/submissions.js b/hypha/static_src/src/app/src/redux/reducers/submissions.js index 116f6435bdf0f48c80668a6715ea705e7877e549..e60332e0d5f28c1361f2ffbdb962a5c249157a78 100644 --- a/hypha/static_src/src/app/src/redux/reducers/submissions.js +++ b/hypha/static_src/src/app/src/redux/reducers/submissions.js @@ -14,7 +14,12 @@ import { SET_CURRENT_REVIEW, CLEAR_CURRENT_REVIEW, FETCH_REVIEW_DRAFT, - CLEAR_REVIEW_DRAFT + CLEAR_REVIEW_DRAFT, + TOGGLE_DETERMINATION_FORM, + SET_CURRENT_DETERMINATION, + CLEAR_CURRENT_DETERMINATION, + FETCH_DETERMINATION_DRAFT, + CLEAR_DETERMINATION_DRAFT, } from '@actions/submissions'; import { CREATE_NOTE, UPDATE_NOTES, UPDATE_NOTE } from '@actions/notes' @@ -35,7 +40,6 @@ function submission(state={comments: []}, action) { isErrored: true, }; case UPDATE_SUBMISSION: - return { ...state, ...action.data, @@ -168,14 +172,48 @@ function isReviewDraftExist(state = false, action) { } } +function toggleDeterminationForm(state= false, action){ + switch(action.type){ + case TOGGLE_DETERMINATION_FORM: + return action.status + default: + return state + } +} + +function currentDetermination(state = null, action) { + switch(action.type) { + case SET_CURRENT_DETERMINATION: + return action.determinationId; + case CLEAR_CURRENT_DETERMINATION: + return null; + default: + return state; + } +} + +function isDeterminationDraftExist(state = false, action) { + switch(action.type) { + case FETCH_DETERMINATION_DRAFT: + return action.data.isDraft ? true : false; + case CLEAR_DETERMINATION_DRAFT: + return false; + default: + return state; + } +} + const submissions = combineReducers({ byID: submissionsByID, current: currentSubmission, showReviewForm : toggleReviewForm, - currentReview: currentReview, - isReviewDraftExist: isReviewDraftExist + currentReview, + isReviewDraftExist, + showDeterminationForm: toggleDeterminationForm, + currentDetermination, + isDeterminationDraftExist, }); export default submissions; diff --git a/hypha/static_src/src/app/src/redux/selectors/submissions.js b/hypha/static_src/src/app/src/redux/selectors/submissions.js index 940b3702c7d32a1fab92a5a4ac536d80420756e8..5bcb0d6f9a0cc79db6f5b09bc814ab36f750d335 100644 --- a/hypha/static_src/src/app/src/redux/selectors/submissions.js +++ b/hypha/static_src/src/app/src/redux/selectors/submissions.js @@ -18,6 +18,12 @@ const getCurrentReview = state => state.submissions.currentReview; const getReviewDraftStatus = state => state.submissions.isReviewDraftExist; +const getDeterminationButtonStatus = state => state.submissions.showDeterminationForm; + +const getCurrentDetermination = state => state.submissions.currentDetermination; + +const getDeterminationDraftStatus = state => state.submissions.isDeterminationDraftExist; + const getCurrentRoundSubmissions = createSelector( [ getCurrentRoundSubmissionIDs, getSubmissions], @@ -61,6 +67,9 @@ export { getReviewButtonStatus, getCurrentReview, getReviewDraftStatus, + getDeterminationButtonStatus, + getCurrentDetermination, + getDeterminationDraftStatus, getSubmissionsByRoundError, getSubmissionsByRoundLoadingState, getSubmissionLoadingState, diff --git a/hypha/static_src/src/app/src/redux/store.js b/hypha/static_src/src/app/src/redux/store.js index e62869fc84ed15331fc876ca99dd44fb7bf4c910..4a454c2292f6532120eaa2635b1bd94934a6b200 100644 --- a/hypha/static_src/src/app/src/redux/store.js +++ b/hypha/static_src/src/app/src/redux/store.js @@ -1,7 +1,6 @@ import { createStore, applyMiddleware } from 'redux' import ReduxThunk from 'redux-thunk' import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly' -import logger from 'redux-logger' import { routerMiddleware } from 'connected-react-router'; import { createBrowserHistory } from 'history'; import createSagaMiddleware from 'redux-saga'; @@ -18,10 +17,6 @@ const MIDDLEWARE = [ sagaMiddleware ]; -if (process.env.NODE_ENV === 'development') { - MIDDLEWARE.push(logger); -} - export default initialState => { const store = createStore(