Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ltd 5141 reporting proposal #2150

Draft
wants to merge 20 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ jobs:
- run:
name: Run tests
command: |
pipenv run pytest --circleci-parallelize --cov=. --cov-report xml --cov-config=.coveragerc --ignore lite_routing --ignore api/anonymised_db_dumps -k "not seeding and not elasticsearch and not performance and not migration and not db_anonymiser"
pipenv run pytest --circleci-parallelize --cov=. --cov-report xml --cov-config=.coveragerc --ignore lite_routing --ignore api/anonymised_db_dumps --ignore api/data_workspace/v2/tests/bdd -k "not seeding and not elasticsearch and not performance and not migration and not db_anonymiser"
- upload_code_coverage:
alias: tests

Expand All @@ -154,7 +154,7 @@ jobs:
- run:
name: Run tests on Postgres 13
command: |
pipenv run pytest --circleci-parallelize --cov=. --cov-report xml --cov-config=.coveragerc --ignore lite_routing --ignore api/anonymised_db_dumps -k "not seeding and not elasticsearch and not performance and not migration and not db_anonymiser"
pipenv run pytest --circleci-parallelize --cov=. --cov-report xml --cov-config=.coveragerc --ignore lite_routing --ignore api/anonymised_db_dumps --ignore api/data_workspace/v2/tests/bdd -k "not seeding and not elasticsearch and not performance and not migration and not db_anonymiser"
- upload_code_coverage:
alias: tests_dbt_platform

Expand Down Expand Up @@ -370,6 +370,38 @@ jobs:
- store_artifacts:
path: cucumber_html

lite_data_bdd_tests:
docker:
- <<: *image_python_node
- <<: *image_postgres13
- <<: *image_opensearch
- <<: *image_redis
working_directory: ~/lite-api
environment:
<<: *common_env_vars
LITE_API_ENABLE_ES: True
parallelism: 5
steps:
- setup
- run:
name: Install cucumber reporter package
command: npm install multiple-cucumber-html-reporter
- run:
name: Create report directories
command: |
mkdir cucumber_results
- run:
name: Run lite_data_bdd tests
command: pipenv run pytest --cov=. --cov-report xml --cov-config=.coveragerc --circleci-parallelize --gherkin-terminal-reporter -vv api/data_workspace/v2/tests/bdd --cucumberjson=cucumber_results/cuc.json
- upload_code_coverage:
alias: lite_data_bdd_tests
- run:
name: Generate html cucumber report
command: node generate_cucumber_report.js
when: always
- store_artifacts:
path: cucumber_html

open_search_tests:
docker:
- <<: *image_python
Expand Down Expand Up @@ -600,6 +632,7 @@ workflows:
- lite_routing_tests_dbt_platform
- lite_routing_bdd_tests
- lite_routing_bdd_tests_dbt_platform
- lite_data_bdd_tests
- open_search_tests_dbt_platform
- open_search_tests
- migration_tests
Expand All @@ -613,6 +646,7 @@ workflows:
- open_search_tests
- migration_tests
- lite_routing_tests
- lite_data_bdd_tests
- check-lite-routing-sha
- e2e_tests
- anonymised_db_dump_tests
Expand Down
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ psycopg = "~=3.1.18"
django-log-formatter-asim = "~=0.0.5"
dbt-copilot-python = "~=0.2.1"
dj-database-url = "~=2.2.0"
djangorestframework-csv = "~=3.0.2"
pytz = "~=2024.1"

[requires]
python_version = "3.9"
Expand Down
895 changes: 467 additions & 428 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions api/applications/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ def _create(cls, model_class, *args, **kwargs):
obj.status = get_case_status_by_status(CaseStatusEnum.SUBMITTED)
if "status" in kwargs and isinstance(kwargs["status"], CaseStatus):
obj.status = kwargs["status"]
if "submitted_at" in kwargs:
obj.submitted_at = kwargs["submitted_at"]
obj.save()
return obj

Expand Down
2 changes: 2 additions & 0 deletions api/data_workspace/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@

from api.data_workspace.v0.urls import router_v0
from api.data_workspace.v1.urls import router_v1
from api.data_workspace.v2.urls import router_v2


app_name = "data_workspace"

urlpatterns = [
path("v0/", include((router_v0.urls, "data_workspace_v0"), namespace="v0")),
path("v1/", include((router_v1.urls, "data_workspace_v1"), namespace="v1")),
path("v2/", include((router_v2.urls, "data_workspace_v2"), namespace="v2")),
]
Empty file.
217 changes: 217 additions & 0 deletions api/data_workspace/v2/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
from functools import wraps

from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers

from api.applications.models import StandardApplication
from api.audit_trail.enums import AuditType
from api.audit_trail.models import Audit
from api.cases.enums import AdviceType
from api.cases.models import Case
from api.licences.enums import LicenceDecisionType
from api.staticdata.statuses.enums import (
CaseStatusEnum,
CaseSubStatusIdEnum,
)


def get_original_application(obj):
if not obj.amendment_of:
return obj
return get_original_application(obj.amendment_of)


class LicenceStatusSerializer(serializers.Serializer):
name = serializers.CharField(source="*")


class LicenceDecisionTypeSerializer(serializers.Serializer):
name = serializers.CharField(source="*")


