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

Official stats prototype #2286

Draft
wants to merge 101 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
5d58f86
Add DRF csv renderer
kevincarrogan Aug 21, 2024
684aac7
Add endpoint for licence statuses
kevincarrogan Aug 20, 2024
6cb8532
Add endpoint for retrieving SIEL licences list
saruniitr Oct 15, 2024
f86d6f3
Address review comments
saruniitr Oct 15, 2024
d90ca37
Update url name and fix fixture
saruniitr Oct 16, 2024
545f1cd
Add licence issue date to licences list
saruniitr Oct 18, 2024
e73df0f
Update Licences list view to excludes licences with no goods
saruniitr Oct 18, 2024
95963ba
Only consider licences for finalised Cases
saruniitr Oct 18, 2024
0ae5925
Ignore cancelled licences
saruniitr Oct 18, 2024
2786805
Rename status to decision as that is what it is
saruniitr Oct 18, 2024
673b60c
Add more common fixtures
saruniitr Oct 21, 2024
7927d70
Seed letter layouts and templates
saruniitr Oct 21, 2024
4553cb0
Update licence test to include steps for generating documents
saruniitr Oct 21, 2024
c0760ef
Use Case as base object the get licences list
saruniitr Oct 22, 2024
060f962
Update licence list API view to be called licence decision
kevincarrogan Oct 22, 2024
c10f0e9
Allow pagination to be disabled for licence decisions
kevincarrogan Oct 22, 2024
7b06322
Include refused cases in the extract
saruniitr Oct 28, 2024
84c0b2f
Simplify query for getting cases with case documents
kevincarrogan Oct 28, 2024
0600888
Determine issued and refused in queryset
kevincarrogan Oct 28, 2024
ce5f7ff
Condense down logic for finding decision made at
kevincarrogan Oct 28, 2024
35f1450
Temporarily skip hawk authentication
kevincarrogan Oct 29, 2024
91b3d21
Fix DW authentication
kevincarrogan Oct 29, 2024
df81a44
Add revoked as a licence decision
kevincarrogan Oct 29, 2024
55c8493
Move into queryset
kevincarrogan Oct 29, 2024
4535ca1
Merge branch 'LTD-stats-init' into LTD-back-populate-test
saruniitr Oct 31, 2024
5818707
Add LicenceDecision model to concretely define a case outcome
saruniitr Oct 31, 2024
3ad9f1a
Add migration to back populate LicenceDecision instances
saruniitr Oct 31, 2024
9ed5377
Add new endpoint to export licence decisions from the model
saruniitr Oct 31, 2024
b97d0ad
Merge branch 'dev' into LTD-back-populate-test
saruniitr Nov 7, 2024
8c3e26a
Remove unused licence decisions endpoint
saruniitr Nov 7, 2024
a5a1780
Add endpoint to export list of applications
saruniitr Nov 7, 2024
2c7e80e
Add endpoint to export list of countries
saruniitr Nov 7, 2024
760f2f5
Add endpoint to export list of destinations on the applications
saruniitr Nov 7, 2024
11d689d
Move reference code into application table
kevincarrogan Nov 11, 2024
8342264
Add endpoint for goods
kevincarrogan Nov 11, 2024
4881448
Add an assessments table
kevincarrogan Nov 11, 2024
d3cc67a
Add licence_id to licence decision for issued licences
kevincarrogan Nov 12, 2024
4e3c3d2
Filter out deleted destinations
kevincarrogan Nov 12, 2024
838d2c4
Add endpoint for goods on licence
kevincarrogan Nov 12, 2024
a2563bf
Fix queryset for applications
kevincarrogan Nov 12, 2024
78bc1fe
Update querysets to replicate application filtering
kevincarrogan Nov 12, 2024
f2656e6
Fix case status filters
kevincarrogan Nov 12, 2024
5f0a9b3
Optimise destinations endpoint
kevincarrogan Nov 12, 2024
c1094fe
Optimise application endpoint
kevincarrogan Nov 12, 2024
05b26f7
Simplify assessment endpoint
kevincarrogan Nov 12, 2024
0c4cdfd
Fix filtering of good on licence
kevincarrogan Nov 12, 2024
083e7a3
Add export type to applications table
kevincarrogan Nov 13, 2024
86e83bf
Update names for endpoints and serializers
kevincarrogan Nov 13, 2024
72a0d89
Add sub type
kevincarrogan Nov 13, 2024
6dd7e31
Optimise checking for incorporation sub-type
kevincarrogan Nov 13, 2024
884c0a4
Merge branch 'dev' into LTD-back-populate-test
saruniitr Nov 13, 2024
8ab9442
Add goods descriptions table
kevincarrogan Nov 13, 2024
1e83fb1
Merge remote-tracking branch 'origin/LTD-back-populate-test' into LTD…
kevincarrogan Nov 13, 2024
83e21f9
Add licence refusal criteria
kevincarrogan Nov 14, 2024
527a408
Add licence refusal criteria endpoint
kevincarrogan Nov 14, 2024
6419cf3
Fix licence decision criteria
kevincarrogan Nov 14, 2024
16681ff
Add excluded_from_statistics_reason to LicenceDecision model
hnryjmes Nov 14, 2024
45acc58
Create a base class for DW view sets
kevincarrogan Nov 14, 2024
9d0fc8c
Update queryset to exclude records with excluded_from_statistics_reason
hnryjmes Nov 14, 2024
25f958a
Merge pull request #2284 from uktrade/LTD-5644_Ignore_mistaken_revoca…
kevincarrogan Nov 15, 2024
10d03f6
Merge branch 'dev' into official-stats-prototype
kevincarrogan Nov 15, 2024
2314486
Merge branch 'dev' into official-stats-prototype
kevincarrogan Nov 15, 2024
64dd5f1
Attach licence to historical Licence decision instances
saruniitr Nov 14, 2024
5ecf3ff
Update migration to populate with the actual licence at that time
saruniitr Nov 15, 2024
e76c1e7
Add previous decision to Licence decision to save previous version of…
saruniitr Nov 18, 2024
c2ba347
Use Licence decision model to export list of licence decisions
saruniitr Nov 18, 2024
e964b61
Add footnotes table
kevincarrogan Nov 18, 2024
31e3ef5
Populate previous decision when creating new licence decisions
saruniitr Nov 18, 2024
aa7fc20
Report latest licence details where there are multiple decisions
saruniitr Nov 18, 2024
2525d25
Add quantity and unit to good serializer
kevincarrogan Nov 19, 2024
ba77c90
Add unit viewset
kevincarrogan Nov 19, 2024
9b7e290
Add proper 404 for unfound unit
kevincarrogan Nov 19, 2024
e913f97
Merge branch 'official-stats-prototype' into LTD-5639-licence-decisio…
saruniitr Nov 19, 2024
61b4ddc
Update the migration name in the test
saruniitr Nov 19, 2024
1c9c8e4
Merge pull request #2290 from uktrade/LTD-5639-licence-decision-addit…
kevincarrogan Nov 19, 2024
1949af2
Optimise licence decisions endpoint
kevincarrogan Nov 19, 2024
4de7eff
Return licence id for revoked licence decision
kevincarrogan Nov 19, 2024
330a17d
Add issued_on_appeal
kevincarrogan Nov 19, 2024
3dc265c
Add issued_on_appeal decision type
saruniitr Nov 19, 2024
3f505c3
Add migration to populate issued on appeal decision
saruniitr Nov 19, 2024
88a88e0
Record Licence decision as issued_on_appeal for successful appeals
saruniitr Nov 19, 2024
211bc3c
Add statuses table
kevincarrogan Nov 19, 2024
e54b3c1
Add status to application
kevincarrogan Nov 19, 2024
f492162
Add sla days to application
kevincarrogan Nov 19, 2024
610c533
Fix issue in handling appeal case and record the decision correctly
saruniitr Nov 19, 2024
06480ca
Add bdd tests for issued_on_appeal licence decision case
saruniitr Nov 20, 2024
bec3ba5
Merge branch 'official-stats-prototype' into LTD-populate-issued-on-a…
saruniitr Nov 20, 2024
d0b2b27
Change sla_days to processing_time
kevincarrogan Nov 21, 2024
07e7b38
Add first closed at
kevincarrogan Nov 21, 2024
1c937d4
Optimise getting first closed status
kevincarrogan Nov 21, 2024
a983d0a
Make iterator a list
kevincarrogan Nov 21, 2024
20edd44
Add denial reasons to licence decision object
kevincarrogan Nov 21, 2024
ad891f7
Handle re-issue of issued on appeal case
saruniitr Nov 21, 2024
8b04198
Add migration to backpopulate licence decisions
kevincarrogan Nov 21, 2024
9435268
Merge branch 'official-stats-prototype' into LTD-populate-issued-on-a…
saruniitr Nov 21, 2024
81d60c6
Fix conflicting migrations in cases
saruniitr Nov 21, 2024
7d1547b
Default ordering is specified in model
saruniitr Nov 21, 2024
5026a4a
Actually commit migration file
saruniitr Nov 22, 2024
d8e0b82
Fix linter errors
saruniitr Nov 22, 2024
8f8de79
Remove duplicated files
saruniitr Nov 22, 2024
2d58ff4
Merge pull request #2292 from uktrade/LTD-populate-issued-on-appeal
saruniitr Nov 22, 2024
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
7 changes: 7 additions & 0 deletions api/cases/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,11 +417,13 @@ class LicenceDecisionType:
ISSUED = "issued"
REFUSED = "refused"
REVOKED = "revoked"
ISSUED_ON_APPEAL = "issued_on_appeal"

