diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 8f3e08dd39..1b7842e9b3 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,5 +1,5 @@ template: | - $PREVIOUS_TAG + previous tag: $PREVIOUS_TAG ## What’s changed diff --git a/moped-database/docker-compose.yml b/moped-database/docker-compose.yml index 018e1058cf..e40fc66922 100644 --- a/moped-database/docker-compose.yml +++ b/moped-database/docker-compose.yml @@ -1,7 +1,7 @@ # Architecture Independent Docker Stack Components for Moped Development Environment services: hasura: - image: hasura/graphql-engine:v2.45.1 + image: hasura/graphql-engine:v2.45.2 depends_on: - moped-pgsql expose: diff --git a/moped-database/metadata/tables.yaml b/moped-database/metadata/tables.yaml index 10d91bc950..df4ba97469 100644 --- a/moped-database/metadata/tables.yaml +++ b/moped-database/metadata/tables.yaml @@ -76,6 +76,7 @@ - geometry_type - interim_project_component_id - interim_project_id + - is_mapped - is_within_city_limits - knack_data_tracker_project_record_id - length_feet_total @@ -156,6 +157,7 @@ - geometry_type - interim_project_component_id - interim_project_id + - is_mapped - is_within_city_limits - knack_data_tracker_project_record_id - length_feet_total @@ -236,6 +238,7 @@ - geometry_type - interim_project_component_id - interim_project_id + - is_mapped - is_within_city_limits - knack_data_tracker_project_record_id - length_feet_total @@ -2734,6 +2737,9 @@ name: moped_proj_funding schema: public object_relationships: + - name: moped_fund_program + using: + foreign_key_constraint_on: funding_program_id - name: moped_fund_source using: foreign_key_constraint_on: funding_source_id diff --git a/moped-database/migrations/1739832264645_fix_agol_view_bug/down.sql b/moped-database/migrations/1739832264645_fix_agol_view_bug/down.sql new file mode 100644 index 0000000000..02ee918610 --- /dev/null +++ b/moped-database/migrations/1739832264645_fix_agol_view_bug/down.sql @@ -0,0 +1,258 @@ +DROP VIEW IF EXISTS exploded_component_arcgis_online_view; +DROP VIEW IF EXISTS component_arcgis_online_view; + +CREATE OR REPLACE VIEW component_arcgis_online_view AS WITH work_types AS ( + SELECT + mpcwt.project_component_id, + string_agg(mwt.name, ', '::text) AS work_types + FROM moped_proj_component_work_types mpcwt + LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + WHERE mpcwt.is_deleted = false + GROUP BY mpcwt.project_component_id +), + +council_districts AS ( + SELECT + features.component_id AS project_component_id, + string_agg(DISTINCT features_council_districts.council_district_id::text, ', '::text) AS council_districts, + string_agg(DISTINCT lpad(features_council_districts.council_district_id::text, 2, '0'::text), ', '::text) AS council_districts_searchable + FROM features_council_districts + LEFT JOIN features ON features_council_districts.feature_id = features.id + WHERE features.is_deleted = false + GROUP BY features.component_id +), + +comp_geography AS ( + SELECT + feature_union.component_id AS project_component_id, + string_agg(DISTINCT feature_union.id::text, ', '::text) AS feature_ids, + st_asgeojson(st_multi(st_union(array_agg(feature_union.geography))))::json AS geometry, + st_asgeojson(st_multi(st_union(array_agg(feature_union.line_geography))))::json AS line_geometry, + string_agg(DISTINCT feature_union.signal_id::text, ', '::text) AS signal_ids, + sum(feature_union.length_feet) AS length_feet_total + FROM ( + SELECT + feature_signals.id, + feature_signals.component_id, + feature_signals.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_signals.geography, 7::double precision)::geometry) AS line_geography, + feature_signals.signal_id, + null::integer AS length_feet + FROM feature_signals + WHERE feature_signals.is_deleted = false + UNION ALL + SELECT + feature_street_segments.id, + feature_street_segments.component_id, + feature_street_segments.geography::geometry AS geography, + feature_street_segments.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_street_segments.length_feet + FROM feature_street_segments + WHERE feature_street_segments.is_deleted = false + UNION ALL + SELECT + feature_intersections.id, + feature_intersections.component_id, + feature_intersections.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_intersections.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_intersections + WHERE feature_intersections.is_deleted = false + UNION ALL + SELECT + feature_drawn_points.id, + feature_drawn_points.component_id, + feature_drawn_points.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_drawn_points.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_drawn_points + WHERE feature_drawn_points.is_deleted = false + UNION ALL + SELECT + feature_drawn_lines.id, + feature_drawn_lines.component_id, + feature_drawn_lines.geography::geometry AS geography, + feature_drawn_lines.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_drawn_lines.length_feet + FROM feature_drawn_lines + WHERE feature_drawn_lines.is_deleted = false + UNION ALL + SELECT + feature_school_beacons.id, + feature_school_beacons.component_id, + feature_school_beacons.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_school_beacons.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_school_beacons + WHERE feature_school_beacons.is_deleted = false + ) feature_union + GROUP BY feature_union.component_id +), + +subcomponents AS ( + SELECT + mpcs.project_component_id, + string_agg(ms.subcomponent_name, ', '::text) AS subcomponents + FROM moped_proj_components_subcomponents mpcs + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + WHERE mpcs.is_deleted = false + GROUP BY mpcs.project_component_id +), + +component_tags AS ( + SELECT + mpct.project_component_id, + string_agg((mct.type || ' - '::text) || mct.name, ', '::text) AS component_tags + FROM moped_proj_component_tags mpct + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + WHERE mpct.is_deleted = false + GROUP BY mpct.project_component_id +), + +related_projects AS ( + SELECT + pmp.project_id, + concat_ws(', '::text, pmp.project_id, string_agg(cmp.project_id::text, ', '::text)) AS related_project_ids_with_self, + concat_ws(', '::text, lpad(pmp.project_id::text, 5, '0'::text), string_agg(lpad(cmp.project_id::text, 5, '0'::text), ', '::text)) AS related_project_ids_searchable_with_self + FROM moped_project pmp + LEFT JOIN moped_project cmp ON pmp.project_id = cmp.parent_project_id + WHERE cmp.is_deleted = false + GROUP BY pmp.project_id +), + +latest_public_meeting_date AS ( + SELECT + mpm.project_id, + coalesce(max(mpm.date_actual), max(mpm.date_estimate)) AS latest + FROM moped_proj_milestones mpm + WHERE mpm.milestone_id = 65 AND mpm.is_deleted = false + GROUP BY mpm.project_id +), + +earliest_active_or_construction_phase_date AS ( + SELECT + mpp.project_id, + min(mpp.phase_start) AS earliest + FROM moped_proj_phases mpp + LEFT JOIN moped_phases mp ON mpp.phase_id = mp.phase_id + WHERE (mp.phase_name_simple = any(ARRAY['Active'::text, 'Construction'::text])) AND mpp.is_deleted = false + GROUP BY mpp.project_id +) + +SELECT + mpc.project_id, + mpc.project_component_id, + comp_geography.feature_ids, + mpc.component_id, + comp_geography.geometry, + comp_geography.line_geometry, + comp_geography.signal_ids, + council_districts.council_districts, + council_districts.council_districts_searchable, + NOT coalesce(council_districts.council_districts IS null OR council_districts.council_districts = ''::text, false) AS is_within_city_limits, + comp_geography.length_feet_total, + round(comp_geography.length_feet_total::numeric / 5280::numeric, 2) AS length_miles_total, + mc.component_name, + mc.component_subtype, + mc.component_name_full, + 'placeholder text'::text AS component_categories, + CASE + WHEN mc.line_representation = true THEN 'Line'::text + ELSE 'Point'::text + END AS geometry_type, + subcomponents.subcomponents AS component_subcomponents, + work_types.work_types AS component_work_types, + component_tags.component_tags, + mpc.description AS component_description, + mpc.interim_project_component_id, + coalesce(mpc.completion_date, plv.substantial_completion_date) AS substantial_completion_date, + plv.substantial_completion_date_estimated, + mpc.srts_id, + mpc.location_description AS component_location_description, + plv.project_name, + plv.project_name_secondary, + plv.project_name_full, + plv.project_description, + plv.ecapris_subproject_id, + plv.project_website, + plv.updated_at AS project_updated_at, + mpc.phase_id AS component_phase_id, + mph.phase_name AS component_phase_name, + mph.phase_name_simple AS component_phase_name_simple, + current_phase.phase_id AS project_phase_id, + current_phase.phase_name AS project_phase_name, + current_phase.phase_name_simple AS project_phase_name_simple, + coalesce(mph.phase_name, current_phase.phase_name) AS current_phase_name, + coalesce(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple, + plv.project_team_members, + plv.project_sponsor, + plv.project_lead, + plv.public_process_status, + plv.interim_project_id, + plv.project_partners, + plv.task_order_names, + plv.funding_source_and_program_names AS funding_sources, + plv.project_status_update, + plv.project_status_update_date_created, + to_char(timezone('US/Central'::text, plv.construction_start_date), 'YYYY-MM-DD'::text) AS construction_start_date, + plv.project_inspector, + plv.project_designer, + plv.project_tags, + plv.workgroup_contractors, + plv.contract_numbers, + plv.parent_project_id, + plv.parent_project_name, + plv.parent_project_url, + plv.parent_project_name AS parent_project_name_full, + rp.related_project_ids_with_self AS related_project_ids, + rp.related_project_ids_searchable_with_self AS related_project_ids_searchable, + plv.knack_project_id AS knack_data_tracker_project_record_id, + plv.project_url, + (plv.project_url || '?tab=map&project_component_id='::text) || mpc.project_component_id::text AS component_url, + get_project_development_status(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple)) AS project_development_status, + project_development_status_date.result AS project_development_status_date, + to_char(project_development_status_date.result, 'YYYY'::text)::integer AS project_development_status_date_calendar_year, + to_char(project_development_status_date.result, 'FMMonth YYYY'::text) AS project_development_status_date_calendar_year_month, + to_char(project_development_status_date.result, 'YYYY-MM'::text) AS project_development_status_date_calendar_year_month_numeric, + date_part('quarter'::text, project_development_status_date.result)::text AS project_development_status_date_calendar_year_quarter, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN (to_char(project_development_status_date.result, 'YYYY'::text)::integer + 1)::text + ELSE to_char(project_development_status_date.result, 'YYYY'::text) + END AS project_development_status_date_fiscal_year, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN 1::double precision + ELSE date_part('quarter'::text, project_development_status_date.result) + 1::double precision + END::text AS project_development_status_date_fiscal_year_quarter, + plv.added_by AS project_added_by +FROM moped_proj_components mpc +LEFT JOIN comp_geography ON mpc.project_component_id = comp_geography.project_component_id +LEFT JOIN council_districts ON mpc.project_component_id = council_districts.project_component_id +LEFT JOIN subcomponents ON mpc.project_component_id = subcomponents.project_component_id +LEFT JOIN work_types ON mpc.project_component_id = work_types.project_component_id +LEFT JOIN component_tags ON mpc.project_component_id = component_tags.project_component_id +LEFT JOIN project_list_view plv ON mpc.project_id = plv.project_id +LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id +LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id +LEFT JOIN moped_components mc ON mpc.component_id = mc.component_id +LEFT JOIN related_projects rp ON mpc.project_id = rp.project_id +LEFT JOIN latest_public_meeting_date lpmd ON mpc.project_id = lpmd.project_id +LEFT JOIN earliest_active_or_construction_phase_date eaocpd ON mpc.project_id = eaocpd.project_id +LEFT JOIN LATERAL (SELECT timezone('US/Central'::text, get_project_development_status_date(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple))) AS result) project_development_status_date ON true +WHERE mpc.is_deleted = false AND plv.is_deleted = false; + +CREATE OR REPLACE VIEW exploded_component_arcgis_online_view AS SELECT + component_arcgis_online_view.project_id, + component_arcgis_online_view.project_component_id, + st_geometrytype(dump.geom) AS geometry_type, + dump.path[1] AS point_index, + component_arcgis_online_view.geometry AS original_geometry, + st_asgeojson(dump.geom) AS exploded_geometry, + component_arcgis_online_view.project_updated_at +FROM component_arcgis_online_view, + LATERAL st_dump(st_geomfromgeojson(component_arcgis_online_view.geometry)) dump (path, geom) +WHERE st_geometrytype(st_geomfromgeojson(component_arcgis_online_view.geometry)) = 'ST_MultiPoint'::text; diff --git a/moped-database/migrations/1739832264645_fix_agol_view_bug/up.sql b/moped-database/migrations/1739832264645_fix_agol_view_bug/up.sql new file mode 100644 index 0000000000..57b055053c --- /dev/null +++ b/moped-database/migrations/1739832264645_fix_agol_view_bug/up.sql @@ -0,0 +1,265 @@ +DROP VIEW IF EXISTS exploded_component_arcgis_online_view; +DROP VIEW IF EXISTS component_arcgis_online_view; + +CREATE OR REPLACE VIEW component_arcgis_online_view AS WITH work_types AS ( + SELECT + mpcwt.project_component_id, + string_agg(mwt.name, ', '::text) AS work_types + FROM moped_proj_component_work_types mpcwt + LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + WHERE mpcwt.is_deleted = false + GROUP BY mpcwt.project_component_id +), + +council_districts AS ( + SELECT + features.component_id AS project_component_id, + string_agg(DISTINCT features_council_districts.council_district_id::text, ', '::text) AS council_districts, + string_agg(DISTINCT lpad(features_council_districts.council_district_id::text, 2, '0'::text), ', '::text) AS council_districts_searchable + FROM features_council_districts + LEFT JOIN features ON features_council_districts.feature_id = features.id + WHERE features.is_deleted = false + GROUP BY features.component_id +), + +comp_geography AS ( + SELECT + feature_union.component_id AS project_component_id, + string_agg(DISTINCT feature_union.id::text, ', '::text) AS feature_ids, + st_asgeojson(st_multi(st_union(array_agg(feature_union.geography))))::json AS geometry, + st_asgeojson(st_multi(st_union(array_agg(feature_union.line_geography))))::json AS line_geometry, + string_agg(DISTINCT feature_union.signal_id::text, ', '::text) AS signal_ids, + sum(feature_union.length_feet) AS length_feet_total + FROM ( + SELECT + feature_signals.id, + feature_signals.component_id, + feature_signals.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_signals.geography, 7::double precision)::geometry) AS line_geography, + feature_signals.signal_id, + null::integer AS length_feet + FROM feature_signals + WHERE feature_signals.is_deleted = false + UNION ALL + SELECT + feature_street_segments.id, + feature_street_segments.component_id, + feature_street_segments.geography::geometry AS geography, + feature_street_segments.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_street_segments.length_feet + FROM feature_street_segments + WHERE feature_street_segments.is_deleted = false + UNION ALL + SELECT + feature_intersections.id, + feature_intersections.component_id, + feature_intersections.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_intersections.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_intersections + WHERE feature_intersections.is_deleted = false + UNION ALL + SELECT + feature_drawn_points.id, + feature_drawn_points.component_id, + feature_drawn_points.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_drawn_points.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_drawn_points + WHERE feature_drawn_points.is_deleted = false + UNION ALL + SELECT + feature_drawn_lines.id, + feature_drawn_lines.component_id, + feature_drawn_lines.geography::geometry AS geography, + feature_drawn_lines.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_drawn_lines.length_feet + FROM feature_drawn_lines + WHERE feature_drawn_lines.is_deleted = false + UNION ALL + SELECT + feature_school_beacons.id, + feature_school_beacons.component_id, + feature_school_beacons.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_school_beacons.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_school_beacons + WHERE feature_school_beacons.is_deleted = false + ) feature_union + GROUP BY feature_union.component_id +), + +subcomponents AS ( + SELECT + mpcs.project_component_id, + string_agg(ms.subcomponent_name, ', '::text) AS subcomponents + FROM moped_proj_components_subcomponents mpcs + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + WHERE mpcs.is_deleted = false + GROUP BY mpcs.project_component_id +), + +component_tags AS ( + SELECT + mpct.project_component_id, + string_agg((mct.type || ' - '::text) || mct.name, ', '::text) AS component_tags + FROM moped_proj_component_tags mpct + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + WHERE mpct.is_deleted = false + GROUP BY mpct.project_component_id +), + +related_projects AS ( + SELECT + pmp.project_id, + concat_ws(', '::text, pmp.project_id, string_agg(cmp.project_id::text, ', '::text)) AS related_project_ids_with_self, + concat_ws(', '::text, lpad(pmp.project_id::text, 5, '0'::text), string_agg(lpad(cmp.project_id::text, 5, '0'::text), ', '::text)) AS related_project_ids_searchable_with_self + FROM moped_project pmp + LEFT JOIN moped_project cmp ON pmp.project_id = cmp.parent_project_id + WHERE cmp.is_deleted = false + GROUP BY pmp.project_id +), + +latest_public_meeting_date AS ( + SELECT + mpm.project_id, + coalesce(max(mpm.date_actual), max(mpm.date_estimate)) AS latest + FROM moped_proj_milestones mpm + WHERE mpm.milestone_id = 65 AND mpm.is_deleted = false + GROUP BY mpm.project_id +), + +earliest_active_or_construction_phase_date AS ( + SELECT + mpp.project_id, + min(mpp.phase_start) AS earliest + FROM moped_proj_phases mpp + LEFT JOIN moped_phases mp ON mpp.phase_id = mp.phase_id + WHERE (mp.phase_name_simple = any(ARRAY['Active'::text, 'Construction'::text])) AND mpp.is_deleted = false + GROUP BY mpp.project_id +) + +SELECT + mpc.project_id, + mpc.project_component_id, + comp_geography.feature_ids, + mpc.component_id, + comp_geography.geometry, + comp_geography.line_geometry, + comp_geography.signal_ids, + council_districts.council_districts, + council_districts.council_districts_searchable, + NOT coalesce(council_districts.council_districts IS null OR council_districts.council_districts = ''::text, false) AS is_within_city_limits, + comp_geography.length_feet_total, + round(comp_geography.length_feet_total::numeric / 5280::numeric, 2) AS length_miles_total, + mc.component_name, + mc.component_subtype, + mc.component_name_full, + 'placeholder text'::text AS component_categories, + CASE + WHEN mc.line_representation = true THEN 'Line'::text + ELSE 'Point'::text + END AS geometry_type, + subcomponents.subcomponents AS component_subcomponents, + work_types.work_types AS component_work_types, + component_tags.component_tags, + mpc.description AS component_description, + mpc.interim_project_component_id, + -- When there is a phase ID and no completion date, use null + -- When these is a phase ID and completion date, use completion date + -- When there is no phase ID, use project substantial completion date + CASE + WHEN mpc.phase_id IS NULL THEN plv.substantial_completion_date + WHEN mpc.phase_id IS NOT NULL AND mpc.completion_date IS NULL THEN NULL + ELSE mpc.completion_date + END AS substantial_completion_date, + plv.substantial_completion_date_estimated, + mpc.srts_id, + mpc.location_description AS component_location_description, + plv.project_name, + plv.project_name_secondary, + plv.project_name_full, + plv.project_description, + plv.ecapris_subproject_id, + plv.project_website, + plv.updated_at AS project_updated_at, + mpc.phase_id AS component_phase_id, + mph.phase_name AS component_phase_name, + mph.phase_name_simple AS component_phase_name_simple, + current_phase.phase_id AS project_phase_id, + current_phase.phase_name AS project_phase_name, + current_phase.phase_name_simple AS project_phase_name_simple, + coalesce(mph.phase_name, current_phase.phase_name) AS current_phase_name, + coalesce(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple, + plv.project_team_members, + plv.project_sponsor, + plv.project_lead, + plv.public_process_status, + plv.interim_project_id, + plv.project_partners, + plv.task_order_names, + plv.funding_source_and_program_names AS funding_sources, + plv.project_status_update, + plv.project_status_update_date_created, + to_char(timezone('US/Central'::text, plv.construction_start_date), 'YYYY-MM-DD'::text) AS construction_start_date, + plv.project_inspector, + plv.project_designer, + plv.project_tags, + plv.workgroup_contractors, + plv.contract_numbers, + plv.parent_project_id, + plv.parent_project_name, + plv.parent_project_url, + plv.parent_project_name AS parent_project_name_full, + rp.related_project_ids_with_self AS related_project_ids, + rp.related_project_ids_searchable_with_self AS related_project_ids_searchable, + plv.knack_project_id AS knack_data_tracker_project_record_id, + plv.project_url, + (plv.project_url || '?tab=map&project_component_id='::text) || mpc.project_component_id::text AS component_url, + get_project_development_status(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple)) AS project_development_status, + project_development_status_date.result AS project_development_status_date, + to_char(project_development_status_date.result, 'YYYY'::text)::integer AS project_development_status_date_calendar_year, + to_char(project_development_status_date.result, 'FMMonth YYYY'::text) AS project_development_status_date_calendar_year_month, + to_char(project_development_status_date.result, 'YYYY-MM'::text) AS project_development_status_date_calendar_year_month_numeric, + date_part('quarter'::text, project_development_status_date.result)::text AS project_development_status_date_calendar_year_quarter, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN (to_char(project_development_status_date.result, 'YYYY'::text)::integer + 1)::text + ELSE to_char(project_development_status_date.result, 'YYYY'::text) + END AS project_development_status_date_fiscal_year, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN 1::double precision + ELSE date_part('quarter'::text, project_development_status_date.result) + 1::double precision + END::text AS project_development_status_date_fiscal_year_quarter, + plv.added_by AS project_added_by +FROM moped_proj_components mpc +LEFT JOIN comp_geography ON mpc.project_component_id = comp_geography.project_component_id +LEFT JOIN council_districts ON mpc.project_component_id = council_districts.project_component_id +LEFT JOIN subcomponents ON mpc.project_component_id = subcomponents.project_component_id +LEFT JOIN work_types ON mpc.project_component_id = work_types.project_component_id +LEFT JOIN component_tags ON mpc.project_component_id = component_tags.project_component_id +LEFT JOIN project_list_view plv ON mpc.project_id = plv.project_id +LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id +LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id +LEFT JOIN moped_components mc ON mpc.component_id = mc.component_id +LEFT JOIN related_projects rp ON mpc.project_id = rp.project_id +LEFT JOIN latest_public_meeting_date lpmd ON mpc.project_id = lpmd.project_id +LEFT JOIN earliest_active_or_construction_phase_date eaocpd ON mpc.project_id = eaocpd.project_id +LEFT JOIN LATERAL (SELECT timezone('US/Central'::text, get_project_development_status_date(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple))) AS result) project_development_status_date ON true +WHERE mpc.is_deleted = false AND plv.is_deleted = false; + +CREATE OR REPLACE VIEW exploded_component_arcgis_online_view AS SELECT + component_arcgis_online_view.project_id, + component_arcgis_online_view.project_component_id, + st_geometrytype(dump.geom) AS geometry_type, + dump.path[1] AS point_index, + component_arcgis_online_view.geometry AS original_geometry, + st_asgeojson(dump.geom) AS exploded_geometry, + component_arcgis_online_view.project_updated_at +FROM component_arcgis_online_view, + LATERAL st_dump(st_geomfromgeojson(component_arcgis_online_view.geometry)) dump (path, geom) +WHERE st_geometrytype(st_geomfromgeojson(component_arcgis_online_view.geometry)) = 'ST_MultiPoint'::text; diff --git a/moped-database/migrations/1739832264646_agol_view_is_mapped/down.sql b/moped-database/migrations/1739832264646_agol_view_is_mapped/down.sql new file mode 100644 index 0000000000..94814c891d --- /dev/null +++ b/moped-database/migrations/1739832264646_agol_view_is_mapped/down.sql @@ -0,0 +1,262 @@ +DROP VIEW IF EXISTS exploded_component_arcgis_online_view; +DROP VIEW IF EXISTS component_arcgis_online_view; + +CREATE OR REPLACE VIEW component_arcgis_online_view AS WITH work_types AS ( + SELECT + mpcwt.project_component_id, + string_agg(mwt.name, ', '::text) AS work_types + FROM moped_proj_component_work_types mpcwt + LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + WHERE mpcwt.is_deleted = false + GROUP BY mpcwt.project_component_id +), + +council_districts AS ( + SELECT + features.component_id AS project_component_id, + string_agg(DISTINCT features_council_districts.council_district_id::text, ', '::text) AS council_districts, + string_agg(DISTINCT lpad(features_council_districts.council_district_id::text, 2, '0'::text), ', '::text) AS council_districts_searchable + FROM features_council_districts + LEFT JOIN features ON features_council_districts.feature_id = features.id + WHERE features.is_deleted = false + GROUP BY features.component_id +), + +comp_geography AS ( + SELECT + feature_union.component_id AS project_component_id, + string_agg(DISTINCT feature_union.id::text, ', '::text) AS feature_ids, + st_asgeojson(st_multi(st_union(array_agg(feature_union.geography))))::json AS geometry, + st_asgeojson(st_multi(st_union(array_agg(feature_union.line_geography))))::json AS line_geometry, + string_agg(DISTINCT feature_union.signal_id::text, ', '::text) AS signal_ids, + sum(feature_union.length_feet) AS length_feet_total + FROM ( + SELECT + feature_signals.id, + feature_signals.component_id, + feature_signals.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_signals.geography, 7::double precision)::geometry) AS line_geography, + feature_signals.signal_id, + null::integer AS length_feet + FROM feature_signals + WHERE feature_signals.is_deleted = false + UNION ALL + SELECT + feature_street_segments.id, + feature_street_segments.component_id, + feature_street_segments.geography::geometry AS geography, + feature_street_segments.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_street_segments.length_feet + FROM feature_street_segments + WHERE feature_street_segments.is_deleted = false + UNION ALL + SELECT + feature_intersections.id, + feature_intersections.component_id, + feature_intersections.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_intersections.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_intersections + WHERE feature_intersections.is_deleted = false + UNION ALL + SELECT + feature_drawn_points.id, + feature_drawn_points.component_id, + feature_drawn_points.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_drawn_points.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_drawn_points + WHERE feature_drawn_points.is_deleted = false + UNION ALL + SELECT + feature_drawn_lines.id, + feature_drawn_lines.component_id, + feature_drawn_lines.geography::geometry AS geography, + feature_drawn_lines.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_drawn_lines.length_feet + FROM feature_drawn_lines + WHERE feature_drawn_lines.is_deleted = false + UNION ALL + SELECT + feature_school_beacons.id, + feature_school_beacons.component_id, + feature_school_beacons.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_school_beacons.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_school_beacons + WHERE feature_school_beacons.is_deleted = false + ) feature_union + GROUP BY feature_union.component_id +), + +subcomponents AS ( + SELECT + mpcs.project_component_id, + string_agg(ms.subcomponent_name, ', '::text) AS subcomponents + FROM moped_proj_components_subcomponents mpcs + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + WHERE mpcs.is_deleted = false + GROUP BY mpcs.project_component_id +), + +component_tags AS ( + SELECT + mpct.project_component_id, + string_agg((mct.type || ' - '::text) || mct.name, ', '::text) AS component_tags + FROM moped_proj_component_tags mpct + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + WHERE mpct.is_deleted = false + GROUP BY mpct.project_component_id +), + +related_projects AS ( + SELECT + pmp.project_id, + concat_ws(', '::text, pmp.project_id, string_agg(cmp.project_id::text, ', '::text)) AS related_project_ids_with_self, + concat_ws(', '::text, lpad(pmp.project_id::text, 5, '0'::text), string_agg(lpad(cmp.project_id::text, 5, '0'::text), ', '::text)) AS related_project_ids_searchable_with_self + FROM moped_project pmp + LEFT JOIN moped_project cmp ON pmp.project_id = cmp.parent_project_id + WHERE cmp.is_deleted = false + GROUP BY pmp.project_id +), + +latest_public_meeting_date AS ( + SELECT + mpm.project_id, + coalesce(max(mpm.date_actual), max(mpm.date_estimate)) AS latest + FROM moped_proj_milestones mpm + WHERE mpm.milestone_id = 65 AND mpm.is_deleted = false + GROUP BY mpm.project_id +), + +earliest_active_or_construction_phase_date AS ( + SELECT + mpp.project_id, + min(mpp.phase_start) AS earliest + FROM moped_proj_phases mpp + LEFT JOIN moped_phases mp ON mpp.phase_id = mp.phase_id + WHERE (mp.phase_name_simple = any(ARRAY['Active'::text, 'Construction'::text])) AND mpp.is_deleted = false + GROUP BY mpp.project_id +) + +SELECT + mpc.project_id, + mpc.project_component_id, + comp_geography.feature_ids, + mpc.component_id, + comp_geography.geometry, + comp_geography.line_geometry, + comp_geography.signal_ids, + council_districts.council_districts, + council_districts.council_districts_searchable, + NOT coalesce(council_districts.council_districts IS null OR council_districts.council_districts = ''::text, false) AS is_within_city_limits, + comp_geography.length_feet_total, + round(comp_geography.length_feet_total::numeric / 5280::numeric, 2) AS length_miles_total, + mc.component_name, + mc.component_subtype, + mc.component_name_full, + 'placeholder text'::text AS component_categories, + CASE + WHEN mc.line_representation = true THEN 'Line'::text + ELSE 'Point'::text + END AS geometry_type, + subcomponents.subcomponents AS component_subcomponents, + work_types.work_types AS component_work_types, + component_tags.component_tags, + mpc.description AS component_description, + mpc.interim_project_component_id, + CASE + WHEN mpc.phase_id IS NULL THEN plv.substantial_completion_date + WHEN mpc.phase_id IS NOT NULL AND mpc.completion_date IS NULL THEN NULL + ELSE mpc.completion_date + END AS substantial_completion_date, + plv.substantial_completion_date_estimated, + mpc.srts_id, + mpc.location_description AS component_location_description, + plv.project_name, + plv.project_name_secondary, + plv.project_name_full, + plv.project_description, + plv.ecapris_subproject_id, + plv.project_website, + plv.updated_at AS project_updated_at, + mpc.phase_id AS component_phase_id, + mph.phase_name AS component_phase_name, + mph.phase_name_simple AS component_phase_name_simple, + current_phase.phase_id AS project_phase_id, + current_phase.phase_name AS project_phase_name, + current_phase.phase_name_simple AS project_phase_name_simple, + coalesce(mph.phase_name, current_phase.phase_name) AS current_phase_name, + coalesce(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple, + plv.project_team_members, + plv.project_sponsor, + plv.project_lead, + plv.public_process_status, + plv.interim_project_id, + plv.project_partners, + plv.task_order_names, + plv.funding_source_and_program_names AS funding_sources, + plv.project_status_update, + plv.project_status_update_date_created, + to_char(timezone('US/Central'::text, plv.construction_start_date), 'YYYY-MM-DD'::text) AS construction_start_date, + plv.project_inspector, + plv.project_designer, + plv.project_tags, + plv.workgroup_contractors, + plv.contract_numbers, + plv.parent_project_id, + plv.parent_project_name, + plv.parent_project_url, + plv.parent_project_name AS parent_project_name_full, + rp.related_project_ids_with_self AS related_project_ids, + rp.related_project_ids_searchable_with_self AS related_project_ids_searchable, + plv.knack_project_id AS knack_data_tracker_project_record_id, + plv.project_url, + (plv.project_url || '?tab=map&project_component_id='::text) || mpc.project_component_id::text AS component_url, + get_project_development_status(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple)) AS project_development_status, + project_development_status_date.result AS project_development_status_date, + to_char(project_development_status_date.result, 'YYYY'::text)::integer AS project_development_status_date_calendar_year, + to_char(project_development_status_date.result, 'FMMonth YYYY'::text) AS project_development_status_date_calendar_year_month, + to_char(project_development_status_date.result, 'YYYY-MM'::text) AS project_development_status_date_calendar_year_month_numeric, + date_part('quarter'::text, project_development_status_date.result)::text AS project_development_status_date_calendar_year_quarter, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN (to_char(project_development_status_date.result, 'YYYY'::text)::integer + 1)::text + ELSE to_char(project_development_status_date.result, 'YYYY'::text) + END AS project_development_status_date_fiscal_year, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN 1::double precision + ELSE date_part('quarter'::text, project_development_status_date.result) + 1::double precision + END::text AS project_development_status_date_fiscal_year_quarter, + plv.added_by AS project_added_by +FROM moped_proj_components mpc +LEFT JOIN comp_geography ON mpc.project_component_id = comp_geography.project_component_id +LEFT JOIN council_districts ON mpc.project_component_id = council_districts.project_component_id +LEFT JOIN subcomponents ON mpc.project_component_id = subcomponents.project_component_id +LEFT JOIN work_types ON mpc.project_component_id = work_types.project_component_id +LEFT JOIN component_tags ON mpc.project_component_id = component_tags.project_component_id +LEFT JOIN project_list_view plv ON mpc.project_id = plv.project_id +LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id +LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id +LEFT JOIN moped_components mc ON mpc.component_id = mc.component_id +LEFT JOIN related_projects rp ON mpc.project_id = rp.project_id +LEFT JOIN latest_public_meeting_date lpmd ON mpc.project_id = lpmd.project_id +LEFT JOIN earliest_active_or_construction_phase_date eaocpd ON mpc.project_id = eaocpd.project_id +LEFT JOIN LATERAL (SELECT timezone('US/Central'::text, get_project_development_status_date(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple))) AS result) project_development_status_date ON true +WHERE mpc.is_deleted = false AND plv.is_deleted = false; + +CREATE OR REPLACE VIEW exploded_component_arcgis_online_view AS SELECT + component_arcgis_online_view.project_id, + component_arcgis_online_view.project_component_id, + st_geometrytype(dump.geom) AS geometry_type, + dump.path[1] AS point_index, + component_arcgis_online_view.geometry AS original_geometry, + st_asgeojson(dump.geom) AS exploded_geometry, + component_arcgis_online_view.project_updated_at +FROM component_arcgis_online_view, + LATERAL st_dump(st_geomfromgeojson(component_arcgis_online_view.geometry)) dump (path, geom) +WHERE st_geometrytype(st_geomfromgeojson(component_arcgis_online_view.geometry)) = 'ST_MultiPoint'::text; diff --git a/moped-database/migrations/1739832264646_agol_view_is_mapped/up.sql b/moped-database/migrations/1739832264646_agol_view_is_mapped/up.sql new file mode 100644 index 0000000000..9eff509b5e --- /dev/null +++ b/moped-database/migrations/1739832264646_agol_view_is_mapped/up.sql @@ -0,0 +1,266 @@ +DROP VIEW IF EXISTS exploded_component_arcgis_online_view; +DROP VIEW IF EXISTS component_arcgis_online_view; + +CREATE OR REPLACE VIEW component_arcgis_online_view AS WITH work_types AS ( + SELECT + mpcwt.project_component_id, + string_agg(mwt.name, ', '::text) AS work_types + FROM moped_proj_component_work_types mpcwt + LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + WHERE mpcwt.is_deleted = false + GROUP BY mpcwt.project_component_id +), + +council_districts AS ( + SELECT + features.component_id AS project_component_id, + string_agg(DISTINCT features_council_districts.council_district_id::text, ', '::text) AS council_districts, + string_agg(DISTINCT lpad(features_council_districts.council_district_id::text, 2, '0'::text), ', '::text) AS council_districts_searchable + FROM features_council_districts + LEFT JOIN features ON features_council_districts.feature_id = features.id + WHERE features.is_deleted = false + GROUP BY features.component_id +), + +comp_geography AS ( + SELECT + feature_union.component_id AS project_component_id, + string_agg(DISTINCT feature_union.id::text, ', '::text) AS feature_ids, + st_asgeojson(st_multi(st_union(array_agg(feature_union.geography))))::json AS geometry, + st_asgeojson(st_multi(st_union(array_agg(feature_union.line_geography))))::json AS line_geometry, + string_agg(DISTINCT feature_union.signal_id::text, ', '::text) AS signal_ids, + sum(feature_union.length_feet) AS length_feet_total + FROM ( + SELECT + feature_signals.id, + feature_signals.component_id, + feature_signals.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_signals.geography, 7::double precision)::geometry) AS line_geography, + feature_signals.signal_id, + null::integer AS length_feet + FROM feature_signals + WHERE feature_signals.is_deleted = false + UNION ALL + SELECT + feature_street_segments.id, + feature_street_segments.component_id, + feature_street_segments.geography::geometry AS geography, + feature_street_segments.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_street_segments.length_feet + FROM feature_street_segments + WHERE feature_street_segments.is_deleted = false + UNION ALL + SELECT + feature_intersections.id, + feature_intersections.component_id, + feature_intersections.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_intersections.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_intersections + WHERE feature_intersections.is_deleted = false + UNION ALL + SELECT + feature_drawn_points.id, + feature_drawn_points.component_id, + feature_drawn_points.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_drawn_points.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_drawn_points + WHERE feature_drawn_points.is_deleted = false + UNION ALL + SELECT + feature_drawn_lines.id, + feature_drawn_lines.component_id, + feature_drawn_lines.geography::geometry AS geography, + feature_drawn_lines.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_drawn_lines.length_feet + FROM feature_drawn_lines + WHERE feature_drawn_lines.is_deleted = false + UNION ALL + SELECT + feature_school_beacons.id, + feature_school_beacons.component_id, + feature_school_beacons.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_school_beacons.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_school_beacons + WHERE feature_school_beacons.is_deleted = false + ) feature_union + GROUP BY feature_union.component_id +), + +subcomponents AS ( + SELECT + mpcs.project_component_id, + string_agg(ms.subcomponent_name, ', '::text) AS subcomponents + FROM moped_proj_components_subcomponents mpcs + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + WHERE mpcs.is_deleted = false + GROUP BY mpcs.project_component_id +), + +component_tags AS ( + SELECT + mpct.project_component_id, + string_agg((mct.type || ' - '::text) || mct.name, ', '::text) AS component_tags + FROM moped_proj_component_tags mpct + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + WHERE mpct.is_deleted = false + GROUP BY mpct.project_component_id +), + +related_projects AS ( + SELECT + pmp.project_id, + concat_ws(', '::text, pmp.project_id, string_agg(cmp.project_id::text, ', '::text)) AS related_project_ids_with_self, + concat_ws(', '::text, lpad(pmp.project_id::text, 5, '0'::text), string_agg(lpad(cmp.project_id::text, 5, '0'::text), ', '::text)) AS related_project_ids_searchable_with_self + FROM moped_project pmp + LEFT JOIN moped_project cmp ON pmp.project_id = cmp.parent_project_id + WHERE cmp.is_deleted = false + GROUP BY pmp.project_id +), + +latest_public_meeting_date AS ( + SELECT + mpm.project_id, + coalesce(max(mpm.date_actual), max(mpm.date_estimate)) AS latest + FROM moped_proj_milestones mpm + WHERE mpm.milestone_id = 65 AND mpm.is_deleted = false + GROUP BY mpm.project_id +), + +earliest_active_or_construction_phase_date AS ( + SELECT + mpp.project_id, + min(mpp.phase_start) AS earliest + FROM moped_proj_phases mpp + LEFT JOIN moped_phases mp ON mpp.phase_id = mp.phase_id + WHERE (mp.phase_name_simple = any(ARRAY['Active'::text, 'Construction'::text])) AND mpp.is_deleted = false + GROUP BY mpp.project_id +) + +SELECT + mpc.project_id, + mpc.project_component_id, + comp_geography.feature_ids, + mpc.component_id, + comp_geography.geometry, + comp_geography.line_geometry, + comp_geography.signal_ids, + council_districts.council_districts, + council_districts.council_districts_searchable, + NOT coalesce(council_districts.council_districts IS null OR council_districts.council_districts = ''::text, false) AS is_within_city_limits, + comp_geography.length_feet_total, + round(comp_geography.length_feet_total::numeric / 5280::numeric, 2) AS length_miles_total, + mc.component_name, + mc.component_subtype, + mc.component_name_full, + 'placeholder text'::text AS component_categories, + CASE + WHEN mc.line_representation = true THEN 'Line'::text + ELSE 'Point'::text + END AS geometry_type, + CASE + WHEN comp_geography.geometry IS NULL THEN 'No'::text + ELSE 'Yes'::text + END AS is_mapped, + subcomponents.subcomponents AS component_subcomponents, + work_types.work_types AS component_work_types, + component_tags.component_tags, + mpc.description AS component_description, + mpc.interim_project_component_id, + CASE + WHEN mpc.phase_id IS null THEN plv.substantial_completion_date + WHEN mpc.phase_id IS NOT null AND mpc.completion_date IS null THEN null::timestamp with time zone + ELSE mpc.completion_date + END AS substantial_completion_date, + plv.substantial_completion_date_estimated, + mpc.srts_id, + mpc.location_description AS component_location_description, + plv.project_name, + plv.project_name_secondary, + plv.project_name_full, + plv.project_description, + plv.ecapris_subproject_id, + plv.project_website, + plv.updated_at AS project_updated_at, + mpc.phase_id AS component_phase_id, + mph.phase_name AS component_phase_name, + mph.phase_name_simple AS component_phase_name_simple, + current_phase.phase_id AS project_phase_id, + current_phase.phase_name AS project_phase_name, + current_phase.phase_name_simple AS project_phase_name_simple, + coalesce(mph.phase_name, current_phase.phase_name) AS current_phase_name, + coalesce(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple, + plv.project_team_members, + plv.project_sponsor, + plv.project_lead, + plv.public_process_status, + plv.interim_project_id, + plv.project_partners, + plv.task_order_names, + plv.funding_source_and_program_names AS funding_sources, + plv.project_status_update, + plv.project_status_update_date_created, + to_char(timezone('US/Central'::text, plv.construction_start_date), 'YYYY-MM-DD'::text) AS construction_start_date, + plv.project_inspector, + plv.project_designer, + plv.project_tags, + plv.workgroup_contractors, + plv.contract_numbers, + plv.parent_project_id, + plv.parent_project_name, + plv.parent_project_url, + plv.parent_project_name AS parent_project_name_full, + rp.related_project_ids_with_self AS related_project_ids, + rp.related_project_ids_searchable_with_self AS related_project_ids_searchable, + plv.knack_project_id AS knack_data_tracker_project_record_id, + plv.project_url, + (plv.project_url || '?tab=map&project_component_id='::text) || mpc.project_component_id::text AS component_url, + get_project_development_status(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple)) AS project_development_status, + project_development_status_date.result AS project_development_status_date, + to_char(project_development_status_date.result, 'YYYY'::text)::integer AS project_development_status_date_calendar_year, + to_char(project_development_status_date.result, 'FMMonth YYYY'::text) AS project_development_status_date_calendar_year_month, + to_char(project_development_status_date.result, 'YYYY-MM'::text) AS project_development_status_date_calendar_year_month_numeric, + date_part('quarter'::text, project_development_status_date.result)::text AS project_development_status_date_calendar_year_quarter, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN (to_char(project_development_status_date.result, 'YYYY'::text)::integer + 1)::text + ELSE to_char(project_development_status_date.result, 'YYYY'::text) + END AS project_development_status_date_fiscal_year, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN 1::double precision + ELSE date_part('quarter'::text, project_development_status_date.result) + 1::double precision + END::text AS project_development_status_date_fiscal_year_quarter, + plv.added_by AS project_added_by +FROM moped_proj_components mpc +LEFT JOIN comp_geography ON mpc.project_component_id = comp_geography.project_component_id +LEFT JOIN council_districts ON mpc.project_component_id = council_districts.project_component_id +LEFT JOIN subcomponents ON mpc.project_component_id = subcomponents.project_component_id +LEFT JOIN work_types ON mpc.project_component_id = work_types.project_component_id +LEFT JOIN component_tags ON mpc.project_component_id = component_tags.project_component_id +LEFT JOIN project_list_view plv ON mpc.project_id = plv.project_id +LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id +LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id +LEFT JOIN moped_components mc ON mpc.component_id = mc.component_id +LEFT JOIN related_projects rp ON mpc.project_id = rp.project_id +LEFT JOIN latest_public_meeting_date lpmd ON mpc.project_id = lpmd.project_id +LEFT JOIN earliest_active_or_construction_phase_date eaocpd ON mpc.project_id = eaocpd.project_id +LEFT JOIN LATERAL (SELECT timezone('US/Central'::text, get_project_development_status_date(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple))) AS result) project_development_status_date ON true +WHERE mpc.is_deleted = false AND plv.is_deleted = false; + +CREATE OR REPLACE VIEW exploded_component_arcgis_online_view AS SELECT + component_arcgis_online_view.project_id, + component_arcgis_online_view.project_component_id, + st_geometrytype(dump.geom) AS geometry_type, + dump.path[1] AS point_index, + component_arcgis_online_view.geometry AS original_geometry, + st_asgeojson(dump.geom) AS exploded_geometry, + component_arcgis_online_view.project_updated_at +FROM component_arcgis_online_view, + LATERAL st_dump(st_geomfromgeojson(component_arcgis_online_view.geometry)) dump (path, geom) +WHERE st_geometrytype(st_geomfromgeojson(component_arcgis_online_view.geometry)) = 'ST_MultiPoint'::text; diff --git a/moped-database/migrations/1739832264647_agol_view_is_mapped_bool/down.sql b/moped-database/migrations/1739832264647_agol_view_is_mapped_bool/down.sql new file mode 100644 index 0000000000..9eff509b5e --- /dev/null +++ b/moped-database/migrations/1739832264647_agol_view_is_mapped_bool/down.sql @@ -0,0 +1,266 @@ +DROP VIEW IF EXISTS exploded_component_arcgis_online_view; +DROP VIEW IF EXISTS component_arcgis_online_view; + +CREATE OR REPLACE VIEW component_arcgis_online_view AS WITH work_types AS ( + SELECT + mpcwt.project_component_id, + string_agg(mwt.name, ', '::text) AS work_types + FROM moped_proj_component_work_types mpcwt + LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + WHERE mpcwt.is_deleted = false + GROUP BY mpcwt.project_component_id +), + +council_districts AS ( + SELECT + features.component_id AS project_component_id, + string_agg(DISTINCT features_council_districts.council_district_id::text, ', '::text) AS council_districts, + string_agg(DISTINCT lpad(features_council_districts.council_district_id::text, 2, '0'::text), ', '::text) AS council_districts_searchable + FROM features_council_districts + LEFT JOIN features ON features_council_districts.feature_id = features.id + WHERE features.is_deleted = false + GROUP BY features.component_id +), + +comp_geography AS ( + SELECT + feature_union.component_id AS project_component_id, + string_agg(DISTINCT feature_union.id::text, ', '::text) AS feature_ids, + st_asgeojson(st_multi(st_union(array_agg(feature_union.geography))))::json AS geometry, + st_asgeojson(st_multi(st_union(array_agg(feature_union.line_geography))))::json AS line_geometry, + string_agg(DISTINCT feature_union.signal_id::text, ', '::text) AS signal_ids, + sum(feature_union.length_feet) AS length_feet_total + FROM ( + SELECT + feature_signals.id, + feature_signals.component_id, + feature_signals.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_signals.geography, 7::double precision)::geometry) AS line_geography, + feature_signals.signal_id, + null::integer AS length_feet + FROM feature_signals + WHERE feature_signals.is_deleted = false + UNION ALL + SELECT + feature_street_segments.id, + feature_street_segments.component_id, + feature_street_segments.geography::geometry AS geography, + feature_street_segments.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_street_segments.length_feet + FROM feature_street_segments + WHERE feature_street_segments.is_deleted = false + UNION ALL + SELECT + feature_intersections.id, + feature_intersections.component_id, + feature_intersections.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_intersections.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_intersections + WHERE feature_intersections.is_deleted = false + UNION ALL + SELECT + feature_drawn_points.id, + feature_drawn_points.component_id, + feature_drawn_points.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_drawn_points.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_drawn_points + WHERE feature_drawn_points.is_deleted = false + UNION ALL + SELECT + feature_drawn_lines.id, + feature_drawn_lines.component_id, + feature_drawn_lines.geography::geometry AS geography, + feature_drawn_lines.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_drawn_lines.length_feet + FROM feature_drawn_lines + WHERE feature_drawn_lines.is_deleted = false + UNION ALL + SELECT + feature_school_beacons.id, + feature_school_beacons.component_id, + feature_school_beacons.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_school_beacons.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_school_beacons + WHERE feature_school_beacons.is_deleted = false + ) feature_union + GROUP BY feature_union.component_id +), + +subcomponents AS ( + SELECT + mpcs.project_component_id, + string_agg(ms.subcomponent_name, ', '::text) AS subcomponents + FROM moped_proj_components_subcomponents mpcs + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + WHERE mpcs.is_deleted = false + GROUP BY mpcs.project_component_id +), + +component_tags AS ( + SELECT + mpct.project_component_id, + string_agg((mct.type || ' - '::text) || mct.name, ', '::text) AS component_tags + FROM moped_proj_component_tags mpct + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + WHERE mpct.is_deleted = false + GROUP BY mpct.project_component_id +), + +related_projects AS ( + SELECT + pmp.project_id, + concat_ws(', '::text, pmp.project_id, string_agg(cmp.project_id::text, ', '::text)) AS related_project_ids_with_self, + concat_ws(', '::text, lpad(pmp.project_id::text, 5, '0'::text), string_agg(lpad(cmp.project_id::text, 5, '0'::text), ', '::text)) AS related_project_ids_searchable_with_self + FROM moped_project pmp + LEFT JOIN moped_project cmp ON pmp.project_id = cmp.parent_project_id + WHERE cmp.is_deleted = false + GROUP BY pmp.project_id +), + +latest_public_meeting_date AS ( + SELECT + mpm.project_id, + coalesce(max(mpm.date_actual), max(mpm.date_estimate)) AS latest + FROM moped_proj_milestones mpm + WHERE mpm.milestone_id = 65 AND mpm.is_deleted = false + GROUP BY mpm.project_id +), + +earliest_active_or_construction_phase_date AS ( + SELECT + mpp.project_id, + min(mpp.phase_start) AS earliest + FROM moped_proj_phases mpp + LEFT JOIN moped_phases mp ON mpp.phase_id = mp.phase_id + WHERE (mp.phase_name_simple = any(ARRAY['Active'::text, 'Construction'::text])) AND mpp.is_deleted = false + GROUP BY mpp.project_id +) + +SELECT + mpc.project_id, + mpc.project_component_id, + comp_geography.feature_ids, + mpc.component_id, + comp_geography.geometry, + comp_geography.line_geometry, + comp_geography.signal_ids, + council_districts.council_districts, + council_districts.council_districts_searchable, + NOT coalesce(council_districts.council_districts IS null OR council_districts.council_districts = ''::text, false) AS is_within_city_limits, + comp_geography.length_feet_total, + round(comp_geography.length_feet_total::numeric / 5280::numeric, 2) AS length_miles_total, + mc.component_name, + mc.component_subtype, + mc.component_name_full, + 'placeholder text'::text AS component_categories, + CASE + WHEN mc.line_representation = true THEN 'Line'::text + ELSE 'Point'::text + END AS geometry_type, + CASE + WHEN comp_geography.geometry IS NULL THEN 'No'::text + ELSE 'Yes'::text + END AS is_mapped, + subcomponents.subcomponents AS component_subcomponents, + work_types.work_types AS component_work_types, + component_tags.component_tags, + mpc.description AS component_description, + mpc.interim_project_component_id, + CASE + WHEN mpc.phase_id IS null THEN plv.substantial_completion_date + WHEN mpc.phase_id IS NOT null AND mpc.completion_date IS null THEN null::timestamp with time zone + ELSE mpc.completion_date + END AS substantial_completion_date, + plv.substantial_completion_date_estimated, + mpc.srts_id, + mpc.location_description AS component_location_description, + plv.project_name, + plv.project_name_secondary, + plv.project_name_full, + plv.project_description, + plv.ecapris_subproject_id, + plv.project_website, + plv.updated_at AS project_updated_at, + mpc.phase_id AS component_phase_id, + mph.phase_name AS component_phase_name, + mph.phase_name_simple AS component_phase_name_simple, + current_phase.phase_id AS project_phase_id, + current_phase.phase_name AS project_phase_name, + current_phase.phase_name_simple AS project_phase_name_simple, + coalesce(mph.phase_name, current_phase.phase_name) AS current_phase_name, + coalesce(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple, + plv.project_team_members, + plv.project_sponsor, + plv.project_lead, + plv.public_process_status, + plv.interim_project_id, + plv.project_partners, + plv.task_order_names, + plv.funding_source_and_program_names AS funding_sources, + plv.project_status_update, + plv.project_status_update_date_created, + to_char(timezone('US/Central'::text, plv.construction_start_date), 'YYYY-MM-DD'::text) AS construction_start_date, + plv.project_inspector, + plv.project_designer, + plv.project_tags, + plv.workgroup_contractors, + plv.contract_numbers, + plv.parent_project_id, + plv.parent_project_name, + plv.parent_project_url, + plv.parent_project_name AS parent_project_name_full, + rp.related_project_ids_with_self AS related_project_ids, + rp.related_project_ids_searchable_with_self AS related_project_ids_searchable, + plv.knack_project_id AS knack_data_tracker_project_record_id, + plv.project_url, + (plv.project_url || '?tab=map&project_component_id='::text) || mpc.project_component_id::text AS component_url, + get_project_development_status(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple)) AS project_development_status, + project_development_status_date.result AS project_development_status_date, + to_char(project_development_status_date.result, 'YYYY'::text)::integer AS project_development_status_date_calendar_year, + to_char(project_development_status_date.result, 'FMMonth YYYY'::text) AS project_development_status_date_calendar_year_month, + to_char(project_development_status_date.result, 'YYYY-MM'::text) AS project_development_status_date_calendar_year_month_numeric, + date_part('quarter'::text, project_development_status_date.result)::text AS project_development_status_date_calendar_year_quarter, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN (to_char(project_development_status_date.result, 'YYYY'::text)::integer + 1)::text + ELSE to_char(project_development_status_date.result, 'YYYY'::text) + END AS project_development_status_date_fiscal_year, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN 1::double precision + ELSE date_part('quarter'::text, project_development_status_date.result) + 1::double precision + END::text AS project_development_status_date_fiscal_year_quarter, + plv.added_by AS project_added_by +FROM moped_proj_components mpc +LEFT JOIN comp_geography ON mpc.project_component_id = comp_geography.project_component_id +LEFT JOIN council_districts ON mpc.project_component_id = council_districts.project_component_id +LEFT JOIN subcomponents ON mpc.project_component_id = subcomponents.project_component_id +LEFT JOIN work_types ON mpc.project_component_id = work_types.project_component_id +LEFT JOIN component_tags ON mpc.project_component_id = component_tags.project_component_id +LEFT JOIN project_list_view plv ON mpc.project_id = plv.project_id +LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id +LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id +LEFT JOIN moped_components mc ON mpc.component_id = mc.component_id +LEFT JOIN related_projects rp ON mpc.project_id = rp.project_id +LEFT JOIN latest_public_meeting_date lpmd ON mpc.project_id = lpmd.project_id +LEFT JOIN earliest_active_or_construction_phase_date eaocpd ON mpc.project_id = eaocpd.project_id +LEFT JOIN LATERAL (SELECT timezone('US/Central'::text, get_project_development_status_date(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple))) AS result) project_development_status_date ON true +WHERE mpc.is_deleted = false AND plv.is_deleted = false; + +CREATE OR REPLACE VIEW exploded_component_arcgis_online_view AS SELECT + component_arcgis_online_view.project_id, + component_arcgis_online_view.project_component_id, + st_geometrytype(dump.geom) AS geometry_type, + dump.path[1] AS point_index, + component_arcgis_online_view.geometry AS original_geometry, + st_asgeojson(dump.geom) AS exploded_geometry, + component_arcgis_online_view.project_updated_at +FROM component_arcgis_online_view, + LATERAL st_dump(st_geomfromgeojson(component_arcgis_online_view.geometry)) dump (path, geom) +WHERE st_geometrytype(st_geomfromgeojson(component_arcgis_online_view.geometry)) = 'ST_MultiPoint'::text; diff --git a/moped-database/migrations/1739832264647_agol_view_is_mapped_bool/up.sql b/moped-database/migrations/1739832264647_agol_view_is_mapped_bool/up.sql new file mode 100644 index 0000000000..7a3bd729e5 --- /dev/null +++ b/moped-database/migrations/1739832264647_agol_view_is_mapped_bool/up.sql @@ -0,0 +1,266 @@ +DROP VIEW IF EXISTS exploded_component_arcgis_online_view; +DROP VIEW IF EXISTS component_arcgis_online_view; + +CREATE OR REPLACE VIEW component_arcgis_online_view AS WITH work_types AS ( + SELECT + mpcwt.project_component_id, + string_agg(mwt.name, ', '::text) AS work_types + FROM moped_proj_component_work_types mpcwt + LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + WHERE mpcwt.is_deleted = false + GROUP BY mpcwt.project_component_id +), + +council_districts AS ( + SELECT + features.component_id AS project_component_id, + string_agg(DISTINCT features_council_districts.council_district_id::text, ', '::text) AS council_districts, + string_agg(DISTINCT lpad(features_council_districts.council_district_id::text, 2, '0'::text), ', '::text) AS council_districts_searchable + FROM features_council_districts + LEFT JOIN features ON features_council_districts.feature_id = features.id + WHERE features.is_deleted = false + GROUP BY features.component_id +), + +comp_geography AS ( + SELECT + feature_union.component_id AS project_component_id, + string_agg(DISTINCT feature_union.id::text, ', '::text) AS feature_ids, + st_asgeojson(st_multi(st_union(array_agg(feature_union.geography))))::json AS geometry, + st_asgeojson(st_multi(st_union(array_agg(feature_union.line_geography))))::json AS line_geometry, + string_agg(DISTINCT feature_union.signal_id::text, ', '::text) AS signal_ids, + sum(feature_union.length_feet) AS length_feet_total + FROM ( + SELECT + feature_signals.id, + feature_signals.component_id, + feature_signals.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_signals.geography, 7::double precision)::geometry) AS line_geography, + feature_signals.signal_id, + null::integer AS length_feet + FROM feature_signals + WHERE feature_signals.is_deleted = false + UNION ALL + SELECT + feature_street_segments.id, + feature_street_segments.component_id, + feature_street_segments.geography::geometry AS geography, + feature_street_segments.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_street_segments.length_feet + FROM feature_street_segments + WHERE feature_street_segments.is_deleted = false + UNION ALL + SELECT + feature_intersections.id, + feature_intersections.component_id, + feature_intersections.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_intersections.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_intersections + WHERE feature_intersections.is_deleted = false + UNION ALL + SELECT + feature_drawn_points.id, + feature_drawn_points.component_id, + feature_drawn_points.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_drawn_points.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_drawn_points + WHERE feature_drawn_points.is_deleted = false + UNION ALL + SELECT + feature_drawn_lines.id, + feature_drawn_lines.component_id, + feature_drawn_lines.geography::geometry AS geography, + feature_drawn_lines.geography::geometry AS line_geography, + null::integer AS signal_id, + feature_drawn_lines.length_feet + FROM feature_drawn_lines + WHERE feature_drawn_lines.is_deleted = false + UNION ALL + SELECT + feature_school_beacons.id, + feature_school_beacons.component_id, + feature_school_beacons.geography::geometry AS geography, + st_exteriorring(st_buffer(feature_school_beacons.geography, 7::double precision)::geometry) AS line_geography, + null::integer AS signal_id, + null::integer AS length_feet + FROM feature_school_beacons + WHERE feature_school_beacons.is_deleted = false + ) feature_union + GROUP BY feature_union.component_id +), + +subcomponents AS ( + SELECT + mpcs.project_component_id, + string_agg(ms.subcomponent_name, ', '::text) AS subcomponents + FROM moped_proj_components_subcomponents mpcs + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + WHERE mpcs.is_deleted = false + GROUP BY mpcs.project_component_id +), + +component_tags AS ( + SELECT + mpct.project_component_id, + string_agg((mct.type || ' - '::text) || mct.name, ', '::text) AS component_tags + FROM moped_proj_component_tags mpct + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + WHERE mpct.is_deleted = false + GROUP BY mpct.project_component_id +), + +related_projects AS ( + SELECT + pmp.project_id, + concat_ws(', '::text, pmp.project_id, string_agg(cmp.project_id::text, ', '::text)) AS related_project_ids_with_self, + concat_ws(', '::text, lpad(pmp.project_id::text, 5, '0'::text), string_agg(lpad(cmp.project_id::text, 5, '0'::text), ', '::text)) AS related_project_ids_searchable_with_self + FROM moped_project pmp + LEFT JOIN moped_project cmp ON pmp.project_id = cmp.parent_project_id + WHERE cmp.is_deleted = false + GROUP BY pmp.project_id +), + +latest_public_meeting_date AS ( + SELECT + mpm.project_id, + coalesce(max(mpm.date_actual), max(mpm.date_estimate)) AS latest + FROM moped_proj_milestones mpm + WHERE mpm.milestone_id = 65 AND mpm.is_deleted = false + GROUP BY mpm.project_id +), + +earliest_active_or_construction_phase_date AS ( + SELECT + mpp.project_id, + min(mpp.phase_start) AS earliest + FROM moped_proj_phases mpp + LEFT JOIN moped_phases mp ON mpp.phase_id = mp.phase_id + WHERE (mp.phase_name_simple = any(ARRAY['Active'::text, 'Construction'::text])) AND mpp.is_deleted = false + GROUP BY mpp.project_id +) + +SELECT + mpc.project_id, + mpc.project_component_id, + comp_geography.feature_ids, + mpc.component_id, + comp_geography.geometry, + comp_geography.line_geometry, + comp_geography.signal_ids, + council_districts.council_districts, + council_districts.council_districts_searchable, + NOT coalesce(council_districts.council_districts IS null OR council_districts.council_districts = ''::text, false) AS is_within_city_limits, + comp_geography.length_feet_total, + round(comp_geography.length_feet_total::numeric / 5280::numeric, 2) AS length_miles_total, + mc.component_name, + mc.component_subtype, + mc.component_name_full, + 'placeholder text'::text AS component_categories, + CASE + WHEN mc.line_representation = true THEN 'Line'::text + ELSE 'Point'::text + END AS geometry_type, + CASE + WHEN comp_geography.geometry IS NULL THEN false + ELSE true + END AS is_mapped, + subcomponents.subcomponents AS component_subcomponents, + work_types.work_types AS component_work_types, + component_tags.component_tags, + mpc.description AS component_description, + mpc.interim_project_component_id, + CASE + WHEN mpc.phase_id IS null THEN plv.substantial_completion_date + WHEN mpc.phase_id IS NOT null AND mpc.completion_date IS null THEN null::timestamp with time zone + ELSE mpc.completion_date + END AS substantial_completion_date, + plv.substantial_completion_date_estimated, + mpc.srts_id, + mpc.location_description AS component_location_description, + plv.project_name, + plv.project_name_secondary, + plv.project_name_full, + plv.project_description, + plv.ecapris_subproject_id, + plv.project_website, + plv.updated_at AS project_updated_at, + mpc.phase_id AS component_phase_id, + mph.phase_name AS component_phase_name, + mph.phase_name_simple AS component_phase_name_simple, + current_phase.phase_id AS project_phase_id, + current_phase.phase_name AS project_phase_name, + current_phase.phase_name_simple AS project_phase_name_simple, + coalesce(mph.phase_name, current_phase.phase_name) AS current_phase_name, + coalesce(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple, + plv.project_team_members, + plv.project_sponsor, + plv.project_lead, + plv.public_process_status, + plv.interim_project_id, + plv.project_partners, + plv.task_order_names, + plv.funding_source_and_program_names AS funding_sources, + plv.project_status_update, + plv.project_status_update_date_created, + to_char(timezone('US/Central'::text, plv.construction_start_date), 'YYYY-MM-DD'::text) AS construction_start_date, + plv.project_inspector, + plv.project_designer, + plv.project_tags, + plv.workgroup_contractors, + plv.contract_numbers, + plv.parent_project_id, + plv.parent_project_name, + plv.parent_project_url, + plv.parent_project_name AS parent_project_name_full, + rp.related_project_ids_with_self AS related_project_ids, + rp.related_project_ids_searchable_with_self AS related_project_ids_searchable, + plv.knack_project_id AS knack_data_tracker_project_record_id, + plv.project_url, + (plv.project_url || '?tab=map&project_component_id='::text) || mpc.project_component_id::text AS component_url, + get_project_development_status(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple)) AS project_development_status, + project_development_status_date.result AS project_development_status_date, + to_char(project_development_status_date.result, 'YYYY'::text)::integer AS project_development_status_date_calendar_year, + to_char(project_development_status_date.result, 'FMMonth YYYY'::text) AS project_development_status_date_calendar_year_month, + to_char(project_development_status_date.result, 'YYYY-MM'::text) AS project_development_status_date_calendar_year_month_numeric, + date_part('quarter'::text, project_development_status_date.result)::text AS project_development_status_date_calendar_year_quarter, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN (to_char(project_development_status_date.result, 'YYYY'::text)::integer + 1)::text + ELSE to_char(project_development_status_date.result, 'YYYY'::text) + END AS project_development_status_date_fiscal_year, + CASE + WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN 1::double precision + ELSE date_part('quarter'::text, project_development_status_date.result) + 1::double precision + END::text AS project_development_status_date_fiscal_year_quarter, + plv.added_by AS project_added_by +FROM moped_proj_components mpc +LEFT JOIN comp_geography ON mpc.project_component_id = comp_geography.project_component_id +LEFT JOIN council_districts ON mpc.project_component_id = council_districts.project_component_id +LEFT JOIN subcomponents ON mpc.project_component_id = subcomponents.project_component_id +LEFT JOIN work_types ON mpc.project_component_id = work_types.project_component_id +LEFT JOIN component_tags ON mpc.project_component_id = component_tags.project_component_id +LEFT JOIN project_list_view plv ON mpc.project_id = plv.project_id +LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id +LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id +LEFT JOIN moped_components mc ON mpc.component_id = mc.component_id +LEFT JOIN related_projects rp ON mpc.project_id = rp.project_id +LEFT JOIN latest_public_meeting_date lpmd ON mpc.project_id = lpmd.project_id +LEFT JOIN earliest_active_or_construction_phase_date eaocpd ON mpc.project_id = eaocpd.project_id +LEFT JOIN LATERAL (SELECT timezone('US/Central'::text, get_project_development_status_date(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple))) AS result) project_development_status_date ON true +WHERE mpc.is_deleted = false AND plv.is_deleted = false; + +CREATE OR REPLACE VIEW exploded_component_arcgis_online_view AS SELECT + component_arcgis_online_view.project_id, + component_arcgis_online_view.project_component_id, + st_geometrytype(dump.geom) AS geometry_type, + dump.path[1] AS point_index, + component_arcgis_online_view.geometry AS original_geometry, + st_asgeojson(dump.geom) AS exploded_geometry, + component_arcgis_online_view.project_updated_at +FROM component_arcgis_online_view, + LATERAL st_dump(st_geomfromgeojson(component_arcgis_online_view.geometry)) dump (path, geom) +WHERE st_geometrytype(st_geomfromgeojson(component_arcgis_online_view.geometry)) = 'ST_MultiPoint'::text; diff --git a/moped-database/views/component_arcgis_online_view.sql b/moped-database/views/component_arcgis_online_view.sql index f60429b702..c30c298ada 100644 --- a/moped-database/views/component_arcgis_online_view.sql +++ b/moped-database/views/component_arcgis_online_view.sql @@ -1,4 +1,4 @@ --- Most recent migration: moped-database/migrations/1739832264644_update_districts_to_jsonb/up.sql +-- Most recent migration: moped-database/migrations/1739832264647_agol_view_is_mapped_bool/up.sql CREATE OR REPLACE VIEW component_arcgis_online_view AS WITH work_types AS ( SELECT @@ -164,12 +164,20 @@ SELECT WHEN mc.line_representation = true THEN 'Line'::text ELSE 'Point'::text END AS geometry_type, + CASE + WHEN comp_geography.geometry IS null THEN false + ELSE true + END AS is_mapped, subcomponents.subcomponents AS component_subcomponents, work_types.work_types AS component_work_types, component_tags.component_tags, mpc.description AS component_description, mpc.interim_project_component_id, - coalesce(mpc.completion_date, plv.substantial_completion_date) AS substantial_completion_date, + CASE + WHEN mpc.phase_id IS null THEN plv.substantial_completion_date + WHEN mpc.phase_id IS NOT null AND mpc.completion_date IS null THEN null::timestamp with time zone + ELSE mpc.completion_date + END AS substantial_completion_date, plv.substantial_completion_date_estimated, mpc.srts_id, mpc.location_description AS component_location_description, diff --git a/moped-database/views/exploded_component_arcgis_online_view.sql b/moped-database/views/exploded_component_arcgis_online_view.sql index e1fc3da9d5..62d4c524bd 100644 --- a/moped-database/views/exploded_component_arcgis_online_view.sql +++ b/moped-database/views/exploded_component_arcgis_online_view.sql @@ -1,4 +1,4 @@ --- Most recent migration: moped-database/migrations/1739832264644_update_districts_to_jsonb/up.sql +-- Most recent migration: moped-database/migrations/1739832264647_agol_view_is_mapped_bool/up.sql CREATE OR REPLACE VIEW exploded_component_arcgis_online_view AS SELECT component_arcgis_online_view.project_id, diff --git a/moped-editor/package-lock.json b/moped-editor/package-lock.json index 4850980770..689efb7ae2 100644 --- a/moped-editor/package-lock.json +++ b/moped-editor/package-lock.json @@ -1,12 +1,12 @@ { "name": "atd-moped-editor", - "version": "2.25.1", + "version": "2.30.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "atd-moped-editor", - "version": "2.25.1", + "version": "2.30.1", "hasInstallScript": true, "license": "CC0-1.0", "dependencies": { @@ -38,7 +38,7 @@ "axios": "^0.28.0", "clsx": "^1.2.1", "date-fns": "^2.30.0", - "dompurify": "^2.5.4", + "dompurify": "^3.2.4", "env-cmd": "^10.1.0", "filepond": "^3.9.0", "filepond-plugin-file-validate-size": "^2.2.8", @@ -18971,9 +18971,13 @@ } }, "node_modules/dompurify": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.4.tgz", - "integrity": "sha512-l5NNozANzaLPPe0XaAwvg3uZcHtDBnziX/HjsY1UcDj1MxTK8Dd0Kv096jyPK5HRzs/XM5IMj20dW8Fk+HnbUA==" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/domutils": { "version": "2.8.0", diff --git a/moped-editor/package.json b/moped-editor/package.json index 512310d748..b0a98d34e1 100644 --- a/moped-editor/package.json +++ b/moped-editor/package.json @@ -2,7 +2,7 @@ "name": "atd-moped-editor", "author": "ATD Data & Technology Services", "license": "CC0-1.0", - "version": "2.30.1", + "version": "2.31.0", "homepage": "/moped", "private": false, "repository": { @@ -71,7 +71,7 @@ "axios": "^0.28.0", "clsx": "^1.2.1", "date-fns": "^2.30.0", - "dompurify": "^2.5.4", + "dompurify": "^3.2.4", "env-cmd": "^10.1.0", "filepond": "^3.9.0", "filepond-plugin-file-validate-size": "^2.2.8", diff --git a/moped-editor/src/components/ApolloErrorHandler.js b/moped-editor/src/components/ApolloErrorHandler.js index 3ce775dbb8..1177e20f72 100644 --- a/moped-editor/src/components/ApolloErrorHandler.js +++ b/moped-editor/src/components/ApolloErrorHandler.js @@ -1,10 +1,10 @@ import React from "react"; import Backdrop from "@mui/material/Backdrop"; import CircularProgress from "@mui/material/CircularProgress"; -import makeStyles from '@mui/styles/makeStyles'; +import makeStyles from "@mui/styles/makeStyles"; import FallbackComponent from "./FallbackComponent"; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ backdrop: { zIndex: theme.zIndex.drawer + 1, color: "#fff", @@ -20,12 +20,14 @@ const useStyles = makeStyles(theme => ({ * @return {JSX.Element} * @constructor */ -const ApolloErrorHandler = props => { +const ApolloErrorHandler = (props) => { const classes = useStyles(); // Error Variables const error = props?.error ?? null; - const errorString = error ? JSON.stringify(error, Object.getOwnPropertyNames(error)) : ""; + const errorString = error + ? JSON.stringify(error, Object.getOwnPropertyNames(error)) + : ""; const jwtError = errorString.includes("JWT") || errorString.includes("token"); if (jwtError) { @@ -41,7 +43,7 @@ const ApolloErrorHandler = props => { ) : error ? ( - + ) : ( props.children )} diff --git a/moped-editor/src/components/DataGridPro/LookupAutocompleteComponent.js b/moped-editor/src/components/DataGridPro/LookupAutocompleteComponent.js new file mode 100644 index 0000000000..6de3e262ca --- /dev/null +++ b/moped-editor/src/components/DataGridPro/LookupAutocompleteComponent.js @@ -0,0 +1,85 @@ +import React, { useCallback } from "react"; +import { Autocomplete, TextField } from "@mui/material"; +import { useGridApiContext } from "@mui/x-data-grid-pro"; +import FullWidthPopper from "src/components/FullWidthPopper"; +import { filterOptions } from "src/utils/autocompleteHelpers"; + +/** + * Component for dropdown select using a lookup table as options + * @param {Number} id - row id in Data Grid + * @param {string} value - Field value + * @param {string} field - name of Field + * @param {Boolean} hasFocus - does field have focus in table + * @param {String} name - name of lookup table relationship + * @param {Array|Objects} options - the lookup table data + * @param {Object} autocompleteProps - props passed to the MUI Autcomplete Component + * @param {Boolean} fullWidthPopper - should component use custom Popper component + * + * @returns {React component} + */ +const LookupAutocompleteComponent = ({ + id, + value, + field, + hasFocus, + name, + options, + fullWidthPopper, + autocompleteProps, +}) => { + const apiRef = useGridApiContext(); + const ref = React.useRef(null); + + React.useEffect(() => { + if (hasFocus) { + ref.current.focus(); + } + }, [hasFocus]); + + const handleChange = (event, newValue) => { + apiRef.current.setEditCellValue({ + id, + field, + value: newValue, + }); + }; + + const defaultGetOptionLabel = useCallback( + (option) => option[`${name}_name`], + [name] + ); + + const defaultIsOptionEqualToValue = useCallback( + (value, option) => value[`${name}_id`] === option[`${name}_id`], + [name] + ); + + return ( + ( + + )} + {...autocompleteProps} + getOptionLabel={ + autocompleteProps?.getOptionLabel + ? autocompleteProps.getOptionLabel + : defaultGetOptionLabel + } + isOptionEqualToValue={ + autocompleteProps?.isOptionEqualToValue + ? autocompleteProps.isOptionEqualToValue + : defaultIsOptionEqualToValue + } + onChange={handleChange} + /> + ); +}; + +export default LookupAutocompleteComponent; diff --git a/moped-editor/src/components/CustomPopper.js b/moped-editor/src/components/FullWidthPopper.js similarity index 82% rename from moped-editor/src/components/CustomPopper.js rename to moped-editor/src/components/FullWidthPopper.js index b9564b5599..2a18ed5d91 100644 --- a/moped-editor/src/components/CustomPopper.js +++ b/moped-editor/src/components/FullWidthPopper.js @@ -3,7 +3,7 @@ import { Popper } from "@mui/material"; // https://stackoverflow.com/questions/63579345/how-can-i-change-the-width-of-material-ui-autocomplete-popover // Customize popper to expand to fit the content -const CustomPopper = (props) => ( +const FullWidthPopper = (props) => ( ( /> ); -export default CustomPopper; +export default FullWidthPopper; diff --git a/moped-editor/src/queries/funding.js b/moped-editor/src/queries/funding.js index 5fda9d7196..4c1f0d5e45 100644 --- a/moped-editor/src/queries/funding.js +++ b/moped-editor/src/queries/funding.js @@ -14,7 +14,15 @@ export const FUNDING_QUERY = gql` funding_amount funding_description funding_program_id + moped_fund_program { + funding_program_id + funding_program_name + } funding_source_id + moped_fund_source { + funding_source_id + funding_source_name + } funding_status_id is_deleted } diff --git a/moped-editor/src/queries/project.js b/moped-editor/src/queries/project.js index ca5ba47149..2d243bf962 100644 --- a/moped-editor/src/queries/project.js +++ b/moped-editor/src/queries/project.js @@ -315,6 +315,7 @@ export const TIMELINE_QUERY = gql` project_milestone_id project_id moped_milestone { + milestone_id milestone_name related_phase_id } diff --git a/moped-editor/src/utils/autocompleteHelpers.js b/moped-editor/src/utils/autocompleteHelpers.js index 474967ddc2..51f5bf0937 100644 --- a/moped-editor/src/utils/autocompleteHelpers.js +++ b/moped-editor/src/utils/autocompleteHelpers.js @@ -4,7 +4,7 @@ export const filterOptions = (options, { inputValue, getOptionLabel }) => { // limits options to ensure fast rendering const limit = 40; - // applies the default autcomplete matching behavior plus our limit filter + // applies the default autocomplete matching behavior plus our limit filter const filteredOptions = options.filter(option => getOptionLabel(option) .toLowerCase() diff --git a/moped-editor/src/views/projects/projectView/ProjectFunding/FundAutocompleteComponent.js b/moped-editor/src/views/projects/projectView/ProjectFunding/FundAutocompleteComponent.js deleted file mode 100644 index 15fddc1a9a..0000000000 --- a/moped-editor/src/views/projects/projectView/ProjectFunding/FundAutocompleteComponent.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react"; -import { Autocomplete, TextField } from "@mui/material"; -import { useGridApiContext } from "@mui/x-data-grid-pro"; -import makeStyles from "@mui/styles/makeStyles"; -import CustomPopper from "src/components/CustomPopper"; - -const useStyles = makeStyles((theme) => ({ - fundSelectStyle: { - width: "190px", - alignContent: "center", - }, -})); - -const FundAutocompleteComponent = (props) => { - const classes = useStyles(); - const { id, value, field, hasFocus } = props; - const apiRef = useGridApiContext(); - const ref = React.useRef(null); - - React.useEffect(() => { - if (hasFocus) { - ref.current.focus(); - } - }, [hasFocus]); - - const handleChange = (event, newValue) => { - apiRef.current.setEditCellValue({ - id, - field, - value: newValue ?? null, - }); - }; - - return ( - ( - - )} - getOptionLabel={(option) => - // if our value is a string, just return the string - typeof option === "string" - ? option - : `${option.fund_id} | ${option.fund_name}` - } - isOptionEqualToValue={(value, option) => - value.fund_id === option.fund_id && value.fund_name === option.fund_name - } - onChange={handleChange} - /> - ); -}; - -export default FundAutocompleteComponent; diff --git a/moped-editor/src/views/projects/projectView/ProjectFunding/FundingDeptUnitAutocomplete.js b/moped-editor/src/views/projects/projectView/ProjectFunding/FundingDeptUnitAutocomplete.js deleted file mode 100644 index 6560695b1e..0000000000 --- a/moped-editor/src/views/projects/projectView/ProjectFunding/FundingDeptUnitAutocomplete.js +++ /dev/null @@ -1,92 +0,0 @@ -import React from "react"; -import { CircularProgress, TextField } from "@mui/material"; -import { Autocomplete, Alert } from "@mui/material"; -import { useSocrataJson } from "src/utils/socrataHelpers"; -import { filterOptions } from "src/utils/autocompleteHelpers"; -import { useGridApiContext } from "@mui/x-data-grid-pro"; -import theme from "src/theme"; - -/* - * Transportation Project Financial Codes - */ -const SOCRATA_ENDPOINT = - "https://data.austintexas.gov/resource/bgrt-2m2z.json?$limit=9999"; - -const DeptUnitInput = (params, error = false, variant, ref) => { - return ( - - ); -}; - -/** - * Material Autocomplete wrapper that enables selecting a dept unit record from a - * Socrata dataset and setting it. - * * @param {Object} classes - MaterialUI style object - * * @param {Object} props - passes down props parent MUI Table - * * @param {Array} value - passes an array of objects, each representing a dept unit - * @return {JSX.Element} - */ -const FundingDeptUnitAutocomplete = ({ classes, props, value }) => { - const { data, loading, error } = useSocrataJson(SOCRATA_ENDPOINT); - - const { id, field, hasFocus } = props; - const apiRef = useGridApiContext(); - const ref = React.useRef(null); - - React.useEffect(() => { - if (hasFocus) { - ref.current.focus(); - } - }, [hasFocus]); - - const handleChange = (newValue) => { - apiRef.current.setEditCellValue({ - id, - field, - value: newValue ? newValue : null, - }); - }; - - const formatLabel = (option) => - !!option.dept - ? `${option.dept} | ${option.unit} | ${option.unit_long_name} ` - : ""; - - if (loading) { - return ; - } else if (error) { - return ( - {`Unable to load dept-units: ${error}`} - ); - } - - return ( - formatLabel(option)} - onChange={(e, value) => handleChange(value)} - loading={loading} - options={data} - renderInput={(params) => DeptUnitInput(params, null, "standard", ref)} - value={value ?? null} - isOptionEqualToValue={(value, option) => - value.unit_long_name === option.unit_long_name - } - /> - ); -}; - -export default FundingDeptUnitAutocomplete; diff --git a/moped-editor/src/views/projects/projectView/ProjectFunding/LookupAutocompleteComponent.js b/moped-editor/src/views/projects/projectView/ProjectFunding/LookupAutocompleteComponent.js deleted file mode 100644 index c0fe24da7e..0000000000 --- a/moped-editor/src/views/projects/projectView/ProjectFunding/LookupAutocompleteComponent.js +++ /dev/null @@ -1,67 +0,0 @@ -import React from "react"; -import { Autocomplete, TextField } from "@mui/material"; -import { useGridApiContext } from "@mui/x-data-grid-pro"; -import { getLookupValueByID } from "src/components/DataGridPro/utils/helpers"; -import CustomPopper from "src/components/CustomPopper"; - -/** - * Component for dropdown select using a lookup table as options - * @param {Number} id - row id in Data Grid - * @param {string} value - Field value - * @param {string} field - name of Field - * @param {Boolean} hasFocus - is field focused - * @param {String} name - name of lookup table - * @param {Array|Objects} lookupTable - the lookup table data - * @returns {React component} - */ -const LookupAutocompleteComponent = ({ - id, - value, - field, - hasFocus, - name, - lookupTable, -}) => { - const apiRef = useGridApiContext(); - const ref = React.useRef(null); - - React.useEffect(() => { - if (hasFocus) { - ref.current.focus(); - } - }, [hasFocus]); - - const handleChange = (event, newValue) => { - apiRef.current.setEditCellValue({ - id, - field, - value: newValue ? newValue[`${name}_id`] : null, - }); - }; - - return ( - ( - - )} - getOptionLabel={(option) => - // if our value is a string, just return the string instead of accessing the name - typeof option === "string" ? option : option[`${name}_name`] - } - isOptionEqualToValue={(value, option) => value[`${name}_name`] === option} - onChange={handleChange} - /> - ); -}; - -export default LookupAutocompleteComponent; diff --git a/moped-editor/src/views/projects/projectView/ProjectFunding/ProjectFundingTable.js b/moped-editor/src/views/projects/projectView/ProjectFunding/ProjectFundingTable.js index b43a41f570..bd51e17407 100644 --- a/moped-editor/src/views/projects/projectView/ProjectFunding/ProjectFundingTable.js +++ b/moped-editor/src/views/projects/projectView/ProjectFunding/ProjectFundingTable.js @@ -23,15 +23,14 @@ import { ADD_PROJECT_FUNDING, DELETE_PROJECT_FUNDING, } from "../../../../queries/funding"; +import { useSocrataJson } from "src/utils/socrataHelpers"; -import FundingDeptUnitAutocomplete from "./FundingDeptUnitAutocomplete"; import DollarAmountIntegerField from "./DollarAmountIntegerField"; import DataGridTextField from "src/components/DataGridPro/DataGridTextField"; import SubprojectFundingModal from "./SubprojectFundingModal"; import ProjectFundingToolbar from "./ProjectFundingToolbar"; import LookupSelectComponent from "../../../../components/LookupSelectComponent"; -import LookupAutocompleteComponent from "./LookupAutocompleteComponent"; -import FundAutocompleteComponent from "./FundAutocompleteComponent"; +import LookupAutocompleteComponent from "src/components/DataGridPro/LookupAutocompleteComponent"; import dataGridProStyleOverrides from "src/styles/dataGridProStylesOverrides"; import DeleteConfirmationModal from "../DeleteConfirmationModal"; import { getLookupValueByID } from "src/components/DataGridPro/utils/helpers"; @@ -89,6 +88,12 @@ const useStyles = makeStyles((theme) => ({ }, })); +/* + * Transportation Project Financial Codes + */ +const SOCRATA_ENDPOINT = + "https://data.austintexas.gov/resource/bgrt-2m2z.json?$limit=999999"; + // memoized hook to concatanate fund dept and unit ids into an fdu string const useFdusArray = (projectFunding) => useMemo(() => { @@ -101,6 +106,24 @@ const useFdusArray = (projectFunding) => ); }, [projectFunding]); +// object to pass to the Fund column's LookupAutocomplete component +const fundAutocompleteProps = { + getOptionLabel: (option) => + option.fund_id ? `${option.fund_id} | ${option.fund_name}` : "", + isOptionEqualToValue: (value, option) => + value.fund_id === option.fund_id && value.fund_name === option.fund_name, +}; + +// object to pass to the Dept Unit column's LookupAutocomplete component +const deptunitAutocompleteProps = { + getOptionLabel: (option) => + !!option.dept + ? `${option.dept} | ${option.unit} | ${option.unit_long_name} ` + : "", + isOptionEqualToValue: (value, option) => + value.unit_long_name === option.unit_long_name, +}; + /** Hook that provides memoized column settings */ const useColumns = ({ data, @@ -109,44 +132,37 @@ const useColumns = ({ handleSaveClick, handleCancelClick, handleEditClick, + deptUnitData, }) => useMemo(() => { return [ { headerName: "Source", - field: "funding_source_id", + field: "moped_fund_source", width: 200, editable: true, - renderCell: ({ value }) => - getLookupValueByID( - data["moped_fund_sources"], - "funding_source", - value - ), + valueFormatter: (value) => value?.funding_source_name, renderEditCell: (props) => ( ), }, { headerName: "Program", - field: "funding_program_id", + field: "moped_fund_program", width: 200, editable: true, - renderCell: ({ value }) => - getLookupValueByID( - data["moped_fund_programs"], - "funding_program", - value - ), + valueFormatter: (value) => value?.funding_program_name, renderEditCell: (props) => ( ), }, @@ -185,7 +201,13 @@ const useColumns = ({ valueFormatter: (value) => !!value?.fund_name ? `${value?.fund_id} | ${value?.fund_name}` : "", renderEditCell: (props) => ( - + ), }, { @@ -199,7 +221,13 @@ const useColumns = ({ ${value?.unit_long_name}` : "", renderEditCell: (props) => ( - + ), }, { @@ -238,6 +266,7 @@ const useColumns = ({ handleSaveClick, handleCancelClick, handleEditClick, + deptUnitData, ]); const ProjectFundingTable = ({ handleSnackbar }) => { @@ -257,6 +286,8 @@ const ProjectFundingTable = ({ handleSnackbar }) => { }, fetchPolicy: "no-cache", }); + const { data: deptUnitData, error: socrataError } = + useSocrataJson(SOCRATA_ENDPOINT); const [addProjectFunding] = useMutation(ADD_PROJECT_FUNDING); const [updateProjectFunding] = useMutation(UPDATE_PROJECT_FUNDING); @@ -335,6 +366,7 @@ const ProjectFundingTable = ({ handleSnackbar }) => { setRows((oldRows) => [ { id, + moped_fund_source: null, funding_source_id: null, funding_program_id: null, funding_description: null, @@ -349,7 +381,7 @@ const ProjectFundingTable = ({ handleSnackbar }) => { ]); setRowModesModel((oldModel) => ({ ...oldModel, - [id]: { mode: GridRowModes.Edit, fieldToFocus: "funding_source_id" }, + [id]: { mode: GridRowModes.Edit, fieldToFocus: "moped_fund_source" }, })); }; @@ -426,6 +458,14 @@ const ProjectFundingTable = ({ handleSnackbar }) => { ? null : updateProjectFundingData.funding_description; + updateProjectFundingData.funding_source_id = + updateProjectFundingData.moped_fund_source?.funding_source_id || null; + delete updateProjectFundingData.moped_fund_source; + + updateProjectFundingData.funding_program_id = + updateProjectFundingData.moped_fund_program?.funding_program_id || null; + delete updateProjectFundingData.moped_fund_program; + if (updatedRow.isNew) { delete updateProjectFundingData.isNew; delete updateProjectFundingData.id; @@ -458,17 +498,14 @@ const ProjectFundingTable = ({ handleSnackbar }) => { // Please note that the processRowUpdate must return the row object to update the Data Grid internal state. .then(() => updatedRow) .catch((error) => { - handleSnackbar( - true, - "Error adding funding source", - "error", - error - ); + handleSnackbar(true, "Error adding funding source", "error", error); }) ); } else { // Remove __typename since we removed it from updatedRow and check if the row has changed delete originalRow.__typename; + delete originalRow.moped_fund_source; + delete originalRow.moped_fund_program; const hasRowChanged = !isEqual(updatedRow, originalRow); if (!hasRowChanged) { @@ -505,8 +542,10 @@ const ProjectFundingTable = ({ handleSnackbar }) => { handleSaveClick, handleCancelClick, handleEditClick, + deptUnitData, }); + if (socrataError) console.error(socrataError); if (loading || !data) return ; const eCaprisID = data?.moped_project[0].ecapris_subproject_id; @@ -526,6 +565,7 @@ const ProjectFundingTable = ({ handleSnackbar }) => { rowModesModel={rowModesModel} onRowModesModelChange={handleRowModesModelChange} processRowUpdate={processRowUpdate} + onProcessRowUpdateError={(error) => console.error(error)} disableRowSelectionOnClick toolbar density="comfortable" diff --git a/moped-editor/src/views/projects/projectView/ProjectMilestones.js b/moped-editor/src/views/projects/projectView/ProjectMilestones.js index c8ba5a322a..5028d306ca 100644 --- a/moped-editor/src/views/projects/projectView/ProjectMilestones.js +++ b/moped-editor/src/views/projects/projectView/ProjectMilestones.js @@ -440,6 +440,7 @@ const ProjectMilestones = ({ rowModesModel={rowModesModel} onRowModesModelChange={handleRowModesModelChange} processRowUpdate={processRowUpdate} + onProcessRowUpdateError={(error) => console.error} onCellKeyDown={checkIfShiftKey} disableRowSelectionOnClick toolbar diff --git a/moped-editor/src/views/projects/projectView/ProjectSummary/SubprojectLookupComponent.js b/moped-editor/src/views/projects/projectView/ProjectSummary/SubprojectLookupComponent.js deleted file mode 100644 index 33de063f05..0000000000 --- a/moped-editor/src/views/projects/projectView/ProjectSummary/SubprojectLookupComponent.js +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import { useGridApiContext } from "@mui/x-data-grid-pro"; -import { FormControl, TextField } from "@mui/material"; -import Autocomplete from "@mui/material/Autocomplete"; - -/** Component for subproject lookup dropdown - * @param {Integer} id - Data Grid row id (same as project id) - * @param {String} value - field value - * @param {String} field - name of field - * @param {Boolean} hasFocus - does this field have focus - * @param {Object} data - data object with subproject options - * @return {JSX.Element} - */ - -const SubprojectLookupComponent = ({ id, value, field, hasFocus, data }) => { - const apiRef = useGridApiContext(); - const ref = useRef(null); - - useEffect(() => { - if (hasFocus) { - ref.current.focus(); - } - }, [hasFocus]); - - const handleChange = (event, newValue) => { - apiRef.current.setEditCellValue({ - id, - field, - // this will be an object containing the project id, name, and status for the project selected - value: newValue ? newValue : null, - }); - }; - - return ( - - - `${option.project_id} - ${option.project_name_full}` - } - value={value || null} - onChange={handleChange} - renderInput={(params) => ( - - )} - /> - - ); -}; - -export default SubprojectLookupComponent; diff --git a/moped-editor/src/views/projects/projectView/ProjectSummary/SubprojectsTable.js b/moped-editor/src/views/projects/projectView/ProjectSummary/SubprojectsTable.js index 99fa0ca429..dc1cf56b13 100644 --- a/moped-editor/src/views/projects/projectView/ProjectSummary/SubprojectsTable.js +++ b/moped-editor/src/views/projects/projectView/ProjectSummary/SubprojectsTable.js @@ -10,7 +10,7 @@ import dataGridProStyleOverrides from "src/styles/dataGridProStylesOverrides"; import SubprojectsToolbar from "./SubprojectsToolbar"; import ApolloErrorHandler from "../../../../components/ApolloErrorHandler"; import ProjectStatusBadge from "../../projectView/ProjectStatusBadge"; -import SubprojectLookupComponent from "./SubprojectLookupComponent"; +import LookupAutocompleteComponent from "src/components/DataGridPro/LookupAutocompleteComponent"; import DeleteConfirmationModal from "../DeleteConfirmationModal"; import RenderFieldLink from "src/components/RenderFieldLink"; @@ -50,7 +50,15 @@ const useColumns = ({ /> ), renderEditCell: (props) => ( - + + `${option.project_id} - ${option.project_name_full}`, + }} + /> ), }, { @@ -274,6 +282,7 @@ const SubprojectsTable = ({ getRowId={(row) => row.id} rowModesModel={rowModesModel} onRowModesModelChange={handleRowModesModelChange} + onProcessRowUpdateError={(error) => console.error} slots={{ toolbar: SubprojectsToolbar }} slotProps={{ toolbar: { onClick: handleAddSubprojectClick } }} editMode="row" diff --git a/moped-editor/src/views/projects/projectsListView/helpers.js b/moped-editor/src/views/projects/projectsListView/helpers.js index 4bf67354d8..9d5371823c 100644 --- a/moped-editor/src/views/projects/projectsListView/helpers.js +++ b/moped-editor/src/views/projects/projectsListView/helpers.js @@ -32,13 +32,15 @@ export const filterProjectTeamMembers = (value, view) => { uniqueNames[fullName] = projectRole; } }); - // if the view is projectsListView, render each team member as a block + // if the view is projectsListView, render each team member as a block, + // adding commas to all blocks except the final one if (view === "projectsListView") { + const array = Object.keys(uniqueNames); return (
- {Object.keys(uniqueNames).map((key) => ( + {array.map((key, i) => ( - {`${key} - ${uniqueNames[key]}`} + {`${key} - ${uniqueNames[key]}${i < array.length - 1 ? "," : ""}`} ))}
@@ -80,33 +82,23 @@ export const resolveHasSubprojects = (array) => { return "No"; }; -const filterComponentFullNames = (value) => { - if (!value.components) { - return ""; +const renderSplitListDisplayBlock = (row, fieldName) => { + if (row[fieldName]) { + const array = row[fieldName].split(","); + return ( +
+ {array.map((value, i) => { + return ( + + {`${value}${i < array.length - 1 ? "," : ""}`} + + ); + })} +
+ ); } - const componentNamesArray = value.components.split(","); - return ( -
- {componentNamesArray.map((comp) => ( - - {comp} - - ))} -
- ); }; -const renderSplitListDisplayBlock = (row, fieldName) => - row[fieldName] && ( -
- {row[fieldName].split(",").map((value, i) => ( - - {value} - - ))} -
- ); - const COLUMN_CONFIG = PROJECT_LIST_VIEW_QUERY_CONFIG.columns; const COLUMN_WIDTHS = { @@ -330,11 +322,15 @@ export const useColumns = ({ hiddenColumns }) => { { headerName: "Designer", field: "project_designer", + renderCell: ({ row }) => + renderSplitListDisplayBlock(row, "project_designer"), width: COLUMN_WIDTHS.small, }, { headerName: "Inspector", field: "project_inspector", + renderCell: ({ row }) => + renderSplitListDisplayBlock(row, "project_inspector"), width: COLUMN_WIDTHS.small, }, { @@ -376,7 +372,7 @@ export const useColumns = ({ hiddenColumns }) => { { headerName: "Components", field: "components", - renderCell: ({ row }) => filterComponentFullNames(row), + renderCell: ({ row }) => renderSplitListDisplayBlock(row, "components"), width: COLUMN_WIDTHS.medium, }, { diff --git a/moped-editor/src/views/projects/projectsListView/useProjectListViewQuery/useAdvancedSearch.js b/moped-editor/src/views/projects/projectsListView/useProjectListViewQuery/useAdvancedSearch.js index 215755cf06..59ad99bf97 100644 --- a/moped-editor/src/views/projects/projectsListView/useProjectListViewQuery/useAdvancedSearch.js +++ b/moped-editor/src/views/projects/projectsListView/useProjectListViewQuery/useAdvancedSearch.js @@ -26,7 +26,7 @@ const makeAdvancedSearchWhereFilters = (filters) => if (!filterConfigForField) { throwFallbackComponentError( - `Field ${field} in this url no longer exists in the project list.` + `Field ${field} in this URL no longer exists in the project list.` ); } @@ -34,6 +34,13 @@ const makeAdvancedSearchWhereFilters = (filters) => // Use operator name to get the GraphQL operator config for that operator const operatorConfig = FILTERS_COMMON_OPERATORS[operator]; + + if (!operatorConfig) { + throwFallbackComponentError( + `Operator ${operator} in this URL no longer exists in the project list.` + ); + } + let { envelope, specialNullValue, diff --git a/moped-etl/arcgis/settings.py b/moped-etl/arcgis/settings.py index de721d6ca0..87de69f470 100644 --- a/moped-etl/arcgis/settings.py +++ b/moped-etl/arcgis/settings.py @@ -83,6 +83,7 @@ substantial_completion_date_estimated task_order_names workgroup_contractors + is_mapped } } """