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

ICMSLST-1190 Refactor COM application template code. #1360

Merged
merged 1 commit into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions pii-ner-exclude.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3890,3 +3890,6 @@ cookie settings</button
GA
View for the cookie consent page
README
CertificateOfGoodManufacturingPracticeApplicationTemplate
TemplateCls
Application Type"
26 changes: 13 additions & 13 deletions web/domains/case/export/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,19 +207,6 @@ def __init__(self, *args, **kwargs):
type_code=ExportApplicationType.Types.MANUFACTURE
).country_group.countries.filter(is_active=True)


class EditCOMForm(OptionalFormMixin, PrepareCertManufactureFormBase):
"""Form used when editing the application.

All fields are optional to allow partial record saving.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Moved from PrepareCertManufactureFormBase to EditCOMForm as it shouldn't
# be set when creating a COM template.
self.fields["contact"].queryset = application_contacts(self.instance)

def clean_is_pesticide_on_free_sale_uk(self):
"""Perform extra logic even thought this is the edit form where every field is optional"""

Expand All @@ -241,6 +228,19 @@ def clean_is_manufacturer(self):
return val


class EditCOMForm(OptionalFormMixin, PrepareCertManufactureFormBase):
"""Form used when editing the application.

All fields are optional to allow partial record saving.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Moved from PrepareCertManufactureFormBase to EditCOMForm as it shouldn't
# be set when creating a COM template.
self.fields["contact"].queryset = application_contacts(self.instance)


class SubmitCOMForm(EditCOMForm):
"""Form used when submitting the application.

Expand Down
42 changes: 28 additions & 14 deletions web/domains/case/export/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,11 @@ def create_application_url(self) -> str:
raise ValueError(f"Unknown Application Type: {self.type_code}") # /PS-IGNORE


class ExportApplication(ApplicationBase):
class ExportApplicationABC(models.Model):
"""Base class for ExportApplication and the templates."""

class Meta:
indexes = [
models.Index(fields=["status"], name="EA_status_idx"),
BTreeIndex(
fields=["reference"],
name="EA_search_case_reference_idx",
opclasses=["text_pattern_ops"],
),
models.Index(fields=["-submit_datetime"], name="EA_submit_datetime_idx"),
]
abstract = True

application_type = models.ForeignKey(
"web.ExportApplicationType", on_delete=models.PROTECT, blank=False, null=False
Expand Down Expand Up @@ -152,6 +146,19 @@ class Meta:
def is_import_application(self) -> bool:
return False


class ExportApplication(ExportApplicationABC, ApplicationBase):
class Meta:
indexes = [
models.Index(fields=["status"], name="EA_status_idx"),
BTreeIndex(
fields=["reference"],
name="EA_search_case_reference_idx",
opclasses=["text_pattern_ops"],
),
models.Index(fields=["-submit_datetime"], name="EA_submit_datetime_idx"),
]

def get_edit_view_name(self) -> str:
if self.process_type == ProcessTypes.COM:
return "export:com-edit"
Expand Down Expand Up @@ -189,10 +196,9 @@ def application_approved(self):
return self.decision == self.APPROVE


@final
class CertificateOfManufactureApplication(ExportApplication):
PROCESS_TYPE = ProcessTypes.COM
IS_FINAL = True
class CertificateOfManufactureApplicationABC(models.Model):
class Meta:
abstract = True

is_pesticide_on_free_sale_uk = models.BooleanField(null=True)
is_manufacturer = models.BooleanField(null=True)
Expand All @@ -202,6 +208,14 @@ class CertificateOfManufactureApplication(ExportApplication):
manufacturing_process = models.TextField(max_length=4000, blank=False, null=True)


@final
class CertificateOfManufactureApplication( # type: ignore[misc]
CertificateOfManufactureApplicationABC, ExportApplication
):
PROCESS_TYPE = ProcessTypes.COM
IS_FINAL = True


@final
class CertificateOfFreeSaleApplication(ExportApplication):
PROCESS_TYPE = ProcessTypes.CFS
Expand Down
5 changes: 4 additions & 1 deletion web/domains/case/export/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,11 @@ def set_template_data(
:param template: Application Template
:param type_code: App type.
"""

# Get data that we can save in the real application
data = model_to_dict(template.com_template, exclude=["id", "template"])
form_class = form_class_for_application_type(type_code)
form = form_class(instance=application, data=template.form_data())
form = form_class(instance=application, data=data)

if form.is_valid():
form.save()
Expand Down
18 changes: 17 additions & 1 deletion web/domains/cat/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import django_filters
from django import forms

from web.models import CertificateApplicationTemplate, ExportApplicationType
from web.domains.case.export.forms import PrepareCertManufactureFormBase
from web.forms.mixins import OptionalFormMixin
from web.models import (
CertificateApplicationTemplate,
CertificateOfManufactureApplicationTemplate,
ExportApplicationType,
)


