Skip to content

Commit

Permalink
feat: implement new Ahjo process UI for handler (HL-1167)
Browse files Browse the repository at this point in the history
* feat(backend): list start and end dates for calculation sum rows

* feat(backend): add calc row description type to expose in api

* feat(backend): add decision proposal draft model and api

* test(backend): write tests for decision proposal drafting

* chore(backend): rewrite template sections - decision and justification to same object

* feat(shared): extend Heading component to include font-weight

* feat(shared): extend Heading to take in `css` attributes via `$css`

* feat(shared): sidebar can be set a bit shorter if sticky bar is used

* fix(prep): remove HTML semicolon, use coatOfArms styling

* fix(prep): application review values font-size according to layout

* fix(prep): tweak approval / rejection radio button group styles

* chore(prep): extend stepper component styles to cover both coat and black

* feat(prep): add setting to toggle new ahjo mode on/off

* test(frontend): application approval and old batch process

* feat(frontend): add a stepper to application review

* feat(frontend): use review stepper and split application review to pages

* chore(frontend): add translation for application reviews

* fix(frontend): use extended version of horizontal list for batches

* fix: rename migrate files as they were in conflict with #2890

* refactor: migrations from hl-1167

* feat(backend): add decision proposal draft model and api

* chore(backend): rewrite template sections - decision and justification to same object

* chore(backend): add application test fixture

* test(backend): add decision template fixture

* feat(backend): use language parameter to filter template sections

* feat(backend): write log entry if decision is of type denied

* fix(backend): denied decision might not have calculation

* fix(backend): tighter validation for decision texts

* feat(shared): allow P type for Heading

* test(frontend): extract upload function to utils

* feat(handler): add ahjo decision template editor

* refactor(handler): generalise params to show different kind of notifications

* chore: cleanup review step2 and extract calculation table to component

* refactor(frontend): clean up validation rules for disabling next button

* feat(handler): stepper subheading on third index

* feat(handler): add loading skeleton css for neat loading animations

* test(frontend): extract shared function to utility

* fix(frontend): more loading skeletons

* refactor(frontend): relocate awaiting for new data ribbon, move ahjo beta button location

* feat(frontend): show submission successful notification for new ahjo process

* test(frontend): write ahjo process tests

* fix: ease on conditions to show handling bar

* chore(frontend): add translation files

* fix: resolve migrations with rebase

* fix: resolve some PR review issues

* refactor: typo in filename

* fix: add line break so that header values will fit in one row

* refactor: move handling views and hooks to own dirs

* test: resolve issues with @rikuke's new Ahjo work

* fix: remove some css props, use variables

* fix: do not log CLAMAV nag on dev environment

* test(fix): issue with localStorage

* fix: add handling bar for sent application but hide button
  • Loading branch information
sirtawast authored Apr 16, 2024
1 parent bc04999 commit 8f3d591
Show file tree
Hide file tree
Showing 92 changed files with 4,087 additions and 468 deletions.
1 change: 1 addition & 0 deletions backend/benefit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Load test fixtures

python manage.py loaddata default_terms.json
python manage.py loaddata groups.json
python manage.py loaddata test_applications.json

This creates terms of service and applicant terms in the database. The attachment PDF files are not actually
created by loading the fixture. In order to actually download the PDF files, log in via the django admin
Expand Down
97 changes: 93 additions & 4 deletions backend/benefit/applications/api/v1/ahjo_decision_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, OpenApiParameter
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.views import APIView

from applications.api.v1.serializers.decision_proposal import (
AhjoDecisionProposalSerializer,
)
from applications.api.v1.serializers.decision_proposal_template import (
DecisionProposalTemplateSectionSerializer,
)
from applications.api.v1.serializers.decision_text import DecisionTextSerializer
from applications.enums import ApplicationStatus
from applications.enums import ApplicationStatus, DecisionType
from applications.models import (
AhjoDecisionProposalDraft,
AhjoDecisionText,
Application,
ApplicationLogEntry,
DecisionProposalTemplateSection,
)
from applications.services.ahjo_decision_service import process_template_sections
Expand Down Expand Up @@ -42,10 +48,16 @@ class DecisionProposalTemplateSectionList(APIView):
)
def get(self, request, format=None) -> Response:
application_id = self.request.query_params.get("application_id")

try:
application = (
Application.objects.filter(
pk=application_id, status=ApplicationStatus.ACCEPTED
pk=application_id,
status__in=[
ApplicationStatus.HANDLING,
ApplicationStatus.ACCEPTED,
ApplicationStatus.REJECTED,
],
)
.prefetch_related("calculation", "company")
.first()
Expand All @@ -55,11 +67,15 @@ def get(self, request, format=None) -> Response:
{"message": "Application not found"}, status=status.HTTP_404_NOT_FOUND
)