choices = [
(ISSUED, "issued"),
(REFUSED, "refused"),
(REVOKED, "revoked"),
(ISSUED_ON_APPEAL, "issued_on_appeal"),
]

decision_map = {
Expand All @@ -439,8 +441,13 @@ def templates(cls):
cls.ISSUED: SIEL_LICENCE_TEMPLATE_ID,
cls.REFUSED: SIEL_REFUSAL_TEMPLATE_ID,
cls.REVOKED: None,
cls.ISSUED_ON_APPEAL: None,
}

@classmethod
def advice_type_to_decision(cls, advice_type):
return cls.decision_map[advice_type]

@classmethod
def get_template(cls, decision):
return cls.templates()[decision]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2024-11-14 16:36

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("cases", "0068_populate_licence_decisions"),
]

operations = [
migrations.AddField(
model_name="licencedecision",
name="excluded_from_statistics_reason",
field=models.TextField(blank=True, default=None, null=True),
),
]
96 changes: 96 additions & 0 deletions api/cases/migrations/0070_attach_licence_to_licence_decision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Generated by Django 4.2.16 on 2024-11-13 17:19
import functools
import operator

from django.contrib.postgres.aggregates import ArrayAgg
from django.db import migrations, transaction
from django.db.models import Case as DBCase, Q, TextField, Value, When
from django.db.models.functions import Cast

