Skip to content

Commit

Permalink
Merge branch 'dev' into fix-endesive
Browse files Browse the repository at this point in the history
  • Loading branch information
depsiatwal authored Dec 6, 2024
2 parents 42b2ae2 + ab83f5e commit 2c54db8
Show file tree
Hide file tree
Showing 53 changed files with 1,899 additions and 1,131 deletions.
2 changes: 1 addition & 1 deletion .InstallPackages
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ swig
imagemagick
poppler-utils
libsqlite3-dev
postgresql-client-14

1 change: 1 addition & 0 deletions .copilot/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ builder:
version: 0.3.339
packs:
- acodeninja/install
- acodeninja/psql
2 changes: 2 additions & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
web: SWIG_LIB=/home/vcap/deps/0/apt/usr/share/swig4.0 CFLAGS=-I/home/vcap/deps/1/python/include/python3.9.18m pip3 install endesive==1.5.9 && python manage.py migrate && gunicorn --worker-class gevent -c api/conf/gconfig.py -b 0.0.0.0:$PORT api.conf.wsgi
web-dbt-platform: python manage.py migrate && gunicorn -c api/conf/gconfig-dbt-platform.py -b 0.0.0.0:$PORT api.conf.wsgi
dump-and-anonymise: python manage.py dump_and_anonymise
rebuild-search-index: python manage.py search_index --rebuild -f
seed-internal-users: ./bin/seed_internal_users.sh
celeryworker: celery -A api.conf worker -l info
celeryscheduler: celery -A api.conf beat
82 changes: 21 additions & 61 deletions api/cases/migrations/0070_attach_licence_to_licence_decision.py
Original file line number Diff line number Diff line change
@@ -1,82 +1,42 @@
# 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
from api.cases.enums import 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")
LicenceDecision = apps.get_model("cases", "LicenceDecision")
GeneratedCaseDocument = apps.get_model("generated_documents", "GeneratedCaseDocument")

licence_decisions_to_update = []

final_decision_qs = Audit.objects.filter(verb=AuditType.CREATED_FINAL_RECOMMENDATION).order_by("-created_at")
siel_licence_template = LicenceDecisionType.templates()[LicenceDecisionType.ISSUED]

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()],
licence_decision_qs = LicenceDecision.objects.filter(decision=LicenceDecisionType.ISSUED, licence__isnull=True)

for obj in licence_decision_qs:
try:
audit_log = Audit.objects.get(
target_object_id=obj.case_id,
verb=AuditType.CREATED_FINAL_RECOMMENDATION,
created_at=obj.created_at,
)
)
.annotate(
decision=DBCase(
*[
When(template_ids=[template_id], then=Value(decision))
for decision, template_id in LicenceDecisionType.templates().items()
]
licence = Licence.objects.get(reference_code=audit_log.payload["licence_reference"])
except Audit.DoesNotExist:
document = GeneratedCaseDocument.objects.get(
case_id=obj.case_id,
advice_type="approve",
template_id=siel_licence_template,
created_at=obj.created_at,
)
)
)

# 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)
licence = document.licence

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
obj.licence = licence
licence_decisions_to_update.append(obj)

LicenceDecision.objects.bulk_update(licence_decisions_to_update, ["licence"])
Expand Down
19 changes: 19 additions & 0 deletions api/cases/migrations/0073_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-28 11:07

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("denial_reasons", "0006_populate_uuid_field"),
("cases", "0072_alter_decision_populate_issued_on_appeal"),
]

operations = [
migrations.AddField(
model_name="licencedecision",
name="denial_reasons",
field=models.ManyToManyField(to="denial_reasons.denialreason"),
),
]
24 changes: 18 additions & 6 deletions api/cases/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ def no_licence_required(self):
notify_exporter_no_licence_required(self)

@transaction.atomic
def finalise(self, request, decisions):
def finalise(self, user, decisions, note):
from api.audit_trail import service as audit_trail_service
from api.cases.libraries.finalise import remove_flags_on_finalisation, remove_flags_from_audit_trail
from api.licences.models import Licence
Expand All @@ -361,7 +361,7 @@ def finalise(self, request, decisions):

