diff --git a/docker-compose.yml b/docker-compose.yml index c758944ee2..690fd43437 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -282,7 +282,7 @@ services: # AUTH_JWT_KEY: ${ENCRYPTION_KEY} # AUTH_JWT_AUD: ${FMTM_DOMAIN} ports: - - "7055:7055" + - "7055:3000" networks: - fmtm-net restart: "unless-stopped" diff --git a/src/backend/app/db/models.py b/src/backend/app/db/models.py index 6ef90816b9..5273974445 100644 --- a/src/backend/app/db/models.py +++ b/src/backend/app/db/models.py @@ -41,6 +41,7 @@ from app.db.enums import ( BackgroundTaskStatus, CommunityType, + EntityState, HTTPStatus, MappingLevel, MappingState, @@ -842,7 +843,7 @@ async def create( Args: db (Connection): The database connection. - project_id (int): The organisation ID. + project_id (int): The project ID. tasks (geojson.FeatureCollection): FeatureCollection of task areas. Returns: @@ -1296,6 +1297,79 @@ async def delete(cls, db: Connection, project_id: int) -> bool: ) +class DbOdkEntities(BaseModel): + """Table odk_entities. + + Mirror tracking the status of Entities in ODK. + """ + + entity_id: UUID + status: EntityState + project_id: int + task_id: int + + @classmethod + async def upsert( + cls, + db: Connection, + project_id: int, + entities: list[Self], + ) -> bool: + """Update or insert Entity data, with statuses. + + Args: + db (Connection): The database connection. + project_id (int): The project ID. + entities (list[Self]): List of DbOdkEntities objects. + + Returns: + bool: Success or failure. + """ + log.info( + f"Updating FMTM database Entities for project {project_id} " + f"with ({len(entities)}) features" + ) + + sql = """ + INSERT INTO public.odk_entities + (entity_id, status, project_id, task_id) + VALUES + """ + + # Prepare data for bulk insert + values = [] + data = {} + for index, entity in enumerate(entities): + entity_index = f"entity_{index}" + values.append( + f"(%({entity_index}_entity_id)s, " + f"%({entity_index}_status)s, " + f"%({entity_index}_project_id)s, " + f"%({entity_index}_task_id)s)" + ) + data[f"{entity_index}_entity_id"] = entity["id"] + data[f"{entity_index}_status"] = EntityState(int(entity["status"])).name + data[f"{entity_index}_project_id"] = project_id + task_id = entity["task_id"] + data[f"{entity_index}_task_id"] = int(task_id) if task_id else None + + sql += ( + ", ".join(values) + + """ + ON CONFLICT (entity_id) DO UPDATE SET + status = EXCLUDED.status, + task_id = EXCLUDED.task_id + RETURNING True; + """ + ) + + async with db.cursor() as cur: + await cur.execute(sql, data) + result = await cur.fetchall() + + return bool(result) + + class DbBackgroundTask(BaseModel): """Table background_tasks. diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_crud.py index 254cb1982f..c334253196 100644 --- a/src/backend/app/projects/project_crud.py +++ b/src/backend/app/projects/project_crud.py @@ -532,8 +532,8 @@ async def generate_project_files( # Split extract by task area log.debug("Splitting data extract per task area") - # TODO in future this splitting could be removed as the task_id is - # no longer used in the XLSForm + # TODO in future this splitting could be removed if the task_id is + # no longer used in the XLSForm for the map filter task_extract_dict = await split_geojson_by_task_areas( db, feature_collection, project_id ) diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index 9e16d025d7..6873a37e0c 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -57,7 +57,14 @@ ProjectRole, XLSFormType, ) -from app.db.models import DbBackgroundTask, DbBasemap, DbProject, DbTask, DbUserRole +from app.db.models import ( + DbBackgroundTask, + DbBasemap, + DbOdkEntities, + DbProject, + DbTask, + DbUserRole, +) from app.db.postgis_utils import ( check_crs, featcol_keep_single_geom_type, @@ -174,14 +181,20 @@ async def get_odk_entities_geojson( response_model=list[central_schemas.EntityMappingStatus], ) async def get_odk_entities_mapping_statuses( + project_id: int, project: Annotated[DbProject, Depends(project_deps.get_project)], db: Annotated[Connection, Depends(db_conn)], ): """Get the ODK entities mapping statuses, i.e. in progress or complete.""" - return await central_crud.get_entities_data( + entities = await central_crud.get_entities_data( project.odk_credentials, project.odkid, ) + # First update the Entity statuses in the db + # FIXME this is a hack and in the long run should be replaced + # https://github.com/hotosm/fmtm/issues/1841 + await DbOdkEntities.upsert(db, project_id, entities) + return entities @router.get( diff --git a/src/backend/migrations/009-entity-table.sql b/src/backend/migrations/009-entity-table.sql new file mode 100644 index 0000000000..8d893d2d9c --- /dev/null +++ b/src/backend/migrations/009-entity-table.sql @@ -0,0 +1,36 @@ +-- ## Migration to: +-- * Add a new table that syncs the ODK Entity status to FMTM. +-- * Add a primary key for entity_id field. +-- * Add two indexes on entity_id + project_id / task_id + + +-- Start a transaction +BEGIN; + +CREATE TABLE IF NOT EXISTS public.odk_entities ( + entity_id UUID NOT NULL, + status entitystate NOT NULL, + project_id integer NOT NULL, + task_id integer +); +ALTER TABLE public.odk_entities OWNER TO fmtm; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'odk_entities_pkey') THEN + ALTER TABLE ONLY public.odk_entities + ADD CONSTRAINT odk_entities_pkey PRIMARY KEY (entity_id); + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS idx_entities_project_id +ON public.odk_entities USING btree ( + entity_id, project_id +); +CREATE INDEX IF NOT EXISTS idx_entities_task_id +ON public.odk_entities USING btree ( + entity_id, task_id +); + +-- Commit the transaction +COMMIT; diff --git a/src/backend/migrations/init/fmtm_base_schema.sql b/src/backend/migrations/init/fmtm_base_schema.sql index e295b56eec..d37230eac0 100644 --- a/src/backend/migrations/init/fmtm_base_schema.sql +++ b/src/backend/migrations/init/fmtm_base_schema.sql @@ -160,7 +160,7 @@ SET default_table_access_method = heap; -- Tables -CREATE TABLE IF NOT EXISTS public._migrations ( +CREATE TABLE public._migrations ( script_name text, date_executed timestamp with time zone ); @@ -339,6 +339,14 @@ CREATE TABLE public.users ( ); ALTER TABLE public.users OWNER TO fmtm; +CREATE TABLE public.odk_entities ( + entity_id UUID NOT NULL, + status entitystate NOT NULL, + project_id integer NOT NULL, + task_id integer +); +ALTER TABLE public.odk_entities OWNER TO fmtm; + CREATE TABLE public.xlsforms ( id integer NOT NULL, title character varying, @@ -435,6 +443,9 @@ ADD CONSTRAINT users_pkey PRIMARY KEY (id); ALTER TABLE ONLY public.users ADD CONSTRAINT users_username_key UNIQUE (username); +ALTER TABLE ONLY public.odk_entities +ADD CONSTRAINT odk_entities_pkey PRIMARY KEY (entity_id); + ALTER TABLE ONLY public.xlsforms ADD CONSTRAINT xlsforms_pkey PRIMARY KEY (id); @@ -447,16 +458,16 @@ ADD CONSTRAINT submission_photos_pkey PRIMARY KEY (id); -- Indexing CREATE INDEX idx_projects_outline ON public.projects USING gist (outline); -CREATE INDEX IF NOT EXISTS idx_projects_mapper_level +CREATE INDEX idx_projects_mapper_level ON public.projects USING btree ( mapper_level ); -CREATE INDEX IF NOT EXISTS idx_projects_organisation_id +CREATE INDEX idx_projects_organisation_id ON public.projects USING btree ( organisation_id ); CREATE INDEX idx_tasks_outline ON public.tasks USING gist (outline); -CREATE INDEX IF NOT EXISTS idx_tasks_composite +CREATE INDEX idx_tasks_composite ON public.tasks USING btree ( id, project_id ); @@ -466,26 +477,34 @@ CREATE INDEX idx_user_roles ON public.user_roles USING btree ( CREATE INDEX idx_org_managers ON public.organisation_managers USING btree ( user_id, organisation_id ); -CREATE INDEX IF NOT EXISTS idx_task_event_composite +CREATE INDEX idx_task_event_composite ON public.task_events USING btree ( task_id, project_id ); -CREATE INDEX IF NOT EXISTS idx_task_event_project_user +CREATE INDEX idx_task_event_project_user ON public.task_events USING btree ( user_id, project_id ); -CREATE INDEX IF NOT EXISTS idx_task_event_project_id +CREATE INDEX idx_task_event_project_id ON public.task_events USING btree ( task_id, project_id ); -CREATE INDEX IF NOT EXISTS idx_task_event_user_id +CREATE INDEX idx_task_event_user_id ON public.task_events USING btree ( task_id, user_id ); -CREATE INDEX IF NOT EXISTS idx_task_history_date +CREATE INDEX idx_task_history_date ON public.task_history USING btree ( task_id, created_at ); +CREATE INDEX idx_entities_project_id +ON public.odk_entities USING btree ( + entity_id, project_id +); +CREATE INDEX idx_entities_task_id +ON public.odk_entities USING btree ( + entity_id, task_id +); -- Foreign keys diff --git a/src/mapper/src/lib/components/page/layer-switcher.svelte b/src/mapper/src/lib/components/page/layer-switcher.svelte index 22a15c4124..570d933768 100644 --- a/src/mapper/src/lib/components/page/layer-switcher.svelte +++ b/src/mapper/src/lib/components/page/layer-switcher.svelte @@ -1,5 +1,5 @@ diff --git a/src/mapper/src/lib/components/page/legend.svelte b/src/mapper/src/lib/components/page/legend.svelte index 91ca4a2357..8a495875d9 100644 --- a/src/mapper/src/lib/components/page/legend.svelte +++ b/src/mapper/src/lib/components/page/legend.svelte @@ -1,7 +1,7 @@