class SIELApplicationSerializer(serializers.ModelSerializer):
id = serializers.SerializerMethodField()

class Meta:
model = StandardApplication
fields = ("id", "status")

def get_id(self, application):
return get_original_application(application).pk


def decision_type_checker(decision_type):
def _decision_type_check(func):
@wraps(func)
def wrapper(application, case_audit_logs):
decision_made_at = func(application, case_audit_logs)
if not decision_made_at:
return None
return decision_type, decision_made_at

return wrapper

return _decision_type_check


@decision_type_checker(LicenceDecisionType.WITHDRAWN)
def withdrawn_check(application, case_audit_logs):
if application.status.status != CaseStatusEnum.WITHDRAWN:
return

withdrawn_audit_logs = case_audit_logs.filter(
payload__status__new__in=["withdrawn", "Withdrawn"],
verb=AuditType.UPDATED_STATUS,
)
audit = withdrawn_audit_logs.latest("created_at")

return audit.created_at


@decision_type_checker(LicenceDecisionType.NLR)
def nlr_check(application, case_audit_logs):
if application.status.status != CaseStatusEnum.FINALISED:
return

if application.sub_status is not None:
return

final_recommendation_audit_logs = case_audit_logs.filter(
payload__decision=AdviceType.NO_LICENCE_REQUIRED,
verb=AuditType.CREATED_FINAL_RECOMMENDATION,
)
if final_recommendation_audit_logs.exists():
audit = final_recommendation_audit_logs.latest("created_at")
return audit.created_at

nlr_letter_audit_logs = case_audit_logs.filter(
payload__template="No licence required letter template",
verb=AuditType.GENERATE_CASE_DOCUMENT,
)
if nlr_letter_audit_logs.exists():
audit = nlr_letter_audit_logs.latest("created_at")
return audit.created_at


@decision_type_checker(LicenceDecisionType.ISSUED)
def issued_check(application, case_audit_logs):
if application.status.status != CaseStatusEnum.FINALISED:
return

if application.sub_status is None:
issued_audit_logs = case_audit_logs.filter(
payload__status="issued",
verb=AuditType.LICENCE_UPDATED_STATUS,
)
if issued_audit_logs.exists():
audit = issued_audit_logs.latest("created_at")
return audit.created_at

application_granted_audit_logs = case_audit_logs.filter(
verb=AuditType.GRANTED_APPLICATION,
)
if application_granted_audit_logs.exists():
audit = application_granted_audit_logs.latest("created_at")
return audit.created_at

return

if str(application.sub_status.pk) != CaseSubStatusIdEnum.FINALISED__APPROVED:
return

issued_audit_logs = case_audit_logs.filter(
payload__status="issued",
verb=AuditType.LICENCE_UPDATED_STATUS,
)
audit = issued_audit_logs.latest("created_at")
return audit.created_at


@decision_type_checker(LicenceDecisionType.REFUSED)
def refused_check(application, case_audit_logs):
if application.status.status != CaseStatusEnum.FINALISED:
return

if application.sub_status is None:
final_recommendation_audit_logs = case_audit_logs.filter(
payload__decision=AdviceType.REFUSE,
verb=AuditType.CREATED_FINAL_RECOMMENDATION,
)
if final_recommendation_audit_logs.exists():
audit = final_recommendation_audit_logs.latest("created_at")
return audit.created_at

refusal_letter_generated_audit_logs = case_audit_logs.filter(
payload__template="Refusal letter template",
verb=AuditType.GENERATE_CASE_DOCUMENT,
)
if refusal_letter_generated_audit_logs.exists():
audit = refusal_letter_generated_audit_logs.latest("created_at")
return audit.created_at

return

if str(application.sub_status.pk) != CaseSubStatusIdEnum.FINALISED__REFUSED:
return

final_recommendation_audit_logs = case_audit_logs.filter(
payload__decision=AdviceType.REFUSE,
verb=AuditType.CREATED_FINAL_RECOMMENDATION,
)
audit = final_recommendation_audit_logs.latest("created_at")

return audit.created_at


class LicenceDecision:
decision_checks = [
withdrawn_check,
nlr_check,
issued_check,
refused_check,
]

def __init__(self, application):
self.application_id = application.pk
self.setup(application)

def get_case_audit_logs(self, application):
target_content_type = ContentType.objects.get_for_model(Case)
return Audit.objects.filter(
target_content_type=target_content_type,
target_object_id=application.get_case().pk,
)

def setup(self, application):
case_audit_logs = self.get_case_audit_logs(application)

for decision_check in self.decision_checks:
decision = decision_check(application, case_audit_logs)
if decision:
self.type, self.decision_made_at = decision
break
else:
raise ValueError(f"Cannot determine type of licence decision for application {application.reference_code}")


class LicenceDecisionSerializer(serializers.ModelSerializer):
application_id = serializers.UUIDField()
decision = serializers.SerializerMethodField()
decision_made_at = serializers.SerializerMethodField()

class Meta:
model = StandardApplication
fields = (
"application_id",
"decision",
"decision_made_at",
)

def to_representation(self, application):
return super().to_representation(LicenceDecision(application))

def get_decision(self, licence_decision):
return licence_decision.type

def get_decision_made_at(self, licence_decision):
return licence_decision.decision_made_at
Empty file.
Empty file.
Loading