language = (
application.applicant_language
if application.applicant_language == "sv"
else "fi"
)
decision_types = self.request.query_params.getlist("decision_type")

section_types = self.request.query_params.getlist("section_type")
template_sections = DecisionProposalTemplateSection.objects.filter(
section_type__in=section_types, decision_type__in=decision_types
decision_type__in=decision_types, language=language
)

replaced_template_sections = process_template_sections(
Expand Down Expand Up @@ -131,3 +147,76 @@ def put(self, request, application_id, decision_id):
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class DecisionProposalDraftUpdate(APIView):
permission_classes = [BFIsHandler]

@action(methods=["PATCH"], detail=False)
def patch(self, request):
app_id = request.data["application_id"]
application = get_object_or_404(Application, id=app_id)

proposal_object = AhjoDecisionProposalDraft.objects.filter(
application=application
).first()

proposal = AhjoDecisionProposalSerializer(
proposal_object, data=request.data, partial=True
)

if not proposal.is_valid():
return Response(proposal.errors, status=status.HTTP_400_BAD_REQUEST)

data = proposal.validated_data
proposal.save()

if data.get("decision_text") and data.get("justification_text"):
ahjo_text = AhjoDecisionText.objects.filter(application=application)
if ahjo_text:
ahjo_text.update(
language=application.applicant_language,
decision_type=(
DecisionType.ACCEPTED
if data["status"] == ApplicationStatus.ACCEPTED
else DecisionType.DENIED
),
decision_text=data["decision_text"]
+ "\n\n"
+ data.get("justification_text"),
)
else:
AhjoDecisionText.objects.create(
application=application,
language=application.applicant_language,
decision_type=(
DecisionType.ACCEPTED
if data["status"] == ApplicationStatus.ACCEPTED
else DecisionType.DENIED
),
decision_text=data.get("decision_text")
+ "\n\n"
+ data.get("justification_text"),
)

if data["review_step"] >= 4:
# Ensure frontend has a page that exists
proposal.review_step = 3
proposal.save()

ApplicationLogEntry.objects.create(
application=application,
from_status=application.status,
to_status=data["status"],
comment=data.get("log_entry_comment") or "",
)
application.status = data.get("status")

application.save()

application.calculation.granted_as_de_minimis_aid = data[
"granted_as_de_minimis_aid"
]
application.calculation.save()

return Response(proposal.data, status=status.HTTP_200_OK)
125 changes: 125 additions & 0 deletions backend/benefit/applications/api/v1/decision_proposal_draft_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from django.db import transaction
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from drf_spectacular.utils import extend_schema
from rest_framework import filters as drf_filters, status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from applications.api.v1.serializers.decision_proposal import (
AhjoDecisionProposalReadOnlySerializer,
AhjoDecisionProposalSerializer,
)
from applications.enums import ApplicationStatus, DecisionType
from applications.models import AhjoDecisionProposalDraft, AhjoDecisionText, Application
from common.permissions import BFIsHandler


class AhjoDecisionProposalDraftFilter(filters.FilterSet):
class Meta:
model = AhjoDecisionProposalDraft
fields = {
"status": ["exact"],
}


@extend_schema(
description=(
"API for create/read/update/delete/export operations on Helsinki benefit"
" application batches"
)
)
class AhjoDecisionProposalDraftViewSet(viewsets.ViewSet):
queryset = AhjoDecisionProposalDraft.objects.all()
serializer_class = AhjoDecisionProposalSerializer
permission_classes = [BFIsHandler]
filter_backends = [
drf_filters.OrderingFilter,
filters.DjangoFilterBackend,
drf_filters.SearchFilter,
]
filterset_class = AhjoDecisionProposalDraftFilter

def get_queryset(self):
order_by = self.request.query_params.get("order_by") or None

if order_by:
self.queryset = self.queryset.order_by(order_by)

return self.queryset

def get_serializer_class(self):
"""
ApplicationBatchSerializer for default behaviour on mutation functions,
ApplicationBatchListSerializer for listing applications on a batch
"""
if self.action in ["create", "update", "partial_update", "destroy"]:
return AhjoDecisionProposalReadOnlySerializer

return AhjoDecisionProposalSerializer

@action(methods=["PATCH"], detail=False)
@transaction.atomic
def modify(self, request, pk=None):
"""
Override default destroy(), batch can only be deleted if it's status is "draft"
"""
app_id = request.data["application_id"]
application = get_object_or_404(Application, id=app_id)

proposal_object = AhjoDecisionProposalDraft.objects.filter(
application=application
).first()

proposal = AhjoDecisionProposalSerializer(
proposal_object, data=request.data, partial=True
)

if not proposal.is_valid():
return Response(proposal.errors, status=status.HTTP_400_BAD_REQUEST)

data = proposal.validated_data
proposal.save()

if data.get("decision_text") and data.get("justification_text"):
ahjo_text = AhjoDecisionText.objects.filter(application=application)
if ahjo_text:
ahjo_text.update(
language=application.applicant_language,
decision_type=(
DecisionType.ACCEPTED
if data["status"] == ApplicationStatus.ACCEPTED
else DecisionType.DENIED
),
decision_text=data["decision_text"]
+ "\n\n"
+ data["justification_text"],
)
else:
AhjoDecisionText.objects.create(
application=application,
language=application.applicant_language,
decision_type=(
DecisionType.ACCEPTED
if data["status"] == ApplicationStatus.ACCEPTED
else DecisionType.DENIED
),
decision_text=data["decision_text"]
+ "\n\n"
+ data["justification_text"],
)

if data["review_step"] >= 4:
# Ensure GUI has a page that exists
proposal.review_step = 3
proposal.save()

application.status = data["status"]
application.save()

application.calculation.granted_as_de_minimis_aid = data[
"granted_as_de_minimis_aid"
]
application.calculation.save()

return Response(proposal.data, status=status.HTTP_200_OK)
18 changes: 18 additions & 0 deletions backend/benefit/applications/api/v1/serializers/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
from applications.api.v1.serializers.attachment import AttachmentSerializer
from applications.api.v1.serializers.batch import ApplicationBatchSerializer
from applications.api.v1.serializers.de_minimis import DeMinimisAidSerializer
from applications.api.v1.serializers.decision_proposal import (
AhjoDecisionProposalReadOnlySerializer,
)
from applications.api.v1.serializers.employee import EmployeeSerializer
from applications.api.v1.serializers.utils import DynamicFieldsModelSerializer
from applications.api.v1.status_transition_validator import (
Expand All @@ -37,6 +40,7 @@
PaySubsidyGranted,
)
from applications.models import (
AhjoDecisionProposalDraft,
AhjoStatus,
Application,
ApplicationBasis,
Expand Down Expand Up @@ -1251,6 +1255,7 @@ def _base_update(self, instance, validated_data):
de_minimis_data = validated_data.pop("de_minimis_aid_set", None)
employee_data = validated_data.pop("employee", None)
approve_terms = validated_data.pop("approve_terms", None)
decision_proposal_data = validated_data.pop("decision_proposal", None)
pre_update_status = instance.status
application = super().update(instance, validated_data)
if de_minimis_data is not None:
Expand All @@ -1259,6 +1264,11 @@ def _base_update(self, instance, validated_data):
if employee_data is not None:
self._update_or_create_employee(application, employee_data)

if decision_proposal_data is not None:
AhjoDecisionProposalDraft.objects.update_or_create(
application=application, defaults=decision_proposal_data
)

if instance.status != pre_update_status:
self.handle_status_transition(
instance,
Expand All @@ -1284,6 +1294,9 @@ def create(self, validated_data):
self.assign_default_fields_from_company(application, validated_data["company"])
self._update_de_minimis_aid(application, de_minimis_data)
self._update_or_create_employee(application, employee_data)
AhjoDecisionProposalDraft.objects.update_or_create(
application=application, defaults={"review_step": 1}
)
return application

def _update_or_create_employee(self, application, employee_data):
Expand Down Expand Up @@ -1519,6 +1532,10 @@ class HandlerApplicationSerializer(BaseApplicationSerializer):
read_only=True, help_text="Application batch of this application, if any"
)

decision_proposal_draft = AhjoDecisionProposalReadOnlySerializer(
required=False, allow_null=True, read_only=True
)

create_application_for_company = serializers.UUIDField(
read_only=True,
required=False,
Expand Down Expand Up @@ -1576,6 +1593,7 @@ class Meta(BaseApplicationSerializer.Meta):
"handled_at",
"application_origin",
"paper_application_date",
"decision_proposal_draft",
]
read_only_fields = BaseApplicationSerializer.Meta.read_only_fields + [
"latest_decision_comment",
Expand Down
Loading

0 comments on commit 8f3d591

Please sign in to comment.