from api.audit_trail.enums import AuditType
from api.cases.enums import AdviceType, LicenceDecisionType


@transaction.atomic
def attach_licence_to_licence_decisions(apps, schema_editor):
LicenceDecision = apps.get_model("cases", "LicenceDecision")

Audit = apps.get_model("audit_trail", "Audit")
GeneratedCaseDocument = apps.get_model("generated_documents", "GeneratedCaseDocument")
LicenceDecision = apps.get_model("cases", "LicenceDecision")
Licence = apps.get_model("licences", "Licence")

licence_decisions_to_update = []

final_decision_qs = Audit.objects.filter(verb=AuditType.CREATED_FINAL_RECOMMENDATION).order_by("-created_at")

document_qs = (
GeneratedCaseDocument.objects.filter(
template_id__in=LicenceDecisionType.templates().values(),
advice_type=AdviceType.APPROVE,
visible_to_exporter=True,
safe=True,
)
.annotate(template_ids=ArrayAgg(Cast("template_id", output_field=TextField()), distinct=True))
.filter(
functools.reduce(
operator.or_,
[Q(template_ids=[template_id]) for template_id in LicenceDecisionType.templates().values()],
)
)
.annotate(
decision=DBCase(
*[
When(template_ids=[template_id], then=Value(decision))
for decision, template_id in LicenceDecisionType.templates().items()
]
)
)
)

