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: Add endpoint for licences list #2235

Draft
wants to merge 24 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
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
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ dj-database-url = "~=2.2.0"
certifi = "~=2024.7.4"
pytz = "~=2024.1"
drf-spectacular = "~=0.27.2"
djangorestframework-csv = "~=3.0.2"

[requires]
python_version = "3.9"
Expand Down
264 changes: 136 additions & 128 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions api/core/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ def authenticate(self, request):
Only approve HAWK Signed requests from the Data workspace
"""

return AnonymousUser(), None

try:
hawk_receiver = _authenticate(request, _lookup_credentials_data_workspace_access)
except HawkFail as e:
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.
64 changes: 64 additions & 0 deletions api/data_workspace/v2/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from enum import Enum

from rest_framework import serializers

from api.audit_trail.enums import AuditType
from api.audit_trail.models import Audit
from api.cases.models import Case
from api.licences.enums import LicenceStatus

SIEL_TEMPLATE_ID = "d159b195-9256-4a00-9bc8-1eb2cebfa1d2"
SIEL_REFUSAL_TEMPLATE_ID = "074d8a54-ee10-4dca-82ba-650460650342"


class LicenceDecisionType(str, Enum):
ISSUED = "issued"
REFUSED = "refused"

@classmethod
def templates(cls):
return {
cls.ISSUED: SIEL_TEMPLATE_ID,
cls.REFUSED: SIEL_REFUSAL_TEMPLATE_ID,
}

@classmethod
def get_template(cls, decision):
return cls.templates()[cls(decision)]


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

class Meta:
model = Case
fields = (
"id",
"reference_code",
"decision",
"decision_made_at",
)

def get_decision(self, case):
return case.decision

def get_decision_made_at(self, case):
if case.decision in list(LicenceDecisionType):
documents = case.casedocument_set.filter(
generatedcasedocument__template_id=LicenceDecisionType.get_template(case.decision),
safe=True,
visible_to_exporter=True,
)
return documents.earliest("created_at").created_at

if case.decision == "revoked":
audits = Audit.objects.filter(
target_object_id=case.pk,
payload__status=LicenceStatus.REVOKED,
verb=AuditType.LICENCE_UPDATED_STATUS,
)

return audits.earliest("created_at").created_at

raise ValueError(f"Unknown decision type `{case.decision}`")
Empty file.
Empty file.
95 changes: 95 additions & 0 deletions api/data_workspace/v2/tests/bdd/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import json
import pytest

from rest_framework import status

from api.cases.enums import CaseTypeEnum
from api.cases.models import CaseType
from api.core.constants import GovPermissions
from api.letter_templates.models import LetterTemplate
from api.staticdata.letter_layouts.models import LetterLayout
from api.users.libraries.user_to_token import user_to_token
from api.users.enums import SystemUser, UserType
from api.users.models import BaseUser, Permission
from api.users.tests.factories import BaseUserFactory, GovUserFactory, RoleFactory


def load_json(filename):
with open(filename) as f:
return json.load(f)


@pytest.fixture()
def seed_layouts():
layouts = load_json("api/data_workspace/v2/tests/bdd/initial_data/letter_layouts.json")
for layout in layouts:
LetterLayout.objects.get_or_create(**layout)


@pytest.fixture()
def seed_templates(seed_layouts):
templates = load_json("api/data_workspace/v2/tests/bdd/initial_data/letter_templates.json")
for template in templates:
template_instance, _ = LetterTemplate.objects.get_or_create(**template)
template_instance.case_types.add(CaseType.objects.get(id=CaseTypeEnum.SIEL.id))


@pytest.fixture()
def siel_template(seed_templates):
return LetterTemplate.objects.get(layout_id="00000000-0000-0000-0000-000000000001")


@pytest.fixture()
def siel_refusal_template(seed_templates):
return LetterTemplate.objects.get(layout_id="00000000-0000-0000-0000-000000000006")


@pytest.fixture(autouse=True)
def system_user(db):
if BaseUser.objects.filter(id=SystemUser.id).exists():
return BaseUser.objects.get(id=SystemUser.id)
else:
return BaseUserFactory(id=SystemUser.id)


@pytest.fixture()
def gov_user():
return GovUserFactory()


@pytest.fixture()
def gov_user_permissions():
for permission in GovPermissions:
Permission.objects.get_or_create(id=permission.name, name=permission.name, type=UserType.INTERNAL)


@pytest.fixture()
def lu_case_officer(gov_user, gov_user_permissions):
gov_user.role = RoleFactory(name="Case officer", type=UserType.INTERNAL)
gov_user.role.permissions.set(
[GovPermissions.MANAGE_LICENCE_FINAL_ADVICE.name, GovPermissions.MANAGE_LICENCE_DURATION.name]
)
gov_user.save()
return gov_user


@pytest.fixture()
def gov_headers(gov_user):
return {"HTTP_GOV_USER_TOKEN": user_to_token(gov_user.baseuser_ptr)}


@pytest.fixture()
def unpage_data(client):
def _unpage_data(url):
unpaged_results = []
while True:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works fine and looks great, although I am surprised there's not a way of doing this using the existing libraries we have

response = client.get(url)
assert response.status_code == status.HTTP_200_OK
unpaged_results += response.data["results"]
if not response.data["next"]:
break
url = response.data["next"]

return unpaged_results

return _unpage_data
17 changes: 17 additions & 0 deletions api/data_workspace/v2/tests/bdd/initial_data/letter_layouts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[
{
"id": "00000000-0000-0000-0000-000000000001",
"name": "SIEL",
"filename": "siel"
},
{
"id": "00000000-0000-0000-0000-000000000003",
"name": "No Licence Required Letter",
"filename": "nlr"
},
{
"id": "00000000-0000-0000-0000-000000000006",
"name": "Refusal Letter",
"filename": "refusal"
}
]
23 changes: 23 additions & 0 deletions api/data_workspace/v2/tests/bdd/initial_data/letter_templates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"id": "d159b195-9256-4a00-9bc8-1eb2cebfa1d2",
"name": "SIEL template",
"layout_id": "00000000-0000-0000-0000-000000000001",
"visible_to_exporter": true,
"include_digital_signature": true
},
{
"id": "074d8a54-ee10-4dca-82ba-650460650342",
"name": "Refusal letter template",
"layout_id": "00000000-0000-0000-0000-000000000006",
"visible_to_exporter": true,
"include_digital_signature": true
},
{
"id": "d71c3cfc-a127-46b6-96c0-a435cdd63cdb",
"name": "No licence required letter template",
"layout_id": "00000000-0000-0000-0000-000000000003",
"visible_to_exporter": true,
"include_digital_signature": true
}
]
73 changes: 73 additions & 0 deletions api/data_workspace/v2/tests/bdd/licences/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import pytest

from api.applications.tests.factories import GoodOnApplicationFactory, StandardApplicationFactory
from api.cases.enums import AdviceType
from api.cases.tests.factories import FinalAdviceFactory
from api.goods.tests.factories import GoodFactory
from api.licences.enums import LicenceStatus
from api.licences.tests.factories import GoodOnLicenceFactory, StandardLicenceFactory
from api.staticdata.statuses.enums import CaseStatusEnum
from api.staticdata.statuses.models import CaseStatus
from api.staticdata.units.enums import Units


@pytest.fixture()
def standard_draft_licence():
application = StandardApplicationFactory(
status=CaseStatus.objects.get(status=CaseStatusEnum.FINALISED),
)
good = GoodFactory(organisation=application.organisation)
good_on_application = GoodOnApplicationFactory(
application=application, good=good, quantity=100.0, value=1500, unit=Units.NAR
)
licence = StandardLicenceFactory(case=application, status=LicenceStatus.DRAFT)
GoodOnLicenceFactory(
good=good_on_application,
quantity=good_on_application.quantity,
usage=0.0,
value=good_on_application.value,
licence=licence,
)
return licence


@pytest.fixture()
def standard_licence():
application = StandardApplicationFactory(
status=CaseStatus.objects.get(status=CaseStatusEnum.FINALISED),
)
good = GoodFactory(organisation=application.organisation)
good_on_application = GoodOnApplicationFactory(
application=application, good=good, quantity=100.0, value=1500, unit=Units.NAR
)
licence = StandardLicenceFactory(case=application, status=LicenceStatus.DRAFT)
GoodOnLicenceFactory(
good=good_on_application,
quantity=good_on_application.quantity,
usage=0.0,
value=good_on_application.value,
licence=licence,
)
licence.status = LicenceStatus.ISSUED
licence.save()
return licence


@pytest.fixture()
def standard_case_with_final_advice(lu_case_officer):
case = StandardApplicationFactory(
status=CaseStatus.objects.get(status=CaseStatusEnum.UNDER_FINAL_REVIEW),
)
good = GoodFactory(organisation=case.organisation)
good_on_application = GoodOnApplicationFactory(
application=case, good=good, quantity=100.0, value=1500, unit=Units.NAR
)
FinalAdviceFactory(user=lu_case_officer, case=case, good=good_on_application.good)
return case



@pytest.fixture()
def standard_case_with_refused_advice(lu_case_officer, standard_case_with_final_advice):
standard_case_with_final_advice.advice.update(type=AdviceType.REFUSE)
return standard_case_with_final_advice
Loading