Skip to content

Commit d03e0cc

Browse files
authored
COMP-697 report generation text changes and last generated timestamps (#670)
1 parent 9e4d6a5 commit d03e0cc

File tree

5 files changed

+189
-27
lines changed

5 files changed

+189
-27
lines changed

compliance-api/src/compliance_api/resources/document_job.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
@cors_preflight("GET, OPTIONS")
3838
@API.route("/inspections/<int:inspection_record_id>/recent", methods=["GET", "OPTIONS"])
39-
class DocumentJobs(Resource):
39+
class DocumentJobRecent(Resource):
4040
"""Resource for managing document jobs per inspection."""
4141

4242
@staticmethod
@@ -56,6 +56,27 @@ def get(inspection_record_id):
5656
return schema.dump(document_job), HTTPStatus.OK
5757

5858

59+
@cors_preflight("GET, OPTIONS")
60+
@API.route("/inspections/<int:inspection_record_id>/last-generated", methods=["GET", "OPTIONS"])
61+
class DocumentJobLastGenerated(Resource):
62+
"""Resource for managing document jobs per inspection."""
63+
64+
@staticmethod
65+
@API.response(code=200, description="Success", model=[DocumentJob])
66+
@ApiHelper.swagger_decorators(
67+
API, endpoint_description="Fetch most recent document job for user and inspection"
68+
)
69+
@auth.require
70+
def get(inspection_record_id):
71+
"""Fetch the last time a document was generated for user and inspection."""
72+
auth_user_guid = g.token_info.get("preferred_username")
73+
current_staff_user = StaffUserService.get_user_by_auth_guid(auth_user_guid)
74+
last_generated_time = DocumentJobService.get_last_generated_time_for_user(
75+
current_staff_user.id, inspection_record_id
76+
)
77+
return {"last_generated_time": last_generated_time}, HTTPStatus.OK
78+
79+
5980
@cors_preflight("OPTIONS, DELETE, PUT")
6081
@API.route("/<int:job_id>", methods=["OPTIONS", "DELETE", "PUT"])
6182
class DocumentJobById(Resource):

compliance-api/src/compliance_api/services/document_job.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ def get_most_recent_document_job_for_user(user_id, inspection_record_id):
2929
})
3030
return jobs[-1] if jobs else None
3131

32+
@staticmethod
33+
def get_last_generated_time_for_user(user_id, inspection_record_id):
34+
"""Get the last time a document was generated for a user."""
35+
jobs = DocumentJob.query.filter_by(
36+
user_id=user_id,
37+
inspection_record_id=inspection_record_id,
38+
).order_by(DocumentJob.completed_at.desc()).all()
39+
document_job = jobs[0] if jobs else None
40+
return document_job.completed_at.isoformat() if document_job else None
41+
3242
@staticmethod
3343
def update(document_job_id, user_id, update_data):
3444
"""Update a document job by its ID."""