if Licence.objects.filter(case=self).count() > 1:
audit_trail_service.create(
actor=request.user,
actor=user,
verb=AuditType.REINSTATED_APPLICATION,
target=self,
payload={
Expand All @@ -376,12 +376,12 @@ def finalise(self, request, decisions):
self.save()

audit_trail_service.create(
actor=request.user,
actor=user,
verb=AuditType.UPDATED_STATUS,
target=self,
payload={
"status": {"new": self.status.status, "old": old_status},
"additional_text": request.data.get("note"),
"additional_text": note,
},
)
logging.info("Case is now finalised")
Expand Down Expand Up @@ -410,16 +410,27 @@ def finalise(self, request, decisions):
if previous_licence_decision.decision == current_decision:
previous_decision = previous_licence_decision

LicenceDecision.objects.create(
licence_decision = LicenceDecision.objects.create(
case=self,
decision=current_decision,
licence=licence,
previous_decision=previous_decision,
)
if advice_type == AdviceType.REFUSE:
denial_reasons = (
self.advice.filter(
level=AdviceLevel.FINAL,
type=AdviceType.REFUSE,
)
.only("denial_reasons__id")
.distinct()
.values_list("denial_reasons__id", flat=True)
)
licence_decision.denial_reasons.set(denial_reasons)

licence_reference = licence.reference_code if licence and advice_type == AdviceType.APPROVE else ""
audit_trail_service.create(
actor=request.user,
actor=user,
verb=AuditType.CREATED_FINAL_RECOMMENDATION,
target=self,
payload={
Expand Down Expand Up @@ -846,6 +857,7 @@ class LicenceDecision(TimestampableModel):
licence = models.ForeignKey(
"licences.Licence", on_delete=models.DO_NOTHING, related_name="licence_decisions", null=True, blank=True
)
denial_reasons = models.ManyToManyField(DenialReason)
excluded_from_statistics_reason = models.TextField(default=None, blank=True, null=True)
previous_decision = models.ForeignKey(
"self", related_name="previous_decisions", default=None, null=True, on_delete=models.DO_NOTHING
Expand Down
23 changes: 19 additions & 4 deletions api/cases/tests/test_finalise_advice.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
from api.audit_trail.enums import AuditType
from api.audit_trail.models import Audit
from api.audit_trail import service as audit_trail_service
from api.cases.enums import AdviceType, CaseTypeEnum, LicenceDecisionType
from api.cases.models import LicenceDecision
from api.cases.enums import (
AdviceLevel,
AdviceType,
CaseTypeEnum,
LicenceDecisionType,
)
from api.cases.tests.factories import FinalAdviceFactory
from api.cases.libraries.get_case import get_case
from api.cases.generated_documents.models import GeneratedCaseDocument
Expand Down Expand Up @@ -65,8 +69,19 @@ def test_refuse_standard_application_success(self, send_exporter_notifications_f
payload__decision=AdviceType.REFUSE,
).exists()
)
self.assertTrue(
LicenceDecision.objects.filter(case=self.application, decision=LicenceDecisionType.REFUSED).exists()

licence_decision = case.licence_decisions.get(decision=LicenceDecisionType.REFUSED)

advice = case.advice.get(
level=AdviceLevel.FINAL,
type=AdviceType.REFUSE,
)

self.assertTrue(licence_decision.denial_reasons.exists())

self.assertQuerySetEqual(
licence_decision.denial_reasons.all(),
advice.denial_reasons.all(),
)

@mock.patch("api.cases.notify.notify_exporter_licence_refused")
Expand Down
2 changes: 1 addition & 1 deletion api/cases/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,7 @@ def put(self, request, pk):
)

# finalises case, grants licence and publishes decision documents
licence_id = case.finalise(request, required_decisions)
licence_id = case.finalise(request.user, required_decisions, request.data.get("note"))

return JsonResponse({"case": pk, "licence": licence_id}, status=status.HTTP_201_CREATED)

Expand Down
1 change: 1 addition & 0 deletions api/conf/caseworker_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
path("applications/", include("api.applications.caseworker.urls")),
path("organisations/", include("api.organisations.caseworker.urls")),
path("static/", include("api.staticdata.caseworker.urls")),
path("gov_users/", include("api.gov_users.caseworker.urls")),
]
5 changes: 4 additions & 1 deletion api/conf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,10 @@ def _build_redis_url(base_url, db_number, **query_args):
LITE_API_ENABLE_ES = env.bool("LITE_API_ENABLE_ES", False)
if LITE_API_ENABLE_ES:
ELASTICSEARCH_DSL = {
"default": {"hosts": env.str("OPENSEARCH_ENDPOINT")},
"default": {
"hosts": env.str("OPENSEARCH_ENDPOINT"),
"timeout": 30,
},
}

ENABLE_SPIRE_SEARCH = env.bool("ENABLE_SPIRE_SEARCH", False)
Expand Down
7 changes: 7 additions & 0 deletions api/core/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ def assert_user_has_permission(user, permission, organisation: Organisation = No
raise PermissionDeniedError()


def assert_user_in_role(user, role):
if isinstance(user, GovUser):
if user.role.id == role:
return True
raise PermissionDeniedError()


def check_user_has_permission(user, permission, organisation: Organisation = None):
if isinstance(user, GovUser):
return user.has_permission(permission)
Expand Down
44 changes: 44 additions & 0 deletions api/data_workspace/v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
)
from api.cases.enums import LicenceDecisionType
from api.cases.models import LicenceDecision
from api.licences.models import GoodOnLicence
from api.staticdata.control_list_entries.models import ControlListEntry
from api.staticdata.countries.models import Country
from api.staticdata.denial_reasons.models import DenialReason
from api.staticdata.report_summaries.models import ReportSummary


Expand Down Expand Up @@ -96,6 +99,15 @@ class Meta:
)


class GoodOnLicenceSerializer(serializers.ModelSerializer):
good_id = serializers.UUIDField()
licence_id = serializers.UUIDField()

class Meta:
model = GoodOnLicence
fields = ("good_id", "licence_id")


class ApplicationSerializer(serializers.ModelSerializer):
licence_type = serializers.CharField(source="case_type.reference")
status = serializers.CharField(source="status.status")
Expand Down Expand Up @@ -129,3 +141,35 @@ def get_first_closed_at(self, application) -> typing.Optional[datetime.datetime]
return application.baseapplication_ptr.case_ptr.closed_status_updates[0].created_at

return None


class UnitSerializer(serializers.Serializer):
code = serializers.CharField()
description = serializers.CharField()


class FootnoteSerializer(serializers.Serializer):
footnote = serializers.CharField()
team_name = serializers.CharField(source="team__name")
application_id = serializers.CharField(source="case__pk")
type = serializers.CharField()


class AssessmentSerializer(serializers.ModelSerializer):
good_id = serializers.UUIDField()

class Meta:
model = ControlListEntry
fields = (
"good_id",
"rating",
)


class LicenceRefusalCriteriaSerializer(serializers.ModelSerializer):
criteria = serializers.CharField(source="display_value")
licence_decision_id = serializers.UUIDField()

class Meta:
model = DenialReason
fields = ("criteria", "licence_decision_id")
Loading

0 comments on commit 2c54db8

Please sign in to comment.