From 1509ff66c1a1d868a3f893e6abbe11d08df1daa8 Mon Sep 17 00:00:00 2001 From: Tomos Williams Date: Mon, 9 Dec 2024 12:03:38 +0000 Subject: [PATCH 1/3] resolve conflicts --- caseworker/advice/conditionals.py | 8 + caseworker/advice/constants.py | 2 + caseworker/advice/forms.py | 651 ------------------ caseworker/advice/forms/approval.py | 239 +++++++ caseworker/advice/forms/consolidate.py | 69 ++ caseworker/advice/forms/countersign.py | 75 ++ caseworker/advice/forms/delete.py | 11 + caseworker/advice/forms/edit.py | 20 + caseworker/advice/forms/forms.py | 138 ++++ caseworker/advice/forms/products.py | 119 ++++ caseworker/advice/forms/refusal.py | 78 +++ caseworker/advice/picklist_helpers.py | 31 + caseworker/advice/urls.py | 6 +- caseworker/advice/views/add_advice.py | 80 +++ caseworker/advice/views/consolidate_advice.py | 15 +- caseworker/advice/views/edit_advice.py | 42 ++ caseworker/advice/views/mixins.py | 110 +++ caseworker/advice/views/views.py | 274 ++------ core/wizard/views.py | 10 + unit_tests/caseworker/advice/test_forms.py | 19 +- unit_tests/caseworker/advice/test_services.py | 4 +- .../advice/views/test_consolidate.py | 14 +- .../advice/views/test_consolidate_edit.py | 21 +- .../advice/views/test_countersign.py | 12 +- .../advice/views/test_countersign_edit.py | 2 +- .../advice/views/test_countersign_view.py | 8 +- .../advice/views/test_edit_advice.py | 4 +- .../views/test_give_approval_advice_view.py | 18 +- .../advice/views/test_lu_consolidate.py | 4 +- .../advice/views/test_refusal_advice_view.py | 6 +- .../advice/views/test_select_advice_view.py | 2 +- .../advice/views/test_view_my_advice_view.py | 2 +- .../advice/views/test_view_ogd_advice.py | 2 +- 33 files changed, 1160 insertions(+), 936 deletions(-) delete mode 100644 caseworker/advice/forms.py create mode 100644 caseworker/advice/forms/approval.py create mode 100644 caseworker/advice/forms/consolidate.py create mode 100644 caseworker/advice/forms/countersign.py create mode 100644 caseworker/advice/forms/delete.py create mode 100644 caseworker/advice/forms/edit.py create mode 100644 caseworker/advice/forms/forms.py create mode 100644 caseworker/advice/forms/products.py create mode 100644 caseworker/advice/forms/refusal.py create mode 100644 caseworker/advice/picklist_helpers.py create mode 100644 caseworker/advice/views/add_advice.py create mode 100644 caseworker/advice/views/edit_advice.py create mode 100644 caseworker/advice/views/mixins.py diff --git a/caseworker/advice/conditionals.py b/caseworker/advice/conditionals.py index dc86ebbabe..f5a8a36301 100644 --- a/caseworker/advice/conditionals.py +++ b/caseworker/advice/conditionals.py @@ -13,3 +13,11 @@ def _get_form_field_boolean(wizard): def is_desnz_team(wizard): return wizard.caseworker["team"]["alias"] in services.DESNZ_TEAMS + + +def is_fcdo_team(wizard): + return wizard.caseworker["team"]["alias"] == services.FCDO_TEAM + + +def default_form(wizard): + return not (is_fcdo_team(wizard) or is_desnz_team(wizard)) diff --git a/caseworker/advice/constants.py b/caseworker/advice/constants.py index dc13a41352..2a6cc02596 100644 --- a/caseworker/advice/constants.py +++ b/caseworker/advice/constants.py @@ -30,5 +30,7 @@ class AdviceType: class AdviceSteps: RECOMMEND_APPROVAL = "recommend_approval" + DESNZ_APPROVAL = "desnz_approval" + FCDO_APPROVAL = "fcdo_approval" LICENCE_CONDITIONS = "licence_conditions" LICENCE_FOOTNOTES = "licence_footnotes" diff --git a/caseworker/advice/forms.py b/caseworker/advice/forms.py deleted file mode 100644 index e496ce7fb6..0000000000 --- a/caseworker/advice/forms.py +++ /dev/null @@ -1,651 +0,0 @@ -from django import forms -from django.forms.formsets import formset_factory -from django.utils.html import format_html - -from core.common.forms import BaseForm -from crispy_forms_gds.helper import FormHelper -from crispy_forms_gds.layout import Field, Layout, Submit -from crispy_forms_gds.choices import Choice - -from core.forms.layouts import ( - ConditionalCheckboxes, - ConditionalCheckboxesQuestion, - ConditionalRadios, - ConditionalRadiosQuestion, - ExpandingFieldset, - RadioTextArea, -) -from core.forms.utils import coerce_str_to_bool -from caseworker.tau.summaries import get_good_on_application_tau_summary -from caseworker.tau.widgets import GoodsMultipleSelect -from core.forms.widgets import GridmultipleSelect - - -def get_approval_advice_form_factory(advice, approval_reason, proviso, footnote_details, data=None): - data = data or { - "proviso": advice["proviso"], - "approval_reasons": advice["text"], - "instructions_to_exporter": advice["note"], - "footnote_details": advice["footnote"], - } - return GiveApprovalAdviceForm( - approval_reason=approval_reason, proviso=proviso, footnote_details=footnote_details, data=data - ) - - -def get_refusal_advice_form_factory(advice, denial_reasons_choices, refusal_reasons, data=None): - data = data or { - "refusal_reasons": advice["text"], - "denial_reasons": [r for r in advice["denial_reasons"]], - } - return RefusalAdviceForm(data=data, choices=denial_reasons_choices, refusal_reasons=refusal_reasons) - - -class PicklistCharField(forms.CharField): - def get_help_html(self, picklist_attrs, help_link_text, help_text_extra=None): - picklist_tags = f'picklist_type="{picklist_attrs.get("type")}" picklist_name="{picklist_attrs.get("name")}" target="{picklist_attrs.get("target")}"' - help_html = f'{help_link_text}' - if help_text_extra: - help_html = f"{help_text_extra}
{help_html}" - return help_html - - def __init__(self, picklist_attrs, label, help_link_text, help_text_extra=None, **kwargs): - min_rows = kwargs.pop("min_rows", 10) - help_link = self.get_help_html(picklist_attrs, help_link_text, help_text_extra) - widget = forms.Textarea(attrs={"rows": str(min_rows), "class": "govuk-!-margin-top-4"}) - super().__init__(label=label, help_text=help_link, widget=widget, **kwargs) - - -class SelectAdviceForm(forms.Form): - CHOICES = [("approve_all", "Approve all"), ("refuse_all", "Refuse all")] - - recommendation = forms.ChoiceField( - choices=CHOICES, - widget=forms.RadioSelect, - label="", - error_messages={"required": "Select if you approve all or refuse all"}, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.add_input(Submit("submit", "Continue")) - - -class ConsolidateSelectAdviceForm(SelectAdviceForm): - DOCUMENT_TITLE = "Recommend and combine case recommendation case" - CHOICES = [("approve", "Approve"), ("refuse", "Refuse")] - recommendation = forms.ChoiceField( - choices=CHOICES, - widget=forms.RadioSelect, - label="", - error_messages={"required": "Select if you approve or refuse"}, - ) - - def __init__(self, team_name, *args, **kwargs): - super().__init__(*args, **kwargs) - - recommendation_label = "What is the combined recommendation" - if team_name: - recommendation_label = f"{recommendation_label} for {team_name}" - self.fields["recommendation"].label = f"{recommendation_label}?" - - -class PicklistAdviceForm(forms.Form): - def _picklist_to_choices(self, picklist_data): - reasons_choices = [] - reasons_text = {"other": ""} - - for result in picklist_data["results"]: - key = "_".join(result.get("name").lower().split()) - choice = Choice(key, result.get("name")) - if result == picklist_data["results"][-1]: - choice = Choice(key, result.get("name"), divider="or") - reasons_choices.append(choice) - reasons_text[key] = result.get("text") - reasons_choices.append(Choice("other", "Other")) - return reasons_choices, reasons_text - - -class GiveApprovalAdviceForm(PicklistAdviceForm): - DOCUMENT_TITLE = "Recommend approval for this case" - approval_reasons = forms.CharField( - widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), - label="", - error_messages={"required": "Enter a reason for approving"}, - ) - proviso = forms.CharField( - widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), - label="", - required=False, - ) - instructions_to_exporter = forms.CharField( - widget=forms.Textarea(attrs={"rows": "3"}), - label="Add any instructions for the exporter (optional)", - help_text="These may be added to the licence cover letter, subject to review by the Licensing Unit.", - required=False, - ) - - footnote_details_radios = forms.ChoiceField( - label="Add a reporting footnote (optional)", - required=False, - widget=forms.RadioSelect, - choices=(), - ) - footnote_details = forms.CharField( - widget=forms.Textarea(attrs={"rows": 3, "class": "govuk-!-margin-top-4"}), - label="", - required=False, - ) - - approval_radios = forms.ChoiceField( - label="What is your reason for approving?", - required=False, - widget=forms.RadioSelect, - choices=(), - ) - proviso_radios = forms.ChoiceField( - label="Add a licence condition (optional)", - required=False, - widget=forms.RadioSelect, - choices=(), - ) - - def __init__(self, *args, **kwargs): - approval_reason = kwargs.pop("approval_reason") - proviso = kwargs.pop("proviso") - footnote_details = kwargs.pop("footnote_details") - super().__init__(*args, **kwargs) - # this follows the same pattern as denial_reasons. - approval_choices, approval_text = self._picklist_to_choices(approval_reason) - self.approval_text = approval_text - - proviso_choices, proviso_text = self._picklist_to_choices(proviso) - self.proviso_text = proviso_text - - footnote_details_choices, footnote_text = self._picklist_to_choices(footnote_details) - self.footnote_text = footnote_text - - self.fields["approval_radios"].choices = approval_choices - self.fields["proviso_radios"].choices = proviso_choices - self.fields["footnote_details_radios"].choices = footnote_details_choices - - self.helper = FormHelper() - self.helper.layout = Layout( - RadioTextArea("approval_radios", "approval_reasons", self.approval_text), - ExpandingFieldset( - RadioTextArea("proviso_radios", "proviso", self.proviso_text), - "instructions_to_exporter", - RadioTextArea("footnote_details_radios", "footnote_details", self.footnote_text), - legend="Add a licence condition, instruction to exporter or footnote", - summary_css_class="supplemental-approval-fields", - ), - Submit("submit", "Submit recommendation"), - ) - - -class ConsolidateApprovalForm(GiveApprovalAdviceForm): - """Approval form minus some fields.""" - - def __init__(self, team_alias, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.helper = FormHelper() - self.helper.layout = Layout( - RadioTextArea("approval_radios", "approval_reasons", self.approval_text), - RadioTextArea("proviso_radios", "proviso", self.proviso_text), - Submit("submit", "Submit recommendation"), - ) - - -class RefusalAdviceForm(PicklistAdviceForm): - denial_reasons = forms.MultipleChoiceField( - widget=forms.SelectMultiple(), - label="What is the refusal criteria?", - help_text=format_html( - f'Select all refusal criteria (opens in a new tab) that apply' - ), - error_messages={"required": "Select at least one refusal criteria"}, - ) - refusal_reasons_radios = forms.ChoiceField( - label="What are your reasons for this refusal?", - widget=forms.RadioSelect, - required=False, - choices=(), - ) - refusal_reasons = forms.CharField( - widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), - label="", - error_messages={"required": "Enter a reason for refusing"}, - ) - - def __init__(self, choices, *args, **kwargs): - refusal_reasons = kwargs.pop("refusal_reasons") - super().__init__(*args, **kwargs) - self.fields["denial_reasons"].choices = choices - label_size = {"label_size": "govuk-label--s"} - - refusal_reasons_choices, refusal_text = self._picklist_to_choices(refusal_reasons) - self.refusal_text = refusal_text - - self.fields["refusal_reasons_radios"].choices = refusal_reasons_choices - - self.helper = FormHelper() - self.helper.layout = Layout( - Field("denial_reasons", context=label_size), - RadioTextArea("refusal_reasons_radios", "refusal_reasons", refusal_text), - Submit("submit", "Submit recommendation"), - ) - - -class LUConsolidateRefusalForm(forms.Form): - refusal_note = forms.CharField( - widget=forms.Textarea(attrs={"rows": "7"}), - label="Enter the refusal note as agreed in the refusal meeting", - error_messages={"required": "Enter the refusal meeting note"}, - ) - - denial_reasons = forms.MultipleChoiceField( - widget=forms.SelectMultiple(), - label="What is the refusal criteria?", - help_text=format_html( - f'Select all refusal criteria (opens in a new tab) that apply' - ), - error_messages={"required": "Select at least one refusal criteria"}, - ) - - def __init__(self, choices, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["denial_reasons"].choices = choices - self.helper = FormHelper() - self.helper.layout = Layout("denial_reasons", "refusal_note", Submit("submit", "Submit recommendation")) - - -class DeleteAdviceForm(forms.Form): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.add_input(Submit("confirm", "Confirm")) - - -def get_formset(form_class, num=1, data=None, initial=None): - factory = formset_factory(form_class, extra=num, min_num=num, max_num=num) - return factory(data=data, initial=initial) - - -class CountersignAdviceForm(forms.Form): - DOCUMENT_TITLE = "Review and countersign this case" - approval_reasons = forms.CharField( - widget=forms.Textarea(attrs={"rows": "10"}), - label="Explain why you are agreeing with this recommendation", - error_messages={"required": "Enter why you agree with the recommendation"}, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_tag = False - self.helper.layout = Layout("approval_reasons") - - -class CountersignDecisionAdviceForm(forms.Form): - DECISION_CHOICES = [(True, "Yes"), (False, "No")] - - outcome_accepted = forms.TypedChoiceField( - choices=DECISION_CHOICES, - widget=forms.RadioSelect, - coerce=coerce_str_to_bool, - label="Do you agree with this recommendation?", - error_messages={"required": "Select yes if you agree with the recommendation"}, - ) - approval_reasons = forms.CharField( - widget=forms.Textarea(attrs={"rows": "10"}), - label="Explain your reasons", - required=False, - ) - rejected_reasons = forms.CharField( - widget=forms.Textarea(attrs={"rows": "10"}), - label="Message to the case officer (explaining why the case is being returned)", - required=False, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_tag = False - self.helper.layout = Layout( - ConditionalRadios( - "outcome_accepted", - ConditionalRadiosQuestion("Yes", Field("approval_reasons")), - ConditionalRadiosQuestion("No", Field("rejected_reasons")), - ), - ) - - def clean_approval_reasons(self): - outcome_accepted = self.cleaned_data.get("outcome_accepted") - approval_reasons = self.cleaned_data.get("approval_reasons") - if outcome_accepted and not self.cleaned_data.get("approval_reasons"): - self.add_error("approval_reasons", "Enter a reason for countersigning") - - return approval_reasons - - def clean_rejected_reasons(self): - outcome_accepted = self.cleaned_data.get("outcome_accepted") - rejected_reasons = self.cleaned_data.get("rejected_reasons") - if outcome_accepted is False and not self.cleaned_data.get("rejected_reasons"): - self.add_error("rejected_reasons", "Enter a message explaining why the case is being returned") - - return rejected_reasons - - -class FCDOApprovalAdviceForm(GiveApprovalAdviceForm): - def __init__(self, countries, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["countries"] = forms.MultipleChoiceField( - choices=countries.items(), - widget=GridmultipleSelect(), - label="Select countries for which you want to give advice", - error_messages={"required": "Select the destinations you want to make recommendations for"}, - ) - parent_layout = self.helper.layout - self.helper = FormHelper() - self.helper.layout = Layout( - "countries", - parent_layout, - ) - - -class FCDORefusalAdviceForm(RefusalAdviceForm): - def __init__(self, choices, countries, *args, **kwargs): - super().__init__(choices, *args, **kwargs) - self.fields["countries"] = forms.MultipleChoiceField( - choices=countries.items(), - widget=GridmultipleSelect(), - label="Select countries for which you want to give advice", - error_messages={"required": "Select the destinations you want to make recommendations for"}, - ) - self.helper.layout = Layout( - "countries", - "denial_reasons", - RadioTextArea("refusal_reasons_radios", "refusal_reasons", self.refusal_text), - Submit("submit", "Submit recommendation"), - ) - - -class MoveCaseForwardForm(forms.Form): - def __init__(self, move_case_button_label="Move case forward", *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.layout = Layout(Submit("submit", move_case_button_label, css_id="move-case-forward-button")) - - -class DESNZTriggerListFormBase(forms.Form): - TRIGGER_LIST_GUIDELINES_CHOICES = [(True, "Yes"), (False, "No")] - NCA_CHOICES = [(True, "Yes"), (False, "No")] - - is_trigger_list_guidelines_applicable = forms.TypedChoiceField( - choices=TRIGGER_LIST_GUIDELINES_CHOICES, - widget=forms.RadioSelect, - coerce=coerce_str_to_bool, - label="Do the trigger list guidelines apply to this product?", - help_text="Select no if the product is on the trigger list but falls outside the guidelines", - error_messages={ - "required": "Select yes if the trigger list guidelines apply to this product", - }, - ) - - is_nca_applicable = forms.TypedChoiceField( - choices=NCA_CHOICES, - coerce=coerce_str_to_bool, - error_messages={ - "required": "Select yes if a Nuclear Cooperation Agreement applies to the product", - }, - label="Does a Nuclear Cooperation Agreement apply?", - widget=forms.RadioSelect, - ) - - nsg_assessment_note = forms.CharField( - label="Add an assessment note (optional)", - required=False, - widget=forms.Textarea, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.layout = Layout( - "is_trigger_list_guidelines_applicable", - "is_nca_applicable", - "nsg_assessment_note", - Submit("submit", "Continue"), - ) - - -class DESNZTriggerListAssessmentForm(DESNZTriggerListFormBase): - def __init__( - self, - request, - queue_pk, - goods, - application_pk, - is_user_rfd, - organisation_documents, - *args, - **kwargs, - ): - super().__init__(*args, **kwargs) - - self.request = request - self.queue_pk = queue_pk - self.application_pk = application_pk - self.is_user_rfd = is_user_rfd - self.organisation_documents = organisation_documents - self.fields["goods"] = forms.MultipleChoiceField( - choices=self.get_goods_choices(goods), - widget=GoodsMultipleSelect(), - label=( - "Select a product to begin. Or you can select multiple products to give them the same assessment.

" - "You will then be asked to make a recommendation for all products on this application." - ), - error_messages={"required": "Select the products that you want to assess"}, - ) - - def get_goods_choices(self, goods): - return [ - ( - good_on_application_id, - { - "good_on_application": good_on_application, - "summary": get_good_on_application_tau_summary( - self.request, - good_on_application, - self.queue_pk, - self.application_pk, - self.is_user_rfd, - self.organisation_documents, - ), - }, - ) - for good_on_application_id, good_on_application in goods.items() - ] - - -class DESNZTriggerListAssessmentEditForm(DESNZTriggerListFormBase): - def __init__( - self, - request, - queue_pk, - application_pk, - is_user_rfd, - organisation_documents, - *args, - **kwargs, - ): - super().__init__(*args, **kwargs) - - self.request = request - self.queue_pk = queue_pk - self.application_pk = application_pk - self.is_user_rfd = is_user_rfd - self.organisation_documents = organisation_documents - - -class RecommendAnApprovalForm(PicklistAdviceForm, BaseForm): - class Layout: - TITLE = "Recommend an approval" - - approval_reasons = forms.CharField( - widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), - label="", - error_messages={"required": "Enter a reason for approving"}, - ) - approval_radios = forms.ChoiceField( - label="What is your reason for approving?", - required=False, - widget=forms.RadioSelect, - choices=(), - ) - add_licence_conditions = forms.BooleanField( - label="Add licence conditions, instructions to exporter or footnotes (optional)", - required=False, - ) - - def __init__(self, *args, **kwargs): - del kwargs["proviso"] - del kwargs["footnote_details"] - approval_reason = kwargs.pop("approval_reason") - # this follows the same pattern as denial_reasons. - approval_choices, approval_text = self._picklist_to_choices(approval_reason) - self.approval_text = approval_text - super().__init__(*args, **kwargs) - - self.fields["approval_radios"].choices = approval_choices - - def get_layout_fields(self): - return ( - RadioTextArea("approval_radios", "approval_reasons", self.approval_text), - "add_licence_conditions", - ) - - -class PicklistApprovalAdviceEditForm(BaseForm): - class Layout: - TITLE = "Add licence conditions, instructions to exporter or footnotes (optional)" - - proviso = forms.CharField( - widget=forms.Textarea(attrs={"rows": 30, "class": "govuk-!-margin-top-4"}), - label="", - required=False, - ) - - def __init__(self, *args, **kwargs): - del kwargs["approval_reason"] - del kwargs["proviso"] - del kwargs["footnote_details"] - super().__init__(*args, **kwargs) - - def get_layout_fields(self): - return ("proviso",) - - -class LicenceConditionsForm(PicklistAdviceForm, BaseForm): - class Layout: - TITLE = "Add licence conditions, instructions to exporter or footnotes (optional)" - - proviso = forms.CharField( - widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), - label="", - required=False, - ) - - approval_radios = forms.ChoiceField( - label="What is your reason for approving?", - required=False, - widget=forms.RadioSelect, - choices=(), - ) - proviso_checkboxes = forms.MultipleChoiceField( - label="Add a licence condition (optional)", - required=False, - widget=forms.CheckboxSelectMultiple, - choices=(), - ) - - def clean(self): - cleaned_data = super().clean() - # only return proviso (text) for selected radios, nothing else matters, join by 2 newlines - return {"proviso": "\r\n\r\n".join([cleaned_data[selected] for selected in cleaned_data["proviso_checkboxes"]])} - - def __init__(self, *args, **kwargs): - del kwargs["approval_reason"] - del kwargs["footnote_details"] - - proviso = kwargs.pop("proviso") - - proviso_choices, proviso_text = self._picklist_to_choices(proviso) - self.proviso_text = proviso_text - - self.conditional_checkbox_choices = ( - ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in proviso_choices - ) - - super().__init__(*args, **kwargs) - - self.fields["proviso_checkboxes"].choices = proviso_choices - for choices in proviso_choices: - self.fields[choices.value] = forms.CharField( - widget=forms.Textarea(attrs={"rows": 3, "class": "govuk-!-margin-top-4"}), - label="Description", - required=False, - initial=proviso_text[choices.value], - ) - - def get_layout_fields(self): - - return (ConditionalCheckboxes("proviso_checkboxes", *self.conditional_checkbox_choices),) - - -class FootnotesApprovalAdviceForm(PicklistAdviceForm, BaseForm): - class Layout: - TITLE = "Instructions for the exporter (optional)" - - instructions_to_exporter = forms.CharField( - widget=forms.Textarea(attrs={"rows": "3"}), - label="Add any instructions for the exporter (optional)", - help_text="These may be added to the licence cover letter, subject to review by the Licensing Unit.", - required=False, - ) - - footnote_details_radios = forms.ChoiceField( - label="Add a reporting footnote (optional)", - required=False, - widget=forms.RadioSelect, - choices=(), - ) - footnote_details = forms.CharField( - widget=forms.Textarea(attrs={"rows": 3, "class": "govuk-!-margin-top-4"}), - label="", - required=False, - ) - - def __init__(self, *args, **kwargs): - del kwargs["approval_reason"] - del kwargs["proviso"] - - footnote_details = kwargs.pop("footnote_details") - footnote_details_choices, footnote_text = self._picklist_to_choices(footnote_details) - self.footnote_text = footnote_text - - super().__init__(*args, **kwargs) - - self.fields["footnote_details_radios"].choices = footnote_details_choices - - def get_layout_fields(self): - return ( - "instructions_to_exporter", - RadioTextArea("footnote_details_radios", "footnote_details", self.footnote_text), - ) diff --git a/caseworker/advice/forms/approval.py b/caseworker/advice/forms/approval.py new file mode 100644 index 0000000000..858d18a40a --- /dev/null +++ b/caseworker/advice/forms/approval.py @@ -0,0 +1,239 @@ +from django import forms + +from core.common.forms import BaseForm +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Layout, Submit +from crispy_forms_gds.choices import Choice + +from core.forms.layouts import ( + ConditionalCheckboxes, + ConditionalCheckboxesQuestion, + RadioTextArea, +) +from core.forms.widgets import GridmultipleSelect + + +class SelectAdviceForm(forms.Form): + CHOICES = [("approve_all", "Approve all"), ("refuse_all", "Refuse all")] + + recommendation = forms.ChoiceField( + choices=CHOICES, + widget=forms.RadioSelect, + label="", + error_messages={"required": "Select if you approve all or refuse all"}, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.add_input(Submit("submit", "Continue")) + + +class PicklistAdviceForm(forms.Form): + def _picklist_to_choices(self, picklist_data): + reasons_choices = [] + reasons_text = {"other": ""} + + for result in picklist_data["results"]: + key = "_".join(result.get("name").lower().split()) + choice = Choice(key, result.get("name")) + if result == picklist_data["results"][-1]: + choice = Choice(key, result.get("name"), divider="or") + reasons_choices.append(choice) + reasons_text[key] = result.get("text") + reasons_choices.append(Choice("other", "Other")) + return reasons_choices, reasons_text + + +class MoveCaseForwardForm(forms.Form): + def __init__(self, move_case_button_label="Move case forward", *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.layout = Layout(Submit("submit", move_case_button_label, css_id="move-case-forward-button")) + + +class RecommendAnApprovalForm(PicklistAdviceForm, BaseForm): + class Layout: + TITLE = "Recommend an approval" + + approval_reasons = forms.CharField( + widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), + label="", + error_messages={"required": "Enter a reason for approving"}, + ) + approval_radios = forms.ChoiceField( + label="What is your reason for approving?", + required=False, + widget=forms.RadioSelect, + choices=(), + ) + add_licence_conditions = forms.BooleanField( + label="Add licence conditions, instructions to exporter or footnotes (optional)", + required=False, + ) + + def __init__(self, *args, **kwargs): + approval_reason = kwargs.pop("approval_reason") + # this follows the same pattern as denial_reasons. + approval_choices, approval_text = self._picklist_to_choices(approval_reason) + self.approval_text = approval_text + super().__init__(*args, **kwargs) + + self.fields["approval_radios"].choices = approval_choices + + def get_layout_fields(self): + return ( + RadioTextArea("approval_radios", "approval_reasons", self.approval_text), + "add_licence_conditions", + ) + + +class FCDOApprovalAdviceForm(RecommendAnApprovalForm): + class Layout: + TITLE = "Recommend an approval" + + def __init__(self, countries, *args, **kwargs): + countries = kwargs.pop("countries") + super().__init__(*args, **kwargs) + self.fields["countries"] = forms.MultipleChoiceField( + choices=countries.items(), + widget=GridmultipleSelect(), + label="Select countries for which you want to give advice", + error_messages={"required": "Select the destinations you want to make recommendations for"}, + ) + + def get_layout_fields(self): + return ( + RadioTextArea("approval_radios", "approval_reasons", self.approval_text), + "countries", + "add_licence_conditions", + ) + + +class DESNZApprovalForm(PicklistAdviceForm, BaseForm): + class Layout: + TITLE = "Recommend an approval" + + approval_reasons = forms.CharField( + widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), + label="", + error_messages={"required": "Enter a reason for approving"}, + ) + approval_radios = forms.ChoiceField( + label="What is your reason for approving?", + required=False, + widget=forms.RadioSelect, + choices=(), + ) + add_licence_conditions = forms.BooleanField( + label="Add licence conditions, instructions to exporter or footnotes (optional)", + required=False, + ) + + def __init__(self, *args, **kwargs): + approval_reason = kwargs.pop("approval_reason") + # this follows the same pattern as denial_reasons. + approval_choices, approval_text = self._picklist_to_choices(approval_reason) + self.approval_text = approval_text + super().__init__(*args, **kwargs) + + self.fields["approval_radios"].choices = approval_choices + + def get_layout_fields(self): + return ( + RadioTextArea("approval_radios", "approval_reasons", self.approval_text), + "add_licence_conditions", + ) + + +class LicenceConditionsForm(PicklistAdviceForm, BaseForm): + class Layout: + TITLE = "Add licence conditions, instructions to exporter or footnotes (optional)" + + proviso = forms.CharField( + widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), + label="", + required=False, + ) + + approval_radios = forms.ChoiceField( + label="What is your reason for approving?", + required=False, + widget=forms.RadioSelect, + choices=(), + ) + proviso_checkboxes = forms.MultipleChoiceField( + label="Add a licence condition (optional)", + required=False, + widget=forms.CheckboxSelectMultiple, + choices=(), + ) + + def clean(self): + cleaned_data = super().clean() + # only return proviso (text) for selected radios, nothing else matters, join by 2 newlines + return {"proviso": "\r\n\r\n".join([cleaned_data[selected] for selected in cleaned_data["proviso_checkboxes"]])} + + def __init__(self, *args, **kwargs): + proviso = kwargs.pop("proviso") + + proviso_choices, proviso_text = self._picklist_to_choices(proviso) + self.proviso_text = proviso_text + + self.conditional_checkbox_choices = ( + ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in proviso_choices + ) + + super().__init__(*args, **kwargs) + + self.fields["proviso_checkboxes"].choices = proviso_choices + for choices in proviso_choices: + self.fields[choices.value] = forms.CharField( + widget=forms.Textarea(attrs={"rows": 3, "class": "govuk-!-margin-top-4"}), + label="Description", + required=False, + initial=proviso_text[choices.value], + ) + + def get_layout_fields(self): + + return (ConditionalCheckboxes("proviso_checkboxes", *self.conditional_checkbox_choices),) + + +class FootnotesApprovalAdviceForm(PicklistAdviceForm, BaseForm): + class Layout: + TITLE = "Instructions for the exporter (optional)" + + instructions_to_exporter = forms.CharField( + widget=forms.Textarea(attrs={"rows": "3"}), + label="Add any instructions for the exporter (optional)", + help_text="These may be added to the licence cover letter, subject to review by the Licensing Unit.", + required=False, + ) + + footnote_details_radios = forms.ChoiceField( + label="Add a reporting footnote (optional)", + required=False, + widget=forms.RadioSelect, + choices=(), + ) + footnote_details = forms.CharField( + widget=forms.Textarea(attrs={"rows": 3, "class": "govuk-!-margin-top-4"}), + label="", + required=False, + ) + + def __init__(self, *args, **kwargs): + footnote_details = kwargs.pop("footnote_details") + footnote_details_choices, footnote_text = self._picklist_to_choices(footnote_details) + self.footnote_text = footnote_text + + super().__init__(*args, **kwargs) + + self.fields["footnote_details_radios"].choices = footnote_details_choices + + def get_layout_fields(self): + return ( + "instructions_to_exporter", + RadioTextArea("footnote_details_radios", "footnote_details", self.footnote_text), + ) diff --git a/caseworker/advice/forms/consolidate.py b/caseworker/advice/forms/consolidate.py new file mode 100644 index 0000000000..da594ced98 --- /dev/null +++ b/caseworker/advice/forms/consolidate.py @@ -0,0 +1,69 @@ +from django import forms +from django.utils.html import format_html + +from caseworker.advice.forms.approval import SelectAdviceForm +from caseworker.advice.forms.forms import GiveApprovalAdviceForm +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Layout, Submit + +from core.forms.layouts import ( + RadioTextArea, +) + + +class ConsolidateSelectAdviceForm(SelectAdviceForm): + DOCUMENT_TITLE = "Recommend and combine case recommendation case" + CHOICES = [("approve", "Approve"), ("refuse", "Refuse")] + recommendation = forms.ChoiceField( + choices=CHOICES, + widget=forms.RadioSelect, + label="", + error_messages={"required": "Select if you approve or refuse"}, + ) + + def __init__(self, team_name, *args, **kwargs): + super().__init__(*args, **kwargs) + + recommendation_label = "What is the combined recommendation" + if team_name: + recommendation_label = f"{recommendation_label} for {team_name}" + self.fields["recommendation"].label = f"{recommendation_label}?" + + +class ConsolidateApprovalForm(GiveApprovalAdviceForm): + """Approval form minus some fields.""" + + def __init__(self, team_alias, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.helper = FormHelper() + self.helper.layout = Layout( + RadioTextArea("approval_radios", "approval_reasons", self.approval_text), + RadioTextArea("proviso_radios", "proviso", self.proviso_text), + Submit("submit", "Submit recommendation"), + ) + + +class LUConsolidateRefusalForm(forms.Form): + refusal_note = forms.CharField( + widget=forms.Textarea(attrs={"rows": "7"}), + label="Enter the refusal note as agreed in the refusal meeting", + error_messages={"required": "Enter the refusal meeting note"}, + ) + + denial_reasons = forms.MultipleChoiceField( + widget=forms.SelectMultiple(), + label="What is the refusal criteria?", + help_text=format_html( + f'Select all refusal criteria (opens in a new tab) that apply' + ), + error_messages={"required": "Select at least one refusal criteria"}, + ) + + def __init__(self, choices, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["denial_reasons"].choices = choices + self.helper = FormHelper() + self.helper.layout = Layout("denial_reasons", "refusal_note", Submit("submit", "Submit recommendation")) diff --git a/caseworker/advice/forms/countersign.py b/caseworker/advice/forms/countersign.py new file mode 100644 index 0000000000..8fd07fcd0d --- /dev/null +++ b/caseworker/advice/forms/countersign.py @@ -0,0 +1,75 @@ +from django import forms + +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Field, Layout + +from core.forms.layouts import ( + ConditionalRadios, + ConditionalRadiosQuestion, +) +from core.forms.utils import coerce_str_to_bool + + +class CountersignAdviceForm(forms.Form): + DOCUMENT_TITLE = "Review and countersign this case" + approval_reasons = forms.CharField( + widget=forms.Textarea(attrs={"rows": "10"}), + label="Explain why you are agreeing with this recommendation", + error_messages={"required": "Enter why you agree with the recommendation"}, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_tag = False + self.helper.layout = Layout("approval_reasons") + + +class CountersignDecisionAdviceForm(forms.Form): + DECISION_CHOICES = [(True, "Yes"), (False, "No")] + + outcome_accepted = forms.TypedChoiceField( + choices=DECISION_CHOICES, + widget=forms.RadioSelect, + coerce=coerce_str_to_bool, + label="Do you agree with this recommendation?", + error_messages={"required": "Select yes if you agree with the recommendation"}, + ) + approval_reasons = forms.CharField( + widget=forms.Textarea(attrs={"rows": "10"}), + label="Explain your reasons", + required=False, + ) + rejected_reasons = forms.CharField( + widget=forms.Textarea(attrs={"rows": "10"}), + label="Message to the case officer (explaining why the case is being returned)", + required=False, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_tag = False + self.helper.layout = Layout( + ConditionalRadios( + "outcome_accepted", + ConditionalRadiosQuestion("Yes", Field("approval_reasons")), + ConditionalRadiosQuestion("No", Field("rejected_reasons")), + ), + ) + + def clean_approval_reasons(self): + outcome_accepted = self.cleaned_data.get("outcome_accepted") + approval_reasons = self.cleaned_data.get("approval_reasons") + if outcome_accepted and not self.cleaned_data.get("approval_reasons"): + self.add_error("approval_reasons", "Enter a reason for countersigning") + + return approval_reasons + + def clean_rejected_reasons(self): + outcome_accepted = self.cleaned_data.get("outcome_accepted") + rejected_reasons = self.cleaned_data.get("rejected_reasons") + if outcome_accepted is False and not self.cleaned_data.get("rejected_reasons"): + self.add_error("rejected_reasons", "Enter a message explaining why the case is being returned") + + return rejected_reasons diff --git a/caseworker/advice/forms/delete.py b/caseworker/advice/forms/delete.py new file mode 100644 index 0000000000..8f148713f1 --- /dev/null +++ b/caseworker/advice/forms/delete.py @@ -0,0 +1,11 @@ +from django import forms + +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Submit + + +class DeleteAdviceForm(forms.Form): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.add_input(Submit("confirm", "Confirm")) diff --git a/caseworker/advice/forms/edit.py b/caseworker/advice/forms/edit.py new file mode 100644 index 0000000000..f8e270a3f0 --- /dev/null +++ b/caseworker/advice/forms/edit.py @@ -0,0 +1,20 @@ +from django import forms + +from core.common.forms import BaseForm + + +class PicklistApprovalAdviceEditForm(BaseForm): + class Layout: + TITLE = "Add licence conditions, instructions to exporter or footnotes (optional)" + + proviso = forms.CharField( + widget=forms.Textarea(attrs={"rows": 30, "class": "govuk-!-margin-top-4"}), + label="", + required=False, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def get_layout_fields(self): + return ("proviso",) diff --git a/caseworker/advice/forms/forms.py b/caseworker/advice/forms/forms.py new file mode 100644 index 0000000000..bf5b274d66 --- /dev/null +++ b/caseworker/advice/forms/forms.py @@ -0,0 +1,138 @@ +from django import forms +from django.forms.formsets import formset_factory + +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Layout, Submit +from crispy_forms_gds.choices import Choice +from core.forms.layouts import ( + ExpandingFieldset, + RadioTextArea, +) +from core.forms.widgets import GridmultipleSelect + + +def get_formset(form_class, num=1, data=None, initial=None): + factory = formset_factory(form_class, extra=num, min_num=num, max_num=num) + return factory(data=data, initial=initial) + + +def get_approval_advice_form_factory(advice, approval_reason, proviso, footnote_details, data=None): + data = data or { + "proviso": advice["proviso"], + "approval_reasons": advice["text"], + "instructions_to_exporter": advice["note"], + "footnote_details": advice["footnote"], + } + return GiveApprovalAdviceForm( + approval_reason=approval_reason, proviso=proviso, footnote_details=footnote_details, data=data + ) + + +class PicklistAdviceForm(forms.Form): + def _picklist_to_choices(self, picklist_data): + reasons_choices = [] + reasons_text = {"other": ""} + + for result in picklist_data["results"]: + key = "_".join(result.get("name").lower().split()) + choice = Choice(key, result.get("name")) + if result == picklist_data["results"][-1]: + choice = Choice(key, result.get("name"), divider="or") + reasons_choices.append(choice) + reasons_text[key] = result.get("text") + reasons_choices.append(Choice("other", "Other")) + return reasons_choices, reasons_text + + +class GiveApprovalAdviceForm(PicklistAdviceForm): + DOCUMENT_TITLE = "Recommend approval for this case" + approval_reasons = forms.CharField( + widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), + label="", + error_messages={"required": "Enter a reason for approving"}, + ) + proviso = forms.CharField( + widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), + label="", + required=False, + ) + instructions_to_exporter = forms.CharField( + widget=forms.Textarea(attrs={"rows": "3"}), + label="Add any instructions for the exporter (optional)", + help_text="These may be added to the licence cover letter, subject to review by the Licensing Unit.", + required=False, + ) + + footnote_details_radios = forms.ChoiceField( + label="Add a reporting footnote (optional)", + required=False, + widget=forms.RadioSelect, + choices=(), + ) + footnote_details = forms.CharField( + widget=forms.Textarea(attrs={"rows": 3, "class": "govuk-!-margin-top-4"}), + label="", + required=False, + ) + + approval_radios = forms.ChoiceField( + label="What is your reason for approving?", + required=False, + widget=forms.RadioSelect, + choices=(), + ) + proviso_radios = forms.ChoiceField( + label="Add a licence condition (optional)", + required=False, + widget=forms.RadioSelect, + choices=(), + ) + + def __init__(self, *args, **kwargs): + approval_reason = kwargs.pop("approval_reason") + proviso = kwargs.pop("proviso") + footnote_details = kwargs.pop("footnote_details") + super().__init__(*args, **kwargs) + # this follows the same pattern as denial_reasons. + approval_choices, approval_text = self._picklist_to_choices(approval_reason) + self.approval_text = approval_text + + proviso_choices, proviso_text = self._picklist_to_choices(proviso) + self.proviso_text = proviso_text + + footnote_details_choices, footnote_text = self._picklist_to_choices(footnote_details) + self.footnote_text = footnote_text + + self.fields["approval_radios"].choices = approval_choices + self.fields["proviso_radios"].choices = proviso_choices + self.fields["footnote_details_radios"].choices = footnote_details_choices + + self.helper = FormHelper() + self.helper.layout = Layout( + RadioTextArea("approval_radios", "approval_reasons", self.approval_text), + ExpandingFieldset( + RadioTextArea("proviso_radios", "proviso", self.proviso_text), + "instructions_to_exporter", + RadioTextArea("footnote_details_radios", "footnote_details", self.footnote_text), + legend="Add a licence condition, instruction to exporter or footnote", + summary_css_class="supplemental-approval-fields", + ), + Submit("submit", "Submit recommendation"), + ) + + +class FCDOApprovalAdviceForm(GiveApprovalAdviceForm): + def __init__(self, countries, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["countries"] = forms.MultipleChoiceField( + choices=countries.items(), + widget=GridmultipleSelect(), + label="Select countries for which you want to give advice", + error_messages={"required": "Select the destinations you want to make recommendations for"}, + ) + parent_layout = self.helper.layout + self.helper = FormHelper() + self.helper.layout = Layout( + "countries", + parent_layout, + ) diff --git a/caseworker/advice/forms/products.py b/caseworker/advice/forms/products.py new file mode 100644 index 0000000000..27bebae893 --- /dev/null +++ b/caseworker/advice/forms/products.py @@ -0,0 +1,119 @@ +from django import forms + +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Layout, Submit + +from core.forms.utils import coerce_str_to_bool +from caseworker.tau.summaries import get_good_on_application_tau_summary +from caseworker.tau.widgets import GoodsMultipleSelect + + +class DESNZTriggerListFormBase(forms.Form): + TRIGGER_LIST_GUIDELINES_CHOICES = [(True, "Yes"), (False, "No")] + NCA_CHOICES = [(True, "Yes"), (False, "No")] + + is_trigger_list_guidelines_applicable = forms.TypedChoiceField( + choices=TRIGGER_LIST_GUIDELINES_CHOICES, + widget=forms.RadioSelect, + coerce=coerce_str_to_bool, + label="Do the trigger list guidelines apply to this product?", + help_text="Select no if the product is on the trigger list but falls outside the guidelines", + error_messages={ + "required": "Select yes if the trigger list guidelines apply to this product", + }, + ) + + is_nca_applicable = forms.TypedChoiceField( + choices=NCA_CHOICES, + coerce=coerce_str_to_bool, + error_messages={ + "required": "Select yes if a Nuclear Cooperation Agreement applies to the product", + }, + label="Does a Nuclear Cooperation Agreement apply?", + widget=forms.RadioSelect, + ) + + nsg_assessment_note = forms.CharField( + label="Add an assessment note (optional)", + required=False, + widget=forms.Textarea, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.layout = Layout( + "is_trigger_list_guidelines_applicable", + "is_nca_applicable", + "nsg_assessment_note", + Submit("submit", "Continue"), + ) + + +class DESNZTriggerListAssessmentForm(DESNZTriggerListFormBase): + def __init__( + self, + request, + queue_pk, + goods, + application_pk, + is_user_rfd, + organisation_documents, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + + self.request = request + self.queue_pk = queue_pk + self.application_pk = application_pk + self.is_user_rfd = is_user_rfd + self.organisation_documents = organisation_documents + self.fields["goods"] = forms.MultipleChoiceField( + choices=self.get_goods_choices(goods), + widget=GoodsMultipleSelect(), + label=( + "Select a product to begin. Or you can select multiple products to give them the same assessment.

" + "You will then be asked to make a recommendation for all products on this application." + ), + error_messages={"required": "Select the products that you want to assess"}, + ) + + def get_goods_choices(self, goods): + return [ + ( + good_on_application_id, + { + "good_on_application": good_on_application, + "summary": get_good_on_application_tau_summary( + self.request, + good_on_application, + self.queue_pk, + self.application_pk, + self.is_user_rfd, + self.organisation_documents, + ), + }, + ) + for good_on_application_id, good_on_application in goods.items() + ] + + +class DESNZTriggerListAssessmentEditForm(DESNZTriggerListFormBase): + def __init__( + self, + request, + queue_pk, + application_pk, + is_user_rfd, + organisation_documents, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + + self.request = request + self.queue_pk = queue_pk + self.application_pk = application_pk + self.is_user_rfd = is_user_rfd + self.organisation_documents = organisation_documents diff --git a/caseworker/advice/forms/refusal.py b/caseworker/advice/forms/refusal.py new file mode 100644 index 0000000000..3e05a88d7a --- /dev/null +++ b/caseworker/advice/forms/refusal.py @@ -0,0 +1,78 @@ +from django import forms +from django.utils.html import format_html + +from caseworker.advice.forms.approval import PicklistAdviceForm +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Field, Layout, Submit + +from core.forms.layouts import ( + RadioTextArea, +) +from core.forms.widgets import GridmultipleSelect + + +def get_refusal_advice_form_factory(advice, denial_reasons_choices, refusal_reasons, data=None): + data = data or { + "refusal_reasons": advice["text"], + "denial_reasons": [r for r in advice["denial_reasons"]], + } + return RefusalAdviceForm(data=data, choices=denial_reasons_choices, refusal_reasons=refusal_reasons) + + +class RefusalAdviceForm(PicklistAdviceForm): + denial_reasons = forms.MultipleChoiceField( + widget=forms.SelectMultiple(), + label="What is the refusal criteria?", + help_text=format_html( + f'Select all refusal criteria (opens in a new tab) that apply' + ), + error_messages={"required": "Select at least one refusal criteria"}, + ) + refusal_reasons_radios = forms.ChoiceField( + label="What are your reasons for this refusal?", + widget=forms.RadioSelect, + required=False, + choices=(), + ) + refusal_reasons = forms.CharField( + widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), + label="", + error_messages={"required": "Enter a reason for refusing"}, + ) + + def __init__(self, choices, *args, **kwargs): + refusal_reasons = kwargs.pop("refusal_reasons") + super().__init__(*args, **kwargs) + self.fields["denial_reasons"].choices = choices + label_size = {"label_size": "govuk-label--s"} + + refusal_reasons_choices, refusal_text = self._picklist_to_choices(refusal_reasons) + self.refusal_text = refusal_text + + self.fields["refusal_reasons_radios"].choices = refusal_reasons_choices + + self.helper = FormHelper() + self.helper.layout = Layout( + Field("denial_reasons", context=label_size), + RadioTextArea("refusal_reasons_radios", "refusal_reasons", refusal_text), + Submit("submit", "Submit recommendation"), + ) + + +class FCDORefusalAdviceForm(RefusalAdviceForm): + def __init__(self, choices, countries, *args, **kwargs): + super().__init__(choices, *args, **kwargs) + self.fields["countries"] = forms.MultipleChoiceField( + choices=countries.items(), + widget=GridmultipleSelect(), + label="Select countries for which you want to give advice", + error_messages={"required": "Select the destinations you want to make recommendations for"}, + ) + self.helper.layout = Layout( + "countries", + "denial_reasons", + RadioTextArea("refusal_reasons_radios", "refusal_reasons", self.refusal_text), + Submit("submit", "Submit recommendation"), + ) diff --git a/caseworker/advice/picklist_helpers.py b/caseworker/advice/picklist_helpers.py new file mode 100644 index 0000000000..4ba00d8a8d --- /dev/null +++ b/caseworker/advice/picklist_helpers.py @@ -0,0 +1,31 @@ +from caseworker.picklists.services import get_picklists_list + + +def approval_picklist(self): + return { + "approval_reason": get_picklists_list( + self.request, type="standard_advice", disable_pagination=True, show_deactivated=False + ) + } + + +def fcdo_picklist(self): + return { + "approval_reason": get_picklists_list( + self.request, type="standard_advice", disable_pagination=True, show_deactivated=False + ) + } + + +def proviso_picklist(self): + return { + "proviso": get_picklists_list(self.request, type="proviso", disable_pagination=True, show_deactivated=False) + } + + +def footnote_picklist(self): + return { + "footnote_details": get_picklists_list( + self.request, type="footnotes", disable_pagination=True, show_deactivated=False + ) + } diff --git a/caseworker/advice/urls.py b/caseworker/advice/urls.py index 9414505c41..a2ff3acd90 100644 --- a/caseworker/advice/urls.py +++ b/caseworker/advice/urls.py @@ -1,17 +1,19 @@ from django.urls import path from caseworker.advice.views import views, consolidate_advice +from caseworker.advice.views.add_advice import GiveApprovalAdviceView +from caseworker.advice.views.edit_advice import EditAdviceView urlpatterns = [ path("", views.AdviceView.as_view(), name="advice_view"), path("case-details/", views.CaseDetailView.as_view(), name="case_details"), path("select-advice/", views.SelectAdviceView.as_view(), name="select_advice"), path("approve-all-legacy/", views.GiveApprovalAdviceViewLegacy.as_view(), name="approve_all_legacy"), - path("approve-all/", views.GiveApprovalAdviceView.as_view(), name="approve_all"), + path("approve-all/", GiveApprovalAdviceView.as_view(), name="approve_all"), path("refuse-all/", views.RefusalAdviceView.as_view(), name="refuse_all"), path("view-my-advice/", views.AdviceDetailView.as_view(), name="view_my_advice"), path("edit-advice-legacy/", views.EditAdviceViewLegacy.as_view(), name="edit_advice_legacy"), - path("edit-advice/", views.EditAdviceView.as_view(), name="edit_advice"), + path("edit-advice/", EditAdviceView.as_view(), name="edit_advice"), path("delete-advice/", views.DeleteAdviceView.as_view(), name="delete_advice"), path("countersign/", views.CountersignAdviceView.as_view(), name="countersign_advice_view"), path("countersign/review-advice/", views.ReviewCountersignView.as_view(), name="countersign_review"), diff --git a/caseworker/advice/views/add_advice.py b/caseworker/advice/views/add_advice.py new file mode 100644 index 0000000000..fe21672147 --- /dev/null +++ b/caseworker/advice/views/add_advice.py @@ -0,0 +1,80 @@ +from http import HTTPStatus +from caseworker.advice.conditionals import form_add_licence_conditions, is_desnz_team +from caseworker.advice.forms.approval import ( + FootnotesApprovalAdviceForm, + LicenceConditionsForm, + RecommendAnApprovalForm, +) +from caseworker.advice.payloads import GiveApprovalAdvicePayloadBuilder +from caseworker.advice.picklist_helpers import approval_picklist, footnote_picklist, proviso_picklist +from core.wizard.views import BaseSessionWizardView +from core.wizard.conditionals import C +from django.shortcuts import redirect +from django.urls import reverse +from caseworker.advice.views.mixins import CaseContextMixin +from caseworker.advice import services + +from caseworker.advice.constants import AdviceSteps +from core.auth.views import LoginRequiredMixin +from core.decorators import expect_status + + +# class SelectAdviceView(LoginRequiredMixin, CaseContextMixin, FormView): +# template_name = "advice/select_advice.html" +# form_class = SelectAdviceForm + +# def get_success_url(self): +# recommendation = self.request.POST.get("recommendation") +# if recommendation == "approve_all": +# return reverse("cases:approve_all", kwargs=self.kwargs) +# else: +# return reverse("cases:refuse_all", kwargs=self.kwargs) + +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) +# return {**context, "security_approvals_classified_display": self.security_approvals_classified_display} + + +class GiveApprovalAdviceView(LoginRequiredMixin, CaseContextMixin, BaseSessionWizardView): + + form_list = [ + (AdviceSteps.RECOMMEND_APPROVAL, RecommendAnApprovalForm), + (AdviceSteps.LICENCE_CONDITIONS, LicenceConditionsForm), + (AdviceSteps.LICENCE_FOOTNOTES, FootnotesApprovalAdviceForm), + ] + + condition_dict = { + AdviceSteps.RECOMMEND_APPROVAL: C(is_desnz_team), + AdviceSteps.LICENCE_CONDITIONS: C(form_add_licence_conditions(AdviceSteps.RECOMMEND_APPROVAL)), + AdviceSteps.LICENCE_FOOTNOTES: C(form_add_licence_conditions(AdviceSteps.RECOMMEND_APPROVAL)), + } + + step_kwargs = { + AdviceSteps.RECOMMEND_APPROVAL: approval_picklist, + AdviceSteps.LICENCE_CONDITIONS: proviso_picklist, + AdviceSteps.LICENCE_FOOTNOTES: footnote_picklist, + } + + def get_success_url(self): + return reverse("cases:view_my_advice", kwargs=self.kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["back_link_url"] = reverse("cases:advice_view", kwargs=self.kwargs) + return context + + @expect_status( + HTTPStatus.CREATED, + "Error adding approval advice", + "Unexpected error adding approval advice", + ) + def post_approval_advice(self, data): + return services.post_approval_advice(self.request, self.case, data) + + def get_payload(self, form_dict): + return GiveApprovalAdvicePayloadBuilder().build(form_dict) + + def done(self, form_list, form_dict, **kwargs): + data = self.get_payload(form_dict) + self.post_approval_advice(data) + return redirect(self.get_success_url()) diff --git a/caseworker/advice/views/consolidate_advice.py b/caseworker/advice/views/consolidate_advice.py index 08c1ef093c..b8dbf40b66 100644 --- a/caseworker/advice/views/consolidate_advice.py +++ b/caseworker/advice/views/consolidate_advice.py @@ -4,9 +4,14 @@ from requests.exceptions import HTTPError +from caseworker.advice.forms.refusal import RefusalAdviceForm from core.auth.views import LoginRequiredMixin -from caseworker.advice import forms +from caseworker.advice.forms.consolidate import ( + ConsolidateApprovalForm, + ConsolidateSelectAdviceForm, + LUConsolidateRefusalForm, +) from caseworker.advice.views.views import CaseContextMixin from caseworker.advice import services from caseworker.core.services import get_denial_reasons, group_denial_reasons @@ -38,7 +43,7 @@ class ConsolidateSelectDecisionView(BaseConsolidationView): """ template_name = "advice/review_consolidate.html" - form_class = forms.ConsolidateSelectAdviceForm + form_class = ConsolidateSelectAdviceForm def dispatch(self, request, *args, **kwargs): self.team_alias = self.caseworker["team"].get("alias", None) @@ -80,7 +85,7 @@ class ConsolidateApproveView(BaseConsolidationView): """ template_name = "advice/review_consolidate.html" - form_class = forms.ConsolidateApprovalForm + form_class = ConsolidateApprovalForm def setup(self, *args, **kwargs): super().setup(*args, **kwargs) @@ -142,7 +147,7 @@ class LUConsolidateRefuseView(BaseConsolidateRefuseView): Consolidate advice and refuse for LU. """ - form_class = forms.LUConsolidateRefusalForm + form_class = LUConsolidateRefusalForm advice_level = "final-advice" def get_form_kwargs(self): @@ -164,7 +169,7 @@ class ConsolidateRefuseView(BaseConsolidateRefuseView): Consolidate advice and refuse for non-LU. Currently MOD-ECJU. """ - form_class = forms.RefusalAdviceForm + form_class = RefusalAdviceForm advice_level = "team-advice" def get_form_kwargs(self): diff --git a/caseworker/advice/views/edit_advice.py b/caseworker/advice/views/edit_advice.py new file mode 100644 index 0000000000..750ef15756 --- /dev/null +++ b/caseworker/advice/views/edit_advice.py @@ -0,0 +1,42 @@ +from caseworker.advice.forms.approval import FootnotesApprovalAdviceForm, RecommendAnApprovalForm +from caseworker.advice.forms.edit import PicklistApprovalAdviceEditForm +from caseworker.advice.views.add_advice import GiveApprovalAdviceView +from caseworker.advice import services +from caseworker.advice.constants import AdviceSteps +from caseworker.advice.picklist_helpers import approval_picklist, footnote_picklist + + +class EditAdviceView(GiveApprovalAdviceView): + + form_list = [ + (AdviceSteps.RECOMMEND_APPROVAL, RecommendAnApprovalForm), + (AdviceSteps.LICENCE_CONDITIONS, PicklistApprovalAdviceEditForm), + (AdviceSteps.LICENCE_FOOTNOTES, FootnotesApprovalAdviceForm), + ] + + step_kwargs = { + AdviceSteps.RECOMMEND_APPROVAL: approval_picklist, + AdviceSteps.LICENCE_CONDITIONS: None, + AdviceSteps.LICENCE_FOOTNOTES: footnote_picklist, + } + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["back_link_url"] = self.get_success_url() + return context + + def get_form_initial(self, step): + my_advice = services.filter_current_user_advice(self.case.advice, self.caseworker_id) + advice = my_advice[0] + + # When the form is prepopulated in the edit flow, + # the radio values are set to other because only the textfield values are stored it's not possible to replay the selected radio. + return { + "add_licence_conditions": bool(advice.get("proviso")), + "approval_reasons": advice.get("text", ""), + "approval_radios": "other", + "proviso": advice.get("proviso"), + "instructions_to_exporter": advice.get("note", ""), + "footnote_details": advice.get("footnote", ""), + "footnote_details_radios": "other", + } diff --git a/caseworker/advice/views/mixins.py b/caseworker/advice/views/mixins.py new file mode 100644 index 0000000000..08d2b11a2c --- /dev/null +++ b/caseworker/advice/views/mixins.py @@ -0,0 +1,110 @@ +from django.utils.functional import cached_property + +from caseworker.advice import services +from caseworker.cases.services import get_case +from caseworker.core.services import get_denial_reasons +from caseworker.users.services import get_gov_user + +from core.constants import SecurityClassifiedApprovalsType + + +class CaseContextMixin: + """Most advice views need a reference to the associated + Case object. This mixin, injects a reference to the Case + in the context. + """ + + @property + def case_id(self): + return str(self.kwargs["pk"]) + + @cached_property + def case(self): + return get_case(self.request, self.case_id) + + @cached_property + def denial_reasons_display(self): + denial_reasons_data = get_denial_reasons(self.request) + return {denial_reason["id"]: denial_reason["display_value"] for denial_reason in denial_reasons_data} + + @property + def security_approvals_classified_display(self): + security_approvals = self.case["data"].get("security_approvals") + if security_approvals: + security_approvals_dict = dict(SecurityClassifiedApprovalsType.choices) + return ", ".join([security_approvals_dict[approval] for approval in security_approvals]) + return "" + + @property + def caseworker_id(self): + return str(self.request.session["lite_api_user_id"]) + + @property + def caseworker(self): + data, _ = get_gov_user(self.request, self.caseworker_id) + return data["user"] + + @property + def goods(self): + for index, good_on_application in enumerate(self.case["data"]["goods"], start=1): + good_on_application["line_number"] = index + + return self.case["data"]["goods"] + + def get_context(self, **kwargs): + return {} + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # Ideally, we would probably want to not use the following + # That said, if you look at the code, it is functional and + # doesn't have anything to do with e.g. lite-forms + # P.S. the case here is needed for rendering the base + # template (layouts/case.html) from which we are inheriting. + + is_in_lu_team = self.caseworker["team"]["alias"] == services.LICENSING_UNIT_TEAM + rejected_lu_countersignature = None + if is_in_lu_team: + rejected_lu_countersignature = self.rejected_countersign_advice() + + return { + **context, + **self.get_context(case=self.case), + "case": self.case, + "queue_pk": self.kwargs["queue_pk"], + "caseworker": self.caseworker, + "is_lu_countersigning": is_in_lu_team, + "rejected_lu_countersignature": rejected_lu_countersignature, + } + + def rejected_countersign_advice(self): + """ + Return rejected countersignature. Due to the routing, there should only ever be one + rejection (case will be returned to edit the advice once a rejection has occurred. + """ + for cs in self.case.get("countersign_advice", []): + if cs["valid"] and not cs["outcome_accepted"]: + return cs + return None + + +class DESNZNuclearMixin: + def is_trigger_list_assessed(self, product): + """Returns True if a product has been assessed for trigger list criteria""" + return product.get("is_trigger_list_guidelines_applicable") in [True, False] + + @property + def unassessed_trigger_list_goods(self): + return [ + product + for product in services.filter_trigger_list_products(self.goods) + if not self.is_trigger_list_assessed(product) + ] + + @property + def assessed_trigger_list_goods(self): + return [ + product + for product in services.filter_trigger_list_products(self.goods) + if self.is_trigger_list_assessed(product) + ] diff --git a/caseworker/advice/views/views.py b/caseworker/advice/views/views.py index dec7db0315..da6da9c4c5 100644 --- a/caseworker/advice/views/views.py +++ b/caseworker/advice/views/views.py @@ -1,8 +1,20 @@ from http import HTTPStatus -from caseworker.advice.conditionals import form_add_licence_conditions, is_desnz_team -from caseworker.advice.payloads import GiveApprovalAdvicePayloadBuilder -from core.wizard.views import BaseSessionWizardView -from core.wizard.conditionals import C +from caseworker.advice.forms.approval import MoveCaseForwardForm, SelectAdviceForm +from caseworker.advice.forms.consolidate import ( + ConsolidateApprovalForm, + ConsolidateSelectAdviceForm, + LUConsolidateRefusalForm, +) +from caseworker.advice.forms.countersign import CountersignAdviceForm, CountersignDecisionAdviceForm +from caseworker.advice.forms.delete import DeleteAdviceForm +from caseworker.advice.forms.forms import ( + FCDOApprovalAdviceForm, + GiveApprovalAdviceForm, + get_approval_advice_form_factory, + get_formset, +) +from caseworker.advice.forms.refusal import FCDORefusalAdviceForm, RefusalAdviceForm, get_refusal_advice_form_factory +from caseworker.advice.views.mixins import CaseContextMixin, DESNZNuclearMixin import sentry_sdk from django.http import Http404, HttpResponseRedirect from django.shortcuts import redirect @@ -11,127 +23,25 @@ from django.views.generic import FormView, TemplateView from requests.exceptions import HTTPError -from caseworker.advice import forms, services, constants -from caseworker.advice.forms import DESNZTriggerListAssessmentForm, DESNZTriggerListAssessmentEditForm +from caseworker.advice import services, constants +from caseworker.advice.forms.products import DESNZTriggerListAssessmentForm, DESNZTriggerListAssessmentEditForm + from caseworker.cases.helpers.case import CaseworkerMixin -from caseworker.cases.services import get_case, get_final_decision_documents +from caseworker.cases.services import get_final_decision_documents from caseworker.cases.helpers.ecju_queries import has_open_queries from caseworker.cases.views.main import CaseTabsMixin from caseworker.core.helpers import get_organisation_documents from caseworker.core.services import get_denial_reasons, group_denial_reasons from caseworker.picklists.services import get_picklists_list from caseworker.tau.summaries import get_good_on_application_tau_summary -from caseworker.users.services import get_gov_user -from caseworker.advice.constants import AdviceType, AdviceSteps +from caseworker.advice.constants import AdviceType from core import client from core.auth.views import LoginRequiredMixin -from core.constants import SecurityClassifiedApprovalsType, OrganisationDocumentType +from core.constants import OrganisationDocumentType from core.decorators import expect_status -class CaseContextMixin: - """Most advice views need a reference to the associated - Case object. This mixin, injects a reference to the Case - in the context. - """ - - @property - def case_id(self): - return str(self.kwargs["pk"]) - - @cached_property - def case(self): - return get_case(self.request, self.case_id) - - @cached_property - def denial_reasons_display(self): - denial_reasons_data = get_denial_reasons(self.request) - return {denial_reason["id"]: denial_reason["display_value"] for denial_reason in denial_reasons_data} - - @property - def security_approvals_classified_display(self): - security_approvals = self.case["data"].get("security_approvals") - if security_approvals: - security_approvals_dict = dict(SecurityClassifiedApprovalsType.choices) - return ", ".join([security_approvals_dict[approval] for approval in security_approvals]) - return "" - - @property - def caseworker_id(self): - return str(self.request.session["lite_api_user_id"]) - - @property - def caseworker(self): - data, _ = get_gov_user(self.request, self.caseworker_id) - return data["user"] - - @property - def goods(self): - for index, good_on_application in enumerate(self.case["data"]["goods"], start=1): - good_on_application["line_number"] = index - - return self.case["data"]["goods"] - - def get_context(self, **kwargs): - return {} - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - # Ideally, we would probably want to not use the following - # That said, if you look at the code, it is functional and - # doesn't have anything to do with e.g. lite-forms - # P.S. the case here is needed for rendering the base - # template (layouts/case.html) from which we are inheriting. - - is_in_lu_team = self.caseworker["team"]["alias"] == services.LICENSING_UNIT_TEAM - rejected_lu_countersignature = None - if is_in_lu_team: - rejected_lu_countersignature = self.rejected_countersign_advice() - - return { - **context, - **self.get_context(case=self.case), - "case": self.case, - "queue_pk": self.kwargs["queue_pk"], - "caseworker": self.caseworker, - "is_lu_countersigning": is_in_lu_team, - "rejected_lu_countersignature": rejected_lu_countersignature, - } - - def rejected_countersign_advice(self): - """ - Return rejected countersignature. Due to the routing, there should only ever be one - rejection (case will be returned to edit the advice once a rejection has occurred. - """ - for cs in self.case.get("countersign_advice", []): - if cs["valid"] and not cs["outcome_accepted"]: - return cs - return None - - -class DESNZNuclearMixin: - def is_trigger_list_assessed(self, product): - """Returns True if a product has been assessed for trigger list criteria""" - return product.get("is_trigger_list_guidelines_applicable") in [True, False] - - @property - def unassessed_trigger_list_goods(self): - return [ - product - for product in services.filter_trigger_list_products(self.goods) - if not self.is_trigger_list_assessed(product) - ] - - @property - def assessed_trigger_list_goods(self): - return [ - product - for product in services.filter_trigger_list_products(self.goods) - if self.is_trigger_list_assessed(product) - ] - - class CaseDetailView(LoginRequiredMixin, CaseContextMixin, TemplateView): """This endpoint renders case detail panel. This will probably not be used stand-alone. This is useful for testing the case @@ -143,7 +53,7 @@ class CaseDetailView(LoginRequiredMixin, CaseContextMixin, TemplateView): class SelectAdviceView(LoginRequiredMixin, CaseContextMixin, FormView): template_name = "advice/select_advice.html" - form_class = forms.SelectAdviceForm + form_class = SelectAdviceForm def get_success_url(self): recommendation = self.request.POST.get("recommendation") @@ -168,12 +78,12 @@ class GiveApprovalAdviceViewLegacy(LoginRequiredMixin, CaseContextMixin, FormVie def get_form(self): if self.caseworker["team"]["alias"] == services.FCDO_TEAM: - return forms.FCDOApprovalAdviceForm( + return FCDOApprovalAdviceForm( services.unadvised_countries(self.caseworker, self.case), **self.get_form_kwargs(), ) else: - return forms.GiveApprovalAdviceForm(**self.get_form_kwargs()) + return GiveApprovalAdviceForm(**self.get_form_kwargs()) def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -215,13 +125,13 @@ def get_form(self): choices = group_denial_reasons(denial_reasons) if self.caseworker["team"]["alias"] == services.FCDO_TEAM: - return forms.FCDORefusalAdviceForm( + return FCDORefusalAdviceForm( choices, services.unadvised_countries(self.caseworker, self.case), **self.get_form_kwargs(), ) else: - return forms.RefusalAdviceForm(choices, **self.get_form_kwargs()) + return RefusalAdviceForm(choices, **self.get_form_kwargs()) def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -246,7 +156,7 @@ def get_context_data(self, **kwargs): class AdviceDetailView(LoginRequiredMixin, CaseTabsMixin, CaseContextMixin, DESNZNuclearMixin, FormView): template_name = "advice/view_my_advice.html" - form_class = forms.MoveCaseForwardForm + form_class = MoveCaseForwardForm def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -312,14 +222,14 @@ def get_form(self): self.request, type="footnotes", disable_pagination=True, show_deactivated=False ) - return forms.get_approval_advice_form_factory( + return get_approval_advice_form_factory( advice, approval_refusal_reasons, proviso, footnote_details, self.request.POST ) elif advice["type"]["key"] == "refuse": self.template_name = "advice/refusal_advice.html" denial_reasons = get_denial_reasons(self.request) choices = group_denial_reasons(denial_reasons) - return forms.get_refusal_advice_form_factory(advice, choices, approval_refusal_reasons, self.request.POST) + return get_refusal_advice_form_factory(advice, choices, approval_refusal_reasons, self.request.POST) else: raise ValueError("Invalid advice type encountered") @@ -338,9 +248,9 @@ def form_valid(self, form): # the advice should be applied and so we pop that in using a method. if self.caseworker["team"]["alias"] == services.FCDO_TEAM: data["countries"] = self.advised_countries() - if isinstance(form, forms.GiveApprovalAdviceForm): + if isinstance(form, GiveApprovalAdviceForm): services.post_approval_advice(self.request, self.case, data) - elif isinstance(form, forms.RefusalAdviceForm): + elif isinstance(form, RefusalAdviceForm): data["text"] = data["refusal_reasons"] services.post_refusal_advice(self.request, self.case, data) else: @@ -357,91 +267,9 @@ def get_context_data(self, **kwargs): return context -class GiveApprovalAdviceView(LoginRequiredMixin, CaseContextMixin, BaseSessionWizardView): - - form_list = [ - (AdviceSteps.RECOMMEND_APPROVAL, forms.RecommendAnApprovalForm), - (AdviceSteps.LICENCE_CONDITIONS, forms.LicenceConditionsForm), - (AdviceSteps.LICENCE_FOOTNOTES, forms.FootnotesApprovalAdviceForm), - ] - - condition_dict = { - AdviceSteps.RECOMMEND_APPROVAL: C(is_desnz_team), - AdviceSteps.LICENCE_CONDITIONS: C(form_add_licence_conditions("recommend_approval")), - AdviceSteps.LICENCE_FOOTNOTES: C(form_add_licence_conditions("recommend_approval")), - } - - def get_form_kwargs(self, step=None): - kwargs = super().get_form_kwargs(step) - kwargs["approval_reason"] = get_picklists_list( - self.request, type="standard_advice", disable_pagination=True, show_deactivated=False - ) - kwargs["proviso"] = get_picklists_list( - self.request, type="proviso", disable_pagination=True, show_deactivated=False - ) - kwargs["footnote_details"] = get_picklists_list( - self.request, type="footnotes", disable_pagination=True, show_deactivated=False - ) - return kwargs - - def get_success_url(self): - return reverse("cases:view_my_advice", kwargs=self.kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["back_link_url"] = reverse("cases:advice_view", kwargs=self.kwargs) - return context - - @expect_status( - HTTPStatus.CREATED, - "Error adding approval advice", - "Unexpected error adding approval advice", - ) - def post_approval_advice(self, data): - return services.post_approval_advice(self.request, self.case, data) - - def get_payload(self, form_dict): - return GiveApprovalAdvicePayloadBuilder().build(form_dict) - - def done(self, form_list, form_dict, **kwargs): - data = self.get_payload(form_dict) - self.post_approval_advice(data) - return redirect(self.get_success_url()) - - -class EditAdviceView(GiveApprovalAdviceView): - - form_list = [ - (AdviceSteps.RECOMMEND_APPROVAL, forms.RecommendAnApprovalForm), - (AdviceSteps.LICENCE_CONDITIONS, forms.PicklistApprovalAdviceEditForm), - (AdviceSteps.LICENCE_FOOTNOTES, forms.FootnotesApprovalAdviceForm), - ] - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["back_link_url"] = self.get_success_url() - return context - - def get_form_initial(self, step): - my_advice = services.filter_current_user_advice(self.case.advice, self.caseworker_id) - advice = my_advice[0] - - # When the form is prepopulated in the edit flow, - # the radio values are set to other because only the textfield values are stored it's not possible to replay the selected radio. - return { - "add_licence_conditions": bool(advice.get("proviso")), - "approval_reasons": advice.get("text", ""), - "approval_radios": "other", - "proviso": advice.get("proviso"), - "instructions_to_exporter": advice.get("note", ""), - "footnote_details": advice.get("footnote", ""), - "footnote_details_radios": "other", - } - - class DeleteAdviceView(LoginRequiredMixin, CaseContextMixin, FormView): template_name = "advice/delete-advice.html" - form_class = forms.DeleteAdviceForm + form_class = DeleteAdviceForm def form_valid(self, form): case = self.get_context_data()["case"] @@ -505,12 +333,12 @@ def get_context(self, **kwargs): class ReviewCountersignView(LoginRequiredMixin, CaseContextMixin, DESNZNuclearMixin, TemplateView): template_name = "advice/review_countersign.html" - form_class = forms.CountersignAdviceForm + form_class = CountersignAdviceForm def get_context(self, **kwargs): context = super().get_context() advice = services.get_advice_to_countersign(self.case.advice, self.caseworker) - context["formset"] = forms.get_formset(self.form_class, len(advice)) + context["formset"] = get_formset(self.form_class, len(advice)) context["advice_to_countersign"] = advice.values() context["denial_reasons_display"] = self.denial_reasons_display context["assessed_trigger_list_goods"] = self.assessed_trigger_list_goods @@ -523,7 +351,7 @@ def get_context(self, **kwargs): def post(self, request, *args, **kwargs): context = self.get_context_data() advice = context["advice_to_countersign"] - formset = forms.get_formset(self.form_class, len(advice), data=request.POST) + formset = get_formset(self.form_class, len(advice), data=request.POST) if formset.is_valid(): services.countersign_advice(request, self.case, self.caseworker, formset.cleaned_data) return HttpResponseRedirect(self.get_success_url()) @@ -572,7 +400,7 @@ def get_context_data(self, **kwargs): class ReviewCountersignDecisionAdviceView(LoginRequiredMixin, CaseContextMixin, TemplateView): template_name = "advice/review_countersign.html" - form_class = forms.CountersignDecisionAdviceForm + form_class = CountersignDecisionAdviceForm def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) @@ -580,7 +408,7 @@ def dispatch(self, request, *args, **kwargs): def get_context(self, **kwargs): context = super().get_context() advice = services.get_advice_to_countersign(self.case.advice, self.caseworker) - context["formset"] = forms.get_formset(self.form_class, len(advice)) + context["formset"] = get_formset(self.form_class, len(advice)) context["advice_to_countersign"] = advice.values() context["lu_can_edit"] = self.caseworker_id in services.get_countersigners_decision_advice( self.case, self.caseworker @@ -593,7 +421,7 @@ def post(self, request, *args, **kwargs): queue_id = str(kwargs["queue_pk"]) context = self.get_context_data() advice = context["advice_to_countersign"] - formset = forms.get_formset(self.form_class, len(advice), data=request.POST) + formset = get_formset(self.form_class, len(advice), data=request.POST) if formset.is_valid(): services.countersign_decision_advice(request, self.case, queue_id, self.caseworker, formset.cleaned_data) return HttpResponseRedirect(self.get_success_url()) @@ -618,12 +446,12 @@ def get_context(self, **kwargs): countersign_advice = services.get_countersign_decision_advice_by_user(self.case, self.caseworker) context["countersign_advice"] = countersign_advice data = self.get_data(countersign_advice) - context["formset"] = forms.get_formset(self.form_class, len(countersign_advice), initial=data) + context["formset"] = get_formset(self.form_class, len(countersign_advice), initial=data) return context def post(self, request, *args, **kwargs): context = self.get_context_data() - formset = forms.get_formset(self.form_class, len(context["countersign_advice"]), data=request.POST) + formset = get_formset(self.form_class, len(context["countersign_advice"]), data=request.POST) if formset.is_valid(): # single form item returned currently so using it to update decisions services.update_countersign_decision_advice(request, self.case, self.caseworker, formset.cleaned_data) @@ -640,7 +468,7 @@ def get_context(self, **kwargs): context = super().get_context() advice = context["advice_to_countersign"] data = self.get_data(advice) - context["formset"] = forms.get_formset(self.form_class, len(advice), initial=data) + context["formset"] = get_formset(self.form_class, len(advice), initial=data) return context @@ -671,11 +499,11 @@ def get_form(self): denial_reasons = get_denial_reasons(self.request) choices = group_denial_reasons(denial_reasons) if self.caseworker["team"]["alias"] == services.LICENSING_UNIT_TEAM: - return forms.LUConsolidateRefusalForm(choices=choices, **form_kwargs) + return LUConsolidateRefusalForm(choices=choices, **form_kwargs) form_kwargs["refusal_reasons"] = get_picklists_list( self.request, type="standard_advice", disable_pagination=True, show_deactivated=False ) - return forms.RefusalAdviceForm(choices, **form_kwargs) + return RefusalAdviceForm(choices, **form_kwargs) if self.kwargs.get("advice_type") == AdviceType.APPROVE: form_kwargs["approval_reason"] = get_picklists_list( @@ -689,10 +517,10 @@ def get_form(self): ) team_alias = self.caseworker["team"].get("alias", None) - return forms.ConsolidateApprovalForm(team_alias=team_alias, **form_kwargs) + return ConsolidateApprovalForm(team_alias=team_alias, **form_kwargs) team_name = self.caseworker["team"]["name"] - return forms.ConsolidateSelectAdviceForm(team_name=team_name, **form_kwargs) + return ConsolidateSelectAdviceForm(team_name=team_name, **form_kwargs) def get_context(self, **kwargs): context = super().get_context() @@ -717,9 +545,9 @@ def form_valid(self, form): user_team_alias = self.caseworker["team"]["alias"] level = "final-advice" if user_team_alias == services.LICENSING_UNIT_TEAM else "team-advice" try: - if isinstance(form, forms.ConsolidateApprovalForm): + if isinstance(form, ConsolidateApprovalForm): services.post_approval_advice(self.request, self.case, form.cleaned_data, level=level) - if isinstance(form, (forms.LUConsolidateRefusalForm, forms.RefusalAdviceForm)): + if isinstance(form, (LUConsolidateRefusalForm, RefusalAdviceForm)): data = form.cleaned_data if data.get("refusal_note"): data["text"] = data["refusal_note"] @@ -797,11 +625,11 @@ def form_valid(self, form): level = "final-advice" try: - if isinstance(form, forms.ConsolidateApprovalForm): + if isinstance(form, ConsolidateApprovalForm): services.update_advice( self.request, self.case, self.caseworker, self.advice_type, form.cleaned_data, level ) - if isinstance(form, forms.LUConsolidateRefusalForm): + if isinstance(form, LUConsolidateRefusalForm): # If refusal_note exists then we will convert the old Advice to refusal note from refusal reason # ATM this function update_advice is only being used by LU final-level for updating. # OGDs use post_refusal_advice function for both Create and Update @@ -823,7 +651,7 @@ def get_success_url(self): # TODO: Move to views/consolidate_advice.py on change class ViewConsolidatedAdviceView(AdviceView, FormView): - form_class = forms.MoveCaseForwardForm + form_class = MoveCaseForwardForm def get_lu_countersign_required(self, rejected_lu_countersignature): case_flag_ids = {flag["id"] for flag in self.case.all_flags} diff --git a/core/wizard/views.py b/core/wizard/views.py index b1c5ad0a56..985d9257b1 100644 --- a/core/wizard/views.py +++ b/core/wizard/views.py @@ -14,6 +14,8 @@ class BaseSessionWizardView(SessionWizardView): file_storage = NoSaveStorage() template_name = "core/form-wizard.html" + step_kwargs = {} + def get_cleaned_data_for_step(self, step): cleaned_data = super().get_cleaned_data_for_step(step) if cleaned_data is None: @@ -30,6 +32,14 @@ def get_context_data(self, form, **kwargs): return context + def get_form_kwargs(self, step=None): + kwargs = super().get_form_kwargs(step) + if self.step_kwargs: + if self.step_kwargs.get(step): + step_kwargs = self.step_kwargs[step](self) + kwargs.update(step_kwargs) + return kwargs + def render_to_response(self, context): if "title" not in context: logger.warning("No title set for `%s`", context["form"].__class__.__name__) diff --git a/unit_tests/caseworker/advice/test_forms.py b/unit_tests/caseworker/advice/test_forms.py index 9593ba7a80..e04c259afc 100644 --- a/unit_tests/caseworker/advice/test_forms.py +++ b/unit_tests/caseworker/advice/test_forms.py @@ -1,6 +1,9 @@ import pytest -from caseworker.advice import forms +from caseworker.advice.forms.approval import SelectAdviceForm +from caseworker.advice.forms.consolidate import ConsolidateSelectAdviceForm +from caseworker.advice.forms.countersign import CountersignAdviceForm, CountersignDecisionAdviceForm +from caseworker.advice.forms.forms import GiveApprovalAdviceForm, FCDOApprovalAdviceForm @pytest.mark.parametrize( @@ -11,7 +14,7 @@ ), ) def test_give_approval_advice_form_valid(data, valid_status): - form = forms.GiveApprovalAdviceForm( + form = GiveApprovalAdviceForm( data=data, approval_reason={"results": []}, proviso={"results": []}, footnote_details={"results": []} ) assert form.is_valid() == valid_status @@ -29,7 +32,7 @@ def test_give_approval_advice_form_valid(data, valid_status): ), ) def test_select_advice_form_valid(data, valid_status): - form = forms.SelectAdviceForm(data=data) + form = SelectAdviceForm(data=data) assert form.is_valid() == valid_status if not valid_status: assert form.errors["recommendation"] == ["Select if you approve all or refuse all"] @@ -45,7 +48,7 @@ def test_select_advice_form_valid(data, valid_status): ), ) def test_consolidate_select_advice_form_valid(data, valid_status): - form = forms.ConsolidateSelectAdviceForm(team_name=None, data=data) + form = ConsolidateSelectAdviceForm(team_name=None, data=data) assert form.is_valid() == valid_status if not valid_status: assert form.errors["recommendation"] == ["Select if you approve or refuse"] @@ -59,7 +62,7 @@ def test_consolidate_select_advice_form_valid(data, valid_status): ), ) def test_consolidate_select_advice_form_recommendation_label(team_name, label): - form = forms.ConsolidateSelectAdviceForm(team_name=team_name) + form = ConsolidateSelectAdviceForm(team_name=team_name) assert form.fields["recommendation"].label == label @@ -72,7 +75,7 @@ def test_consolidate_select_advice_form_recommendation_label(team_name, label): ), ) def test_countersign_advice_form_valid(data, valid_status): - form = forms.CountersignAdviceForm(data=data) + form = CountersignAdviceForm(data=data) assert form.is_valid() == valid_status if not valid_status: assert form.errors["approval_reasons"] == ["Enter why you agree with the recommendation"] @@ -102,7 +105,7 @@ def test_countersign_advice_form_valid(data, valid_status): ), ) def test_countersign_decision_advice_form_valid(data, valid_status, errors): - form = forms.CountersignDecisionAdviceForm(data=data) + form = CountersignDecisionAdviceForm(data=data) assert form.is_valid() == valid_status if not valid_status: assert form.errors == errors @@ -124,7 +127,7 @@ def test_countersign_decision_advice_form_valid(data, valid_status, errors): ), ) def test_give_fcdo_approval_advice_form_valid(data, valid_status): - form = forms.FCDOApprovalAdviceForm( + form = FCDOApprovalAdviceForm( data=data, countries={"GB": "United Kingdom"}, approval_reason={"results": []}, diff --git a/unit_tests/caseworker/advice/test_services.py b/unit_tests/caseworker/advice/test_services.py index d10a1400c1..6c492281eb 100644 --- a/unit_tests/caseworker/advice/test_services.py +++ b/unit_tests/caseworker/advice/test_services.py @@ -262,7 +262,7 @@ def test_update_countersign_decision_advice( ] -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_update_advice_by_team_other_than_LU_raises_error( mock_get_gov_user, advice, @@ -284,7 +284,7 @@ def test_update_advice_by_team_other_than_LU_raises_error( update_advice(requests_mock, case, current_user, "refuse", {}, "final-advice") -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_update_advice_not_supported_advice_type_raises_error( mock_get_gov_user, advice, diff --git a/unit_tests/caseworker/advice/views/test_consolidate.py b/unit_tests/caseworker/advice/views/test_consolidate.py index 7c324ca89f..804e7448c4 100644 --- a/unit_tests/caseworker/advice/views/test_consolidate.py +++ b/unit_tests/caseworker/advice/views/test_consolidate.py @@ -3,8 +3,12 @@ from bs4 import BeautifulSoup from django.urls import reverse -from caseworker.advice import forms +from caseworker.advice.forms.consolidate import ( + ConsolidateApprovalForm, + ConsolidateSelectAdviceForm, +) from caseworker.advice import services +from caseworker.advice.forms.refusal import RefusalAdviceForm from caseworker.advice.services import ( LICENSING_UNIT_TEAM, MOD_ECJU_TEAM, @@ -333,7 +337,7 @@ def test_consolidate_review_refusal_advice( response = authorized_client.get(url) assert response.status_code == 200 form = response.context["form"] - assert isinstance(form, forms.ConsolidateSelectAdviceForm) + assert isinstance(form, ConsolidateSelectAdviceForm) response = authorized_client.post(url, data={"recommendation": recommendation}) assert response.status_code == 302 assert redirect in response.url @@ -368,7 +372,7 @@ def test_consolidate_review_refusal_advice_recommendation_label( response = authorized_client.get(url) assert response.status_code == 200 form = response.context["form"] - assert isinstance(form, forms.ConsolidateSelectAdviceForm) + assert isinstance(form, ConsolidateSelectAdviceForm) assert form.fields["recommendation"].label == recommendation_label @@ -512,8 +516,8 @@ def test_view_consolidate_refuse_outcome( @pytest.mark.parametrize( "path, form_class", ( - ("approve/", forms.ConsolidateApprovalForm), - ("refuse/", forms.RefusalAdviceForm), + ("approve/", ConsolidateApprovalForm), + ("refuse/", RefusalAdviceForm), ), ) def test_consolidate_raises_exception_for_other_team( diff --git a/unit_tests/caseworker/advice/views/test_consolidate_edit.py b/unit_tests/caseworker/advice/views/test_consolidate_edit.py index c936a3c50f..941d332144 100644 --- a/unit_tests/caseworker/advice/views/test_consolidate_edit.py +++ b/unit_tests/caseworker/advice/views/test_consolidate_edit.py @@ -7,8 +7,9 @@ from bs4 import BeautifulSoup from django.urls import reverse +from caseworker.advice.forms.consolidate import ConsolidateApprovalForm from core import client -from caseworker.advice import forms, services +from caseworker.advice import services from caseworker.advice.constants import AdviceType from unit_tests.caseworker.conftest import countersignatures_for_advice from caseworker.advice.services import LICENSING_UNIT_TEAM, MOD_ECJU_TEAM @@ -240,7 +241,7 @@ def test_edit_refuse_advice_post( "team, advice_level", ((services.LICENSING_UNIT_TEAM, "final"), (services.MOD_ECJU_TEAM, "team")), ) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_edit_advice_get( mock_get_gov_user, team, @@ -266,10 +267,10 @@ def test_edit_advice_get( response = authorized_client.get(url) form = response.context["form"] # The final advice was approval advice so we should see an approval form - assert isinstance(form, forms.ConsolidateApprovalForm) + assert isinstance(form, ConsolidateApprovalForm) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_edit_consolidated_advice_approve_by_lu_put( mock_get_gov_user, authorized_client, @@ -298,7 +299,7 @@ def test_edit_consolidated_advice_approve_by_lu_put( ] -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_edit_consolidated_advice_approve__with_nlr_products_by_lu_put( mock_get_gov_user, authorized_client, @@ -337,7 +338,7 @@ def test_edit_consolidated_advice_approve__with_nlr_products_by_lu_put( ) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_edit_consolidated_advice_refuse_note_by_lu_put( mock_get_gov_user, authorized_client, @@ -375,7 +376,7 @@ def test_edit_consolidated_advice_refuse_note_by_lu_put( ] -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_edit_consolidated_advice_by_LU_error_from_API( mock_get_gov_user, authorized_client, @@ -410,7 +411,7 @@ def test_edit_consolidated_advice_by_LU_error_from_API( ("MOD", "Countersigned by MOD User"), ), ) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_edit_advice_get_displays_correct_counteradvice( mock_get_gov_user, authorized_client, @@ -458,7 +459,7 @@ def test_edit_advice_get_displays_correct_counteradvice( assert countersignatures[0].find("p").text == fcdo_or_mod_advice[0]["countersign_comments"] -@patch("caseworker.advice.views.views.get_gov_user") # Pass to the mock version; mock_get_gov_user +@patch("caseworker.advice.views.mixins.get_gov_user") # Pass to the mock version; mock_get_gov_user def test_edit_refusal_note_exists( mock_get_gov_user, authorized_client, @@ -483,7 +484,7 @@ def test_edit_refusal_note_exists( assert note_element.get_text(strip=True) == "The refusal note assess_1_2" -@patch("caseworker.advice.views.views.get_gov_user") # Pass to the mock version; mock_get_gov_user +@patch("caseworker.advice.views.mixins.get_gov_user") # Pass to the mock version; mock_get_gov_user def test_mod_ecju_edit_exists( mock_get_gov_user, authorized_client, diff --git a/unit_tests/caseworker/advice/views/test_countersign.py b/unit_tests/caseworker/advice/views/test_countersign.py index 9e60ed9ed6..860000c2e3 100644 --- a/unit_tests/caseworker/advice/views/test_countersign.py +++ b/unit_tests/caseworker/advice/views/test_countersign.py @@ -468,7 +468,7 @@ def test_lu_countersign_decision_post_success( ] -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_lu_countersign_get_shows_previous_countersignature( mock_get_gov_user, authorized_client, @@ -523,7 +523,7 @@ def user_not_allowed_to_countersign(response): return True -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") @patch("caseworker.core.rules.get_logged_in_caseworker") def test_case_officer_cannot_countersign_as_licensing_manager( mock_caseworker, @@ -546,7 +546,7 @@ def test_case_officer_cannot_countersign_as_licensing_manager( assert user_not_allowed_to_countersign(response) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") @patch("caseworker.core.rules.get_logged_in_caseworker") def test_licensing_manager_countersigner_not_same_as_case_officer( mock_caseworker, @@ -575,7 +575,7 @@ def test_licensing_manager_countersigner_not_same_as_case_officer( # Senior Licensing manager tests -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") @patch("caseworker.core.rules.get_logged_in_caseworker") def test_case_officer_cannot_countersign_as_senior_licensing_manager( mock_caseworker, @@ -598,7 +598,7 @@ def test_case_officer_cannot_countersign_as_senior_licensing_manager( assert user_not_allowed_to_countersign(response) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") @patch("caseworker.core.rules.get_logged_in_caseworker") def test_licensing_manager_cannot_countersign_as_senior_licensing_manager( mock_caseworker, @@ -621,7 +621,7 @@ def test_licensing_manager_cannot_countersign_as_senior_licensing_manager( assert user_not_allowed_to_countersign(response) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") @patch("caseworker.core.rules.get_logged_in_caseworker") def test_senior_manager_countersigner_not_same_as_case_officer_or_countersigner( mock_caseworker, diff --git a/unit_tests/caseworker/advice/views/test_countersign_edit.py b/unit_tests/caseworker/advice/views/test_countersign_edit.py index 1eddd2e684..72b9614584 100644 --- a/unit_tests/caseworker/advice/views/test_countersign_edit.py +++ b/unit_tests/caseworker/advice/views/test_countersign_edit.py @@ -272,7 +272,7 @@ def test_lu_countersign_decision_edit_post_success( ] -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_lu_countersign_edit_get_shows_previous_countersignature( mock_get_gov_user, authorized_client, diff --git a/unit_tests/caseworker/advice/views/test_countersign_view.py b/unit_tests/caseworker/advice/views/test_countersign_view.py index 287723b102..dc195dfb2f 100644 --- a/unit_tests/caseworker/advice/views/test_countersign_view.py +++ b/unit_tests/caseworker/advice/views/test_countersign_view.py @@ -52,7 +52,7 @@ def test_countersign_view_trigger_list_products( assert response.context["assessed_trigger_list_goods"] == expected_context -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_single_lu_countersignature( mock_get_gov_user, authorized_client, @@ -89,7 +89,7 @@ def test_single_lu_countersignature( assert not rejected_warning -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_double_lu_countersignature( mock_get_gov_user, authorized_client, @@ -140,7 +140,7 @@ def test_double_lu_countersignature( ], ), ) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_single_lu_rejected_countersignature( mock_get_gov_user, countersigning_data, @@ -205,7 +205,7 @@ def test_single_lu_rejected_countersignature( ], ), ) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_lu_rejected_senior_countersignature( mock_get_gov_user, countersigning_data, diff --git a/unit_tests/caseworker/advice/views/test_edit_advice.py b/unit_tests/caseworker/advice/views/test_edit_advice.py index fdaa26b50a..90a0789bf6 100644 --- a/unit_tests/caseworker/advice/views/test_edit_advice.py +++ b/unit_tests/caseworker/advice/views/test_edit_advice.py @@ -245,7 +245,7 @@ def test_DESNZ_give_approval_advice_post_valid( }, None, ) - mocker.patch("caseworker.advice.views.views.get_gov_user", return_value=get_gov_user_value) + mocker.patch("caseworker.advice.views.mixins.get_gov_user", return_value=get_gov_user_value) case_data = deepcopy(data_standard_case) case_data["case"]["data"]["goods"] = standard_case_with_advice["data"]["goods"] case_data["case"]["advice"] = standard_case_with_advice["advice"] @@ -355,7 +355,7 @@ def test_DESNZ_give_approval_advice_post_valid_add_conditional( }, None, ) - mocker.patch("caseworker.advice.views.views.get_gov_user", return_value=get_gov_user_value) + mocker.patch("caseworker.advice.views.mixins.get_gov_user", return_value=get_gov_user_value) case_data = deepcopy(data_standard_case) case_data["case"]["data"]["goods"] = standard_case_with_advice["data"]["goods"] case_data["case"]["advice"] = standard_case_with_advice["advice"] diff --git a/unit_tests/caseworker/advice/views/test_give_approval_advice_view.py b/unit_tests/caseworker/advice/views/test_give_approval_advice_view.py index a8d240e89a..5a7e6a5eb4 100644 --- a/unit_tests/caseworker/advice/views/test_give_approval_advice_view.py +++ b/unit_tests/caseworker/advice/views/test_give_approval_advice_view.py @@ -33,7 +33,7 @@ def test_select_advice_post(authorized_client, requests_mock, data_standard_case assert response.status_code == 302 -@mock.patch("caseworker.advice.views.views.get_gov_user") +@mock.patch("caseworker.advice.views.mixins.get_gov_user") def test_fco_give_approval_advice_get(mock_get_gov_user, authorized_client, url): mock_get_gov_user.return_value = ( {"user": {"team": {"id": "67b9a4a3-6f3d-4511-8a19-23ccff221a74", "name": "FCO", "alias": services.FCDO_TEAM}}}, @@ -48,7 +48,7 @@ def test_fco_give_approval_advice_get(mock_get_gov_user, authorized_client, url) ] -@mock.patch("caseworker.advice.views.views.get_gov_user") +@mock.patch("caseworker.advice.views.mixins.get_gov_user") def test_fco_give_approval_advice_existing_get(mock_get_gov_user, authorized_client, url, data_standard_case): mock_get_gov_user.return_value = ( {"user": {"team": {"id": "67b9a4a3-6f3d-4511-8a19-23ccff221a74", "name": "FCO", "alias": services.FCDO_TEAM}}}, @@ -93,8 +93,8 @@ def test_fco_give_approval_advice_existing_get(mock_get_gov_user, authorized_cli ([], "", 200), ], ) -@mock.patch("caseworker.advice.views.views.get_gov_user") -def test_fco_give_approval_advice_post( +@mock.patch("caseworker.advice.views.mixins.get_gov_user") +def test_fcdo_give_approval_advice_post( mock_get_gov_user, authorized_client, requests_mock, @@ -127,7 +127,7 @@ def post_to_step(post_to_step_factory, url_desnz): return post_to_step_factory(url_desnz) -@mock.patch("caseworker.advice.views.views.get_gov_user") +@mock.patch("caseworker.advice.views.mixins.get_gov_user") def test_DESNZ_give_approval_advice_post_valid( mock_get_gov_user, authorized_client, @@ -160,7 +160,7 @@ def test_DESNZ_give_approval_advice_post_valid( assert response.status_code == 302 -@mock.patch("caseworker.advice.views.views.get_gov_user") +@mock.patch("caseworker.advice.views.mixins.get_gov_user") def test_DESNZ_give_approval_advice_post_valid_add_conditional( mock_get_gov_user, authorized_client, @@ -212,7 +212,7 @@ def test_DESNZ_give_approval_advice_post_valid_add_conditional( assert add_instructions_response.status_code == 302 -@mock.patch("caseworker.advice.views.views.get_gov_user") +@mock.patch("caseworker.advice.views.mixins.get_gov_user") def test_DESNZ_give_approval_advice_post_valid_add_conditional_optional( mock_get_gov_user, authorized_client, @@ -265,7 +265,7 @@ def test_DESNZ_give_approval_advice_post_valid_add_conditional_optional( assert add_instructions_response.status_code == 302 -@mock.patch("caseworker.advice.views.views.get_gov_user") +@mock.patch("caseworker.advice.views.mixins.get_gov_user") def test_DESNZ_give_approval_advice_post_invalid( mock_get_gov_user, authorized_client, @@ -300,7 +300,7 @@ def test_DESNZ_give_approval_advice_post_invalid( soup = beautiful_soup(response.content) -@mock.patch("caseworker.advice.views.views.get_gov_user") +@mock.patch("caseworker.advice.views.mixins.get_gov_user") def test_DESNZ_give_approval_advice_post_invalid_user( mock_get_gov_user, authorized_client, diff --git a/unit_tests/caseworker/advice/views/test_lu_consolidate.py b/unit_tests/caseworker/advice/views/test_lu_consolidate.py index 206308d7ff..381e2f00a8 100644 --- a/unit_tests/caseworker/advice/views/test_lu_consolidate.py +++ b/unit_tests/caseworker/advice/views/test_lu_consolidate.py @@ -22,7 +22,7 @@ def url(request, data_queue, data_standard_case): ) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_no_advice_summary_for_lu( mock_get_gov_user, url, @@ -53,7 +53,7 @@ def test_no_advice_summary_for_lu( ], ), ) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_lu_consolidate_check_countersignatures_other_recommendations( mock_get_gov_user, countersigning_data, diff --git a/unit_tests/caseworker/advice/views/test_refusal_advice_view.py b/unit_tests/caseworker/advice/views/test_refusal_advice_view.py index fe57174fc1..58e73a01e9 100644 --- a/unit_tests/caseworker/advice/views/test_refusal_advice_view.py +++ b/unit_tests/caseworker/advice/views/test_refusal_advice_view.py @@ -41,7 +41,7 @@ def test_refuse_all_post(authorized_client, url, denial_reasons, refusal_reasons assert response.status_code == expected_status_code -@mock.patch("caseworker.advice.views.views.get_gov_user") +@mock.patch("caseworker.advice.views.mixins.get_gov_user") def test_fco_give_refusal_advice_get(mock_get_gov_user, authorized_client, url): mock_get_gov_user.return_value = ( {"user": {"team": {"id": "67b9a4a3-6f3d-4511-8a19-23ccff221a74", "name": "FCO", "alias": services.FCDO_TEAM}}}, @@ -56,7 +56,7 @@ def test_fco_give_refusal_advice_get(mock_get_gov_user, authorized_client, url): ] -@mock.patch("caseworker.advice.views.views.get_gov_user") +@mock.patch("caseworker.advice.views.mixins.get_gov_user") def test_fco_give_refusal_advice_existing_get(mock_get_gov_user, authorized_client, url, data_standard_case): mock_get_gov_user.return_value = ( {"user": {"team": {"id": "67b9a4a3-6f3d-4511-8a19-23ccff221a74", "name": "FCO", "alias": services.FCDO_TEAM}}}, @@ -101,7 +101,7 @@ def test_fco_give_refusal_advice_existing_get(mock_get_gov_user, authorized_clie ([], "", 200), ], ) -@mock.patch("caseworker.advice.views.views.get_gov_user") +@mock.patch("caseworker.advice.views.mixins.get_gov_user") def test_fco_give_approval_advice_post( mock_get_gov_user, authorized_client, diff --git a/unit_tests/caseworker/advice/views/test_select_advice_view.py b/unit_tests/caseworker/advice/views/test_select_advice_view.py index 91e293fdf5..58155c632a 100644 --- a/unit_tests/caseworker/advice/views/test_select_advice_view.py +++ b/unit_tests/caseworker/advice/views/test_select_advice_view.py @@ -46,7 +46,7 @@ def test_select_advice_post_desnz(authorized_client, url, data_standard_case, mo }, None, ) - mocker.patch("caseworker.advice.views.views.get_gov_user", return_value=get_gov_user_value) + mocker.patch("caseworker.advice.views.mixins.get_gov_user", return_value=get_gov_user_value) response = authorized_client.post(url, data={"recommendation": "approve_all"}) assert response.status_code == 302 assert ( diff --git a/unit_tests/caseworker/advice/views/test_view_my_advice_view.py b/unit_tests/caseworker/advice/views/test_view_my_advice_view.py index 36072d26b3..327dfdbdd4 100644 --- a/unit_tests/caseworker/advice/views/test_view_my_advice_view.py +++ b/unit_tests/caseworker/advice/views/test_view_my_advice_view.py @@ -267,7 +267,7 @@ def test_move_case_forward_permission( assert len(soup.find_all("form")) == (1 if is_user_case_advisor else 0) -@patch("caseworker.advice.views.views.get_gov_user") +@patch("caseworker.advice.views.mixins.get_gov_user") def test_lu_countersignatures_not_shown( mock_get_gov_user, authorized_client, diff --git a/unit_tests/caseworker/advice/views/test_view_ogd_advice.py b/unit_tests/caseworker/advice/views/test_view_ogd_advice.py index 71927ba210..43e0467833 100644 --- a/unit_tests/caseworker/advice/views/test_view_ogd_advice.py +++ b/unit_tests/caseworker/advice/views/test_view_ogd_advice.py @@ -78,7 +78,7 @@ def test_advice_view_heading_ogd_advice( assert team_headings == {"A team has approved and refused", "B team has approved"} -@mock.patch("caseworker.advice.views.views.get_gov_user") +@mock.patch("caseworker.advice.views.mixins.get_gov_user") def test_fcdo_cannot_advice_when_all_destinations_covered( mock_get_gov_user, authorized_client, data_queue, data_standard_case ): From fec9881c6d3d3cf2176bfe697e2245142183c6d3 Mon Sep 17 00:00:00 2001 From: Tomos Williams Date: Thu, 5 Dec 2024 11:29:53 +0000 Subject: [PATCH 2/3] removed unused code --- caseworker/advice/conditionals.py | 8 --- caseworker/advice/forms/approval.py | 59 ------------------- caseworker/advice/picklist_helpers.py | 8 --- caseworker/advice/urls.py | 16 +++-- .../views/{add_advice.py => approval.py} | 16 ----- .../{consolidate_advice.py => consolidate.py} | 0 .../advice/views/{edit_advice.py => edit.py} | 2 +- 7 files changed, 8 insertions(+), 101 deletions(-) rename caseworker/advice/views/{add_advice.py => approval.py} (78%) rename caseworker/advice/views/{consolidate_advice.py => consolidate.py} (100%) rename caseworker/advice/views/{edit_advice.py => edit.py} (96%) diff --git a/caseworker/advice/conditionals.py b/caseworker/advice/conditionals.py index f5a8a36301..dc86ebbabe 100644 --- a/caseworker/advice/conditionals.py +++ b/caseworker/advice/conditionals.py @@ -13,11 +13,3 @@ def _get_form_field_boolean(wizard): def is_desnz_team(wizard): return wizard.caseworker["team"]["alias"] in services.DESNZ_TEAMS - - -def is_fcdo_team(wizard): - return wizard.caseworker["team"]["alias"] == services.FCDO_TEAM - - -def default_form(wizard): - return not (is_fcdo_team(wizard) or is_desnz_team(wizard)) diff --git a/caseworker/advice/forms/approval.py b/caseworker/advice/forms/approval.py index 858d18a40a..f9771416ea 100644 --- a/caseworker/advice/forms/approval.py +++ b/caseworker/advice/forms/approval.py @@ -10,7 +10,6 @@ ConditionalCheckboxesQuestion, RadioTextArea, ) -from core.forms.widgets import GridmultipleSelect class SelectAdviceForm(forms.Form): @@ -88,64 +87,6 @@ def get_layout_fields(self): ) -class FCDOApprovalAdviceForm(RecommendAnApprovalForm): - class Layout: - TITLE = "Recommend an approval" - - def __init__(self, countries, *args, **kwargs): - countries = kwargs.pop("countries") - super().__init__(*args, **kwargs) - self.fields["countries"] = forms.MultipleChoiceField( - choices=countries.items(), - widget=GridmultipleSelect(), - label="Select countries for which you want to give advice", - error_messages={"required": "Select the destinations you want to make recommendations for"}, - ) - - def get_layout_fields(self): - return ( - RadioTextArea("approval_radios", "approval_reasons", self.approval_text), - "countries", - "add_licence_conditions", - ) - - -class DESNZApprovalForm(PicklistAdviceForm, BaseForm): - class Layout: - TITLE = "Recommend an approval" - - approval_reasons = forms.CharField( - widget=forms.Textarea(attrs={"rows": 7, "class": "govuk-!-margin-top-4"}), - label="", - error_messages={"required": "Enter a reason for approving"}, - ) - approval_radios = forms.ChoiceField( - label="What is your reason for approving?", - required=False, - widget=forms.RadioSelect, - choices=(), - ) - add_licence_conditions = forms.BooleanField( - label="Add licence conditions, instructions to exporter or footnotes (optional)", - required=False, - ) - - def __init__(self, *args, **kwargs): - approval_reason = kwargs.pop("approval_reason") - # this follows the same pattern as denial_reasons. - approval_choices, approval_text = self._picklist_to_choices(approval_reason) - self.approval_text = approval_text - super().__init__(*args, **kwargs) - - self.fields["approval_radios"].choices = approval_choices - - def get_layout_fields(self): - return ( - RadioTextArea("approval_radios", "approval_reasons", self.approval_text), - "add_licence_conditions", - ) - - class LicenceConditionsForm(PicklistAdviceForm, BaseForm): class Layout: TITLE = "Add licence conditions, instructions to exporter or footnotes (optional)" diff --git a/caseworker/advice/picklist_helpers.py b/caseworker/advice/picklist_helpers.py index 4ba00d8a8d..812ad03364 100644 --- a/caseworker/advice/picklist_helpers.py +++ b/caseworker/advice/picklist_helpers.py @@ -9,14 +9,6 @@ def approval_picklist(self): } -def fcdo_picklist(self): - return { - "approval_reason": get_picklists_list( - self.request, type="standard_advice", disable_pagination=True, show_deactivated=False - ) - } - - def proviso_picklist(self): return { "proviso": get_picklists_list(self.request, type="proviso", disable_pagination=True, show_deactivated=False) diff --git a/caseworker/advice/urls.py b/caseworker/advice/urls.py index a2ff3acd90..b104d7a086 100644 --- a/caseworker/advice/urls.py +++ b/caseworker/advice/urls.py @@ -1,8 +1,8 @@ from django.urls import path -from caseworker.advice.views import views, consolidate_advice -from caseworker.advice.views.add_advice import GiveApprovalAdviceView -from caseworker.advice.views.edit_advice import EditAdviceView +from caseworker.advice.views import consolidate, views +from caseworker.advice.views.approval import GiveApprovalAdviceView +from caseworker.advice.views.edit import EditAdviceView urlpatterns = [ path("", views.AdviceView.as_view(), name="advice_view"), @@ -30,14 +30,12 @@ name="countersign_decision_edit", ), path("consolidate/", views.ConsolidateAdviceView.as_view(), name="consolidate_advice_view"), - path("consolidate/review/", consolidate_advice.ConsolidateSelectDecisionView.as_view(), name="consolidate_review"), - path( - "consolidate/review/approve/", consolidate_advice.ConsolidateApproveView.as_view(), name="consolidate_approve" - ), - path("consolidate/review/refuse/", consolidate_advice.ConsolidateRefuseView.as_view(), name="consolidate_refuse"), + path("consolidate/review/", consolidate.ConsolidateSelectDecisionView.as_view(), name="consolidate_review"), + path("consolidate/review/approve/", consolidate.ConsolidateApproveView.as_view(), name="consolidate_approve"), + path("consolidate/review/refuse/", consolidate.ConsolidateRefuseView.as_view(), name="consolidate_refuse"), path( "consolidate/review/lu-refuse/", - consolidate_advice.LUConsolidateRefuseView.as_view(), + consolidate.LUConsolidateRefuseView.as_view(), name="consolidate_refuse_lu", ), path("consolidate/edit/", views.ConsolidateEditView.as_view(), name="consolidate_edit"), diff --git a/caseworker/advice/views/add_advice.py b/caseworker/advice/views/approval.py similarity index 78% rename from caseworker/advice/views/add_advice.py rename to caseworker/advice/views/approval.py index fe21672147..b27d3e7fc9 100644 --- a/caseworker/advice/views/add_advice.py +++ b/caseworker/advice/views/approval.py @@ -19,22 +19,6 @@ from core.decorators import expect_status -# class SelectAdviceView(LoginRequiredMixin, CaseContextMixin, FormView): -# template_name = "advice/select_advice.html" -# form_class = SelectAdviceForm - -# def get_success_url(self): -# recommendation = self.request.POST.get("recommendation") -# if recommendation == "approve_all": -# return reverse("cases:approve_all", kwargs=self.kwargs) -# else: -# return reverse("cases:refuse_all", kwargs=self.kwargs) - -# def get_context_data(self, **kwargs): -# context = super().get_context_data(**kwargs) -# return {**context, "security_approvals_classified_display": self.security_approvals_classified_display} - - class GiveApprovalAdviceView(LoginRequiredMixin, CaseContextMixin, BaseSessionWizardView): form_list = [ diff --git a/caseworker/advice/views/consolidate_advice.py b/caseworker/advice/views/consolidate.py similarity index 100% rename from caseworker/advice/views/consolidate_advice.py rename to caseworker/advice/views/consolidate.py diff --git a/caseworker/advice/views/edit_advice.py b/caseworker/advice/views/edit.py similarity index 96% rename from caseworker/advice/views/edit_advice.py rename to caseworker/advice/views/edit.py index 750ef15756..58a71f099a 100644 --- a/caseworker/advice/views/edit_advice.py +++ b/caseworker/advice/views/edit.py @@ -1,6 +1,6 @@ from caseworker.advice.forms.approval import FootnotesApprovalAdviceForm, RecommendAnApprovalForm from caseworker.advice.forms.edit import PicklistApprovalAdviceEditForm -from caseworker.advice.views.add_advice import GiveApprovalAdviceView +from caseworker.advice.views.approval import GiveApprovalAdviceView from caseworker.advice import services from caseworker.advice.constants import AdviceSteps from caseworker.advice.picklist_helpers import approval_picklist, footnote_picklist From 09256a64c430469db6a63c5a8fa1414b6841c6a8 Mon Sep 17 00:00:00 2001 From: Tomos Williams Date: Mon, 9 Dec 2024 15:08:29 +0000 Subject: [PATCH 3/3] remove unreachable code --- caseworker/advice/views/views.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/caseworker/advice/views/views.py b/caseworker/advice/views/views.py index da6da9c4c5..1709e1e5d4 100644 --- a/caseworker/advice/views/views.py +++ b/caseworker/advice/views/views.py @@ -2,7 +2,6 @@ from caseworker.advice.forms.approval import MoveCaseForwardForm, SelectAdviceForm from caseworker.advice.forms.consolidate import ( ConsolidateApprovalForm, - ConsolidateSelectAdviceForm, LUConsolidateRefusalForm, ) from caseworker.advice.forms.countersign import CountersignAdviceForm, CountersignDecisionAdviceForm @@ -519,9 +518,6 @@ def get_form(self): team_alias = self.caseworker["team"].get("alias", None) return ConsolidateApprovalForm(team_alias=team_alias, **form_kwargs) - team_name = self.caseworker["team"]["name"] - return ConsolidateSelectAdviceForm(team_name=team_name, **form_kwargs) - def get_context(self, **kwargs): context = super().get_context() team_alias = (