Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/bcgov/PIMS
Browse files Browse the repository at this point in the history
  • Loading branch information
Sharala-Perumal committed Jan 16, 2025
2 parents 9d795c9 + a52c1a7 commit d30f4f7
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 102 deletions.
24 changes: 21 additions & 3 deletions express-api/src/controllers/properties/propertiesController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Request, Response } from 'express';
import propertyServices from '@/services/properties/propertiesServices';
import propertyServices, { PropertyIdsByType } from '@/services/properties/propertiesServices';
import {
ImportResultFilterSchema,
MapFilter,
Expand All @@ -16,6 +16,7 @@ import { ImportResult } from '@/typeorm/Entities/ImportResult';
import { readFile } from 'xlsx';
import logger from '@/utilities/winstonLogger';
import { Roles } from '@/constants/roles';
import { PropertyType } from '@/constants/propertyType';

/**
* @description Search for a single keyword across multiple different fields in both parcels and buildings.
Expand Down Expand Up @@ -96,7 +97,7 @@ export const getPropertiesForMap = async (req: Request, res: Response) => {
requestedAgencies,
permittedRoles,
);
// If not agencies were requested or if the user doesn't have those requested agencies
// If no agencies were requested or if the user doesn't have those requested agencies
if (!requestedAgencies || !userHasAgencies) {
// Then only show that user's agencies instead.
const usersAgencies = await userServices.getAgencies(user.Username);
Expand All @@ -105,7 +106,24 @@ export const getPropertiesForMap = async (req: Request, res: Response) => {
}

const properties = await propertyServices.getPropertiesForMap(filterResult);
// Convert to GeoJSON format

// If it was requested as an Excel export, get the properties in export form
if (filterResult.ExcelExport) {
const filteredIds: PropertyIdsByType = {
parcelIds: properties
.filter(
(p) =>
p.PropertyTypeId === PropertyType.LAND || p.PropertyTypeId === PropertyType.SUBDIVISION,
)
.map((p) => p.Id),
buildingIds: properties
.filter((p) => p.PropertyTypeId === PropertyType.BUILDING)
.map((p) => p.Id),
};
const exportData = await propertyServices.getPropertiesForExport({}, filteredIds);
return res.status(200).send(exportData);
}
// Standard operation is to return the properties in GeoJSON format
const mapFeatures = properties.map((property) => ({
type: 'Feature',
properties: { ...property },
Expand Down
1 change: 1 addition & 0 deletions express-api/src/controllers/properties/propertiesSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const MapFilterSchema = z.object({
RegionalDistrictIds: arrayFromString(numberSchema),
UserAgencies: z.array(z.number().int()).optional(),
Polygon: z.string().optional(),
ExcelExport: z.coerce.boolean().optional(),
});

export type MapFilter = z.infer<typeof MapFilterSchema>;
Expand Down
77 changes: 77 additions & 0 deletions express-api/src/routes/properties.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,83 @@ paths:
type: array
items:
$ref: '#/components/schemas/PropertyGeojson'
/properties/search/geo/export:
get:
security:
- bearerAuth: []
tags:
- Properties
summary: Gets a list of properties in format for Excel export, based on the map filter.
parameters:
- in: query
name: PID
description: Should not include leading zeroes or hyphens.
schema:
type: integer
example: 24545457
- in: query
name: PIN
schema:
type: integer
- in: query
name: Address
schema:
type: string
- in: query
name: Name
schema:
type: string
- in: query
name: AgencyIds
description: Comma-separated list of IDs.
schema:
type: string
example: 81,166
- in: query
name: AdministrativeAreaIds
description: Comma-separated list of IDs.
schema:
type: string
example: 219,300
- in: query
name: ClassificationIds
description: Comma-separated list of IDs.
schema:
type: string
example: 0,1
- in: query
name: PropertyTypeIds
description: Comma-separated list of IDs.
schema:
type: string
example: 0,1
- in: query
name: RegionalDistrictIds
description: Comma-separated list of IDs.
schema:
type: string
example: 19,25
- in: query
name: ProjectStatusId
description: The status of the most recent active project this property was in.
schema:
type: integer
example: 1
- in: query
name: Polygon
desciption: A stringified array of polygons and their coordinates
schema:
type: string
example: '[[[1,1], [2,2], [3,3]]]'
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/PropertyJoinView'
/properties/import:
post:
security:
Expand Down
18 changes: 17 additions & 1 deletion express-api/src/routes/propertiesRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,23 @@ const {

router.route('/search/fuzzy').get(userAuthCheck(), catchErrors(getPropertiesFuzzySearch));

router.route('/search/geo').get(userAuthCheck(), catchErrors(getPropertiesForMap)); // Formerly wfs route
// Formerly wfs route
router.route('/search/geo').get(
userAuthCheck(),
catchErrors((req, res) => {
req.query.ExcelExport = undefined; // Prevents Excel export
return getPropertiesForMap(req, res);
}),
);

// Similar to above, but separated for documentation purposes
router.route('/search/geo/export').get(
userAuthCheck(),
catchErrors((req, res) => {
req.query.ExcelExport = 'true'; // Forces Excel export
return getPropertiesForMap(req, res);
}),
);

router.route('/search/linkedProjects').get(userAuthCheck(), catchErrors(getLinkedProjects));

Expand Down
22 changes: 19 additions & 3 deletions express-api/src/services/properties/propertiesServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -965,14 +965,22 @@ const getPropertiesUnion = async (filter: PropertyUnionFilter) => {
return { data, totalCount };
};

export interface PropertyIdsByType {
buildingIds: number[];
parcelIds: number[];
}

/**
* Retrieves properties for export based on the provided filter.
* Filters the properties by type (LAND, BUILDING, SUBDIVISION) and fetches additional details for each property.
* Returns an array of Parcel and Building entities.
* @param filter - The filter criteria to apply when retrieving properties.
* @returns An array of Parcel and Building entities for export.
*/
const getPropertiesForExport = async (filter: PropertyUnionFilter) => {
const getPropertiesForExport = async (
filter: PropertyUnionFilter,
specificProperties?: PropertyIdsByType,
) => {
const result = await getPropertiesUnion(filter);
const filteredProperties = result.data;
const parcelIds = filteredProperties
Expand Down Expand Up @@ -1025,7 +1033,11 @@ const getPropertiesForExport = async (filter: PropertyUnionFilter) => {
const buildingFiscals = resolvedFinds.at(5) as BuildingFiscal[];
const projectProperties = resolvedFinds.at(6) as ProjectProperty[];
const parcels: Parcel[] = (resolvedFinds.at(0) as Parcel[])
.filter((p: Parcel) => parcelIds.includes(p.Id))
.filter((p: Parcel) =>
parcelIds.includes(p.Id) && specificProperties
? specificProperties.parcelIds.includes(p.Id)
: true,
)
.map((p: Parcel) => {
const evaluation = parcelEvaluations.find((pe) => pe.ParcelId === p.Id);
const fiscal = parcelFiscals.find((pf) => pf.ParcelId === p.Id);
Expand All @@ -1038,7 +1050,11 @@ const getPropertiesForExport = async (filter: PropertyUnionFilter) => {
};
});
const buildings: Building[] = (resolvedFinds.at(1) as Building[])
.filter((b: Building) => buildingIds.includes(b.Id))
.filter((b: Building) =>
buildingIds.includes(b.Id) && specificProperties
? specificProperties.buildingIds.includes(b.Id)
: true,
)
.map((b: Building) => {
const evaluation = buildingEvaluations.find((be) => be.BuildingId === b.Id);
const fiscal = buildingFiscals.find((bf) => bf.BuildingId === b.Id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ const _getPropertiesForMap = jest.fn().mockImplementation(async () => [
},
]);

const _getPropertiesForExport = jest
.fn()
.mockImplementation(async () => [produceBuilding(), produceParcel()]);

const _getPropertyUnion = jest.fn().mockImplementation(async () => [producePropertyUnion()]);

const _getImportResults = jest.fn().mockImplementation(async () => [produceImportResult()]);
Expand All @@ -88,6 +92,7 @@ jest.mock('@/services/properties/propertiesServices', () => ({
getPropertiesUnion: () => _getPropertyUnion(),
getImportResults: () => _getImportResults(),
findLinkedProjectsForProperty: () => _findLinkedProjectsForProperty(),
getPropertiesForExport: () => _getPropertiesForExport(),
}));

const _getAgencies = jest.fn().mockImplementation(async () => [1, 2, 3]);
Expand Down Expand Up @@ -181,10 +186,32 @@ describe('UNIT - Properties', () => {
jest
.spyOn(AppDataSource.getRepository(Agency), 'find')
.mockImplementation(async () => [produceAgency()]);
mockRequest.setPimsUser({ RoleId: Roles.GENERAL_USER });
await getPropertiesForMap(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue.length).toBeGreaterThanOrEqual(1);
});
});

describe('GET /properties/search/geo/export', () => {
const _checkUserAgencyPermission = jest.fn().mockImplementation(async () => true);
jest.mock('@/utilities/authorizationChecks', () => ({
checkUserAgencyPermission: _checkUserAgencyPermission,
}));
beforeEach(() => {
jest.clearAllMocks();
});

it('should return 200 with a list of properties in export form', async () => {
mockRequest.setUser({ client_roles: [Roles.ADMIN] });
mockRequest.setPimsUser({ RoleId: Roles.ADMIN });
mockRequest.query = {
ExcelExport: 'true',
};
await getPropertiesForMap(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue.length).toBeGreaterThanOrEqual(1);
expect(_checkUserAgencyPermission).toHaveBeenCalledTimes(0);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jest

const _agenciesFind = jest
.spyOn(AppDataSource.getRepository(Agency), 'find')
.mockImplementation(async () => [produceAgency()]);
.mockImplementation(async () => [produceAgency({ Id: 100 })]);

describe('UNIT - User services', () => {
beforeEach(() => {
Expand Down
10 changes: 5 additions & 5 deletions react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@
"@emotion/styled": "11.14.0",
"@mdi/js": "7.4.47",
"@mdi/react": "1.6.1",
"@mui/icons-material": "6.1.0",
"@mui/icons-material": "6.3.1",
"@mui/lab": "5.0.0-alpha.170",
"@mui/material": "6.1.2",
"@mui/material": "6.3.1",
"@mui/x-data-grid": "7.23.0",
"@mui/x-date-pickers": "7.23.0",
"@react-leaflet/core": "2.1.0",
"@turf/turf": "7.1.0",
"@turf/turf": "7.2.0",
"dayjs": "1.11.10",
"react": "18.3.1",
"react-dom": "18.3.1",
Expand All @@ -52,7 +52,7 @@
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"@vitejs/plugin-react": "4.3.0",
"eslint": "9.16.0",
"eslint": "9.17.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-prettier": "5.2.1",
"eslint-plugin-react": "7.37.1",
Expand All @@ -63,7 +63,7 @@
"ts-jest": "29.2.0",
"ts-node": "10.9.2",
"typescript": "5.7.2",
"typescript-eslint": "8.18.0",
"typescript-eslint": "8.19.1",
"vite": "6.0.3",
"vite-tsconfig-paths": "5.1.2"
},
Expand Down
Loading

0 comments on commit d30f4f7

Please sign in to comment.