class CATFilter(django_filters.FilterSet):
Expand Down Expand Up @@ -34,3 +40,13 @@ class Meta:
model = CertificateApplicationTemplate
fields = ("name", "description", "sharing")
widgets = {"description": forms.Textarea({"rows": 4})}


class CertificateOfManufactureTemplateForm(OptionalFormMixin, PrepareCertManufactureFormBase):
class Meta:
fields = [f for f in PrepareCertManufactureFormBase.Meta.fields if f != "contact"]
help_texts = PrepareCertManufactureFormBase.Meta.help_texts
labels = PrepareCertManufactureFormBase.Meta.labels
widgets = PrepareCertManufactureFormBase.Meta.widgets

model = CertificateOfManufactureApplicationTemplate
94 changes: 42 additions & 52 deletions web/domains/cat/models.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,40 @@
import copy
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING

from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models

from web.domains.case.export.models import (
CertificateOfManufactureApplicationABC,
ExportApplicationABC,
)
from web.models import ExportApplicationType
from web.types import TypedTextChoices

if TYPE_CHECKING:
from web.models import User


def encode_model_or_queryset(o: models.Model | models.QuerySet | Any) -> Any:
match o:
case models.Model():
return o.pk
case models.QuerySet():
return [encode_model_or_queryset(instance) for instance in o]
case _:
return o


def encode_json_field_data(data: dict[str, Any]) -> dict[str, Any]:
"""Serialize Model and QuerySet instances so that they can be encoded using DjangoJSONEncoder.

Previously this was done via a custom DjangoQuerysetJSONEncoder but didn't work
after upgrading to Psycopg 3.
"""

return {key: encode_model_or_queryset(value) for key, value in data.items()}


class CertificateApplicationTemplateManager(models.Manager):
"""Custom manager to preserve behaviour that relied on old custom DjangoQuerysetJSONEncoder."""

def create(self, **kwargs):
data = kwargs.pop("data", {})
valid_data = encode_json_field_data(data)

return super().create(data=valid_data, **kwargs)


class CertificateApplicationTemplate(models.Model):
class SharingStatuses(TypedTextChoices):
PRIVATE = ("private", "Private (do not share)")
VIEW = ("view", "Share (view only)")
EDIT = ("edit", "Share (allow edit)")

# Create manager to handle legacy behaviour that relied on old custom DjangoQuerysetJSONEncoder.
objects = CertificateApplicationTemplateManager()
name = models.CharField(verbose_name="Template Name", max_length=70)

description = models.CharField(verbose_name="Template Description", max_length=500)

name = models.CharField(
verbose_name="Template Name",
max_length=70,
application_type = models.CharField(
verbose_name="Application Type",
max_length=3,
choices=ExportApplicationType.Types.choices,
help_text=(
"DIT does not issue Certificates of Free Sale for food, food supplements, pesticides"
" and CE marked medical devices. Certificates of Manufacture are applicable only to"
" pesticides that are for export only and not on free sale on the domestic market."
),
)

description = models.CharField(verbose_name="Template Description", max_length=500)

application_type = models.CharField(
verbose_name="Application Type", max_length=3, choices=ExportApplicationType.Types.choices
)

sharing = models.CharField(
max_length=7,
choices=SharingStatuses.choices,
Expand All @@ -80,11 +48,6 @@ class SharingStatuses(TypedTextChoices):
created = models.DateTimeField(auto_now_add=True)
last_updated = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True)
data = models.JSONField(default=dict, encoder=DjangoJSONEncoder)

def form_data(self) -> dict:
"""Data to use as a Django form's data argument."""
return copy.deepcopy(self.data)

def user_can_view(self, user: "User") -> bool:
# A template may have sensitive information so we check if the user
Expand All @@ -95,7 +58,34 @@ def user_can_edit(self, user: "User") -> bool:
# Whether the user can edit the template itself.
return user == self.owner

def save(self, *args, **kwargs):
self.data = encode_json_field_data(self.data)

return super().save(*args, **kwargs)
class CertificateOfManufactureApplicationTemplate( # type: ignore[misc]
ExportApplicationABC, CertificateOfManufactureApplicationABC
):
# Relationships to ignore from ExportApplicationABC
application_type = None
last_updated_by = None
variation_requests = None
case_notes = None
further_information_requests = None
update_requests = None
case_emails = None
submitted_by = None
created_by = None
exporter = None
exporter_office = None
contact = None
agent = None
agent_office = None
case_owner = None
cleared_by = None

MattHolmes123 marked this conversation as resolved.
Show resolved Hide resolved
template = models.OneToOneField(
"web.CertificateApplicationTemplate", on_delete=models.CASCADE, related_name="com_template"
)


class CertificateOfFreeSaleApplicationTemplate: ... # noqa: E701


class CertificateOfGoodManufacturingPracticeApplicationTemplate: ... # noqa: E701
Loading
Loading