compliance-api/tests/integration/api/test_document_job.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,3 +403,104 @@ def test_get_document_job_requires_auth(client, inspection_record):
403403
url = urljoin(API_BASE_URL, f"document-jobs/inspections/{inspection_record.id}/recent")
404404
result = client.get(url)
405405
assert result.status_code in (HTTPStatus.UNAUTHORIZED, 401)
406+
407+
408+
def test_last_generated_success(client, created_staff, jwt, inspection_record, mock_track_service, mock_auth_service):
409+
"""Test getting last-generated time for user and inspection."""
410+
staff_user = created_staff
411+
# Create a document job for the staff user
412+
now = datetime.now(timezone.utc)
413+
job_data = {
414+
"user_id": staff_user.id,
415+
"inspection_record_id": inspection_record.id,
416+
"status": DocumentJobStatusEnum.COMPLETED.value,
417+
"download_name": "last_generated.pdf",
418+
"relative_url": "documents/last_generated.pdf",
419+
"started_at": now,
420+
"completed_at": now,
421+
}
422+
DocumentJobService.create(job_data)
423+
424+
config = get_named_config("testing")
425+
header_claims = make_header_claims(staff_user, config)
426+
auth_header = factory_auth_header(jwt=jwt, claims=header_claims)
427+
428+
url = urljoin(API_BASE_URL, f"document-jobs/inspections/{inspection_record.id}/last-generated")
429+
result = client.get(url, headers=auth_header)
430+
431+
assert result.status_code == HTTPStatus.OK
432+
response_data = result.json
433+
assert "last_generated_time" in response_data
434+
assert response_data["last_generated_time"] == now.isoformat()
435+
436+
437+
def test_last_generated_requires_auth(client, inspection_record):
438+
"""Test last-generated endpoint requires auth."""
439+
url = urljoin(API_BASE_URL, f"document-jobs/inspections/{inspection_record.id}/last-generated")
440+
result = client.get(url)
441+
assert result.status_code in (HTTPStatus.UNAUTHORIZED, 401)
442+
443+
444+
def test_last_generated_no_jobs_returns_null(
445+
client,
446+
created_staff,
447+
jwt,
448+
inspection_record,
449+
mock_track_service,
450+
mock_auth_service
451+
):
452+
"""Test last-generated returns null if no jobs exist for user/inspection."""
453+
staff_user = created_staff
454+
config = get_named_config("testing")
455+
header_claims = make_header_claims(staff_user, config)
456+
auth_header = factory_auth_header(jwt=jwt, claims=header_claims)
457+
458+
url = urljoin(API_BASE_URL, f"document-jobs/inspections/{inspection_record.id}/last-generated")
459+
result = client.get(url, headers=auth_header)
460+
461+
assert result.status_code == HTTPStatus.OK
462+
response_data = result.json
463+
assert "last_generated_time" in response_data
464+
assert response_data["last_generated_time"] is None
465+
466+
467+
def test_last_generated_other_user_job_returns_null(
468+
client,
469+
created_staff,
470+
jwt,
471+
inspection_record,
472+
mock_track_service,
473+
mock_auth_service
474+
):
475+
"""Test last-generated returns null if only a different user has a job for the inspection."""
476+
staff_user = created_staff
477+
# Create a different staff user
478+
user_data = StaffScenario.default_data.value
479+
auth_user_guid = f"other_{datetime.now().timestamp()}"
480+
user_data["auth_user_guid"] = auth_user_guid
481+
other_staff = StaffScenario.create(user_data)
482+
assert other_staff.id != staff_user.id
483+
484+
# Create a document job for the other staff user
485+
job_data = {
486+
"user_id": other_staff.id,
487+
"inspection_record_id": inspection_record.id,
488+
"status": DocumentJobStatusEnum.COMPLETED.value,
489+
"download_name": "other_user_last_generated.pdf",
490+
"relative_url": "documents/other_user_last_generated.pdf",
491+
"started_at": datetime.now(timezone.utc),
492+
"completed_at": datetime.now(timezone.utc),
493+
}
494+
DocumentJobService.create(job_data)
495+
496+
config = get_named_config("testing")
497+
header_claims = make_header_claims(staff_user, config)
498+
auth_header = factory_auth_header(jwt=jwt, claims=header_claims)
499+
500+
url = urljoin(API_BASE_URL, f"document-jobs/inspections/{inspection_record.id}/last-generated")
501+
result = client.get(url, headers=auth_header)
502+
503+
assert result.status_code == HTTPStatus.OK
504+
response_data = result.json
505+
assert "last_generated_time" in response_data
506+
assert response_data["last_generated_time"] is None

compliance-web/src/components/App/Inspections/Profile/Reports/PreviewDownloadButton.tsx

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { AxiosError } from "axios";
3232
import { notify } from "@/store/snackbarStore";
3333
import {
3434
useDeleteDocumentJobs,
35+
useLastGeneratedTimeForUser,
3536
useMostRecentDocumentJobForUser,
3637
} from "@/hooks/useDocumentJobs";
3738
import { DocumentJob, DocumentJobStatus } from "@/models/documentJob";
@@ -46,6 +47,11 @@ const PreviewDownloadButton = () => {
4647
inspectionReportsData?.id ?? 0
4748
);
4849

