diff --git a/migrations/sql/V2024.05.22.13.00__create_regions_table.sql b/migrations/sql/V2024.05.22.13.00__create_regions_table.sql new file mode 100644 index 0000000000..fd4b89df2e --- /dev/null +++ b/migrations/sql/V2024.05.22.13.00__create_regions_table.sql @@ -0,0 +1,41 @@ +CREATE TABLE IF NOT EXISTS regions ( + regional_district_id INT NOT NULL PRIMARY KEY, + name varchar(255) NOT NULL, + create_user character varying(60) NOT NULL, + create_timestamp timestamp with time zone DEFAULT now() NOT NULL, + update_user character varying(60) NOT NULL, + update_timestamp timestamp with time zone DEFAULT now() NOT NULL +); + +INSERT INTO regions (regional_district_id, name, create_user, update_user) +VALUES (4786586, 'Alberni-Clayoquot', 'system-mds', 'system-mds'), + (4786587, 'Bulkley-Nechako', 'system-mds', 'system-mds'), + (4786588, 'Capital', 'system-mds', 'system-mds'), + (4786590, 'Cariboo', 'system-mds', 'system-mds'), + (4786592, 'Central Coast', 'system-mds', 'system-mds'), + (4786597, 'Central Kootenay', 'system-mds', 'system-mds'), + (4786600, 'Central Okanagan', 'system-mds', 'system-mds'), + (4786601, 'Columbia Shuswap', 'system-mds', 'system-mds'), + (4786602, 'Comox Valley', 'system-mds', 'system-mds'), + (51541265, 'Comox-Strathcona (Island)', 'system-mds', 'system-mds'), + (51541257, 'Comox-Strathcona (Mainland)', 'system-mds', 'system-mds'), + (4786603, 'Cowichan Valley', 'system-mds', 'system-mds'), + (4786604, 'East Kootenay', 'system-mds', 'system-mds'), + (4786605, 'Fraser Valley', 'system-mds', 'system-mds'), + (4786636, 'Fraser-Fort George', 'system-mds', 'system-mds'), + (4786650, 'Kitimat-Stikine', 'system-mds', 'system-mds'), + (4786651, 'Kootenay Boundary', 'system-mds', 'system-mds'), + (4786671, 'Metro Vancouver', 'system-mds', 'system-mds'), + (4786672, 'Mount Waddington', 'system-mds', 'system-mds'), + (4786678, 'Nanaimo', 'system-mds', 'system-mds'), + (4786699, 'North Coast', 'system-mds', 'system-mds'), + (4786683, 'North Okanagan', 'system-mds', 'system-mds'), + (4786684, 'Northern Rockies', 'system-mds', 'system-mds'), + (4786686, 'Okanagan-Similkameen', 'system-mds', 'system-mds'), + (4786687, 'Peace River', 'system-mds', 'system-mds'), + (4786690, 'qathet', 'system-mds', 'system-mds'), + (4786712, 'Squamish-Lillooet', 'system-mds', 'system-mds'), + (7043686, 'Stikine', 'system-mds', 'system-mds'), + (4786713, 'Strathcona', 'system-mds', 'system-mds'), + (4786714, 'Sunshine Coast', 'system-mds', 'system-mds'), + (4786722, 'Thompson-Nicola', 'system-mds', 'system-mds'); \ No newline at end of file diff --git a/migrations/sql/V2024.05.22.14.30__add_region_to_project_summary.sql b/migrations/sql/V2024.05.22.14.30__add_region_to_project_summary.sql new file mode 100644 index 0000000000..aed6725fbf --- /dev/null +++ b/migrations/sql/V2024.05.22.14.30__add_region_to_project_summary.sql @@ -0,0 +1,7 @@ +ALTER TABLE project_summary + ADD regional_district_id integer; + +ALTER TABLE project_summary + ADD CONSTRAINT fk_regional_district_id + FOREIGN KEY (regional_district_id) + REFERENCES regions(regional_district_id); \ No newline at end of file diff --git a/services/common/src/components/projectSummary/BasicInformation.spec.tsx b/services/common/src/components/projectSummary/BasicInformation.spec.tsx index f68c94fb40..3c4e4f5696 100644 --- a/services/common/src/components/projectSummary/BasicInformation.spec.tsx +++ b/services/common/src/components/projectSummary/BasicInformation.spec.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render } from "@testing-library/react"; import FormWrapper from "@mds/common/components/forms/FormWrapper"; -import { FORM } from "@mds/common"; +import { FORM } from "@mds/common/constants"; import { PROJECTS } from "@mds/common/constants/reducerTypes"; import { ReduxWrapper } from "@mds/common/tests/utils/ReduxWrapper"; import { PROJECT } from "@mds/common/tests/mocks/dataMocks"; diff --git a/services/common/src/components/projectSummary/FacilityOperator.tsx b/services/common/src/components/projectSummary/FacilityOperator.tsx index 4fca5574fb..fe3e014e3e 100644 --- a/services/common/src/components/projectSummary/FacilityOperator.tsx +++ b/services/common/src/components/projectSummary/FacilityOperator.tsx @@ -17,11 +17,15 @@ import { getDropdownProvinceOptions } from "@mds/common/redux/selectors/staticCo import RenderRadioButtons from "../forms/RenderRadioButtons"; import RenderAutoSizeField from "../forms/RenderAutoSizeField"; import { normalizePhone } from "@mds/common/redux/utils/helpers"; +import { getRegionOptions } from "@mds/common/redux/slices/regionsSlice"; export const FacilityOperator: FC = () => { const formValues = useSelector(getFormValues(FORM.ADD_EDIT_PROJECT_SUMMARY)); + const { zoning } = formValues; + const regionOptions = useSelector(getRegionOptions); + const address_type_code = "CAN"; const provinceOptions = useSelector(getDropdownProvinceOptions).filter( @@ -49,6 +53,18 @@ export const FacilityOperator: FC = () => { rows={3} component={RenderAutoSizeField} /> + + + + + Facility Address diff --git a/services/common/src/constants/API.ts b/services/common/src/constants/API.ts index 4e4ca61943..a3009b5df0 100644 --- a/services/common/src/constants/API.ts +++ b/services/common/src/constants/API.ts @@ -363,3 +363,6 @@ export const DAM = (damGuid) => (damGuid ? `/dams/${damGuid}` : "/dams"); export const DOCUMENTS_COMPRESSION = (mineGuid) => `/mines/${mineGuid}/documents/zip`; export const POLL_DOCUMENTS_COMPRESSION_PROGRESS = (taskId) => `/mines/documents/zip/${taskId}`; + +// Regions +export const REGIONS_LIST = "/regions"; diff --git a/services/common/src/redux/reducers/rootReducerShared.ts b/services/common/src/redux/reducers/rootReducerShared.ts index 74d92e7029..75a1e16604 100644 --- a/services/common/src/redux/reducers/rootReducerShared.ts +++ b/services/common/src/redux/reducers/rootReducerShared.ts @@ -33,6 +33,7 @@ import { } from "../reducers"; import reportSubmissionReducer from "@mds/common/components/reports/reportSubmissionSlice"; import verifiableCredentialsReducer from "@mds/common/redux/slices/verifiableCredentialsSlice"; +import regionsReducer from "@mds/common/redux/slices/regionsSlice"; import complianceCodeReducer, { complianceCodeReducerType } from "../slices/complianceCodesSlice"; export const sharedReducer = { ...activityReducer, @@ -77,5 +78,6 @@ export const sharedReducer = { loadingBar: loadingBarReducer, reportSubmission: reportSubmissionReducer, verifiableCredentials: verifiableCredentialsReducer, + regions: regionsReducer, [complianceCodeReducerType]: complianceCodeReducer, }; diff --git a/services/common/src/redux/slices/regionsSlice.ts b/services/common/src/redux/slices/regionsSlice.ts new file mode 100644 index 0000000000..ff3c25d393 --- /dev/null +++ b/services/common/src/redux/slices/regionsSlice.ts @@ -0,0 +1,70 @@ +import { createAppSlice } from "@mds/common/redux/createAppSlice"; +import { hideLoading, showLoading } from "react-redux-loading-bar"; +import CustomAxios from "@mds/common/redux/customAxios"; +import { ENVIRONMENT, REGIONS_LIST } from "@mds/common/constants"; +import { RootState } from "@mds/common/redux/rootState"; + +const createRequestHeader = REQUEST_HEADER.createRequestHeader; + +const rejectHandler = (action) => { + console.log(action.error); + console.log(action.error.stack); +}; + +interface Region { + name: string; + regional_district_id: number; +} + +interface RegionsState { + regions: Region[]; +} + +const initialState: RegionsState = { + regions: [], +}; + +const regionsSlice = createAppSlice({ + name: "regionsSlice", + initialState, + reducers: (create) => ({ + fetchRegions: create.asyncThunk( + async (_, thunkAPI) => { + const headers = createRequestHeader(); + thunkAPI.dispatch(showLoading()); + + const response = await CustomAxios({ + errorToastMessage: "default", + }).get(`${ENVIRONMENT.apiUrl}${REGIONS_LIST}`, headers); + + thunkAPI.dispatch(hideLoading()); + + return response.data; + }, + { + fulfilled: (state, action) => { + state.regions = action.payload; + }, + rejected: (state: RegionsState, action) => { + rejectHandler(action); + }, + } + ), + }), + selectors: { + getRegionOptions: (state: RegionsState) => { + return state.regions.map((region) => ({ + label: region.name, + value: region.regional_district_id, + })); + }, + }, +}); + +export const { fetchRegions } = regionsSlice.actions; +export const { getRegionOptions } = regionsSlice.getSelectors( + (rootState: RootState) => rootState.regions +); + +const regionsReducer = regionsSlice.reducer; +export default regionsReducer; diff --git a/services/common/src/tests/mocks/dataMocks.tsx b/services/common/src/tests/mocks/dataMocks.tsx index 6d9396aeaa..33b4767f9f 100644 --- a/services/common/src/tests/mocks/dataMocks.tsx +++ b/services/common/src/tests/mocks/dataMocks.tsx @@ -7168,7 +7168,31 @@ export const PROJECT = { project_title: "Test Project Title", mine_name: "Sample Mine", mine_guid: "40fb0ca4-4dfb-4660-a184-6d031a21f3e9", - contacts: [], + contacts: [ + { + project_contact_guid: "65a02cd8-3edd-491f-acdd-585f9f9742ee", + project_guid: "aa5bbbeb-f8ab-496f-aff2-e71d314bcc3e", + job_title: null, + company_name: null, + email: "test@test.com", + phone_number: "999-999-9999", + phone_extension: null, + is_primary: true, + first_name: "Test", + last_name: "Testerson", + address: [ + { + suite_no: null, + address_line_1: "111 street", + address_line_2: null, + city: "Victoria", + sub_division_code: "BC", + post_code: "T5M0V3", + address_type_code: "CAN", + }, + ], + }, + ], project_summary: { documents: [], }, @@ -7277,106 +7301,44 @@ export const PROJECT_SUMMARY = { "OTHER", "WATER_LICENCE", ], - authorizations: { - MINES_ACT_PERMIT: [ - { - project_summary_authorization_guid: "83053925-c0ab-468e-b41c-32eb1607d1be", - project_summary_guid: "0bfc0d9e-542f-4424-91b5-c563dba02619", - project_summary_permit_type: ["NEW"], - project_summary_authorization_type: "MINES_ACT_PERMIT", - existing_permits_authorizations: [""], - amendment_changes: null, - amendment_severity: null, - is_contaminated: null, - new_type: null, - authorization_description: null, - exemption_requested: null, - }, - ], - WATER_LICENCE: [ - { - project_summary_authorization_guid: "ec3dcc27-cce9-4539-a9cd-1b3faa41fc9f", - project_summary_guid: "0bfc0d9e-542f-4424-91b5-c563dba02619", - project_summary_permit_type: ["AMENDMENT"], - project_summary_authorization_type: "WATER_LICENCE", - existing_permits_authorizations: ["PX-1234", "CX-5678"], - amendment_changes: null, - amendment_severity: null, - is_contaminated: null, - new_type: null, - authorization_description: null, - exemption_requested: null, - }, - ], - OCCUPANT_CUT_LICENCE: [ - { - project_summary_authorization_guid: "0d047eeb-7000-4649-89ae-6d295a12fc6a", - project_summary_guid: "0bfc0d9e-542f-4424-91b5-c563dba02619", - project_summary_permit_type: ["NEW", "NOTIFICATION"], - project_summary_authorization_type: "OCCUPANT_CUT_LICENCE", - existing_permits_authorizations: null, - amendment_changes: null, - amendment_severity: null, - is_contaminated: null, - new_type: null, - authorization_description: null, - exemption_requested: null, - }, - ], - OTHER: [ - { - project_summary_authorization_guid: "5600e1e3-b303-4070-b3e2-9986264c8c38", - project_summary_guid: "0bfc0d9e-542f-4424-91b5-c563dba02619", - project_summary_permit_type: ["OTHER"], - project_summary_authorization_type: "OTHER", - existing_permits_authorizations: null, - amendment_changes: null, - amendment_severity: null, - is_contaminated: null, - new_type: null, - authorization_description: "other legislation details", - exemption_requested: null, - }, - ], - AIR_EMISSIONS_DISCHARGE_PERMIT: { - types: ["AMENDMENT"], - NEW: [], - AMENDMENT: [ - { - project_summary_authorization_guid: "b8c63caa-e6e5-4e2e-80c9-327921f7efbf", - project_summary_guid: "0bfc0d9e-542f-4424-91b5-c563dba02619", - project_summary_permit_type: ["AMENDMENT"], - project_summary_authorization_type: "AIR_EMISSIONS_DISCHARGE_PERMIT", - existing_permits_authorizations: ["1234"], - amendment_changes: ["ILT"], - amendment_severity: "SIG", - is_contaminated: false, - new_type: null, - authorization_description: "adsf", - exemption_requested: true, - }, - ], - }, - MUNICIPAL_WASTEWATER_REGULATION: { - types: ["NEW"], - NEW: [ - { - project_summary_authorization_guid: "2ee7161f-7735-40c6-a8a4-ecb6cb79675e", - project_summary_guid: "0bfc0d9e-542f-4424-91b5-c563dba02619", - project_summary_permit_type: ["NEW"], - project_summary_authorization_type: "MUNICIPAL_WASTEWATER_REGULATION", - existing_permits_authorizations: null, - amendment_changes: null, - amendment_severity: null, - is_contaminated: null, - new_type: "APP", - authorization_description: "purpose of application", - exemption_requested: false, - }, - ], - AMENDMENT: [], + authorizations: [ + { + project_summary_authorization_guid: "92e1a34b-c181-4be8-8ad0-f96dcd3ab7cc", + project_summary_guid: "8b4b9781-2e59-43ef-8164-4cc3b964417a", + project_summary_permit_type: ["NEW"], + project_summary_authorization_type: "AIR_EMISSIONS_DISCHARGE_PERMIT", + existing_permits_authorizations: null, + amendment_changes: null, + amendment_severity: null, + is_contaminated: null, + new_type: "PER", + authorization_description: "sdfa", + amendment_documents: [], + exemption_requested: false, + ams_tracking_number: null, + ams_outcome: null, + ams_status_code: null, + ams_submission_timestamp: "2024-05-24T19:10:09.825194+00:00", + }, + { + project_summary_authorization_guid: "624d3acc-b62b-491e-82a3-67ef3b1bbf88", + project_summary_guid: "8b4b9781-2e59-43ef-8164-4cc3b964417a", + project_summary_permit_type: ["NEW"], + project_summary_authorization_type: "REFUSE_DISCHARGE_PERMIT", + existing_permits_authorizations: null, + amendment_changes: null, + amendment_severity: null, + is_contaminated: null, + new_type: "PER", + authorization_description: "asdf", + amendment_documents: [], + exemption_requested: false, + ams_tracking_number: null, + ams_outcome: null, + ams_status_code: null, + ams_submission_timestamp: "2024-05-24T19:17:09.212499+00:00", }, - }, + ], }; export const AUTHORIZATION_INVOLVED = { @@ -8176,3 +8138,26 @@ export const MINES_ACT_PERMITS_VC_LIST = [ cred_rev_id: "1234", }, ]; + +export const REGIONS = [ + { + name: "Alberni-Clayoquot", + regional_district_id: 4786586, + }, + { + name: "Bulkley-Nechako", + regional_district_id: 4786587, + }, + { + name: "Capital", + regional_district_id: 4786588, + }, + { + name: "Cariboo", + regional_district_id: 4786590, + }, + { + name: "Central Coast", + regional_district_id: 4786592, + }, +]; diff --git a/services/core-api/app/__init__.py b/services/core-api/app/__init__.py index 8cc1942372..6bcac5ad81 100644 --- a/services/core-api/app/__init__.py +++ b/services/core-api/app/__init__.py @@ -41,6 +41,7 @@ from app.api.dams.namespace import api as dams_api from app.api.verifiable_credentials.namespace import api as verifiable_credential_api from app.api.report_error.namespace import api as report_error_api +from app.api.regions.namespace import api as regions_api from app.commands import register_commands from app.config import Config @@ -196,6 +197,7 @@ def register_routes(app): root_api_namespace.add_namespace(dams_api) root_api_namespace.add_namespace(verifiable_credential_api) root_api_namespace.add_namespace(report_error_api) + root_api_namespace.add_namespace(regions_api) @root_api_namespace.route('/version/') class VersionCheck(Resource): diff --git a/services/core-api/app/api/projects/project_summary/models/project_summary.py b/services/core-api/app/api/projects/project_summary/models/project_summary.py index 55631c8c04..2cc8e5bd39 100644 --- a/services/core-api/app/api/projects/project_summary/models/project_summary.py +++ b/services/core-api/app/api/projects/project_summary/models/project_summary.py @@ -7,6 +7,7 @@ from werkzeug.exceptions import BadRequest from app.api.parties.party import PartyOrgBookEntity +from app.api.regions.models.regions import Regions from app.api.services.ams_api_service import AMSApiService from app.extensions import db @@ -68,6 +69,7 @@ class ProjectSummary(SoftDeleteMixin, AuditMixin, Base): zoning = db.Column(db.Boolean, nullable=True) zoning_reason = db.Column(db.String, nullable=True) nearest_municipality_guid = db.Column(UUID(as_uuid=True), db.ForeignKey('municipality.municipality_guid')) + regional_district_id = db.Column(db.Integer(), db.ForeignKey('regions.regional_district_id'), nullable=True) company_alias = db.Column(db.String(200), nullable=True) is_legal_address_same_as_mailing_address = db.Column(db.Boolean, nullable=True) @@ -984,6 +986,7 @@ def update(self, is_billing_address_same_as_legal_address=None, contacts=None, company_alias=None, + regional_district_id=None, add_to_session=True): # Update simple properties. @@ -1049,6 +1052,7 @@ def update(self, self.facility_lease_no = facility_lease_no self.zoning = zoning self.zoning_reason = zoning_reason + self.regional_district_id = regional_district_id if facility_operator and facility_type: if not facility_operator['party_type_code']: @@ -1105,6 +1109,10 @@ def update(self, for authorization in authorizations: self.create_or_update_authorization(authorization) + regional_district_name = regional_district_id is not None and Regions.find_by_id( + regional_district_id).name or None + + if ams_authorizations: ams_results = [] if self.status_code == 'SUB': @@ -1132,7 +1140,8 @@ def update(self, facility_pid_pin_crown_file_no, company_alias, zoning, - zoning_reason) + zoning_reason, + regional_district_name) for authorization in ams_authorizations.get('amendments', []): self.create_or_update_authorization(authorization) diff --git a/services/core-api/app/api/projects/project_summary/resources/project_summary.py b/services/core-api/app/api/projects/project_summary/resources/project_summary.py index 9b9e48fb3a..6c275a85fd 100644 --- a/services/core-api/app/api/projects/project_summary/resources/project_summary.py +++ b/services/core-api/app/api/projects/project_summary/resources/project_summary.py @@ -94,10 +94,10 @@ class ProjectSummaryResource(Resource, UserMixin): required=False, ) parser.add_argument( - 'agent', - type=dict, - location='json', - store_missing=False, + 'agent', + type=dict, + location='json', + store_missing=False, required=False ) parser.add_argument('is_agent', type=bool, help="True if an agent is applying on behalf of the Applicant", location='json', store_missing=False, required=False) @@ -130,10 +130,10 @@ class ProjectSummaryResource(Resource, UserMixin): required=False, ) parser.add_argument( - 'facility_operator', - type=dict, - location='json', - store_missing=False, + 'facility_operator', + type=dict, + location='json', + store_missing=False, required=False ) parser.add_argument('facility_type', type=str, store_missing=False, required=False) @@ -141,7 +141,7 @@ class ProjectSummaryResource(Resource, UserMixin): parser.add_argument('facility_latitude', type=lambda x: Decimal(x) if x else None, store_missing=False, required=False) parser.add_argument('facility_longitude', type=lambda x: Decimal(x) if x else None, store_missing=False, required=False) - + parser.add_argument('facility_coords_source', type=str, store_missing=False, required=False) parser.add_argument('facility_coords_source_desc', type=str, store_missing=False, required=False) parser.add_argument('facility_pid_pin_crown_file_no', type=str, store_missing=False, required=False) @@ -150,6 +150,7 @@ class ProjectSummaryResource(Resource, UserMixin): parser.add_argument('zoning', type=bool, store_missing=False, required=False) parser.add_argument('zoning_reason', type=str, store_missing=False, required=False) parser.add_argument('nearest_municipality', type=str, store_missing=False, required=False) + parser.add_argument('regional_district_id', type=int, store_missing=False, required=False) parser.add_argument( 'applicant', @@ -195,7 +196,7 @@ def put(self, project_guid, project_summary_guid): is_minespace_user()) project = Project.find_by_project_guid(project_guid) data = self.parser.parse_args() - + project_summary_validation = project_summary.validate_project_summary(data) if any(project_summary_validation[i] != [] for i in project_summary_validation): current_app.logger.error(f'Project Summary schema validation failed with errors: {project_summary_validation}') @@ -240,7 +241,8 @@ def put(self, project_guid, project_summary_guid): data.get('is_billing_address_same_as_mailing_address'), data.get('is_billing_address_same_as_legal_address'), data.get('contacts'), - data.get('company_alias')) + data.get('company_alias'), + data.get('regional_district_id')) project_summary.save() if prev_status == 'DFT' and project_summary.status_code == 'SUB': diff --git a/services/core-api/app/api/projects/response_models.py b/services/core-api/app/api/projects/response_models.py index dd8de16a1a..9abd8276f6 100644 --- a/services/core-api/app/api/projects/response_models.py +++ b/services/core-api/app/api/projects/response_models.py @@ -202,7 +202,8 @@ def format(self, value): 'is_billing_address_same_as_mailing_address': fields.Boolean, 'is_billing_address_same_as_legal_address': fields.Boolean, 'applicant': fields.Nested(PARTY), - 'municipality': fields.Nested(MUNICIPALITY_MODEL) + 'municipality': fields.Nested(MUNICIPALITY_MODEL), + 'regional_district_id': fields.Integer, }) REQUIREMENTS_MODEL = api.model( diff --git a/services/core-api/app/api/regions/models/__init__.py b/services/core-api/app/api/regions/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/core-api/app/api/regions/models/regions.py b/services/core-api/app/api/regions/models/regions.py new file mode 100644 index 0000000000..d44e0c8a31 --- /dev/null +++ b/services/core-api/app/api/regions/models/regions.py @@ -0,0 +1,17 @@ +from app.extensions import db +from app.api.utils.models_mixins import AuditMixin, Base + + +class Regions(AuditMixin, Base): + __tablename__ = "regions" + + regional_district_id = db.Column(db.Integer, nullable=False, primary_key=True) + name = db.Column(db.String, nullable=False) + + @classmethod + def get_all(cls): + return cls.query.all() + + @classmethod + def find_by_id(cls, regional_district_id): + return cls.query.filter_by(regional_district_id=regional_district_id).first() diff --git a/services/core-api/app/api/regions/namespace.py b/services/core-api/app/api/regions/namespace.py new file mode 100644 index 0000000000..4b6bb79370 --- /dev/null +++ b/services/core-api/app/api/regions/namespace.py @@ -0,0 +1,7 @@ +from flask_restx import Namespace + +from app.api.regions.resources.region_list_resource import RegionListResource + +api = Namespace('regions', description='Region options') + +api.add_resource(RegionListResource, '') diff --git a/services/core-api/app/api/regions/resources/__init__.py b/services/core-api/app/api/regions/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/core-api/app/api/regions/resources/region_list_resource.py b/services/core-api/app/api/regions/resources/region_list_resource.py new file mode 100644 index 0000000000..3aa83113c0 --- /dev/null +++ b/services/core-api/app/api/regions/resources/region_list_resource.py @@ -0,0 +1,21 @@ +from flask_restx import Resource +from werkzeug.exceptions import NotFound + +from app.api.regions.models.regions import Regions +from app.api.regions.response_models import REGION +from app.api.utils.access_decorators import requires_any_of, VIEW_ALL, MINESPACE_PROPONENT +from app.api.utils.resources_mixins import UserMixin +from app.extensions import api + + +class RegionListResource(Resource, UserMixin): + @api.doc( + description="List all regions", + ) + @api.marshal_with(REGION, code=200, as_list=True) + @requires_any_of([VIEW_ALL, MINESPACE_PROPONENT]) + def get(self): + regions = Regions.get_all() + if not regions: + raise NotFound('Region not found') + return regions diff --git a/services/core-api/app/api/regions/response_models.py b/services/core-api/app/api/regions/response_models.py new file mode 100644 index 0000000000..e0d5f39451 --- /dev/null +++ b/services/core-api/app/api/regions/response_models.py @@ -0,0 +1,9 @@ +from flask_restx import fields + +from app.extensions import api + +REGION = api.model( + 'Region', { + 'name': fields.String, + 'regional_district_id': fields.Integer, + }) diff --git a/services/core-api/app/api/services/ams_api_service.py b/services/core-api/app/api/services/ams_api_service.py index a584ecbff3..b774047ef5 100644 --- a/services/core-api/app/api/services/ams_api_service.py +++ b/services/core-api/app/api/services/ams_api_service.py @@ -84,7 +84,8 @@ def create_new_ams_authorization(cls, facility_pid_pin_crown_file_no, company_alias, zoning, - zoning_reason + zoning_reason, + regional_district_name ): """Creates a new AMS authorization application""" @@ -203,7 +204,10 @@ def create_new_ams_authorization(cls, 'facilityoperator': facility_operator.get('name', ''), 'facilityoperatorphonenumber': cls.__format_phone_number(facility_operator.get('phone_no', '')), 'facilityoperatoremail': facility_operator.get('email', ''), - 'facilityoperatortitle': facility_operator.get('job_title', '') + 'facilityoperatortitle': facility_operator.get('job_title', ''), + 'regionaldistrict': { + 'name': regional_district_name + } } payload = json.dumps(ams_authorization_data) response = requests.post(Config.AMS_URL, data=payload, headers=headers) diff --git a/services/minespace-web/src/components/Forms/projects/projectSummary/ProjectSummaryForm.tsx b/services/minespace-web/src/components/Forms/projects/projectSummary/ProjectSummaryForm.tsx index 6d5a12be6c..8bba60b095 100644 --- a/services/minespace-web/src/components/Forms/projects/projectSummary/ProjectSummaryForm.tsx +++ b/services/minespace-web/src/components/Forms/projects/projectSummary/ProjectSummaryForm.tsx @@ -1,17 +1,7 @@ import React, { FC } from "react"; -import { connect } from "react-redux"; -import { RouteComponentProps, withRouter } from "react-router-dom"; +import { useSelector } from "react-redux"; import { flattenObject, resetForm } from "@common/utils/helpers"; -import { compose, bindActionCreators } from "redux"; -import { - reduxForm, - change, - arrayPush, - formValueSelector, - getFormValues, - getFormSyncErrors, - InjectedFormProps, -} from "redux-form"; +import { formValueSelector, getFormSyncErrors, getFormValues } from "redux-form"; import * as FORM from "@/constants/forms"; import DocumentUpload from "@/components/Forms/projects/projectSummary/DocumentUpload"; import ProjectContacts from "@/components/Forms/projects/projectSummary/ProjectContacts"; @@ -22,13 +12,14 @@ import Step from "@common/components/Step"; import ProjectLinks from "@mds/common/components/projects/ProjectLinks"; import { EDIT_PROJECT } from "@/constants/routes"; import { useFeatureFlag } from "@mds/common/providers/featureFlags/useFeatureFlag"; -import { Feature, IProjectSummary, IProjectSummaryDocument } from "@mds/common"; +import { Feature, IProjectSummary } from "@mds/common"; import { Agent } from "./Agent"; import { LegalLandOwnerInformation } from "@mds/common/components/projectSummary/LegalLandOwnerInformation"; import { FacilityOperator } from "@mds/common/components/projectSummary/FacilityOperator"; import BasicInformation from "@mds/common/components/projectSummary/BasicInformation"; import Applicant from "@/components/Forms/projects/projectSummary/Applicant"; import Declaration from "@mds/common/components/projectSummary/Declaration"; +import FormWrapper from "@mds/common/components/forms/FormWrapper"; import { ApplicationSummary } from "./ApplicationSummary"; interface ProjectSummaryFormProps { @@ -42,14 +33,6 @@ interface ProjectSummaryFormProps { activeTab: string; } -interface StateProps { - documents: IProjectSummaryDocument; - formValues: any; - formErrors: any; - anyTouched: boolean; - // amendmentDocuments: IProjectSummaryDocument[]; -} - // converted to a function to make feature flag easier to work with // when removing feature flag, convert back to array export const getProjectFormTabs = (amsFeatureEnabled: boolean) => { @@ -79,10 +62,24 @@ export const getProjectFormTabs = (amsFeatureEnabled: boolean) => { ]; }; -export const ProjectSummaryForm: FC & - RouteComponentProps> = ({ documents = [], ...props }) => { +export const ProjectSummaryForm: FC = ({ ...props }) => { + const selector = formValueSelector(FORM.ADD_EDIT_PROJECT_SUMMARY); + + const formValues = + useSelector((state) => getFormValues(FORM.ADD_EDIT_PROJECT_SUMMARY)(state)) || {}; + const documents = useSelector((state) => selector(state, "documents")) || []; + const formErrors = useSelector((state) => + getFormSyncErrors(FORM.ADD_EDIT_PROJECT_SUMMARY)(state) + ); + const anyTouched = useSelector((state) => selector(state, "anyTouched")); + + const childProps = { + ...props, + formValues, + formErrors, + anyTouched, + }; + const { isFeatureEnabled } = useFeatureFlag(); const majorProjectsFeatureEnabled = isFeatureEnabled(Feature.MAJOR_PROJECT_LINK_PROJECTS); const amsFeatureEnabled = isFeatureEnabled(Feature.AMS_AGENT); @@ -101,61 +98,46 @@ export const ProjectSummaryForm: FC, "mine-components-and-offsite-infrastructure": , "purpose-and-authorization": ( - + ), "document-upload": ( - + ), "application-summary": , declaration: , }[tab]); - const errors = Object.keys(flattenObject(props.formErrors)); + const errors = Object.keys(flattenObject(formErrors)); const disabledTabs = errors.length > 0; return ( - {}} + initialValues={props.initialValues} + reduxFormConfig={{ + touchOnBlur: true, + touchOnChange: false, + onSubmitSuccess: resetForm(FORM.ADD_EDIT_PROJECT_SUMMARY), + }} > - {projectFormTabs - .filter((tab) => majorProjectsFeatureEnabled || tab !== "related-projects") - .map((tab) => ( - - {renderTabComponent(tab)} - - ))} - + + {projectFormTabs + .filter((tab) => majorProjectsFeatureEnabled || tab !== "related-projects") + .map((tab) => ( + + {renderTabComponent(tab)} + + ))} + + ); }; -const selector = formValueSelector(FORM.ADD_EDIT_PROJECT_SUMMARY); -const mapStateToProps = (state) => ({ - documents: selector(state, "documents"), - formValues: getFormValues(FORM.ADD_EDIT_PROJECT_SUMMARY)(state) || {}, - formErrors: getFormSyncErrors(FORM.ADD_EDIT_PROJECT_SUMMARY)(state), - anyTouched: selector(state, "anyTouched"), -}); - -const mapDispatchToProps = (dispatch) => - bindActionCreators( - { - change, - arrayPush, - }, - dispatch - ); - -export default compose( - connect(mapStateToProps, mapDispatchToProps), - reduxForm({ - form: FORM.ADD_EDIT_PROJECT_SUMMARY, - touchOnBlur: true, - touchOnChange: false, - onSubmitSuccess: resetForm(FORM.ADD_EDIT_PROJECT_SUMMARY), - onSubmit: () => {}, - }) -)(withRouter(ProjectSummaryForm)) as FC; +export default ProjectSummaryForm; diff --git a/services/minespace-web/src/components/pages/Project/ProjectSummaryPage.tsx b/services/minespace-web/src/components/pages/Project/ProjectSummaryPage.tsx index 88d197c1d1..8e4233151b 100644 --- a/services/minespace-web/src/components/pages/Project/ProjectSummaryPage.tsx +++ b/services/minespace-web/src/components/pages/Project/ProjectSummaryPage.tsx @@ -1,77 +1,43 @@ -import React, { FC, useEffect, useState } from "react"; -import { connect } from "react-redux"; -import { bindActionCreators } from "redux"; +import React, { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; import { flattenObject } from "@common/utils/helpers"; import { Link, Prompt, useHistory, useLocation, useParams } from "react-router-dom"; -import { - submit, - formValueSelector, - getFormSyncErrors, - getFormValues, - reset, - touch, - change, -} from "redux-form"; -import { Row, Col, Typography, Divider } from "antd"; +import { getFormSyncErrors, getFormValues, reset, submit, touch } from "redux-form"; +import { Col, Divider, Row, Typography } from "antd"; import ArrowLeftOutlined from "@ant-design/icons/ArrowLeftOutlined"; import { getMines } from "@mds/common/redux/selectors/mineSelectors"; import { - getProjectSummary, getFormattedProjectSummary, getProject, + getProjectSummary, } from "@mds/common/redux/selectors/projectSelectors"; import { - getProjectSummaryDocumentTypesHash, getProjectSummaryAuthorizationTypesArray, + getProjectSummaryDocumentTypesHash, } from "@mds/common/redux/selectors/staticContentSelectors"; import { createProjectSummary, - updateProjectSummary, fetchProjectById, updateProject, + updateProjectSummary, } from "@mds/common/redux/actionCreators/projectActionCreator"; import { fetchMineRecordById } from "@mds/common/redux/actionCreators/mineActionCreator"; import { clearProjectSummary } from "@mds/common/redux/actions/projectActions"; import * as FORM from "@/constants/forms"; import Loading from "@/components/common/Loading"; import { - EDIT_PROJECT_SUMMARY, - MINE_DASHBOARD, ADD_PROJECT_SUMMARY, EDIT_PROJECT, + EDIT_PROJECT_SUMMARY, + MINE_DASHBOARD, } from "@/constants/routes"; import ProjectSummaryForm, { getProjectFormTabs, } from "@/components/Forms/projects/projectSummary/ProjectSummaryForm"; -import { IMine, IProjectSummary, IProject, Feature, removeNullValuesRecursive } from "@mds/common"; -import { ActionCreator } from "@mds/common/interfaces/actionCreator"; +import { Feature, removeNullValuesRecursive } from "@mds/common"; import { useFeatureFlag } from "@mds/common/providers/featureFlags/useFeatureFlag"; import { isArray } from "lodash"; - -interface ProjectSummaryPageProps { - mines: Partial[]; - projectSummary: Partial; - project: Partial; - fetchProjectById: ActionCreator; - createProjectSummary: ActionCreator; - updateProjectSummary: ActionCreator; - fetchMineRecordById: ActionCreator; - updateProject: ActionCreator; - clearProjectSummary: () => any; - projectSummaryDocumentTypesHash: Record; - submit: (arg1?: string) => any; - formValueSelector: (arg1: string, arg2?: any) => any; - getFormSyncErrors: (arg1: string) => any; - reset: (arg1: string) => any; - touch: (arg1?: string, arg2?: any) => any; - formErrors: Record; - formValues: any; - projectSummaryAuthorizationTypesArray: any[]; - anyTouched: boolean; - formattedProjectSummary: any; - location: Record; - change: any; -} +import { fetchRegions } from "@mds/common/redux/slices/regionsSlice"; interface IParams { mineGuid?: string; @@ -80,29 +46,22 @@ interface IParams { tab?: any; } -export const ProjectSummaryPage: FC = (props) => { - const { - mines, - formattedProjectSummary, - project, - projectSummary, - projectSummaryAuthorizationTypesArray, - projectSummaryDocumentTypesHash, - formValues, - formErrors, - submit, - touch, - anyTouched, - reset, - fetchProjectById, - fetchMineRecordById, - clearProjectSummary, - createProjectSummary, - updateProjectSummary, - updateProject, - change, - } = props; +export const ProjectSummaryPage = () => { + const anyTouched = useSelector( + (state) => state.form[FORM.ADD_EDIT_PROJECT_SUMMARY]?.anyTouched || false + ); + const mines = useSelector(getMines); + const projectSummary = useSelector(getProjectSummary); + const formattedProjectSummary = useSelector(getFormattedProjectSummary); + const project = useSelector(getProject); + const projectSummaryDocumentTypesHash = useSelector(getProjectSummaryDocumentTypesHash); + const projectSummaryAuthorizationTypesArray = useSelector( + getProjectSummaryAuthorizationTypesArray + ); + const formErrors = useSelector(getFormSyncErrors(FORM.ADD_EDIT_PROJECT_SUMMARY)); + const formValues = useSelector(getFormValues(FORM.ADD_EDIT_PROJECT_SUMMARY)); + const dispatch = useDispatch(); const { isFeatureEnabled } = useFeatureFlag(); const amsFeatureEnabled = isFeatureEnabled(Feature.AMS_AGENT); const { mineGuid, projectGuid, projectSummaryGuid, tab } = useParams(); @@ -116,17 +75,24 @@ export const ProjectSummaryPage: FC = (props) => { const handleFetchData = () => { if (projectGuid && projectSummaryGuid) { setIsEditMode(true); - return fetchProjectById(projectGuid); + dispatch(fetchRegions(undefined)); + return dispatch(fetchProjectById(projectGuid)); } - return fetchMineRecordById(mineGuid); + return dispatch(fetchMineRecordById(mineGuid)); }; + useEffect(() => { + if (project) { + setIsLoaded(true); + } + }, [project]); + useEffect(() => { if (!isLoaded) { - handleFetchData().then(() => setIsLoaded(true)); + handleFetchData(); } return () => { - clearProjectSummary(); + dispatch(clearProjectSummary()); }; }, []); @@ -152,12 +118,18 @@ export const ProjectSummaryPage: FC = (props) => { } else { newAmsAuthorizations = newAmsAuthorizations.concat( authsOfType?.NEW.map((a) => - transformAuthorization(type, { ...a, project_summary_permit_type: ["NEW"] }) + transformAuthorization(type, { + ...a, + project_summary_permit_type: ["NEW"], + }) ) ); amendAmsAuthorizations = amendAmsAuthorizations.concat( authsOfType?.AMENDMENT.map((a) => - transformAuthorization(type, { ...a, project_summary_permit_type: ["AMENDMENT"] }) + transformAuthorization(type, { + ...a, + project_summary_permit_type: ["AMENDMENT"], + }) ) ); } @@ -185,20 +157,27 @@ export const ProjectSummaryPage: FC = (props) => { const handleUpdateProjectSummary = async (values, message) => { const payload = handleTransformPayload(values); setIsLoaded(false); - return updateProjectSummary( - { - projectGuid, - projectSummaryGuid, - }, - payload, - message + return dispatch( + updateProjectSummary( + { + projectGuid, + projectSummaryGuid, + }, + payload, + message + ) ) .then(async () => { - await updateProject( - { projectGuid }, - { mrc_review_required: payload.mrc_review_required, contacts: payload.contacts }, - "Successfully updated project.", - false + await dispatch( + updateProject( + { projectGuid }, + { + mrc_review_required: payload.mrc_review_required, + contacts: payload.contacts, + }, + "Successfully updated project.", + false + ) ); }) .then(async () => { @@ -210,12 +189,14 @@ export const ProjectSummaryPage: FC = (props) => { }; const handleCreateProjectSummary = async (values, message) => { - return createProjectSummary( - { - mineGuid: mineGuid, - }, - handleTransformPayload(values), - message + return dispatch( + createProjectSummary( + { + mineGuid: mineGuid, + }, + handleTransformPayload(values), + message + ) ).then(({ data: { project_guid, project_summary_guid } }) => { history.replace( EDIT_PROJECT_SUMMARY.dynamicRoute(project_guid, project_summary_guid, projectFormTabs[1]) @@ -248,8 +229,8 @@ export const ProjectSummaryPage: FC = (props) => { const errors = Object.keys(flattenObject(formErrors)); const values = { ...formValues, status_code: status_code }; - submit(FORM.ADD_EDIT_PROJECT_SUMMARY); - touch(FORM.ADD_EDIT_PROJECT_SUMMARY); + dispatch(submit(FORM.ADD_EDIT_PROJECT_SUMMARY)); + dispatch(touch(FORM.ADD_EDIT_PROJECT_SUMMARY)); if (errors.length === 0) { try { if (!isEditMode) { @@ -272,8 +253,8 @@ export const ProjectSummaryPage: FC = (props) => { const message = "Successfully saved a draft project description."; const values = { ...formValues, status_code: "DFT" }; - submit(FORM.ADD_EDIT_PROJECT_SUMMARY); - touch(FORM.ADD_EDIT_PROJECT_SUMMARY); + dispatch(submit(FORM.ADD_EDIT_PROJECT_SUMMARY)); + dispatch(touch(FORM.ADD_EDIT_PROJECT_SUMMARY)); const errors = Object.keys(flattenObject(formErrors)); if (errors.length === 0) { try { @@ -299,7 +280,10 @@ export const ProjectSummaryPage: FC = (props) => { : `New project description for ${mineName}`; const initialValues = isEditMode - ? { ...formattedProjectSummary, mrc_review_required: project.mrc_review_required } + ? { + ...formattedProjectSummary, + mrc_review_required: project.mrc_review_required, + } : {}; return ( @@ -309,7 +293,7 @@ export const ProjectSummaryPage: FC = (props) => { when={anyTouched} message={(newLocation, action) => { if (action === "REPLACE") { - reset(FORM.ADD_EDIT_PROJECT_SUMMARY); + dispatch(reset(FORM.ADD_EDIT_PROJECT_SUMMARY)); } return location.pathname !== newLocation.pathname && !newLocation.pathname.includes("project-description") && @@ -354,36 +338,4 @@ export const ProjectSummaryPage: FC = (props) => { ); }; -const selector = formValueSelector(FORM.ADD_EDIT_PROJECT_SUMMARY); -const mapStateToProps = (state) => ({ - anyTouched: state.form[FORM.ADD_EDIT_PROJECT_SUMMARY]?.anyTouched || false, - fieldsTouched: state.form[FORM.ADD_EDIT_PROJECT_SUMMARY]?.fields || {}, - mines: getMines(state), - projectSummary: getProjectSummary(state), - formattedProjectSummary: getFormattedProjectSummary(state), - project: getProject(state), - projectSummaryDocumentTypesHash: getProjectSummaryDocumentTypesHash(state), - projectSummaryAuthorizationTypesArray: getProjectSummaryAuthorizationTypesArray(state), - formErrors: getFormSyncErrors(FORM.ADD_EDIT_PROJECT_SUMMARY)(state), - formValues: getFormValues(FORM.ADD_EDIT_PROJECT_SUMMARY)(state), - contacts: selector(state, "contacts"), -}); - -const mapDispatchToProps = (dispatch) => - bindActionCreators( - { - createProjectSummary, - updateProjectSummary, - fetchMineRecordById, - clearProjectSummary, - fetchProjectById, - updateProject, - submit, - reset, - touch, - change, - }, - dispatch - ); - -export default connect(mapStateToProps, mapDispatchToProps)(ProjectSummaryPage); +export default ProjectSummaryPage; diff --git a/services/minespace-web/src/tests/components/project/projectSummaryPage/ProjectSummaryPage.spec.tsx b/services/minespace-web/src/tests/components/project/projectSummaryPage/ProjectSummaryPage.spec.tsx index bc5d81e480..9101ee9ecc 100644 --- a/services/minespace-web/src/tests/components/project/projectSummaryPage/ProjectSummaryPage.spec.tsx +++ b/services/minespace-web/src/tests/components/project/projectSummaryPage/ProjectSummaryPage.spec.tsx @@ -1,26 +1,15 @@ import React from "react"; -import { shallow } from "enzyme"; import { ProjectSummaryPage } from "@/components/pages/Project/ProjectSummaryPage"; -import * as MOCK from "@/tests/mocks/dataMocks"; - -const props: any = {}; -const dispatchProps: any = {}; - -const setupProps = () => { - props.projectSummaryDocumentTypesHash = MOCK.PROJECT_SUMMARY_DOCUMENT_TYPES_HASH; - props.mines = {}; - props.fieldsTouched = {}; - props.formattedProjectSummary = { - mine_guid: "123", - }; -}; - -const setupDispatchProps = () => { - dispatchProps.fetchProjectSummaryById = jest.fn(() => Promise.resolve()); - dispatchProps.createProjectSummary = jest.fn(() => Promise.resolve()); - dispatchProps.updateProjectSummary = jest.fn(() => Promise.resolve()); - dispatchProps.fetchMineRecordById = jest.fn(() => Promise.resolve()); -}; +import { render } from "@testing-library/react"; +import { + BULK_STATIC_CONTENT_RESPONSE, + PROJECT, + PROJECT_SUMMARY, + REGIONS, +} from "@mds/common/tests/mocks/dataMocks"; +import { PROJECTS, STATIC_CONTENT } from "@mds/common/constants/reducerTypes"; +import { ReduxWrapper } from "@/tests/utils/ReduxWrapper"; +import { BrowserRouter } from "react-router-dom"; function mockFunction() { const original = jest.requireActual("react-router-dom"); @@ -30,6 +19,7 @@ function mockFunction() { projectGuid: "74120872-74f2-4e27-82e6-878ddb472e5a", projectSummaryGuid: "70414192-ca71-4d03-93a5-630491e9c554", tab: "basic-information", + mineGuid: "12345678-74f2-4e27-82e6-878ddb472e5a", }), useLocation: jest.fn().mockReturnValue({ pathname: @@ -40,14 +30,47 @@ function mockFunction() { jest.mock("react-router-dom", () => mockFunction()); -beforeEach(() => { - setupProps(); - setupDispatchProps(); -}); +const initialState = { + regions: { + regions: REGIONS, + }, + [PROJECTS]: { + project: PROJECT, + projectSummary: PROJECT_SUMMARY, + }, + [STATIC_CONTENT]: { + projectSummaryAuthorizationTypes: BULK_STATIC_CONTENT_RESPONSE.projectSummaryAuthorizationTypes, + projectSummaryDocumentTypes: BULK_STATIC_CONTENT_RESPONSE.projectSummaryDocumentTypes, + }, +}; + +/** + * There seems to be a provider issue when rendering the ProjectSummaryForm component + * the FormWrapper which is used as a child component requires the ReduxWrapper from the common directory + * while the ProjectSummaryPage component requires the ReduxWrapper from the minespace directory. + * + * So for now, the ProjectSummaryForm component is being mocked to avoid the provider issue. + * + * There is work planned to move the Project Summary related components to the common directory, + * so this test will need to be updated once that work is completed. + */ +jest.mock("@/components/Forms/projects/projectSummary/ProjectSummaryForm", () => ({ + __esModule: true, + default: jest.fn(() => Mock Project Summary Form), + getProjectFormTabs: jest.fn().mockReturnValue(["mockTab1", "mockTab2"]), +})); describe("ProjectSummaryPage", () => { - it("renders properly", () => { - const component = shallow(); - expect(component).toMatchSnapshot(); + it("renders properly", async () => { + const { container, findByText } = render( + + + + + + ); + + await findByText(/Edit project description - Sample title/i); + expect(container).toMatchSnapshot(); }); }); diff --git a/services/minespace-web/src/tests/components/project/projectSummaryPage/__snapshots__/ProjectSummaryPage.spec.tsx.snap b/services/minespace-web/src/tests/components/project/projectSummaryPage/__snapshots__/ProjectSummaryPage.spec.tsx.snap index df93bef6e2..0f3b16447e 100644 --- a/services/minespace-web/src/tests/components/project/projectSummaryPage/__snapshots__/ProjectSummaryPage.spec.tsx.snap +++ b/services/minespace-web/src/tests/components/project/projectSummaryPage/__snapshots__/ProjectSummaryPage.spec.tsx.snap @@ -1,3 +1,61 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ProjectSummaryPage renders properly 1`] = ``; +exports[`ProjectSummaryPage renders properly 1`] = ` + + + + + Edit project description - Sample title + + + + + + + + + + + + Back to: + Test Project Title + Project Overview page + + + + + + Mock Project Summary Form + + +`;