From 16fbd3f6eeb1df5023054a605374a58e024d3ed2 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Wed, 6 Nov 2024 14:56:43 -0800 Subject: [PATCH 01/19] chore: build transfers grid components and page --- .../models/transfers/transferColumns.ts | 39 +++++++++++++ .../models/transfers/transferGroupColumns.ts | 52 +++++++++++++++++ .../components/transfers/TransferDataGrid.tsx | 56 +++++++++++++++++++ .../transfers/TransferDataGridPage.tsx | 30 ++++++++++ .../transfers/fetchTransferEventsPageData.tsx | 47 ++++++++++++++++ .../app/components/transfers/types.ts | 15 +++++ .../app/idir/cas_admin/transfers/page.tsx | 17 ++++++ bciers/libs/utils/src/formatTimestamp.ts | 24 ++++++++ 8 files changed, 280 insertions(+) create mode 100644 bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts create mode 100644 bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts create mode 100644 bciers/apps/registration/app/components/transfers/TransferDataGrid.tsx create mode 100644 bciers/apps/registration/app/components/transfers/TransferDataGridPage.tsx create mode 100644 bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx create mode 100644 bciers/apps/registration/app/components/transfers/types.ts create mode 100644 bciers/apps/registration/app/idir/cas_admin/transfers/page.tsx create mode 100644 bciers/libs/utils/src/formatTimestamp.ts diff --git a/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts b/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts new file mode 100644 index 0000000000..05d354e7a4 --- /dev/null +++ b/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts @@ -0,0 +1,39 @@ +import { GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; + +const transferColumns = ( + ActionCell: (params: GridRenderCellParams) => JSX.Element, +) => { + const columns: GridColDef[] = [ + { + field: "submission_date", + headerName: "Submission Date", + sortable: false, + // Set flex to 1 to make the column take up all the remaining width if user zooms out + width: 200, + }, + { field: "operation", headerName: "Operation", width: 200 }, + { field: "facility", headerName: "Facility", flex: 1 }, + { + field: "status", + headerName: "Status", + width: 200, + }, + { + field: "effective_date", + headerName: "Effective Date", + sortable: false, + width: 200, + }, + { + field: "action", + headerName: "Actions", + renderCell: ActionCell, + sortable: false, + width: 120, + }, + ]; + + return columns; +}; + +export default transferColumns; diff --git a/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts b/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts new file mode 100644 index 0000000000..d0f840580c --- /dev/null +++ b/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts @@ -0,0 +1,52 @@ +import { + GridColumnGroupHeaderParams, + GridColumnGroupingModel, +} from "@mui/x-data-grid"; +import EmptyGroupCell from "@bciers/components/datagrid/cells/EmptyGroupCell"; + +const contactGroupColumns = ( + SearchCell: (params: GridColumnGroupHeaderParams) => JSX.Element, +) => { + const columnGroupModel: GridColumnGroupingModel = [ + { + groupId: "submission_date", + headerName: "Submission Date", + renderHeaderGroup: EmptyGroupCell, + children: [{ field: "submission_date" }], + }, + { + groupId: "operation", + headerName: "Operation", + renderHeaderGroup: SearchCell, + children: [{ field: "operation" }], + }, + { + groupId: "facility", + headerName: "Facility", + renderHeaderGroup: SearchCell, + children: [{ field: "facility" }], + }, + { + groupId: "status", + headerName: "Status", + renderHeaderGroup: SearchCell, + children: [{ field: "status" }], + }, + { + groupId: "effective_date", + headerName: "Effective Date", + renderHeaderGroup: EmptyGroupCell, + children: [{ field: "effective_date" }], + }, + { + groupId: "action", + headerName: "Actions", + renderHeaderGroup: EmptyGroupCell, + children: [{ field: "action" }], + }, + ]; + + return columnGroupModel; +}; + +export default contactGroupColumns; diff --git a/bciers/apps/registration/app/components/transfers/TransferDataGrid.tsx b/bciers/apps/registration/app/components/transfers/TransferDataGrid.tsx new file mode 100644 index 0000000000..3d220cdd8a --- /dev/null +++ b/bciers/apps/registration/app/components/transfers/TransferDataGrid.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useMemo, useState } from "react"; +import DataGrid from "@bciers/components/datagrid/DataGrid"; +import { TransferRow } from "./types"; +import HeaderSearchCell from "@bciers/components/datagrid/cells/HeaderSearchCell"; +import ActionCellFactory from "@bciers/components/datagrid/cells/ActionCellFactory"; +import fetchTransfersPageData from "./fetchTransferEventsPageData"; +import { GridRenderCellParams } from "@mui/x-data-grid"; +import transferColumns from "@/registration/app/components/datagrid/models/transfers/transferColumns"; +import transferGroupColumns from "@/registration/app/components/datagrid/models/transfers/transferGroupColumns"; + +const TransfersActionCell = ActionCellFactory({ + generateHref: (params: GridRenderCellParams) => { + return `/transfers/${params.row.id}`; + }, + cellText: "View Details", +}); + +const TransfersDataGrid = ({ + initialData, +}: { + initialData: { + rows: TransferRow[]; + row_count: number; + }; +}) => { + console.log("initialData", initialData); + const [lastFocusedField, setLastFocusedField] = useState(null); + + const SearchCell = useMemo( + () => HeaderSearchCell({ lastFocusedField, setLastFocusedField }), + [lastFocusedField, setLastFocusedField], + ); + + const ActionCell = useMemo(() => TransfersActionCell, []); + + const columns = useMemo(() => transferColumns(ActionCell), [ActionCell]); + + const columnGroup = useMemo( + () => transferGroupColumns(SearchCell), + [SearchCell], + ); + + return ( + + ); +}; + +export default TransfersDataGrid; diff --git a/bciers/apps/registration/app/components/transfers/TransferDataGridPage.tsx b/bciers/apps/registration/app/components/transfers/TransferDataGridPage.tsx new file mode 100644 index 0000000000..60a784fbe3 --- /dev/null +++ b/bciers/apps/registration/app/components/transfers/TransferDataGridPage.tsx @@ -0,0 +1,30 @@ +import TransferDataGrid from "./TransferDataGrid"; +import { Suspense } from "react"; +import Loading from "@bciers/components/loading/SkeletonGrid"; +import { TransferRow, TransfersSearchParams } from "./types"; +import fetchTransferEventsPageData from "./fetchTransferEventsPageData"; + +// 🧩 Main component +export default async function TransferDataGridPage({ + searchParams, +}: { + searchParams: TransfersSearchParams; +}) { + // Fetch transfers data + const transfers: { + rows: TransferRow[]; + row_count: number; + } = await fetchTransferEventsPageData(searchParams); + if (!transfers) { + return
No transfers data in database.
; + } + + // Render the DataGrid component + return ( + }> +
+ +
+
+ ); +} diff --git a/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx b/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx new file mode 100644 index 0000000000..67defd81ab --- /dev/null +++ b/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx @@ -0,0 +1,47 @@ +import buildQueryParams from "@bciers/utils/src/buildQueryParams"; +import { actionHandler } from "@bciers/actions"; +import { TransfersSearchParams } from "./types"; +import { GridRowsProp } from "@mui/x-data-grid"; +import formatTimestamp from "@bciers/utils/src/formatTimestamp"; + +export const formatTransferRows = (rows: GridRowsProp) => { + return rows.map( + ({ + id, + operation, + facilities, + status, + effective_date, + submission_date, + }) => { + return { + id, + operation, + facilities: facilities ?? "N/A", + status, + submission_date: formatTimestamp(submission_date), + effective_date: formatTimestamp(effective_date), + }; + }, + ); +}; +// 🛠️ Function to fetch transfers +export default async function fetchTransferEventsPageData( + searchParams: TransfersSearchParams, +) { + try { + const queryParams = buildQueryParams(searchParams); + // fetch data from server + const pageData = await actionHandler( + `registration/transfer-events${queryParams}`, + "GET", + "", + ); + return { + rows: formatTransferRows(pageData.items), + row_count: pageData.count, + }; + } catch (error) { + throw error; + } +} diff --git a/bciers/apps/registration/app/components/transfers/types.ts b/bciers/apps/registration/app/components/transfers/types.ts new file mode 100644 index 0000000000..45e8469e79 --- /dev/null +++ b/bciers/apps/registration/app/components/transfers/types.ts @@ -0,0 +1,15 @@ +export interface TransferRow { + id: number; + operation: string; + facilities: string; + status: string; + effective_date: string; + submission_date: string; +} + +export interface TransfersSearchParams { + [key: string]: string | number | undefined; + operation?: string; + facilities?: string; + status?: string; +} diff --git a/bciers/apps/registration/app/idir/cas_admin/transfers/page.tsx b/bciers/apps/registration/app/idir/cas_admin/transfers/page.tsx new file mode 100644 index 0000000000..76803c106c --- /dev/null +++ b/bciers/apps/registration/app/idir/cas_admin/transfers/page.tsx @@ -0,0 +1,17 @@ +// 🚩 flagging that for shared routes between roles, "Page" code is a component for code maintainability +import Loading from "@bciers/components/loading/SkeletonGrid"; +import TransferDataGridPage from "@/registration/app/components/transfers/TransferDataGridPage"; +import { Suspense } from "react"; +import { TransfersSearchParams } from "@/registration/app/components/transfers/types"; + +export default async function Page({ + searchParams, +}: { + searchParams: TransfersSearchParams; +}) { + return ( + }> + + + ); +} diff --git a/bciers/libs/utils/src/formatTimestamp.ts b/bciers/libs/utils/src/formatTimestamp.ts new file mode 100644 index 0000000000..6185f14981 --- /dev/null +++ b/bciers/libs/utils/src/formatTimestamp.ts @@ -0,0 +1,24 @@ +const formatTimestamp = (timestamp: string) => { + if (!timestamp) return undefined; + + const date = new Date(timestamp).toLocaleString("en-CA", { + month: "short", + day: "numeric", + year: "numeric", + timeZone: "America/Vancouver", + }); + + const timeWithTimeZone = new Date(timestamp).toLocaleString("en-CA", { + hour: "numeric", + minute: "numeric", + second: "numeric", + timeZoneName: "short", + timeZone: "America/Vancouver", + }); + + // Return with a line break so we can display date and time on separate lines + // in the DataGrid cell using whiteSpace: "pre-line" CSS + return `${date}\n${timeWithTimeZone}`; +}; + +export default formatTimestamp; From 75c0a0e32e7a9a8a531446f95f6dfa958e6a156e Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Wed, 6 Nov 2024 15:35:38 -0800 Subject: [PATCH 02/19] chore: transfer events BE work --- bc_obps/registration/api/v1/__init__.py | 1 + .../registration/api/v1/transfer_events.py | 36 +++++++++++++++++++ bc_obps/registration/constants.py | 1 + .../registration/schema/v1/transfer_event.py | 29 +++++++++++++++ bc_obps/service/transfer_event_service.py | 21 +++++++++++ 5 files changed, 88 insertions(+) create mode 100644 bc_obps/registration/api/v1/transfer_events.py create mode 100644 bc_obps/registration/schema/v1/transfer_event.py create mode 100644 bc_obps/service/transfer_event_service.py diff --git a/bc_obps/registration/api/v1/__init__.py b/bc_obps/registration/api/v1/__init__.py index 3321019fbd..8e139e0658 100644 --- a/bc_obps/registration/api/v1/__init__.py +++ b/bc_obps/registration/api/v1/__init__.py @@ -13,6 +13,7 @@ users, facilities, contacts, + transfer_events, ) from ._operations import operation_id from ._operations._operation_id import update_status, facilities, operation_representatives diff --git a/bc_obps/registration/api/v1/transfer_events.py b/bc_obps/registration/api/v1/transfer_events.py new file mode 100644 index 0000000000..9c6cd9e74e --- /dev/null +++ b/bc_obps/registration/api/v1/transfer_events.py @@ -0,0 +1,36 @@ +from typing import List, Literal, Optional +from registration.models.event.transfer_event import TransferEvent +from registration.schema.v1.transfer_event import TransferEventFilterSchema, TransferEventListOut +from service.transfer_event_service import TransferEventService +from common.permissions import authorize +from django.http import HttpRequest +from registration.utils import CustomPagination +from registration.constants import TRANSFER_EVENT_TAGS +from ninja.pagination import paginate +from registration.decorators import handle_http_errors +from ..router import router +from service.error_service.custom_codes_4xx import custom_codes_4xx +from ninja import Query +from django.db.models import QuerySet +from registration.schema.generic import Message + + +@router.get( + "/transfer-events", + response={200: List[TransferEventListOut], custom_codes_4xx: Message}, + tags=TRANSFER_EVENT_TAGS, + description="""Retrieves a paginated list of transfer events based on the provided filters. + The endpoint allows authorized users to view and sort transfer events filtered by various criteria such as operation, facility, and status.""", + auth=authorize("approved_authorized_roles"), +) +@handle_http_errors() +@paginate(CustomPagination) +def list_transfer_events( + request: HttpRequest, + filters: TransferEventFilterSchema = Query(...), + sort_field: Optional[str] = "status", + sort_order: Optional[Literal["desc", "asc"]] = "desc", + paginate_result: bool = Query(True, description="Whether to paginate the results"), +) -> QuerySet[TransferEvent]: + # NOTE: PageNumberPagination raises an error if we pass the response as a tuple (like 200, ...) + return TransferEventService.list_transfer_events(sort_field, sort_order, filters) diff --git a/bc_obps/registration/constants.py b/bc_obps/registration/constants.py index 15b8e057c7..0856a17e61 100644 --- a/bc_obps/registration/constants.py +++ b/bc_obps/registration/constants.py @@ -29,4 +29,5 @@ USER_OPERATOR_TAGS_V2 = ["User Operator V2"] MISC_TAGS = ["Misc V1"] CONTACT_TAGS = ["Contact V1"] +TRANSFER_EVENT_TAGS = ["Transfer Event V1"] V2 = ["V2"] diff --git a/bc_obps/registration/schema/v1/transfer_event.py b/bc_obps/registration/schema/v1/transfer_event.py new file mode 100644 index 0000000000..cc9c1e43a1 --- /dev/null +++ b/bc_obps/registration/schema/v1/transfer_event.py @@ -0,0 +1,29 @@ +from typing import List, Optional +from registration.models.facility import Facility +from registration.models.event.transfer_event import TransferEvent +from ninja import ModelSchema, Field, FilterSchema + + +class FacilityForTransferEventGrid(ModelSchema): + class Meta: + model = Facility + fields = ['name'] + + +class TransferEventListOut(ModelSchema): + operation: str = Field(None, alias="operation.name") + facilities: Optional[List[FacilityForTransferEventGrid]] = None + + class Meta: + model = TransferEvent + fields = ['id', 'effective_date', 'status'] + + +class TransferEventFilterSchema(FilterSchema): + # NOTE: we could simply use the `q` parameter to filter by related fields but, + # due to this issue: https://github.com/vitalik/django-ninja/issues/1037 mypy is unhappy so I'm using the `json_schema_extra` parameter + # If we want to achieve more by using the `q` parameter, we should use it and ignore the mypy error + effective_date: Optional[str] = Field(None, json_schema_extra={'q': 'effective_date__icontains'}) + operation: Optional[str] = Field(None, json_schema_extra={'q': 'operation__name__icontains'}) + facilities: Optional[str] = Field(None, json_schema_extra={'q': 'facilities__icontains'}) + status: Optional[str] = Field(None, json_schema_extra={'q': 'status__icontains'}) diff --git a/bc_obps/service/transfer_event_service.py b/bc_obps/service/transfer_event_service.py new file mode 100644 index 0000000000..e654fae227 --- /dev/null +++ b/bc_obps/service/transfer_event_service.py @@ -0,0 +1,21 @@ +from registration.models.event.transfer_event import TransferEvent +from typing import Optional +from django.db.models import QuerySet + + +from registration.schema.v1.transfer_event import TransferEventFilterSchema +from ninja import Query + + +class TransferEventService: + @classmethod + def list_transfer_events( + cls, + sort_field: Optional[str], + sort_order: Optional[str], + filters: TransferEventFilterSchema = Query(...), + ) -> QuerySet[TransferEvent]: + sort_direction = "-" if sort_order == "desc" else "" + sort_by = f"{sort_direction}{sort_field}" + base_qs = TransferEvent.objects.all() + return filters.filter(base_qs).order_by(sort_by) From e2e3666cbba02cbf9de305b3a0d4836d43fe0ba7 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Thu, 7 Nov 2024 11:51:15 -0800 Subject: [PATCH 03/19] chore: migration to change transfer statuses --- ...historicaltransferevent_status_and_more.py | 39 +++++++++++++++++++ .../models/event/transfer_event.py | 4 +- 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 bc_obps/registration/migrations/0055_alter_historicaltransferevent_status_and_more.py diff --git a/bc_obps/registration/migrations/0055_alter_historicaltransferevent_status_and_more.py b/bc_obps/registration/migrations/0055_alter_historicaltransferevent_status_and_more.py new file mode 100644 index 0000000000..90833024a1 --- /dev/null +++ b/bc_obps/registration/migrations/0055_alter_historicaltransferevent_status_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.0.7 on 2024-11-07 19:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('registration', '0054_V1_13_0'), + ] + + operations = [ + migrations.AlterField( + model_name='historicaltransferevent', + name='status', + field=models.CharField( + choices=[ + ('Complete', 'Complete'), + ('To be transferred', 'To Be Transferred'), + ('Transferred', 'Transferred'), + ], + default='To be transferred', + max_length=100, + ), + ), + migrations.AlterField( + model_name='transferevent', + name='status', + field=models.CharField( + choices=[ + ('Complete', 'Complete'), + ('To be transferred', 'To Be Transferred'), + ('Transferred', 'Transferred'), + ], + default='To be transferred', + max_length=100, + ), + ), + ] diff --git a/bc_obps/registration/models/event/transfer_event.py b/bc_obps/registration/models/event/transfer_event.py index b3f7dd993a..5125b57c59 100644 --- a/bc_obps/registration/models/event/transfer_event.py +++ b/bc_obps/registration/models/event/transfer_event.py @@ -7,7 +7,7 @@ class TransferEvent(EventBaseModel): class Statuses(models.TextChoices): COMPLETE = "Complete" - PENDING = "Pending" + TO_BE_TRANSFERRED = "To be transferred" TRANSFERRED = "Transferred" class FutureDesignatedOperatorChoices(models.TextChoices): @@ -36,7 +36,7 @@ class FutureDesignatedOperatorChoices(models.TextChoices): status = models.CharField( max_length=100, choices=Statuses.choices, - default=Statuses.PENDING, + default=Statuses.TO_BE_TRANSFERRED, ) history = HistoricalRecords( table_name='erc_history"."transfer_event_history', From 197a654d16a24fa6ac56b88e76932ba2aec020f2 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Thu, 7 Nov 2024 16:29:53 -0800 Subject: [PATCH 04/19] chore: spec work --- .../fixtures/mock/transfer_event.json | 9 ++-- ...ent_future_designated_operator_and_more.py | 21 +++++++++ .../models/event/event_base_model.py | 1 + .../models/event/transfer_event.py | 12 +----- .../registration/schema/v1/transfer_event.py | 4 +- .../models/transfers/transferGroupColumns.ts | 4 +- .../transfers/fetchTransferEventsPageData.tsx | 43 ++++++++++++------- 7 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 bc_obps/registration/migrations/0056_remove_historicaltransferevent_future_designated_operator_and_more.py diff --git a/bc_obps/registration/fixtures/mock/transfer_event.json b/bc_obps/registration/fixtures/mock/transfer_event.json index 3e0adca234..d352d38761 100644 --- a/bc_obps/registration/fixtures/mock/transfer_event.json +++ b/bc_obps/registration/fixtures/mock/transfer_event.json @@ -11,8 +11,7 @@ "description": "Sold my operation to the other operator so that I could move to Hawaii. Don't tell my employees they don't know.", "status": "Transferred", "other_operator": "4242ea9d-b917-4129-93c2-db00b7451051", - "other_operator_contact": 13, - "future_designated_operator": "Other Operator" + "other_operator_contact": 13 } }, { @@ -24,8 +23,7 @@ "effective_date": "2024-08-21T09:00:00Z", "description": "I was the designated operator but I'm being relieved of my duties because my other operators don't trust me anymore. Transfer this operation over to another one of the operators in our multiple-operators relationship. See if you can do better Mia!", "other_operator": "4242ea9d-b917-4129-93c2-db00b7451051", - "other_operator_contact": 15, - "future_designated_operator": "Other Operator" + "other_operator_contact": 15 } }, { @@ -43,8 +41,7 @@ "effective_date": "2024-12-25T09:00:00Z", "description": "I don't know what I'm doing but this seems like the right thing to do.", "other_operator": "4242ea9d-b917-4129-93c2-db00b7451051", - "other_operator_contact": 15, - "future_designated_operator": "Not sure" + "other_operator_contact": 15 } } ] diff --git a/bc_obps/registration/migrations/0056_remove_historicaltransferevent_future_designated_operator_and_more.py b/bc_obps/registration/migrations/0056_remove_historicaltransferevent_future_designated_operator_and_more.py new file mode 100644 index 0000000000..d52094a79d --- /dev/null +++ b/bc_obps/registration/migrations/0056_remove_historicaltransferevent_future_designated_operator_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.7 on 2024-11-07 23:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('registration', '0055_alter_historicaltransferevent_status_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='historicaltransferevent', + name='future_designated_operator', + ), + migrations.RemoveField( + model_name='transferevent', + name='future_designated_operator', + ), + ] diff --git a/bc_obps/registration/models/event/event_base_model.py b/bc_obps/registration/models/event/event_base_model.py index b3f4c3db30..489e67e168 100644 --- a/bc_obps/registration/models/event/event_base_model.py +++ b/bc_obps/registration/models/event/event_base_model.py @@ -4,6 +4,7 @@ class EventBaseModel(TimeStampedModel): + # brianna how does this work operation/facilities? id = models.UUIDField(primary_key=True, default=uuid.uuid4, db_comment="Primary key to identify the event") effective_date = models.DateTimeField(db_comment="The effective date of the event") operation = models.ForeignKey(Operation, null=True, blank=True, on_delete=models.PROTECT) diff --git a/bc_obps/registration/models/event/transfer_event.py b/bc_obps/registration/models/event/transfer_event.py index 5125b57c59..23a679f6ad 100644 --- a/bc_obps/registration/models/event/transfer_event.py +++ b/bc_obps/registration/models/event/transfer_event.py @@ -5,22 +5,12 @@ class TransferEvent(EventBaseModel): + # ok to display the db ones in the grid class Statuses(models.TextChoices): COMPLETE = "Complete" TO_BE_TRANSFERRED = "To be transferred" TRANSFERRED = "Transferred" - - class FutureDesignatedOperatorChoices(models.TextChoices): - MY_OPERATOR = "My Operator" - OTHER_OPERATOR = "Other Operator" - NOT_SURE = "Not Sure" - description = models.TextField(db_comment="Description of the transfer or change in designated operator.") - future_designated_operator = models.CharField( - max_length=1000, - choices=FutureDesignatedOperatorChoices.choices, - db_comment="The designated operator of the entit(y)/(ies) associated with the transfer, who will be responsible for matters related to GGERR.", - ) other_operator = models.ForeignKey( Operator, on_delete=models.PROTECT, diff --git a/bc_obps/registration/schema/v1/transfer_event.py b/bc_obps/registration/schema/v1/transfer_event.py index cc9c1e43a1..7acbdc0d72 100644 --- a/bc_obps/registration/schema/v1/transfer_event.py +++ b/bc_obps/registration/schema/v1/transfer_event.py @@ -11,12 +11,12 @@ class Meta: class TransferEventListOut(ModelSchema): - operation: str = Field(None, alias="operation.name") + operation: Optional[str] = Field(None, alias="operation.name") facilities: Optional[List[FacilityForTransferEventGrid]] = None class Meta: model = TransferEvent - fields = ['id', 'effective_date', 'status'] + fields = ['id', 'effective_date', 'status','created_at'] class TransferEventFilterSchema(FilterSchema): diff --git a/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts b/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts index d0f840580c..4cb1a59848 100644 --- a/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts +++ b/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts @@ -4,7 +4,7 @@ import { } from "@mui/x-data-grid"; import EmptyGroupCell from "@bciers/components/datagrid/cells/EmptyGroupCell"; -const contactGroupColumns = ( +const trasnsferGroupColumns = ( SearchCell: (params: GridColumnGroupHeaderParams) => JSX.Element, ) => { const columnGroupModel: GridColumnGroupingModel = [ @@ -49,4 +49,4 @@ const contactGroupColumns = ( return columnGroupModel; }; -export default contactGroupColumns; +export default trasnsferGroupColumns; diff --git a/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx b/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx index 67defd81ab..900f3f42d6 100644 --- a/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx +++ b/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx @@ -5,25 +5,34 @@ import { GridRowsProp } from "@mui/x-data-grid"; import formatTimestamp from "@bciers/utils/src/formatTimestamp"; export const formatTransferRows = (rows: GridRowsProp) => { - return rows.map( - ({ - id, - operation, - facilities, - status, - effective_date, - submission_date, - }) => { + const operationRows = rows + .filter((row) => row.operation) + .map(({ id, operation, status, effective_date, created_at }) => { return { id, - operation, - facilities: facilities ?? "N/A", + operation: operation, + facility: "N/A", status, - submission_date: formatTimestamp(submission_date), + submission_date: formatTimestamp(created_at), effective_date: formatTimestamp(effective_date), }; - }, - ); + }); + const facilityRows = rows + .filter((row) => row.facilities) + .flatMap(({ id, facilities, status, effective_date, created_at }) => { + return (facilities || []).map((facility) => { + console.log("facility", facility); + return { + id, + operation: "N/A", + facility: facility.name, // Use individual facility here + status, + submission_date: formatTimestamp(created_at), + effective_date: formatTimestamp(effective_date), + }; + }); + }); + return [...operationRows, ...facilityRows]; }; // 🛠️ Function to fetch transfers export default async function fetchTransferEventsPageData( @@ -37,9 +46,11 @@ export default async function fetchTransferEventsPageData( "GET", "", ); + const formattedRows = formatTransferRows(pageData.items); + // brianna why is frontend showing Facility 2 twice? initial data is ok return { - rows: formatTransferRows(pageData.items), - row_count: pageData.count, + rows: formattedRows, + row_count: formattedRows.length, }; } catch (error) { throw error; From 539bac1176bdddc068ac6cf6746e2b2c1fa15157 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Fri, 8 Nov 2024 14:14:33 -0800 Subject: [PATCH 05/19] chore: fix filtering --- bc_obps/registration/schema/v1/transfer_event.py | 3 ++- .../datagrid/models/transfers/transferColumns.ts | 2 +- .../datagrid/models/transfers/transferGroupColumns.ts | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/bc_obps/registration/schema/v1/transfer_event.py b/bc_obps/registration/schema/v1/transfer_event.py index 7acbdc0d72..011639b0c7 100644 --- a/bc_obps/registration/schema/v1/transfer_event.py +++ b/bc_obps/registration/schema/v1/transfer_event.py @@ -25,5 +25,6 @@ class TransferEventFilterSchema(FilterSchema): # If we want to achieve more by using the `q` parameter, we should use it and ignore the mypy error effective_date: Optional[str] = Field(None, json_schema_extra={'q': 'effective_date__icontains'}) operation: Optional[str] = Field(None, json_schema_extra={'q': 'operation__name__icontains'}) - facilities: Optional[str] = Field(None, json_schema_extra={'q': 'facilities__icontains'}) + # facilities: Optional[str] = None + facilities: Optional[str] = Field(None, json_schema_extra={'q': 'facilities__name__icontains'}) status: Optional[str] = Field(None, json_schema_extra={'q': 'status__icontains'}) diff --git a/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts b/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts index 05d354e7a4..583c06ff76 100644 --- a/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts +++ b/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts @@ -12,7 +12,7 @@ const transferColumns = ( width: 200, }, { field: "operation", headerName: "Operation", width: 200 }, - { field: "facility", headerName: "Facility", flex: 1 }, + { field: "facilities", headerName: "Facility", flex: 1 }, { field: "status", headerName: "Status", diff --git a/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts b/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts index 4cb1a59848..80f73713e8 100644 --- a/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts +++ b/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts @@ -4,7 +4,7 @@ import { } from "@mui/x-data-grid"; import EmptyGroupCell from "@bciers/components/datagrid/cells/EmptyGroupCell"; -const trasnsferGroupColumns = ( +const transferGroupColumns = ( SearchCell: (params: GridColumnGroupHeaderParams) => JSX.Element, ) => { const columnGroupModel: GridColumnGroupingModel = [ @@ -21,10 +21,10 @@ const trasnsferGroupColumns = ( children: [{ field: "operation" }], }, { - groupId: "facility", + groupId: "facilities", headerName: "Facility", renderHeaderGroup: SearchCell, - children: [{ field: "facility" }], + children: [{ field: "facilities" }], }, { groupId: "status", @@ -49,4 +49,4 @@ const trasnsferGroupColumns = ( return columnGroupModel; }; -export default trasnsferGroupColumns; +export default transferGroupColumns; From b83b73e19c65ca5bd5e23da3ec4005b742a23cf7 Mon Sep 17 00:00:00 2001 From: SeSo Date: Fri, 8 Nov 2024 15:27:39 -0800 Subject: [PATCH 06/19] chore: spec work - facilities into one row --- .../registration/schema/v1/transfer_event.py | 21 +++++++++++++------ bc_obps/service/transfer_event_service.py | 3 +-- .../models/transfers/transferColumns.ts | 6 +++--- .../models/transfers/transferGroupColumns.ts | 12 +++++------ .../components/transfers/TransferDataGrid.tsx | 1 - .../transfers/fetchTransferEventsPageData.tsx | 6 +++--- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/bc_obps/registration/schema/v1/transfer_event.py b/bc_obps/registration/schema/v1/transfer_event.py index 011639b0c7..bc25b1fd10 100644 --- a/bc_obps/registration/schema/v1/transfer_event.py +++ b/bc_obps/registration/schema/v1/transfer_event.py @@ -1,4 +1,6 @@ from typing import List, Optional +from uuid import UUID, uuid4 + from registration.models.facility import Facility from registration.models.event.transfer_event import TransferEvent from ninja import ModelSchema, Field, FilterSchema @@ -11,12 +13,20 @@ class Meta: class TransferEventListOut(ModelSchema): - operation: Optional[str] = Field(None, alias="operation.name") - facilities: Optional[List[FacilityForTransferEventGrid]] = None + # operation: Optional[str] = Field(None, alias="operation.name") + # facilities: Optional[List[FacilityForTransferEventGrid]] = None + operation__name: Optional[str] = Field(None, alias="operation__name") + facilities__name: Optional[str] = Field(None, alias="facilities__name") + facility__id: Optional[UUID] = Field(None, alias="facilities__id") + id: UUID class Meta: model = TransferEvent - fields = ['id', 'effective_date', 'status','created_at'] + fields = ['effective_date', 'status','created_at'] + + @staticmethod + def resolve_id(obj): + return uuid4() class TransferEventFilterSchema(FilterSchema): @@ -24,7 +34,6 @@ class TransferEventFilterSchema(FilterSchema): # due to this issue: https://github.com/vitalik/django-ninja/issues/1037 mypy is unhappy so I'm using the `json_schema_extra` parameter # If we want to achieve more by using the `q` parameter, we should use it and ignore the mypy error effective_date: Optional[str] = Field(None, json_schema_extra={'q': 'effective_date__icontains'}) - operation: Optional[str] = Field(None, json_schema_extra={'q': 'operation__name__icontains'}) - # facilities: Optional[str] = None - facilities: Optional[str] = Field(None, json_schema_extra={'q': 'facilities__name__icontains'}) + operation__name: Optional[str] = Field(None, json_schema_extra={'q': 'operation__name__icontains'}) + facilities__name: Optional[str] = Field(None, json_schema_extra={'q': 'facilities__name__icontains'}) status: Optional[str] = Field(None, json_schema_extra={'q': 'status__icontains'}) diff --git a/bc_obps/service/transfer_event_service.py b/bc_obps/service/transfer_event_service.py index e654fae227..86b18562dd 100644 --- a/bc_obps/service/transfer_event_service.py +++ b/bc_obps/service/transfer_event_service.py @@ -17,5 +17,4 @@ def list_transfer_events( ) -> QuerySet[TransferEvent]: sort_direction = "-" if sort_order == "desc" else "" sort_by = f"{sort_direction}{sort_field}" - base_qs = TransferEvent.objects.all() - return filters.filter(base_qs).order_by(sort_by) + return filters.filter(TransferEvent.objects.order_by(sort_by)).values('effective_date', 'status','created_at', 'operation__name', 'facilities__name', 'facilities__id').distinct() diff --git a/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts b/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts index 583c06ff76..a4157516fd 100644 --- a/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts +++ b/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts @@ -5,14 +5,14 @@ const transferColumns = ( ) => { const columns: GridColDef[] = [ { - field: "submission_date", + field: "created_at", headerName: "Submission Date", sortable: false, // Set flex to 1 to make the column take up all the remaining width if user zooms out width: 200, }, - { field: "operation", headerName: "Operation", width: 200 }, - { field: "facilities", headerName: "Facility", flex: 1 }, + { field: "operation__name", headerName: "Operation", width: 200 }, + { field: "facilities__name", headerName: "Facility", flex: 1 }, { field: "status", headerName: "Status", diff --git a/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts b/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts index 80f73713e8..d635ebee8d 100644 --- a/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts +++ b/bciers/apps/registration/app/components/datagrid/models/transfers/transferGroupColumns.ts @@ -9,22 +9,22 @@ const transferGroupColumns = ( ) => { const columnGroupModel: GridColumnGroupingModel = [ { - groupId: "submission_date", + groupId: "created_at", headerName: "Submission Date", renderHeaderGroup: EmptyGroupCell, - children: [{ field: "submission_date" }], + children: [{ field: "created_at" }], }, { - groupId: "operation", + groupId: "operation__name", headerName: "Operation", renderHeaderGroup: SearchCell, - children: [{ field: "operation" }], + children: [{ field: "operation__name" }], }, { - groupId: "facilities", + groupId: "facilities__name", headerName: "Facility", renderHeaderGroup: SearchCell, - children: [{ field: "facilities" }], + children: [{ field: "facilities__name" }], }, { groupId: "status", diff --git a/bciers/apps/registration/app/components/transfers/TransferDataGrid.tsx b/bciers/apps/registration/app/components/transfers/TransferDataGrid.tsx index 3d220cdd8a..95879d78cb 100644 --- a/bciers/apps/registration/app/components/transfers/TransferDataGrid.tsx +++ b/bciers/apps/registration/app/components/transfers/TransferDataGrid.tsx @@ -25,7 +25,6 @@ const TransfersDataGrid = ({ row_count: number; }; }) => { - console.log("initialData", initialData); const [lastFocusedField, setLastFocusedField] = useState(null); const SearchCell = useMemo( diff --git a/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx b/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx index 900f3f42d6..069e5c14d9 100644 --- a/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx +++ b/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx @@ -46,11 +46,11 @@ export default async function fetchTransferEventsPageData( "GET", "", ); - const formattedRows = formatTransferRows(pageData.items); + // const formattedRows = formatTransferRows(pageData.items); // brianna why is frontend showing Facility 2 twice? initial data is ok return { - rows: formattedRows, - row_count: formattedRows.length, + rows: pageData.items, + row_count: pageData.count, }; } catch (error) { throw error; From 749c5765a59041beb6d18e024ec5def9fff0bad5 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Tue, 12 Nov 2024 08:14:54 -0800 Subject: [PATCH 07/19] chore: format transfers grid --- .../transfers/fetchTransferEventsPageData.tsx | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx b/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx index 069e5c14d9..739a2f4aff 100644 --- a/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx +++ b/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx @@ -5,34 +5,26 @@ import { GridRowsProp } from "@mui/x-data-grid"; import formatTimestamp from "@bciers/utils/src/formatTimestamp"; export const formatTransferRows = (rows: GridRowsProp) => { - const operationRows = rows - .filter((row) => row.operation) - .map(({ id, operation, status, effective_date, created_at }) => { + console.log("rows", rows); + return rows.map( + ({ + id, + operation__name, + facilities__name, + status, + effective_date, + created_at, + }) => { return { id, - operation: operation, - facility: "N/A", + operation__name: operation__name || "N/A", + facilities__name: facilities__name || "N/A", status, - submission_date: formatTimestamp(created_at), + created_at: formatTimestamp(created_at), effective_date: formatTimestamp(effective_date), }; - }); - const facilityRows = rows - .filter((row) => row.facilities) - .flatMap(({ id, facilities, status, effective_date, created_at }) => { - return (facilities || []).map((facility) => { - console.log("facility", facility); - return { - id, - operation: "N/A", - facility: facility.name, // Use individual facility here - status, - submission_date: formatTimestamp(created_at), - effective_date: formatTimestamp(effective_date), - }; - }); - }); - return [...operationRows, ...facilityRows]; + }, + ); }; // 🛠️ Function to fetch transfers export default async function fetchTransferEventsPageData( @@ -46,10 +38,8 @@ export default async function fetchTransferEventsPageData( "GET", "", ); - // const formattedRows = formatTransferRows(pageData.items); - // brianna why is frontend showing Facility 2 twice? initial data is ok return { - rows: pageData.items, + rows: formatTransferRows(pageData.items), row_count: pageData.count, }; } catch (error) { From f5ed515275b16ef31ab5a3c11f32a30889dbdda8 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Tue, 12 Nov 2024 08:23:24 -0800 Subject: [PATCH 08/19] chore: be cleanup --- bc_obps/registration/models/event/event_base_model.py | 1 - bc_obps/registration/schema/v1/transfer_event.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/bc_obps/registration/models/event/event_base_model.py b/bc_obps/registration/models/event/event_base_model.py index 489e67e168..b3f4c3db30 100644 --- a/bc_obps/registration/models/event/event_base_model.py +++ b/bc_obps/registration/models/event/event_base_model.py @@ -4,7 +4,6 @@ class EventBaseModel(TimeStampedModel): - # brianna how does this work operation/facilities? id = models.UUIDField(primary_key=True, default=uuid.uuid4, db_comment="Primary key to identify the event") effective_date = models.DateTimeField(db_comment="The effective date of the event") operation = models.ForeignKey(Operation, null=True, blank=True, on_delete=models.PROTECT) diff --git a/bc_obps/registration/schema/v1/transfer_event.py b/bc_obps/registration/schema/v1/transfer_event.py index bc25b1fd10..733e644ad4 100644 --- a/bc_obps/registration/schema/v1/transfer_event.py +++ b/bc_obps/registration/schema/v1/transfer_event.py @@ -13,8 +13,6 @@ class Meta: class TransferEventListOut(ModelSchema): - # operation: Optional[str] = Field(None, alias="operation.name") - # facilities: Optional[List[FacilityForTransferEventGrid]] = None operation__name: Optional[str] = Field(None, alias="operation__name") facilities__name: Optional[str] = Field(None, alias="facilities__name") facility__id: Optional[UUID] = Field(None, alias="facilities__id") From 2da6ecc4ca25fd9f6d91e90ff75c48b5b12f78aa Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Tue, 12 Nov 2024 11:25:55 -0800 Subject: [PATCH 09/19] chore: fix uuid in ninja schema --- bc_obps/registration/schema/v1/transfer_event.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bc_obps/registration/schema/v1/transfer_event.py b/bc_obps/registration/schema/v1/transfer_event.py index 733e644ad4..c38ef8684f 100644 --- a/bc_obps/registration/schema/v1/transfer_event.py +++ b/bc_obps/registration/schema/v1/transfer_event.py @@ -13,6 +13,7 @@ class Meta: class TransferEventListOut(ModelSchema): + operation__id: Optional[UUID] = None operation__name: Optional[str] = Field(None, alias="operation__name") facilities__name: Optional[str] = Field(None, alias="facilities__name") facility__id: Optional[UUID] = Field(None, alias="facilities__id") @@ -24,7 +25,8 @@ class Meta: @staticmethod def resolve_id(obj): - return uuid4() + return obj['operation__id'] if obj['operation__id'] else obj['facilities__id'] + class TransferEventFilterSchema(FilterSchema): From 32d4a1fd2a2478f257c663755175f1971f87f196 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Tue, 12 Nov 2024 11:26:08 -0800 Subject: [PATCH 10/19] chore: add sorting to date columns --- .../app/components/datagrid/models/transfers/transferColumns.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts b/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts index a4157516fd..dc13a5be88 100644 --- a/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts +++ b/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts @@ -7,7 +7,6 @@ const transferColumns = ( { field: "created_at", headerName: "Submission Date", - sortable: false, // Set flex to 1 to make the column take up all the remaining width if user zooms out width: 200, }, @@ -21,7 +20,6 @@ const transferColumns = ( { field: "effective_date", headerName: "Effective Date", - sortable: false, width: 200, }, { From 411e6c9612e45eb5e1126d9a21be0a0fba38b174 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Tue, 12 Nov 2024 12:00:28 -0800 Subject: [PATCH 11/19] test: BE tests for transfers --- .../auth/test_endpoint_permissions.py | 1 + .../endpoints/v1/test_transfer_events.py | 140 ++++++++++++++++++ .../registration/tests/utils/baker_recipes.py | 1 - .../tests/test_transfer_event_service.py | 23 +++ bc_obps/service/transfer_event_service.py | 2 +- 5 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 bc_obps/registration/tests/endpoints/v1/test_transfer_events.py create mode 100644 bc_obps/service/tests/test_transfer_event_service.py diff --git a/bc_obps/common/tests/endpoints/auth/test_endpoint_permissions.py b/bc_obps/common/tests/endpoints/auth/test_endpoint_permissions.py index acb682b59a..ad3d3d8869 100644 --- a/bc_obps/common/tests/endpoints/auth/test_endpoint_permissions.py +++ b/bc_obps/common/tests/endpoints/auth/test_endpoint_permissions.py @@ -181,6 +181,7 @@ class TestEndpointPermissions(TestCase): {"method": "patch", "endpoint_name": "operation_bcghg_id", "kwargs": {"operation_id": mock_uuid}}, {"method": "patch", "endpoint_name": "facility_bcghg_id", "kwargs": {'facility_id': mock_uuid}}, {"method": "patch", "endpoint_name": "operation_bcghg_id", "kwargs": {'operation_id': mock_uuid}}, + {"method": "get", "endpoint_name": "list_transfer_events"}, ], "approved_authorized_roles": [ {"method": "get", "endpoint_name": "list_operations"}, diff --git a/bc_obps/registration/tests/endpoints/v1/test_transfer_events.py b/bc_obps/registration/tests/endpoints/v1/test_transfer_events.py new file mode 100644 index 0000000000..d6a76616ba --- /dev/null +++ b/bc_obps/registration/tests/endpoints/v1/test_transfer_events.py @@ -0,0 +1,140 @@ +from datetime import datetime, timedelta +from django.utils import timezone +from registration.models.event.transfer_event import TransferEvent +from bc_obps.settings import NINJA_PAGINATION_PER_PAGE +from model_bakery import baker +from localflavor.ca.models import CAPostalCodeField +from registration.models import ( + BcObpsRegulatedOperation, + NaicsCode, + Operation, + UserOperator, +) +from registration.tests.utils.helpers import CommonTestSetup, TestUtils + +from registration.tests.utils.bakers import operation_baker, operator_baker, user_operator_baker +from registration.constants import PAGE_SIZE +from registration.utils import custom_reverse_lazy + + +class TestTransferEventEndpoint(CommonTestSetup): + url = custom_reverse_lazy('list_transfer_events') + # GET + def test_list_transfer_events_unpaginated(self): + # transfer of an operation + baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation')) + # transfer of 50 facilities + baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility',_quantity=50)) + for role in ['cas_admin', 'cas_analyst']: + response = TestUtils.mock_get_with_auth_role( + self, role, self.url+ "?paginate_result=False" + ) + assert response.status_code == 200 + assert response.json().keys() == {'count', 'items'} + assert response.json()['count'] == 51 + + items = response.json().get('items', []) + for item in items: + assert set(item.keys()) == {'operation__name','operation__id','effective_date','status','id','facilities__name','facility__id','created_at'} + + + + def test_list_transfer_events_paginated(self): + # transfer of an operation + baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation')) + # transfer of 50 facilities + baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility',_quantity=50)) + # Get the default page 1 response + response = TestUtils.mock_get_with_auth_role(self, "cas_admin", custom_reverse_lazy("list_transfer_events")) + assert response.status_code == 200 + + response_items_1 = response.json().get('items') + response_count_1 = response.json().get('count') + # save the id of the first paginated response item + page_1_response_id = response_items_1[0].get('id') + assert len(response_items_1) == NINJA_PAGINATION_PER_PAGE + assert response_count_1 == 51 # total count of transfers + # Get the page 2 response + response = TestUtils.mock_get_with_auth_role( + self, + "cas_admin", + self.url + "?page=2&sort_field=created_at&sort_order=desc", + ) + assert response.status_code == 200 + response_items_2 = response.json().get('items') + response_count_2 = response.json().get('count') + # save the id of the first paginated response item + page_2_response_id = response_items_2[0].get('id') + assert len(response_items_2) == NINJA_PAGINATION_PER_PAGE + # assert that the first item in the page 1 response is not the same as the first item in the page 2 response + assert page_1_response_id != page_2_response_id + assert response_count_2 == response_count_1 # total count of transfer_events should be the same + # Get the page 2 response but with a different sort order + response = TestUtils.mock_get_with_auth_role( + self, + "cas_admin", + self.url + "?page=2&sort_field=created_at&sort_order=asc", + ) + assert response.status_code == 200 + response_items_2_reverse = response.json().get('items') + # save the id of the first paginated response item + page_2_response_id_reverse = response_items_2_reverse[0].get('id') + assert len(response_items_2_reverse) == NINJA_PAGINATION_PER_PAGE + # assert that the first item in the page 2 response is not the same as the first item in the page 2 response with reversed order + assert page_2_response_id != page_2_response_id_reverse + + def test_transfer_events_endpoint_list_transfer_events_with_sorting(self): + today = timezone.make_aware(datetime.now()) + yesterday = today - timedelta(days=1) + # transfer of an operation + baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation'),effective_date=today) + # transfer of 50 facilities + baker.make_recipe('utils.transfer_event', effective_date=yesterday, facilities=baker.make_recipe('utils.facility',_quantity=50)) + + response_ascending = TestUtils.mock_get_with_auth_role(self, "cas_admin", self.url+ "?page=1&sort_field=effective_date&sort_order=asc") + # save the id of the first paginated response item + first_item_ascending = response_ascending.json()['items'][0] + + # # Sort created at descending + response_descending = TestUtils.mock_get_with_auth_role(self, "cas_admin", self.url+ "?page=1&sort_field=effective_date&sort_order=desc") + first_item_descending = response_descending.json()['items'][0] + assert first_item_descending['effective_date'] > first_item_ascending['effective_date'] + + + + + def test_transfer_events_endpoint_list_transfer_events_with_filter(self): + # transfer of an operation + baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation', name='Test Operation')) + # transfer of 50 facilities + baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility',_quantity=50)) + + # Get the default page 1 response + response = TestUtils.mock_get_with_auth_role( + self, "cas_admin", self.url + "?facilities__name=010" + ) # filtering facilities__name + assert response.status_code == 200 + response_items_1 = response.json().get('items') + for item in response_items_1: + assert item.get('facilities__name') == "Facility 010" + + # Test with a status filter that doesn't exist + response = TestUtils.mock_get_with_auth_role( + self, "cas_admin", self.url + "?status=unreal" + ) + assert response.status_code == 200 + assert response.json().get('count') == 0 + + # Test with two filters + facilities__name_to_filter, status_to_filter = response_items_1[0].get('facilities__name'), response_items_1[0].get( + 'status' + ) + response = TestUtils.mock_get_with_auth_role( + self, "cas_admin", self.url + f"?facilities__name={facilities__name_to_filter}&status={status_to_filter}" + ) + assert response.status_code == 200 + response_items_2 = response.json().get('items') + assert len(response_items_2) == 1 + assert response.json().get('count') == 1 + assert response_items_2[0].get('facilities__name') == facilities__name_to_filter + assert response_items_2[0].get('status') == status_to_filter \ No newline at end of file diff --git a/bc_obps/registration/tests/utils/baker_recipes.py b/bc_obps/registration/tests/utils/baker_recipes.py index 708e87d00a..6f8d2c2a87 100644 --- a/bc_obps/registration/tests/utils/baker_recipes.py +++ b/bc_obps/registration/tests/utils/baker_recipes.py @@ -138,7 +138,6 @@ transfer_event = Recipe( TransferEvent, - future_designated_operator=TransferEvent.FutureDesignatedOperatorChoices.MY_OPERATOR, other_operator=foreign_key(other_operator_for_transfer_event), other_operator_contact=foreign_key(contact_for_transfer_event), ) diff --git a/bc_obps/service/tests/test_transfer_event_service.py b/bc_obps/service/tests/test_transfer_event_service.py new file mode 100644 index 0000000000..59a08e80a7 --- /dev/null +++ b/bc_obps/service/tests/test_transfer_event_service.py @@ -0,0 +1,23 @@ +from registration.schema.v1.transfer_event import TransferEventFilterSchema +from registration.models.event.transfer_event import TransferEvent +from service.transfer_event_service import TransferEventService +import pytest +from registration.models import Activity +from service.activity_service import ActivityService +from model_bakery import baker + +pytestmark = pytest.mark.django_db + + +class TestTransferEventService: + @staticmethod + def test_list_transfer_events(): + # transfer of 3 operations + baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation'),_quantity=3) + # transfer of 4 facilities + baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility',_quantity=4)) + # sorting and filtering are tested in the endpoint test in conjunction with pagination + result = TransferEventService.list_transfer_events("status","desc",TransferEventFilterSchema(effective_date=None, operation__name=None, facilities__name=None, status=None)) + assert result.count() == 7 + + diff --git a/bc_obps/service/transfer_event_service.py b/bc_obps/service/transfer_event_service.py index 86b18562dd..29665cc585 100644 --- a/bc_obps/service/transfer_event_service.py +++ b/bc_obps/service/transfer_event_service.py @@ -17,4 +17,4 @@ def list_transfer_events( ) -> QuerySet[TransferEvent]: sort_direction = "-" if sort_order == "desc" else "" sort_by = f"{sort_direction}{sort_field}" - return filters.filter(TransferEvent.objects.order_by(sort_by)).values('effective_date', 'status','created_at', 'operation__name', 'facilities__name', 'facilities__id').distinct() + return filters.filter(TransferEvent.objects.order_by(sort_by)).values('effective_date', 'status','created_at', 'operation__name', 'operation__id','facilities__name', 'facilities__id').distinct() From 4834c9b05fd10b410e3d4e0677e14c38d476c4ad Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Tue, 12 Nov 2024 15:28:07 -0800 Subject: [PATCH 12/19] chore: FE work --- .../{TransferDataGrid.tsx => TransfersDataGrid.tsx} | 1 + ...TransferDataGridPage.tsx => TransfersDataGridPage.tsx} | 5 +++-- .../apps/registration/app/components/transfers/types.ts | 8 ++++---- .../registration/app/idir/cas_admin/transfers/page.tsx | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) rename bciers/apps/registration/app/components/transfers/{TransferDataGrid.tsx => TransfersDataGrid.tsx} (97%) rename bciers/apps/registration/app/components/transfers/{TransferDataGridPage.tsx => TransfersDataGridPage.tsx} (83%) diff --git a/bciers/apps/registration/app/components/transfers/TransferDataGrid.tsx b/bciers/apps/registration/app/components/transfers/TransfersDataGrid.tsx similarity index 97% rename from bciers/apps/registration/app/components/transfers/TransferDataGrid.tsx rename to bciers/apps/registration/app/components/transfers/TransfersDataGrid.tsx index 95879d78cb..9f529c717d 100644 --- a/bciers/apps/registration/app/components/transfers/TransferDataGrid.tsx +++ b/bciers/apps/registration/app/components/transfers/TransfersDataGrid.tsx @@ -25,6 +25,7 @@ const TransfersDataGrid = ({ row_count: number; }; }) => { + console.log("initial", initialData); const [lastFocusedField, setLastFocusedField] = useState(null); const SearchCell = useMemo( diff --git a/bciers/apps/registration/app/components/transfers/TransferDataGridPage.tsx b/bciers/apps/registration/app/components/transfers/TransfersDataGridPage.tsx similarity index 83% rename from bciers/apps/registration/app/components/transfers/TransferDataGridPage.tsx rename to bciers/apps/registration/app/components/transfers/TransfersDataGridPage.tsx index 60a784fbe3..dd9b913327 100644 --- a/bciers/apps/registration/app/components/transfers/TransferDataGridPage.tsx +++ b/bciers/apps/registration/app/components/transfers/TransfersDataGridPage.tsx @@ -1,11 +1,11 @@ -import TransferDataGrid from "./TransferDataGrid"; +import TransferDataGrid from "./TransfersDataGrid"; import { Suspense } from "react"; import Loading from "@bciers/components/loading/SkeletonGrid"; import { TransferRow, TransfersSearchParams } from "./types"; import fetchTransferEventsPageData from "./fetchTransferEventsPageData"; // 🧩 Main component -export default async function TransferDataGridPage({ +export default async function TransfersDataGridPage({ searchParams, }: { searchParams: TransfersSearchParams; @@ -15,6 +15,7 @@ export default async function TransferDataGridPage({ rows: TransferRow[]; row_count: number; } = await fetchTransferEventsPageData(searchParams); + console.log("transfers", transfers); if (!transfers) { return
No transfers data in database.
; } diff --git a/bciers/apps/registration/app/components/transfers/types.ts b/bciers/apps/registration/app/components/transfers/types.ts index 45e8469e79..235633bc52 100644 --- a/bciers/apps/registration/app/components/transfers/types.ts +++ b/bciers/apps/registration/app/components/transfers/types.ts @@ -1,10 +1,10 @@ export interface TransferRow { - id: number; - operation: string; - facilities: string; + id: string; + operation__name: string; + facilities__name: string; status: string; + created_at: string; effective_date: string; - submission_date: string; } export interface TransfersSearchParams { diff --git a/bciers/apps/registration/app/idir/cas_admin/transfers/page.tsx b/bciers/apps/registration/app/idir/cas_admin/transfers/page.tsx index 76803c106c..10c79cad15 100644 --- a/bciers/apps/registration/app/idir/cas_admin/transfers/page.tsx +++ b/bciers/apps/registration/app/idir/cas_admin/transfers/page.tsx @@ -1,6 +1,6 @@ // 🚩 flagging that for shared routes between roles, "Page" code is a component for code maintainability import Loading from "@bciers/components/loading/SkeletonGrid"; -import TransferDataGridPage from "@/registration/app/components/transfers/TransferDataGridPage"; +import TransfersDataGridPage from "@/registration/app/components/transfers/TransfersDataGridPage"; import { Suspense } from "react"; import { TransfersSearchParams } from "@/registration/app/components/transfers/types"; @@ -11,7 +11,7 @@ export default async function Page({ }) { return ( }> - + ); } From b43d2e27ead0751aae6c4af906aca5a8e5b1c183 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Tue, 12 Nov 2024 15:28:24 -0800 Subject: [PATCH 13/19] test: vitests for transfers --- .../transfers/TransfersDataGrid.test.tsx | 131 ++++++++++++++++++ .../transfers/TransfersDataGridPage.test.tsx | 91 ++++++++++++ bciers/libs/testConfig/src/mocks.ts | 2 + 3 files changed, 224 insertions(+) create mode 100644 bciers/apps/registration/tests/components/transfers/TransfersDataGrid.test.tsx create mode 100644 bciers/apps/registration/tests/components/transfers/TransfersDataGridPage.test.tsx diff --git a/bciers/apps/registration/tests/components/transfers/TransfersDataGrid.test.tsx b/bciers/apps/registration/tests/components/transfers/TransfersDataGrid.test.tsx new file mode 100644 index 0000000000..cb8cb0df4c --- /dev/null +++ b/bciers/apps/registration/tests/components/transfers/TransfersDataGrid.test.tsx @@ -0,0 +1,131 @@ +import "@testing-library/jest-dom"; +import { render, screen, within } from "@testing-library/react"; +import { useRouter, useSearchParams } from "@bciers/testConfig/mocks"; +import React from "react"; +import TransfersDataGrid from "@/registration/app/components/transfers/TransfersDataGrid"; + +useRouter.mockReturnValue({ + query: {}, + replace: vi.fn(), +}); + +useSearchParams.mockReturnValue({ + get: vi.fn(), +}); + +const mockResponse = { + rows: [ + { + id: "3b5b95ea-2a1a-450d-8e2e-2e15feed96c9", + operation__name: "Operation 1", + facilities__name: "N/A", + status: "Transferred", + created_at: "Jan 5, 2024\n4:25:37 p.m. PDT", + effective_date: "Feb 1, 2025\n1:00:00 a.m. PST", + }, + { + id: "d99725a7-1c3a-47cb-a59b-e2388ce0fa18", + operation__name: "Operation 2", + facilities__name: "N/A", + status: "To be transferred", + created_at: "Jul 5, 2024\n4:25:37 p.m. PDT", + effective_date: "Aug 21, 2024\n2:00:00 a.m. PDT", + }, + { + id: "f486f2fb-62ed-438d-bb3e-0819b51e3aeb", + operation__name: "N/A", + facilities__name: "Facility 1", + status: "Completed", + created_at: "Jul 5, 2024\n4:25:37 p.m. PDT", + effective_date: "Dec 25, 2024\n1:00:00 a.m. PST", + }, + { + id: "459b80f9-b5f3-48aa-9727-90c30eaf3a58", + operation__name: "N/A", + facilities__name: "Facility 2", + status: "Completed", + created_at: "Jul 5, 2024\n4:25:37 p.m. PDT", + effective_date: "Dec 25, 2024\n1:00:00 a.m. PST", + }, + ], + row_count: 4, +}; + +describe("TransfersDataGrid component", () => { + beforeEach(async () => { + vi.clearAllMocks(); + }); + + it("renders the TransfersDataGrid grid ", async () => { + render(); + + // correct headers + expect( + screen.getByRole("columnheader", { name: "Submission Date" }), + ).toBeVisible(); + expect( + screen.queryByRole("columnheader", { name: "Operation" }), + ).toBeInTheDocument(); + expect( + screen.getByRole("columnheader", { name: "Facility" }), + ).toBeVisible(); + expect(screen.getByRole("columnheader", { name: "Status" })).toBeVisible(); + expect( + screen.getByRole("columnheader", { name: "Effective Date" }), + ).toBeVisible(); + + expect(screen.queryAllByPlaceholderText(/Search/i)).toHaveLength(3); + + const rows = screen.getAllByRole("row"); // row 0 is headers and row 1 is filter cells + + const operation1Row = rows[2]; + expect( + within(operation1Row).getByText("Jan 5, 2024 4:25:37 p.m. PDT"), + ).toBeInTheDocument(); + expect(within(operation1Row).getByText("Operation 1")).toBeInTheDocument(); + expect(within(operation1Row).getByText("N/A")).toBeInTheDocument(); + expect(within(operation1Row).getByText("Transferred")).toBeInTheDocument(); + expect( + within(operation1Row).getByText("Feb 1, 2025 1:00:00 a.m. PST"), + ).toBeInTheDocument(); + expect(within(operation1Row).getByText("View Details")).toBeInTheDocument(); + + const opeartion2Row = rows[3]; + expect( + within(opeartion2Row).getByText("Jul 5, 2024 4:25:37 p.m. PDT"), + ).toBeInTheDocument(); + expect(within(opeartion2Row).getByText("Operation 2")).toBeInTheDocument(); + expect(within(opeartion2Row).getByText("N/A")).toBeInTheDocument(); + expect( + within(opeartion2Row).getByText("To be transferred"), + ).toBeInTheDocument(); + expect( + within(opeartion2Row).getByText("Aug 21, 2024 2:00:00 a.m. PDT"), + ).toBeInTheDocument(); + expect(within(opeartion2Row).getByText("View Details")).toBeInTheDocument(); + const facility1Row = rows[4]; + + expect( + within(facility1Row).getByText("Jul 5, 2024 4:25:37 p.m. PDT"), + ).toBeInTheDocument(); + expect(within(facility1Row).getByText("N/A")).toBeInTheDocument(); + expect(within(facility1Row).getByText("Facility 1")).toBeInTheDocument(); + expect(within(facility1Row).getByText("Completed")).toBeInTheDocument(); + expect( + within(facility1Row).getByText("Dec 25, 2024 1:00:00 a.m. PST"), + ).toBeInTheDocument(); + expect(within(facility1Row).getByText("View Details")).toBeInTheDocument(); + + const facility2Row = rows[5]; + expect( + within(facility2Row).getByText("Jul 5, 2024 4:25:37 p.m. PDT"), + ).toBeInTheDocument(); + expect(within(facility2Row).getByText("N/A")).toBeInTheDocument(); + expect(within(facility2Row).getByText("Facility 2")).toBeInTheDocument(); + expect(within(facility2Row).getByText("Completed")).toBeInTheDocument(); + expect( + within(facility2Row).getByText("Dec 25, 2024 1:00:00 a.m. PST"), + ).toBeInTheDocument(); + expect(within(facility2Row).getByText("View Details")).toBeInTheDocument(); + }); +}); diff --git a/bciers/apps/registration/tests/components/transfers/TransfersDataGridPage.test.tsx b/bciers/apps/registration/tests/components/transfers/TransfersDataGridPage.test.tsx new file mode 100644 index 0000000000..7e353ef591 --- /dev/null +++ b/bciers/apps/registration/tests/components/transfers/TransfersDataGridPage.test.tsx @@ -0,0 +1,91 @@ +import { render, screen } from "@testing-library/react"; +import { + fetchTransferEventsPageData, + useRouter, + useSearchParams, +} from "@bciers/testConfig/mocks"; +import TransfersDataGridPage from "@/registration/app/components/transfers/TransfersDataGridPage"; + +useRouter.mockReturnValue({ + query: {}, + replace: vi.fn(), +}); + +useSearchParams.mockReturnValue({ + get: vi.fn(), +}); + +vi.mock( + "apps/registration/app/components/transfers/fetchTransferEventsPageData", + () => ({ + default: fetchTransferEventsPageData, + }), +); + +const mockResponse = { + items: [ + { + operation__id: "3b5b95ea-2a1a-450d-8e2e-2e15feed96c9", + operation__name: "Operation 3", + facilities__name: null, + facility__id: null, + id: "3b5b95ea-2a1a-450d-8e2e-2e15feed96c9", + effective_date: "2025-02-01T09:00:00Z", + status: "Transferred", + created_at: "2024-07-05T23:25:37.892Z", + }, + { + operation__id: "d99725a7-1c3a-47cb-a59b-e2388ce0fa18", + operation__name: "Operation 6", + facilities__name: null, + facility__id: null, + id: "d99725a7-1c3a-47cb-a59b-e2388ce0fa18", + effective_date: "2024-08-21T09:00:00Z", + status: "To be transferred", + created_at: "2024-07-05T23:25:37.892Z", + }, + { + operation__id: null, + operation__name: null, + facilities__name: "Facility 1", + facility__id: "f486f2fb-62ed-438d-bb3e-0819b51e3aeb", + id: "f486f2fb-62ed-438d-bb3e-0819b51e3aeb", + effective_date: "2024-12-25T09:00:00Z", + status: "Completed", + created_at: "2024-07-05T23:25:37.892Z", + }, + { + operation__id: null, + operation__name: null, + facilities__name: "Facility 2", + facility__id: "459b80f9-b5f3-48aa-9727-90c30eaf3a58", + id: "459b80f9-b5f3-48aa-9727-90c30eaf3a58", + effective_date: "2024-12-25T09:00:00Z", + status: "Completed", + created_at: "2024-07-05T23:25:37.892Z", + }, + ], + row_count: 4, +}; + +describe("Transfers component", () => { + beforeEach(async () => { + vi.clearAllMocks(); + }); + + it("renders a message when there are no transfers in the database", async () => { + fetchTransferEventsPageData.mockReturnValueOnce(undefined); + render(await TransfersDataGridPage({ searchParams: {} })); + expect(screen.queryByRole("grid")).not.toBeInTheDocument(); + expect(screen.getByText(/No transfers data in database./i)).toBeVisible(); + }); + + it("renders the TransfersDataGrid component when there are transfers in the database", async () => { + fetchTransferEventsPageData.mockReturnValueOnce(mockResponse); + render(await TransfersDataGridPage({ searchParams: {} })); + expect(screen.getByRole("grid")).toBeVisible(); + expect( + screen.queryByText(/No transfers data in database./i), + ).not.toBeInTheDocument(); + }); +}); diff --git a/bciers/libs/testConfig/src/mocks.ts b/bciers/libs/testConfig/src/mocks.ts index e71a39018b..794169476c 100644 --- a/bciers/libs/testConfig/src/mocks.ts +++ b/bciers/libs/testConfig/src/mocks.ts @@ -35,6 +35,7 @@ const useSession = vi.fn(); const auth = vi.fn(); const fetchOperationsPageData = vi.fn(); const fetchOperatorsPageData = vi.fn(); +const fetchTransferEventsPageData = vi.fn(); export { actionHandler, @@ -49,4 +50,5 @@ export { fetchOperationsPageData, fetchOperatorsPageData, notFound, + fetchTransferEventsPageData, }; From 1be2603b4e635bf9b896cfb5fbf8a61af155391b Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Wed, 13 Nov 2024 08:31:16 -0800 Subject: [PATCH 14/19] chore: fix transfer endpoint permissions --- bc_obps/registration/api/v1/transfer_events.py | 2 +- bc_obps/registration/tests/models/event/test_transfer.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/bc_obps/registration/api/v1/transfer_events.py b/bc_obps/registration/api/v1/transfer_events.py index 9c6cd9e74e..97c2dfc1a3 100644 --- a/bc_obps/registration/api/v1/transfer_events.py +++ b/bc_obps/registration/api/v1/transfer_events.py @@ -21,7 +21,7 @@ tags=TRANSFER_EVENT_TAGS, description="""Retrieves a paginated list of transfer events based on the provided filters. The endpoint allows authorized users to view and sort transfer events filtered by various criteria such as operation, facility, and status.""", - auth=authorize("approved_authorized_roles"), + auth=authorize("authorized_irc_user"), ) @handle_http_errors() @paginate(CustomPagination) diff --git a/bc_obps/registration/tests/models/event/test_transfer.py b/bc_obps/registration/tests/models/event/test_transfer.py index 19754134e4..149dc53693 100644 --- a/bc_obps/registration/tests/models/event/test_transfer.py +++ b/bc_obps/registration/tests/models/event/test_transfer.py @@ -42,14 +42,12 @@ def setUpTestData(cls): ("facilities", "facilities", None, None), ("other_operator", "other operator", None, None), ("other_operator_contact", "other operator contact", None, None), - ("future_designated_operator", "future designated operator", None, None), ] super().setUpTestData() def test_event_with_operation_only(self): self.create_event_with_operation_only( description="Why the transfer is happening", - future_designated_operator="My Operator", other_operator=operator_baker(), other_operator_contact=contact_baker(), ) @@ -57,7 +55,6 @@ def test_event_with_operation_only(self): def test_event_with_facilities_only(self): self.create_event_with_facilities_only( description="Why the transfer is happening returns", - future_designated_operator="My Operator", other_operator=operator_baker(), other_operator_contact=contact_baker(), ) From 67cb3635ca4a0ed51885b66228d59974535a1bb0 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Wed, 13 Nov 2024 10:12:13 -0800 Subject: [PATCH 15/19] chore: make pre-commit happy --- .../registration/api/v1/transfer_events.py | 6 +- .../models/event/transfer_event.py | 1 + .../registration/schema/v1/transfer_event.py | 21 +++--- .../endpoints/v1/test_transfer_events.py | 68 +++++++++---------- .../tests/test_transfer_event_service.py | 15 ++-- bc_obps/service/transfer_event_service.py | 20 ++++-- .../transfers/TransfersDataGridPage.tsx | 1 - .../transfers/fetchTransferEventsPageData.tsx | 1 - .../app/components/transfers/types.ts | 8 +-- 9 files changed, 76 insertions(+), 65 deletions(-) diff --git a/bc_obps/registration/api/v1/transfer_events.py b/bc_obps/registration/api/v1/transfer_events.py index 97c2dfc1a3..970084118e 100644 --- a/bc_obps/registration/api/v1/transfer_events.py +++ b/bc_obps/registration/api/v1/transfer_events.py @@ -1,5 +1,4 @@ -from typing import List, Literal, Optional -from registration.models.event.transfer_event import TransferEvent +from typing import List, Literal, Optional, Dict, Any from registration.schema.v1.transfer_event import TransferEventFilterSchema, TransferEventListOut from service.transfer_event_service import TransferEventService from common.permissions import authorize @@ -11,7 +10,6 @@ from ..router import router from service.error_service.custom_codes_4xx import custom_codes_4xx from ninja import Query -from django.db.models import QuerySet from registration.schema.generic import Message @@ -31,6 +29,6 @@ def list_transfer_events( sort_field: Optional[str] = "status", sort_order: Optional[Literal["desc", "asc"]] = "desc", paginate_result: bool = Query(True, description="Whether to paginate the results"), -) -> QuerySet[TransferEvent]: +) -> List[Dict[str, Any]]: # NOTE: PageNumberPagination raises an error if we pass the response as a tuple (like 200, ...) return TransferEventService.list_transfer_events(sort_field, sort_order, filters) diff --git a/bc_obps/registration/models/event/transfer_event.py b/bc_obps/registration/models/event/transfer_event.py index 23a679f6ad..ffd6a7bef1 100644 --- a/bc_obps/registration/models/event/transfer_event.py +++ b/bc_obps/registration/models/event/transfer_event.py @@ -10,6 +10,7 @@ class Statuses(models.TextChoices): COMPLETE = "Complete" TO_BE_TRANSFERRED = "To be transferred" TRANSFERRED = "Transferred" + description = models.TextField(db_comment="Description of the transfer or change in designated operator.") other_operator = models.ForeignKey( Operator, diff --git a/bc_obps/registration/schema/v1/transfer_event.py b/bc_obps/registration/schema/v1/transfer_event.py index c38ef8684f..a3fb870364 100644 --- a/bc_obps/registration/schema/v1/transfer_event.py +++ b/bc_obps/registration/schema/v1/transfer_event.py @@ -1,5 +1,5 @@ -from typing import List, Optional -from uuid import UUID, uuid4 +from typing import Optional +from uuid import UUID from registration.models.facility import Facility from registration.models.event.transfer_event import TransferEvent @@ -19,14 +19,19 @@ class TransferEventListOut(ModelSchema): facility__id: Optional[UUID] = Field(None, alias="facilities__id") id: UUID - class Meta: - model = TransferEvent - fields = ['effective_date', 'status','created_at'] - @staticmethod - def resolve_id(obj): - return obj['operation__id'] if obj['operation__id'] else obj['facilities__id'] + def resolve_id(obj: TransferEvent) -> UUID: + operation_id = getattr(obj, 'operation__id', None) + facility_id = getattr(obj, 'facilities__id', None) + record_id = operation_id if operation_id else facility_id + if not isinstance(record_id, UUID): + raise Exception('Missing valid UUID') + return record_id + + class Meta: + model = TransferEvent + fields = ['effective_date', 'status', 'created_at'] class TransferEventFilterSchema(FilterSchema): diff --git a/bc_obps/registration/tests/endpoints/v1/test_transfer_events.py b/bc_obps/registration/tests/endpoints/v1/test_transfer_events.py index d6a76616ba..c598ccd5ad 100644 --- a/bc_obps/registration/tests/endpoints/v1/test_transfer_events.py +++ b/bc_obps/registration/tests/endpoints/v1/test_transfer_events.py @@ -1,19 +1,9 @@ from datetime import datetime, timedelta from django.utils import timezone -from registration.models.event.transfer_event import TransferEvent from bc_obps.settings import NINJA_PAGINATION_PER_PAGE from model_bakery import baker -from localflavor.ca.models import CAPostalCodeField -from registration.models import ( - BcObpsRegulatedOperation, - NaicsCode, - Operation, - UserOperator, -) from registration.tests.utils.helpers import CommonTestSetup, TestUtils -from registration.tests.utils.bakers import operation_baker, operator_baker, user_operator_baker -from registration.constants import PAGE_SIZE from registration.utils import custom_reverse_lazy @@ -24,26 +14,31 @@ def test_list_transfer_events_unpaginated(self): # transfer of an operation baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation')) # transfer of 50 facilities - baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility',_quantity=50)) + baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility', _quantity=50)) for role in ['cas_admin', 'cas_analyst']: - response = TestUtils.mock_get_with_auth_role( - self, role, self.url+ "?paginate_result=False" - ) + response = TestUtils.mock_get_with_auth_role(self, role, self.url + "?paginate_result=False") assert response.status_code == 200 assert response.json().keys() == {'count', 'items'} assert response.json()['count'] == 51 - + items = response.json().get('items', []) for item in items: - assert set(item.keys()) == {'operation__name','operation__id','effective_date','status','id','facilities__name','facility__id','created_at'} - - + assert set(item.keys()) == { + 'operation__name', + 'operation__id', + 'effective_date', + 'status', + 'id', + 'facilities__name', + 'facility__id', + 'created_at', + } def test_list_transfer_events_paginated(self): # transfer of an operation baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation')) # transfer of 50 facilities - baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility',_quantity=50)) + baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility', _quantity=50)) # Get the default page 1 response response = TestUtils.mock_get_with_auth_role(self, "cas_admin", custom_reverse_lazy("list_transfer_events")) assert response.status_code == 200 @@ -87,27 +82,32 @@ def test_transfer_events_endpoint_list_transfer_events_with_sorting(self): today = timezone.make_aware(datetime.now()) yesterday = today - timedelta(days=1) # transfer of an operation - baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation'),effective_date=today) + baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation'), effective_date=today) # transfer of 50 facilities - baker.make_recipe('utils.transfer_event', effective_date=yesterday, facilities=baker.make_recipe('utils.facility',_quantity=50)) + baker.make_recipe( + 'utils.transfer_event', + effective_date=yesterday, + facilities=baker.make_recipe('utils.facility', _quantity=50), + ) - response_ascending = TestUtils.mock_get_with_auth_role(self, "cas_admin", self.url+ "?page=1&sort_field=effective_date&sort_order=asc") + response_ascending = TestUtils.mock_get_with_auth_role( + self, "cas_admin", self.url + "?page=1&sort_field=effective_date&sort_order=asc" + ) # save the id of the first paginated response item first_item_ascending = response_ascending.json()['items'][0] - + # # Sort created at descending - response_descending = TestUtils.mock_get_with_auth_role(self, "cas_admin", self.url+ "?page=1&sort_field=effective_date&sort_order=desc") + response_descending = TestUtils.mock_get_with_auth_role( + self, "cas_admin", self.url + "?page=1&sort_field=effective_date&sort_order=desc" + ) first_item_descending = response_descending.json()['items'][0] assert first_item_descending['effective_date'] > first_item_ascending['effective_date'] - - - def test_transfer_events_endpoint_list_transfer_events_with_filter(self): # transfer of an operation baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation', name='Test Operation')) # transfer of 50 facilities - baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility',_quantity=50)) + baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility', _quantity=50)) # Get the default page 1 response response = TestUtils.mock_get_with_auth_role( @@ -119,16 +119,14 @@ def test_transfer_events_endpoint_list_transfer_events_with_filter(self): assert item.get('facilities__name') == "Facility 010" # Test with a status filter that doesn't exist - response = TestUtils.mock_get_with_auth_role( - self, "cas_admin", self.url + "?status=unreal" - ) + response = TestUtils.mock_get_with_auth_role(self, "cas_admin", self.url + "?status=unreal") assert response.status_code == 200 assert response.json().get('count') == 0 # Test with two filters - facilities__name_to_filter, status_to_filter = response_items_1[0].get('facilities__name'), response_items_1[0].get( - 'status' - ) + facilities__name_to_filter, status_to_filter = response_items_1[0].get('facilities__name'), response_items_1[ + 0 + ].get('status') response = TestUtils.mock_get_with_auth_role( self, "cas_admin", self.url + f"?facilities__name={facilities__name_to_filter}&status={status_to_filter}" ) @@ -137,4 +135,4 @@ def test_transfer_events_endpoint_list_transfer_events_with_filter(self): assert len(response_items_2) == 1 assert response.json().get('count') == 1 assert response_items_2[0].get('facilities__name') == facilities__name_to_filter - assert response_items_2[0].get('status') == status_to_filter \ No newline at end of file + assert response_items_2[0].get('status') == status_to_filter diff --git a/bc_obps/service/tests/test_transfer_event_service.py b/bc_obps/service/tests/test_transfer_event_service.py index 59a08e80a7..607456f5c6 100644 --- a/bc_obps/service/tests/test_transfer_event_service.py +++ b/bc_obps/service/tests/test_transfer_event_service.py @@ -1,9 +1,6 @@ from registration.schema.v1.transfer_event import TransferEventFilterSchema -from registration.models.event.transfer_event import TransferEvent from service.transfer_event_service import TransferEventService import pytest -from registration.models import Activity -from service.activity_service import ActivityService from model_bakery import baker pytestmark = pytest.mark.django_db @@ -13,11 +10,13 @@ class TestTransferEventService: @staticmethod def test_list_transfer_events(): # transfer of 3 operations - baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation'),_quantity=3) + baker.make_recipe('utils.transfer_event', operation=baker.make_recipe('utils.operation'), _quantity=3) # transfer of 4 facilities - baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility',_quantity=4)) + baker.make_recipe('utils.transfer_event', facilities=baker.make_recipe('utils.facility', _quantity=4)) # sorting and filtering are tested in the endpoint test in conjunction with pagination - result = TransferEventService.list_transfer_events("status","desc",TransferEventFilterSchema(effective_date=None, operation__name=None, facilities__name=None, status=None)) + result = TransferEventService.list_transfer_events( + "status", + "desc", + TransferEventFilterSchema(effective_date=None, operation__name=None, facilities__name=None, status=None), + ) assert result.count() == 7 - - diff --git a/bc_obps/service/transfer_event_service.py b/bc_obps/service/transfer_event_service.py index 29665cc585..7b5530a5cc 100644 --- a/bc_obps/service/transfer_event_service.py +++ b/bc_obps/service/transfer_event_service.py @@ -1,6 +1,5 @@ from registration.models.event.transfer_event import TransferEvent -from typing import Optional -from django.db.models import QuerySet +from typing import Optional, Dict, Any, List from registration.schema.v1.transfer_event import TransferEventFilterSchema @@ -14,7 +13,20 @@ def list_transfer_events( sort_field: Optional[str], sort_order: Optional[str], filters: TransferEventFilterSchema = Query(...), - ) -> QuerySet[TransferEvent]: + ) -> List[Dict[str, Any]]: sort_direction = "-" if sort_order == "desc" else "" sort_by = f"{sort_direction}{sort_field}" - return filters.filter(TransferEvent.objects.order_by(sort_by)).values('effective_date', 'status','created_at', 'operation__name', 'operation__id','facilities__name', 'facilities__id').distinct() + queryset = ( + filters.filter(TransferEvent.objects.order_by(sort_by)) + .values( + 'effective_date', + 'status', + 'created_at', + 'operation__name', + 'operation__id', + 'facilities__name', + 'facilities__id', + ) + .distinct() + ) + return list(queryset) diff --git a/bciers/apps/registration/app/components/transfers/TransfersDataGridPage.tsx b/bciers/apps/registration/app/components/transfers/TransfersDataGridPage.tsx index dd9b913327..ecc4431fc3 100644 --- a/bciers/apps/registration/app/components/transfers/TransfersDataGridPage.tsx +++ b/bciers/apps/registration/app/components/transfers/TransfersDataGridPage.tsx @@ -15,7 +15,6 @@ export default async function TransfersDataGridPage({ rows: TransferRow[]; row_count: number; } = await fetchTransferEventsPageData(searchParams); - console.log("transfers", transfers); if (!transfers) { return
No transfers data in database.
; } diff --git a/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx b/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx index 739a2f4aff..ce469e0929 100644 --- a/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx +++ b/bciers/apps/registration/app/components/transfers/fetchTransferEventsPageData.tsx @@ -5,7 +5,6 @@ import { GridRowsProp } from "@mui/x-data-grid"; import formatTimestamp from "@bciers/utils/src/formatTimestamp"; export const formatTransferRows = (rows: GridRowsProp) => { - console.log("rows", rows); return rows.map( ({ id, diff --git a/bciers/apps/registration/app/components/transfers/types.ts b/bciers/apps/registration/app/components/transfers/types.ts index 235633bc52..4f91c7784e 100644 --- a/bciers/apps/registration/app/components/transfers/types.ts +++ b/bciers/apps/registration/app/components/transfers/types.ts @@ -1,10 +1,10 @@ export interface TransferRow { id: string; - operation__name: string; - facilities__name: string; + operation__name?: string; + facilities__name?: string; status: string; - created_at: string; - effective_date: string; + created_at: string | undefined; + effective_date?: string | undefined; } export interface TransfersSearchParams { From d6c84a9a46a72774b833a65be9a9c463c326e909 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Thu, 14 Nov 2024 14:49:10 -0800 Subject: [PATCH 16/19] chore: fix filtering for N/A --- bc_obps/registration/schema/v1/transfer_event.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bc_obps/registration/schema/v1/transfer_event.py b/bc_obps/registration/schema/v1/transfer_event.py index a3fb870364..d0f494859e 100644 --- a/bc_obps/registration/schema/v1/transfer_event.py +++ b/bc_obps/registration/schema/v1/transfer_event.py @@ -4,6 +4,8 @@ from registration.models.facility import Facility from registration.models.event.transfer_event import TransferEvent from ninja import ModelSchema, Field, FilterSchema +from django.db.models import Q +import re class FacilityForTransferEventGrid(ModelSchema): @@ -39,6 +41,17 @@ class TransferEventFilterSchema(FilterSchema): # due to this issue: https://github.com/vitalik/django-ninja/issues/1037 mypy is unhappy so I'm using the `json_schema_extra` parameter # If we want to achieve more by using the `q` parameter, we should use it and ignore the mypy error effective_date: Optional[str] = Field(None, json_schema_extra={'q': 'effective_date__icontains'}) - operation__name: Optional[str] = Field(None, json_schema_extra={'q': 'operation__name__icontains'}) + operation__name: Optional[str] = None facilities__name: Optional[str] = Field(None, json_schema_extra={'q': 'facilities__name__icontains'}) status: Optional[str] = Field(None, json_schema_extra={'q': 'status__icontains'}) + + def filtering_including_not_applicable(self, field: str, value: str) -> Q: + if value and re.search(value, 'n/a', re.IGNORECASE): + return Q(**{f"{field}__icontains": value}) | Q(**{f"{field}__isnull": True}) + return Q(**{f"{field}__icontains": value}) if value else Q() + + def filter_operation__name(self, value: str) -> Q: + return self.filtering_including_not_applicable('operation__name', value) + + def filter_facilities__name(self, value: str) -> Q: + return self.filtering_including_not_applicable('facilities__name', value) From 3713d551bdb3b259fba21ec606dd55fa763303ae Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Thu, 14 Nov 2024 14:54:26 -0800 Subject: [PATCH 17/19] chore: fix filtering for N/A --- bc_obps/registration/schema/v1/transfer_event.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bc_obps/registration/schema/v1/transfer_event.py b/bc_obps/registration/schema/v1/transfer_event.py index d0f494859e..52ca201fe4 100644 --- a/bc_obps/registration/schema/v1/transfer_event.py +++ b/bc_obps/registration/schema/v1/transfer_event.py @@ -6,6 +6,7 @@ from ninja import ModelSchema, Field, FilterSchema from django.db.models import Q import re +from typing import Dict, Any class FacilityForTransferEventGrid(ModelSchema): @@ -22,9 +23,9 @@ class TransferEventListOut(ModelSchema): id: UUID @staticmethod - def resolve_id(obj: TransferEvent) -> UUID: - operation_id = getattr(obj, 'operation__id', None) - facility_id = getattr(obj, 'facilities__id', None) + def resolve_id(obj: Dict[str, Any]) -> UUID: + operation_id = obj.get('operation__id', None) + facility_id = obj.get('facilities__id', None) record_id = operation_id if operation_id else facility_id if not isinstance(record_id, UUID): From adf3f931cd97ea73f07aaa4a1b0ae145c6fb27ea Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Fri, 15 Nov 2024 15:23:27 -0800 Subject: [PATCH 18/19] chore: post-rebase updates --- ...historicaltransferevent_status_and_more.py | 39 ------------------- ...ent_future_designated_operator_and_more.py | 32 +++++++++++++-- 2 files changed, 29 insertions(+), 42 deletions(-) delete mode 100644 bc_obps/registration/migrations/0055_alter_historicaltransferevent_status_and_more.py diff --git a/bc_obps/registration/migrations/0055_alter_historicaltransferevent_status_and_more.py b/bc_obps/registration/migrations/0055_alter_historicaltransferevent_status_and_more.py deleted file mode 100644 index 90833024a1..0000000000 --- a/bc_obps/registration/migrations/0055_alter_historicaltransferevent_status_and_more.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 5.0.7 on 2024-11-07 19:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('registration', '0054_V1_13_0'), - ] - - operations = [ - migrations.AlterField( - model_name='historicaltransferevent', - name='status', - field=models.CharField( - choices=[ - ('Complete', 'Complete'), - ('To be transferred', 'To Be Transferred'), - ('Transferred', 'Transferred'), - ], - default='To be transferred', - max_length=100, - ), - ), - migrations.AlterField( - model_name='transferevent', - name='status', - field=models.CharField( - choices=[ - ('Complete', 'Complete'), - ('To be transferred', 'To Be Transferred'), - ('Transferred', 'Transferred'), - ], - default='To be transferred', - max_length=100, - ), - ), - ] diff --git a/bc_obps/registration/migrations/0056_remove_historicaltransferevent_future_designated_operator_and_more.py b/bc_obps/registration/migrations/0056_remove_historicaltransferevent_future_designated_operator_and_more.py index d52094a79d..6f4928c812 100644 --- a/bc_obps/registration/migrations/0056_remove_historicaltransferevent_future_designated_operator_and_more.py +++ b/bc_obps/registration/migrations/0056_remove_historicaltransferevent_future_designated_operator_and_more.py @@ -1,12 +1,12 @@ -# Generated by Django 5.0.7 on 2024-11-07 23:46 +# Generated by Django 5.0.9 on 2024-11-15 23:20 -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('registration', '0055_alter_historicaltransferevent_status_and_more'), + ('registration', '0055_V1_14_0'), ] operations = [ @@ -18,4 +18,30 @@ class Migration(migrations.Migration): model_name='transferevent', name='future_designated_operator', ), + migrations.AlterField( + model_name='historicaltransferevent', + name='status', + field=models.CharField( + choices=[ + ('Complete', 'Complete'), + ('To be transferred', 'To Be Transferred'), + ('Transferred', 'Transferred'), + ], + default='To be transferred', + max_length=100, + ), + ), + migrations.AlterField( + model_name='transferevent', + name='status', + field=models.CharField( + choices=[ + ('Complete', 'Complete'), + ('To be transferred', 'To Be Transferred'), + ('Transferred', 'Transferred'), + ], + default='To be transferred', + max_length=100, + ), + ), ] From 7be260a0efadff87f476ab967212ad96c8b38044 Mon Sep 17 00:00:00 2001 From: Brianna Cerkiewicz Date: Wed, 20 Nov 2024 14:52:29 -0800 Subject: [PATCH 19/19] chore: apply suggestions from code review --- bc_obps/registration/api/v1/transfer_events.py | 6 ++++-- bc_obps/registration/schema/v1/transfer_event.py | 10 ++-------- bc_obps/service/transfer_event_service.py | 10 +++++----- .../components/operations/OperationDataGridPage.tsx | 5 ++--- .../app/components/operators/OperatorDataGridPage.tsx | 5 ++--- .../operations/OperationDataGridPage.test.tsx | 7 ++++--- .../components/operators/OperatorDataGridPage.test.tsx | 8 +++++--- .../datagrid/models/transfers/transferColumns.ts | 1 - .../app/components/transfers/TransfersDataGrid.tsx | 1 - .../app/components/transfers/TransfersDataGridPage.tsx | 5 ++--- .../idir/{cas_admin => cas_analyst}/transfers/page.tsx | 0 .../transfers/TransfersDataGridPage.test.tsx | 7 ++++--- 12 files changed, 30 insertions(+), 35 deletions(-) rename bciers/apps/registration/app/idir/{cas_admin => cas_analyst}/transfers/page.tsx (100%) diff --git a/bc_obps/registration/api/v1/transfer_events.py b/bc_obps/registration/api/v1/transfer_events.py index 970084118e..41789e1648 100644 --- a/bc_obps/registration/api/v1/transfer_events.py +++ b/bc_obps/registration/api/v1/transfer_events.py @@ -1,4 +1,5 @@ -from typing import List, Literal, Optional, Dict, Any +from typing import List, Literal, Optional +from registration.models.event.transfer_event import TransferEvent from registration.schema.v1.transfer_event import TransferEventFilterSchema, TransferEventListOut from service.transfer_event_service import TransferEventService from common.permissions import authorize @@ -11,6 +12,7 @@ from service.error_service.custom_codes_4xx import custom_codes_4xx from ninja import Query from registration.schema.generic import Message +from django.db.models import QuerySet @router.get( @@ -29,6 +31,6 @@ def list_transfer_events( sort_field: Optional[str] = "status", sort_order: Optional[Literal["desc", "asc"]] = "desc", paginate_result: bool = Query(True, description="Whether to paginate the results"), -) -> List[Dict[str, Any]]: +) -> QuerySet[TransferEvent]: # NOTE: PageNumberPagination raises an error if we pass the response as a tuple (like 200, ...) return TransferEventService.list_transfer_events(sort_field, sort_order, filters) diff --git a/bc_obps/registration/schema/v1/transfer_event.py b/bc_obps/registration/schema/v1/transfer_event.py index 52ca201fe4..b11801472e 100644 --- a/bc_obps/registration/schema/v1/transfer_event.py +++ b/bc_obps/registration/schema/v1/transfer_event.py @@ -1,7 +1,6 @@ from typing import Optional from uuid import UUID -from registration.models.facility import Facility from registration.models.event.transfer_event import TransferEvent from ninja import ModelSchema, Field, FilterSchema from django.db.models import Q @@ -9,12 +8,6 @@ from typing import Dict, Any -class FacilityForTransferEventGrid(ModelSchema): - class Meta: - model = Facility - fields = ['name'] - - class TransferEventListOut(ModelSchema): operation__id: Optional[UUID] = None operation__name: Optional[str] = Field(None, alias="operation__name") @@ -46,7 +39,8 @@ class TransferEventFilterSchema(FilterSchema): facilities__name: Optional[str] = Field(None, json_schema_extra={'q': 'facilities__name__icontains'}) status: Optional[str] = Field(None, json_schema_extra={'q': 'status__icontains'}) - def filtering_including_not_applicable(self, field: str, value: str) -> Q: + @staticmethod + def filtering_including_not_applicable(field: str, value: str) -> Q: if value and re.search(value, 'n/a', re.IGNORECASE): return Q(**{f"{field}__icontains": value}) | Q(**{f"{field}__isnull": True}) return Q(**{f"{field}__icontains": value}) if value else Q() diff --git a/bc_obps/service/transfer_event_service.py b/bc_obps/service/transfer_event_service.py index 7b5530a5cc..70680dc531 100644 --- a/bc_obps/service/transfer_event_service.py +++ b/bc_obps/service/transfer_event_service.py @@ -1,7 +1,7 @@ +from typing import cast +from django.db.models import QuerySet from registration.models.event.transfer_event import TransferEvent -from typing import Optional, Dict, Any, List - - +from typing import Optional from registration.schema.v1.transfer_event import TransferEventFilterSchema from ninja import Query @@ -13,7 +13,7 @@ def list_transfer_events( sort_field: Optional[str], sort_order: Optional[str], filters: TransferEventFilterSchema = Query(...), - ) -> List[Dict[str, Any]]: + ) -> QuerySet[TransferEvent]: sort_direction = "-" if sort_order == "desc" else "" sort_by = f"{sort_direction}{sort_field}" queryset = ( @@ -29,4 +29,4 @@ def list_transfer_events( ) .distinct() ) - return list(queryset) + return cast(QuerySet[TransferEvent], queryset) diff --git a/bciers/apps/administration/app/components/operations/OperationDataGridPage.tsx b/bciers/apps/administration/app/components/operations/OperationDataGridPage.tsx index 8cbf325c05..c14369a504 100644 --- a/bciers/apps/administration/app/components/operations/OperationDataGridPage.tsx +++ b/bciers/apps/administration/app/components/operations/OperationDataGridPage.tsx @@ -20,9 +20,8 @@ export default async function OperationDataGridPage({ rows: OperationRow[]; row_count: number; } = await fetchOperationsPageData(searchParams); - if (!operations) { - return
No operations data in database.
; - } + if (!operations || "error" in operations) + throw new Error("Failed to retrieve operations"); const isAuthorizedAdminUser = [ FrontEndRoles.CAS_ADMIN, diff --git a/bciers/apps/administration/app/components/operators/OperatorDataGridPage.tsx b/bciers/apps/administration/app/components/operators/OperatorDataGridPage.tsx index 2ec985815c..569047c578 100644 --- a/bciers/apps/administration/app/components/operators/OperatorDataGridPage.tsx +++ b/bciers/apps/administration/app/components/operators/OperatorDataGridPage.tsx @@ -15,9 +15,8 @@ export default async function Operators({ rows: OperatorRow[]; row_count: number; } = await fetchOperatorsPageData(searchParams); - if (!operators) { - return
No operator data in database.
; - } + if (!operators || "error" in operators) + throw new Error("Failed to retrieve operators"); // Render the DataGrid component return ( diff --git a/bciers/apps/administration/tests/components/operations/OperationDataGridPage.test.tsx b/bciers/apps/administration/tests/components/operations/OperationDataGridPage.test.tsx index 5e3dc37e6d..b09bf991c5 100644 --- a/bciers/apps/administration/tests/components/operations/OperationDataGridPage.test.tsx +++ b/bciers/apps/administration/tests/components/operations/OperationDataGridPage.test.tsx @@ -51,11 +51,12 @@ describe("Operations component", () => { vi.clearAllMocks(); }); - it("renders a message when there are no operations in the database", async () => { + it("throws an error when there's a problem fetching data", async () => { fetchOperationsPageData.mockReturnValueOnce(undefined); - render(await Operations({ searchParams: {} })); + await expect(async () => { + render(await Operations({ searchParams: {} })); + }).rejects.toThrow("Failed to retrieve operations"); expect(screen.queryByRole("grid")).not.toBeInTheDocument(); - expect(screen.getByText(/No operations data in database./i)).toBeVisible(); }); it("renders the OperationDataGrid component when there are operations in the database", async () => { diff --git a/bciers/apps/administration/tests/components/operators/OperatorDataGridPage.test.tsx b/bciers/apps/administration/tests/components/operators/OperatorDataGridPage.test.tsx index b9122f83af..e1ce46058d 100644 --- a/bciers/apps/administration/tests/components/operators/OperatorDataGridPage.test.tsx +++ b/bciers/apps/administration/tests/components/operators/OperatorDataGridPage.test.tsx @@ -54,11 +54,13 @@ describe("OperatorDataGridPage component", () => { vi.clearAllMocks(); }); - it("renders a message when there are no operators in the database", async () => { + it("throws an error when there's a problem fetching data", async () => { fetchOperatorsPageData.mockReturnValueOnce(undefined); - render(await Operators({ searchParams: {} })); + await expect(async () => { + render(await Operators({ searchParams: {} })); + }).rejects.toThrow("Failed to retrieve operators"); + expect(screen.queryByRole("grid")).not.toBeInTheDocument(); - expect(screen.getByText(/No operator data in database./i)).toBeVisible(); }); it("renders the OperatorDataGrid component when there are operators in the database", async () => { diff --git a/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts b/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts index dc13a5be88..0a36dd3eb4 100644 --- a/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts +++ b/bciers/apps/registration/app/components/datagrid/models/transfers/transferColumns.ts @@ -7,7 +7,6 @@ const transferColumns = ( { field: "created_at", headerName: "Submission Date", - // Set flex to 1 to make the column take up all the remaining width if user zooms out width: 200, }, { field: "operation__name", headerName: "Operation", width: 200 }, diff --git a/bciers/apps/registration/app/components/transfers/TransfersDataGrid.tsx b/bciers/apps/registration/app/components/transfers/TransfersDataGrid.tsx index 9f529c717d..95879d78cb 100644 --- a/bciers/apps/registration/app/components/transfers/TransfersDataGrid.tsx +++ b/bciers/apps/registration/app/components/transfers/TransfersDataGrid.tsx @@ -25,7 +25,6 @@ const TransfersDataGrid = ({ row_count: number; }; }) => { - console.log("initial", initialData); const [lastFocusedField, setLastFocusedField] = useState(null); const SearchCell = useMemo( diff --git a/bciers/apps/registration/app/components/transfers/TransfersDataGridPage.tsx b/bciers/apps/registration/app/components/transfers/TransfersDataGridPage.tsx index ecc4431fc3..10db935b36 100644 --- a/bciers/apps/registration/app/components/transfers/TransfersDataGridPage.tsx +++ b/bciers/apps/registration/app/components/transfers/TransfersDataGridPage.tsx @@ -15,9 +15,8 @@ export default async function TransfersDataGridPage({ rows: TransferRow[]; row_count: number; } = await fetchTransferEventsPageData(searchParams); - if (!transfers) { - return
No transfers data in database.
; - } + if (!transfers || "error" in transfers) + throw new Error("Failed to retrieve transfers"); // Render the DataGrid component return ( diff --git a/bciers/apps/registration/app/idir/cas_admin/transfers/page.tsx b/bciers/apps/registration/app/idir/cas_analyst/transfers/page.tsx similarity index 100% rename from bciers/apps/registration/app/idir/cas_admin/transfers/page.tsx rename to bciers/apps/registration/app/idir/cas_analyst/transfers/page.tsx diff --git a/bciers/apps/registration/tests/components/transfers/TransfersDataGridPage.test.tsx b/bciers/apps/registration/tests/components/transfers/TransfersDataGridPage.test.tsx index 7e353ef591..997c4e6765 100644 --- a/bciers/apps/registration/tests/components/transfers/TransfersDataGridPage.test.tsx +++ b/bciers/apps/registration/tests/components/transfers/TransfersDataGridPage.test.tsx @@ -73,11 +73,12 @@ describe("Transfers component", () => { vi.clearAllMocks(); }); - it("renders a message when there are no transfers in the database", async () => { + it("throws an error when there's a problem fetching data", async () => { fetchTransferEventsPageData.mockReturnValueOnce(undefined); - render(await TransfersDataGridPage({ searchParams: {} })); + await expect(async () => { + render(await TransfersDataGridPage({ searchParams: {} })); + }).rejects.toThrow("Failed to retrieve transfers"); expect(screen.queryByRole("grid")).not.toBeInTheDocument(); - expect(screen.getByText(/No transfers data in database./i)).toBeVisible(); }); it("renders the TransfersDataGrid component when there are transfers in the database", async () => {