Skip to content

Commit 98fa139

Browse files
committed
chore: more admin tests
1 parent 568ae1d commit 98fa139

4 files changed

Lines changed: 20 additions & 278 deletions

File tree

bc_obps/registration/utils.py

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import base64
21
import logging
3-
import os
42
from typing import Any, Dict, Iterable, Optional, TypeVar, Union
53

6-
import requests
74
from django.conf import settings
85
from django.core.exceptions import ValidationError
96
from django.db import IntegrityError, models
@@ -89,34 +86,6 @@ def generate_useful_error(error: Union[ValidationError, NinjaValidationError]) -
8986
return messages[0] if messages else None
9087

9188

92-
# File helpers
93-
def file_to_data_url(document: Document) -> Optional[str]: # type: ignore[return] # we dont break the function if something goes wrong in this function
94-
"""
95-
Transforms a Django FieldField record into a data url that RJSF can process.
96-
"""
97-
timeout_seconds = 10
98-
# Handles local storage when running in CI
99-
if os.environ.get("CI", None) == "true" or os.environ.get("ENVIRONMENT", None) == "local":
100-
encoded_content = base64.b64encode(document.get_file_content().read()).decode("utf-8")
101-
return "data:application/pdf;name=" + document.file.name.split("/")[-1] + ";scanstatus=" + document.status + ";base64," + encoded_content # type: ignore[no-any-return]
102-
else:
103-
try:
104-
response = requests.get(document.get_file_url(), timeout=timeout_seconds)
105-
if response.status_code == 200:
106-
document_content = response.content
107-
encoded_content = base64.b64encode(document_content).decode("utf-8")
108-
# only pdf format is allowed
109-
return "data:application/pdf;name=" + document.file.name.split("/")[-1] + ";scanstatus=" + document.status + ";base64," + encoded_content # type: ignore[no-any-return]
110-
else:
111-
logger.error(f"Request to retrieve file failed with status code {response.status_code}")
112-
except requests.exceptions.Timeout:
113-
# Handle the timeout exception
114-
logger.exception(f"Request timed out after {timeout_seconds} seconds")
115-
except requests.exceptions.RequestException as e:
116-
# Handle other types of exceptions (e.g., connection error)
117-
logger.exception(f"An error occurred: {e}")
118-
119-
12089
def custom_reverse_lazy(view_name: str, *args: Any, **kwargs: Any) -> Union[str, Any]:
12190
"""
12291
A custom reverse_lazy function that includes the default API namespace.
Lines changed: 0 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -1,235 +0,0 @@
1-
"use client";
2-
3-
import { useState } from "react";
4-
import { useRouter, useSearchParams } from "next/navigation";
5-
import useKey from "@bciers/utils/src/useKey";
6-
7-
import { UUID } from "crypto";
8-
import SingleStepTaskListForm from "@bciers/components/form/SingleStepTaskListForm";
9-
import { RJSFSchema, UiSchema } from "@rjsf/utils";
10-
import { IChangeEvent } from "@rjsf/core";
11-
import {
12-
OperationInformationFormData,
13-
OperationInformationPartialFormData,
14-
} from "./types";
15-
import { actionHandler } from "@bciers/actions";
16-
import {
17-
RegistrationPurposes,
18-
regulatedOperationPurposes,
19-
} from "apps/registration/app/components/operations/registration/enums";
20-
import {
21-
FormMode,
22-
FrontEndRoles,
23-
OperationTypes,
24-
} from "@bciers/utils/src/enums";
25-
import { useSessionRole } from "@bciers/utils/src/sessionUtils";
26-
import Note from "@bciers/components/layout/Note";
27-
import Link from "next/link";
28-
import ConfirmChangeOfFieldModal from "@/registration/app/components/operations/registration/ConfirmChangeOfFieldModal";
29-
import { useFileUploadWidget } from "@bciers/components/form/widgets/FileWidget";
30-
31-
const OperationInformationForm = ({
32-
formData,
33-
operationId,
34-
schema: initialSchema,
35-
eioSchema,
36-
generalSchema,
37-
uiSchema,
38-
}: {
39-
formData: OperationInformationPartialFormData;
40-
operationId: UUID;
41-
schema: RJSFSchema;
42-
eioSchema: RJSFSchema;
43-
generalSchema: RJSFSchema;
44-
uiSchema: UiSchema;
45-
}) => {
46-
const [error, setError] = useState(undefined);
47-
const [schema, setSchema] = useState(initialSchema);
48-
const [confirmedFormData, setConfirmedFormData] = useState(formData);
49-
const [
50-
pendingChangeRegistrationPurpose,
51-
setPendingChangeRegistrationPurpose,
52-
] = useState("");
53-
const [isConfirmPurposeChangeModalOpen, setIsConfirmPurposeChangeModalOpen] =
54-
useState<boolean>(false);
55-
const [key, resetKey] = useKey();
56-
const [formMode, setFormMode] = useState(FormMode.READ_ONLY);
57-
58-
const router = useRouter();
59-
// To get the user's role from the session
60-
const role = useSessionRole();
61-
const searchParams = useSearchParams();
62-
63-
const [fileWidgetContext, submitWithFiles] = useFileUploadWidget();
64-
65-
const isRedirectedFromContacts = searchParams.get("from_contacts") as string;
66-
67-
function checkMissingRepresentative(data: any) {
68-
if (data && data.status && data.registration_purpose) {
69-
return (
70-
data.status === "Registered" &&
71-
(!data.operation_representatives ||
72-
data.operation_representatives.length === 0)
73-
);
74-
} else return false;
75-
}
76-
const [isMissingRepresentative, setIsMissingRepresentative] = useState(
77-
checkMissingRepresentative(formData),
78-
);
79-
80-
const updateConfirmedFormData = (newPurpose: string) => {
81-
const isEio =
82-
newPurpose === RegistrationPurposes.ELECTRICITY_IMPORT_OPERATION;
83-
const newFormData = {
84-
...confirmedFormData,
85-
registration_purpose: newPurpose,
86-
// When switching to EIO, set the type to EIO since that's the only valid option
87-
...(isEio && { type: OperationTypes.EIO }),
88-
};
89-
setConfirmedFormData(newFormData);
90-
91-
if (isEio) {
92-
setSchema(eioSchema);
93-
} else {
94-
setSchema(generalSchema);
95-
}
96-
};
97-
98-
const handleSubmit = async (data: {
99-
formData?: OperationInformationFormData;
100-
}) => {
101-
setError(undefined);
102-
const pathToRevalidate = `/operations/${operationId}`;
103-
104-
const response = await submitWithFiles(
105-
data.formData,
106-
`registration/operations/${operationId}`,
107-
"POST",
108-
`/operations/${operationId}`,
109-
);
110-
111-
if (response?.error) {
112-
// Users get this error when they select a contact that's missing address information. We include a link to the Contacts page because the user has to fix the error from there, not here in the operation form.
113-
if (response.error.includes("Please return to Contacts")) {
114-
const splitError = response.error.split("Contacts");
115-
response.error = (
116-
<>
117-
{splitError[0]} <Link href={"/contacts"}>Contacts</Link>
118-
{splitError[1]}
119-
</>
120-
);
121-
}
122-
setError(response.error);
123-
return { error: response.error };
124-
}
125-
126-
if (!data.formData?.opted_in_operation) return;
127-
const response2 = await actionHandler(
128-
`registration/operations/${operationId}/registration/opted-in-operation-detail`,
129-
"PUT",
130-
pathToRevalidate,
131-
{
132-
body: JSON.stringify(data.formData?.opted_in_operation),
133-
},
134-
);
135-
136-
if (response2?.error) {
137-
setError(response2.error);
138-
return { error: response2.error };
139-
}
140-
};
141-
142-
const cancelRegistrationPurposeChange = () => {
143-
setPendingChangeRegistrationPurpose("");
144-
setFormMode(FormMode.EDIT); // Keep form in edit mode after remount
145-
resetKey();
146-
setIsConfirmPurposeChangeModalOpen(false);
147-
};
148-
149-
const confirmRegistrationPurposeChange = () => {
150-
if (pendingChangeRegistrationPurpose !== "") {
151-
updateConfirmedFormData(pendingChangeRegistrationPurpose);
152-
setFormMode(FormMode.EDIT); // Keep form in edit mode after remount
153-
resetKey();
154-
setIsConfirmPurposeChangeModalOpen(false);
155-
}
156-
setPendingChangeRegistrationPurpose("");
157-
};
158-
159-
const handleSelectedPurposeChange = (newSelectedPurpose: string) => {
160-
if (newSelectedPurpose && confirmedFormData.registration_purpose) {
161-
setIsConfirmPurposeChangeModalOpen(true);
162-
setPendingChangeRegistrationPurpose(newSelectedPurpose);
163-
}
164-
};
165-
166-
return (
167-
<>
168-
{isRedirectedFromContacts && !role.includes("cas_") && (
169-
<Note variant="important">
170-
To remove the current operation representative, please select a new
171-
contact to replace them.
172-
</Note>
173-
)}
174-
<ConfirmChangeOfFieldModal
175-
open={isConfirmPurposeChangeModalOpen}
176-
onCancel={cancelRegistrationPurposeChange}
177-
onConfirm={confirmRegistrationPurposeChange}
178-
modalText={
179-
<>
180-
<div>
181-
Are you sure you want to change your registration purpose? If you
182-
proceed,
183-
</div>
184-
<ul className="list-disc pl-5 mt-2">
185-
<li>
186-
Some operation information you have entered will be deleted.
187-
</li>
188-
<li>
189-
If this operation’s report is in progress, it will be deleted
190-
and restarted.
191-
</li>
192-
<li>Past years’ reports will be unaffected.</li>
193-
</ul>
194-
</>
195-
}
196-
confirmButtonText="Change registration purpose"
197-
/>
198-
<SingleStepTaskListForm
199-
key={key}
200-
allowEdit={!role.includes("cas_")}
201-
mode={formMode}
202-
error={error}
203-
schema={schema}
204-
uiSchema={uiSchema}
205-
formData={confirmedFormData ?? {}}
206-
onSubmit={handleSubmit}
207-
onChange={(e: IChangeEvent) => {
208-
const newSelectedPurpose = e.formData?.section3?.registration_purpose;
209-
if (newSelectedPurpose !== confirmedFormData.registration_purpose) {
210-
handleSelectedPurposeChange(newSelectedPurpose);
211-
}
212-
setIsMissingRepresentative(checkMissingRepresentative(e.formData));
213-
}}
214-
onCancel={() => router.push("/operations")}
215-
formContext={{
216-
operationId,
217-
isRegulatedOperation: regulatedOperationPurposes.includes(
218-
confirmedFormData.registration_purpose as RegistrationPurposes,
219-
),
220-
isOptedOut:
221-
formData.opted_in_operation?.final_reporting_year !== null,
222-
isCasDirector: role === FrontEndRoles.CAS_DIRECTOR,
223-
isEio: confirmedFormData.registration_purpose?.match(
224-
RegistrationPurposes.ELECTRICITY_IMPORT_OPERATION.valueOf(),
225-
),
226-
status: confirmedFormData.status,
227-
missing_representative_alert: isMissingRepresentative,
228-
...fileWidgetContext,
229-
}}
230-
/>
231-
</>
232-
);
233-
};
234-
235-
export default OperationInformationForm;

bciers/apps/administration/tests/components/operations/OperationInformationForm.test.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,10 @@ const newEntrantFormData = {
187187
name: "Operation 5",
188188
type: "Single Facility Operation",
189189
registration_purpose: RegistrationPurposes.NEW_ENTRANT_OPERATION,
190-
new_entrant_application: mockDataUri,
190+
new_entrant_application: JSON.stringify({
191+
name: "testpdf.pdf",
192+
status: "Clean",
193+
}),
191194
};
192195

193196
const operationId = "8be4c7aa-6ab3-4aad-9206-0ef914fea063";
@@ -357,7 +360,7 @@ describe("the OperationInformationForm component", () => {
357360
expect(screen.queryByRole("button", { name: "Edit" })).toBeNull();
358361
});
359362

360-
it("should edit and save the form", async () => {
363+
it.only("should edit and save the form", async () => {
361364
const uiSchema = await createAdministrationOperationInformationUiSchema();
362365
render(
363366
<OperationInformationForm
@@ -392,14 +395,17 @@ describe("the OperationInformationForm component", () => {
392395
expect(actionHandler).toHaveBeenCalledTimes(1);
393396
expect(actionHandler).toHaveBeenCalledWith(
394397
`registration/operations/${operationId}`,
395-
"PUT",
398+
"POST",
396399
`/operations/${operationId}`,
397-
{
398-
body: JSON.stringify({
399-
name: "Operation 4",
400-
type: "Single Facility Operation",
401-
}),
402-
},
400+
expect.toSatisfy((submittedFormData) => {
401+
expect(Object.fromEntries(submittedFormData.body).payload).toEqual(
402+
JSON.stringify({
403+
name: "Operation 4",
404+
type: "Single Facility Operation",
405+
}),
406+
);
407+
return true;
408+
}),
403409
);
404410

405411
// Expect the form to be submitted
@@ -785,10 +791,10 @@ describe("the OperationInformationForm component", () => {
785791
).toBeVisible();
786792
expect(screen.getByText("testpdf.pdf")).toBeVisible();
787793
expect(
788-
screen.getByRole("link", {
794+
screen.getByRole("button", {
789795
name: /preview/i,
790796
}),
791-
).toHaveAttribute("href", mockDataUri);
797+
).toBeVisible();
792798
});
793799

794800
it("should edit and save the new entrant application form", async () => {

bciers/libs/components/src/form/widgets/FileWidget.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,16 @@ export function FileElement({
9393

9494
if (localFile) {
9595
const anchorTag = document.createElement("a");
96+
const urlObject = URL.createObjectURL(localFile);
9697
Object.assign(anchorTag, {
9798
target: "_blank",
9899
rel: "noopener noreferrer",
99-
href: URL.createObjectURL(localFile),
100+
href: urlObject,
100101
download: localFile.name,
101102
});
102103
anchorTag.click();
103104
anchorTag.remove();
105+
URL.revokeObjectURL(urlObject);
104106
return;
105107
}
106108

0 commit comments

Comments
 (0)