50+
const { data: lastGeneratedTimeObj } = useLastGeneratedTimeForUser(
51+
inspectionReportsData?.id ?? 0
52+
);
53+
const lastGeneratedTime = lastGeneratedTimeObj?.last_generated_time;
54+
4955
const [previewClicked, setPreviewClicked] = useState(false);
5056
const [generatingDocx, setGeneratingDocx] = useState(false);
5157
const [open, setOpen] = useState(false);
@@ -362,36 +368,43 @@ const PreviewDownloadButton = () => {
362368
width: "100%",
363369
}}
364370
>
365-
{isCompleted ? (
366-
<Box
367-
sx={{
368-
display: "flex",
369-
flexDirection: "column",
370-
gap: 0.25,
371-
pl: 2,
372-
}}
373-
>
371+
<Box
372+
sx={{
373+
display: "flex",
374+
flexDirection: "column",
375+
gap: 0.25,
376+
pl: 2,
377+
}}
378+
>
379+
{isCompleted || lastGeneratedTime ? (
380+
<>
381+
<Typography
382+
variant="caption"
383+
color={BCDesignTokens.typographyColorPlaceholder}
384+
>
385+
Last PDF Generated:
386+
</Typography>
387+
<Typography
388+
variant="caption"
389+
color={BCDesignTokens.typographyColorPrimary}
390+
>
391+
{dateUtils.formatDate(
392+
isCompleted
393+
? (documentJob?.completed_at ?? "")
394+
: (lastGeneratedTime ?? ""),
395+
"MMM D, YYYY hh:mm A"
396+
)}
397+
</Typography>
398+
</>
399+
) : (
374400
<Typography
375401
variant="caption"
376402
color={BCDesignTokens.typographyColorPlaceholder}
377403
>
378-
Last PDF Generated:
379-
</Typography>
380-
<Typography
381-
variant="caption"
382-
color={BCDesignTokens.typographyColorPrimary}
383-
>
384-
{dateUtils.formatDate(
385-
documentJob?.completed_at ?? "",
386-
"MMM D, YYYY hh:mm A"
387-
)}
404+
Nothing generated yet
388405
</Typography>
389-
</Box>
390-
) : (
391-
<Typography variant="caption" px={2}>
392-
No PDF generated yet.
393-
</Typography>
394-
)}
406+
)}
407+
</Box>
395408
<Link
396409
onClick={
397410
isCompleted ? handleDownloadReportFromURL : () => {}
@@ -407,7 +420,7 @@ const PreviewDownloadButton = () => {
407420
}}
408421
>
409422
<DownloadRounded sx={{ fontSize: 16 }} />
410-
Download Last PDF
423+
Download
411424
</Link>
412425
</Box>
413426
</Box>

compliance-web/src/hooks/useDocumentJobs.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ const fetchMostRecentDocumentJobForUser = (
1010
});
1111
};
1212

13+
const fetchLastGeneratedTimeForUser = (
14+
inspectionReportID: number
15+
): Promise<{ last_generated_time: string }> => {
16+
return request({
17+
url: `/document-jobs/inspections/${inspectionReportID}/last-generated`,
18+
});
19+
};
20+
1321
const updateDocumentJob = (
1422
jobId: string,
1523
data: Partial<DocumentJob>
@@ -59,6 +67,15 @@ export const useMostRecentDocumentJobForUser = (inspectionReportID: number) => {
5967
});
6068
};
6169

70+
export const useLastGeneratedTimeForUser = (inspectionReportID: number) => {
71+
return useQuery<{ last_generated_time: string }, Error>({
72+
queryKey: ["lastGeneratedTime", inspectionReportID],
73+
queryFn: () => fetchLastGeneratedTimeForUser(inspectionReportID ?? 0),
74+
refetchInterval: 30000,
75+
enabled: !!inspectionReportID,
76+
});
77+
};
78+
6279
export const useDeleteDocumentJobs = () => {
6380
return useMutation<void, Error, string>({
6481
mutationFn: (jobId: string) => deleteDocumentJob(jobId),

0 commit comments

Comments
 (0)