# When running tests audit entries are not available so filtering documents
# the audit log created date earlier fails
if final_decision_qs:
earliest_audit_log = final_decision_qs.last()
document_qs = document_qs.filter(
created_at__date__lt=earliest_audit_log.created_at.date(),
)

for audit_log in final_decision_qs:
advice_type = audit_log.payload["decision"]
if advice_type != AdviceType.APPROVE:
continue

decision = LicenceDecisionType.advice_type_to_decision(advice_type)
obj = LicenceDecision.objects.get(
case_id=str(audit_log.target_object_id),
decision=decision,
created_at=audit_log.created_at,
)
obj.licence = Licence.objects.get(reference_code=audit_log.payload["licence_reference"])
licence_decisions_to_update.append(obj)

for document in document_qs:
obj = LicenceDecision.objects.get(
case_id=str(document.case_id),
decision=document.decision,
created_at=document.created_at,
)
obj.licence = document.licence
licence_decisions_to_update.append(obj)

LicenceDecision.objects.bulk_update(licence_decisions_to_update, ["licence"])


class Migration(migrations.Migration):

dependencies = [
("cases", "0069_licencedecision_excluded_from_statistics_reason"),
]

operations = [
migrations.RunPython(
attach_licence_to_licence_decisions,
migrations.RunPython.noop,
),
]
79 changes: 79 additions & 0 deletions api/cases/migrations/0071_licencedecision_previous_decision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Generated by Django 4.2.16 on 2024-11-18 13:14

from django.db import migrations, models, transaction
from django.db.models import Count
import django.db.models.deletion


@transaction.atomic
def populate_previous_decisions(apps, schema_editor):
Case = apps.get_model("cases", "Case")
LicenceDecision = apps.get_model("cases", "LicenceDecision")

licence_decisions_to_update = []

# We only need to update cases where there are multiple decisions
# In case of single decision then previous_decision field of licence decision is
# not set by default so nothing to update
case_qs = (
Case.objects.all()
.annotate(
num_decisions=Count("licence_decisions"),
)
.filter(
num_decisions__gt=1,
)
)

# By default previous_decision is not set for all decisions.
# Previous_decision is reset whenever there is a change in decision otherwise it
# points to the decision in previous instance.
#
# When exporting we just filter all decisions where previous_decision is None which
# gives us the earliest decision time as required
# ---------------------------------------------------
# [ld1, ld2, ..., ldn] | [previous_decision field value]
# ---------------------------------------------------
# [ISSUED, ISSUED] | [None, ld1]
# [ISSUED, ISSUED, ISSUED] | [None, ld1, ld2]
# [ISSUED, REFUSED] | [None, None]
# [REFUSED, ISSUED] | [None, None]
# [ISSUED, REVOKED] | [None, None]
# [ISSUED, REVOKED, ISSUED] | [None, None, None]
# ---------------------------------------------------
#
for case in case_qs:
previous_decision = None
for item in case.licence_decisions.order_by("created_at"):
if previous_decision and item.decision == previous_decision.decision:
item.previous_decision = previous_decision
licence_decisions_to_update.append(item)

previous_decision = item

LicenceDecision.objects.bulk_update(licence_decisions_to_update, ["previous_decision"])


class Migration(migrations.Migration):

dependencies = [
("cases", "0070_attach_licence_to_licence_decision"),
]

operations = [
migrations.AddField(
model_name="licencedecision",
name="previous_decision",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="previous_decisions",
to="cases.licencedecision",
),
),
migrations.RunPython(
populate_previous_decisions,
migrations.RunPython.noop,
),
]
19 changes: 19 additions & 0 deletions api/cases/migrations/0072_licencedecision_denial_reasons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.16 on 2024-11-21 12:52

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("denial_reasons", "0006_populate_uuid_field"),
("cases", "0071_licencedecision_previous_decision"),
]

operations = [
migrations.AddField(
model_name="licencedecision",
name="denial_reasons",
field=models.ManyToManyField(to="denial_reasons.denialreason"),
),
]
56 changes: 56 additions & 0 deletions api/cases/migrations/0073_update_licencedecision_denial_reasons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Generated by Django 4.2.16 on 2024-11-21 13:24

from django.db import migrations

from api.audit_trail.enums import AuditType


def update_licencedecision_denial_reasons(apps, schema_editor):
LicenceDecision = apps.get_model("cases", "LicenceDecision")
Advice = apps.get_model("cases", "Advice")

denial_reasons = (
Advice.objects.filter(
case__licence_decisions__decision="refused",
team_id="58e77e47-42c8-499f-a58d-94f94541f8c6", # Just care about LU advice
)
.only("denial_reasons__display_value", "case__licence_decisions__id")
.exclude(denial_reasons__display_value__isnull=True) # This removes refusals without any criteria
.values_list("denial_reasons__display_value", "case__licence_decisions__id")
.order_by() # We need to remove the order_by to make sure the distinct works
.distinct()
)

updated_cases = set()
for denial_reason, licence_decision_id in denial_reasons:
licence_decision = LicenceDecision.objects.get(pk=licence_decision_id)
licence_decision.denial_reasons.add(denial_reason)
updated_cases.add(licence_decision.case.pk)

Audit = apps.get_model("audit_trail", "Audit")
Case = apps.get_model("cases", "Case")
refusal_criteria_audits = Audit.objects.exclude(target_object_id__in=updated_cases).filter(
verb=AuditType.CREATE_REFUSAL_CRITERIA
)
for audit in refusal_criteria_audits:
case = Case.objects.get(pk=audit.target_object_id)
refusal_licence_decisions = case.licence_decisions.filter(decision="refused")
if not refusal_licence_decisions.exists():
continue
for licence_decision in refusal_licence_decisions:
denial_reasons = audit.payload["additional_text"].replace(".", "").replace(" ", "").split(",")
licence_decision.denial_reasons.set(denial_reasons)


class Migration(migrations.Migration):

dependencies = [
("cases", "0072_licencedecision_denial_reasons"),
]

operations = [
migrations.RunPython(
update_licencedecision_denial_reasons,
migrations.RunPython.noop,
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Generated by Django 4.2.16 on 2024-11-21 23:21

from django.db import migrations, models, transaction

from django.db.models import Count

from api.cases.enums import LicenceDecisionType


@transaction.atomic
def populate_issued_on_appeal(apps, schema_editor):
Case = apps.get_model("cases", "Case")
LicenceDecision = apps.get_model("cases", "LicenceDecision")

licence_decisions_to_update = []

# Filter cases that have two decisions because for appeals
# the first decision will be refused which is issued on appeal later
case_qs = (
Case.objects.all()
.annotate(
num_decisions=Count("licence_decisions"),
)
.filter(
num_decisions=2,
)
)

for case in case_qs:
prev, current = case.licence_decisions.all()
if prev.decision == LicenceDecisionType.REFUSED and current.decision == LicenceDecisionType.ISSUED:
current.decision = LicenceDecisionType.ISSUED_ON_APPEAL
licence_decisions_to_update.append(current)

LicenceDecision.objects.bulk_update(licence_decisions_to_update, ["decision"])


class Migration(migrations.Migration):

dependencies = [
("cases", "0073_update_licencedecision_denial_reasons"),
]

operations = [
migrations.AlterModelOptions(
name="licencedecision",
options={"ordering": ("created_at",)},
),
migrations.AlterField(
model_name="licencedecision",
name="decision",
field=models.CharField(
choices=[
("issued", "issued"),
("refused", "refused"),
("revoked", "revoked"),
("issued_on_appeal", "issued_on_appeal"),
],
max_length=50,
),
),
migrations.RunPython(
populate_issued_on_appeal,
migrations.RunPython.noop,
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest

from api.cases.enums import LicenceDecisionType

INITIAL_MIGRATION = "0069_licencedecision_excluded_from_statistics_reason"
MIGRATION_UNDER_TEST = "0070_attach_licence_to_licence_decision"


@pytest.mark.django_db()
def test_attach_licence_to_licence_decisions(migrator):

old_state = migrator.apply_initial_migration(("cases", INITIAL_MIGRATION))
new_state = migrator.apply_tested_migration(("cases", MIGRATION_UNDER_TEST))

LicenceDecision = new_state.apps.get_model("cases", "LicenceDecision")

assert LicenceDecision.objects.filter(decision=LicenceDecisionType.ISSUED, licence__isnull=True).count() == 0
Loading