diff --git a/.env.example b/.env.example
index 359ea6237c..6ca0f86dd2 100644
--- a/.env.example
+++ b/.env.example
@@ -19,7 +19,7 @@ OSM_CLIENT_ID=${OSM_CLIENT_ID}
OSM_CLIENT_SECRET=${OSM_CLIENT_SECRET}
OSM_URL=${OSM_URL:-"https://www.openstreetmap.org"}
OSM_SCOPE=${OSM_SCOPE:-'["read_prefs", "send_messages"]'}
-OSM_LOGIN_REDIRECT_URI="http${FMTM_DOMAIN:+s}://${FMTM_DOMAIN:-127.0.0.1:7051}/osmauth/"
+OSM_LOGIN_REDIRECT_URI="http${FMTM_DOMAIN:+s}://${FMTM_DOMAIN:-127.0.0.1:7051}/osmauth"
OSM_SECRET_KEY=${OSM_SECRET_KEY}
### S3 File Storage ###
diff --git a/INSTALL.md b/INSTALL.md
index a98b133d02..daafad9544 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -99,8 +99,8 @@ To properly configure your FMTM project, you will need to create keys for OSM.
2. Register your FMTM instance to OAuth 2 applications.
- Put your login redirect url as `http://127.0.0.1:7051/osmauth/` if running locally,
- or for production replace with https://{YOUR_DOMAIN}/osmauth/
+ Put your login redirect url as `http://127.0.0.1:7051/osmauth` if running locally,
+ or for production replace with https://{YOUR_DOMAIN}/osmauth
> Note: `127.0.0.1` is required for debugging instead of `localhost`
> due to OSM restrictions.
diff --git a/scripts/gen-env.sh b/scripts/gen-env.sh
index 1fbc35b874..e059f16c0f 100644
--- a/scripts/gen-env.sh
+++ b/scripts/gen-env.sh
@@ -305,7 +305,7 @@ set_domains() {
set_osm_credentials() {
pretty_echo "OSM OAuth2 Credentials"
- redirect_uri="http${FMTM_DOMAIN:+s}://${FMTM_DOMAIN:-127.0.0.1:7051}/osmauth/"
+ redirect_uri="http${FMTM_DOMAIN:+s}://${FMTM_DOMAIN:-127.0.0.1:7051}/osmauth"
echo "App credentials are generated from your OSM user profile."
echo
diff --git a/src/backend/app/auth/auth_routes.py b/src/backend/app/auth/auth_routes.py
index 19463286cc..122afcc2a6 100644
--- a/src/backend/app/auth/auth_routes.py
+++ b/src/backend/app/auth/auth_routes.py
@@ -49,7 +49,7 @@
)
-@router.get("/osm-login/")
+@router.get("/osm-login")
async def login_url(osm_auth=Depends(init_osm_auth)):
"""Get Login URL for OSM Oauth Application.
@@ -69,7 +69,7 @@ async def login_url(osm_auth=Depends(init_osm_auth)):
return JSONResponse(content=login_url, status_code=HTTPStatus.OK)
-@router.get("/callback/")
+@router.get("/callback")
async def callback(
request: Request, osm_auth: Annotated[AuthUser, Depends(init_osm_auth)]
) -> JSONResponse:
@@ -135,7 +135,7 @@ async def callback(
) from e
-@router.get("/logout/")
+@router.get("/logout")
async def logout():
"""Reset httpOnly cookie to sign out user."""
response = Response(status_code=HTTPStatus.OK)
@@ -178,15 +178,21 @@ async def get_or_create_user(
profile_img = EXCLUDED.profile_img
RETURNING id, username, profile_img, role
)
+
SELECT
u.id, u.username, u.profile_img, u.role,
+
+ -- Aggregate the organisation IDs managed by the user
array_agg(
DISTINCT om.organisation_id
- ) FILTER (WHERE om.organisation_id IS NOT NULL) as orgs_managed,
+ ) FILTER (WHERE om.organisation_id IS NOT NULL) AS orgs_managed,
+
+ -- Aggregate project roles for the user, as project:role pairs
jsonb_object_agg(
ur.project_id,
COALESCE(ur.role, 'MAPPER')
- ) FILTER (WHERE ur.project_id IS NOT NULL) as project_roles
+ ) FILTER (WHERE ur.project_id IS NOT NULL) AS project_roles
+
FROM upserted_user u
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN organisation_managers om ON u.id = om.user_id
@@ -229,7 +235,7 @@ async def get_or_create_user(
) from e
-@router.get("/me/", response_model=FMTMUser)
+@router.get("/me", response_model=FMTMUser)
async def my_data(
db: Annotated[Connection, Depends(db_conn)],
current_user: Annotated[AuthUser, Depends(login_required)],
diff --git a/src/backend/app/auth/auth_schemas.py b/src/backend/app/auth/auth_schemas.py
index 6d380fc403..52e1d5752d 100644
--- a/src/backend/app/auth/auth_schemas.py
+++ b/src/backend/app/auth/auth_schemas.py
@@ -16,10 +16,9 @@
#
"""Pydantic models for Auth."""
-from typing import Any, Optional, TypedDict
+from typing import Optional, TypedDict
from pydantic import BaseModel, ConfigDict, PrivateAttr, computed_field
-from pydantic.functional_validators import field_validator
from app.db.enums import ProjectRole, UserRole
from app.db.models import DbOrganisation, DbProject, DbUser
@@ -80,30 +79,5 @@ class FMTMUser(BaseModel):
username: str
profile_img: str
role: UserRole
- project_roles: Optional[dict[int, ProjectRole]] = {}
- orgs_managed: Optional[list[int]] = []
-
- @field_validator("role", mode="before")
- @classmethod
- def convert_user_role_str_to_ints(cls, role: Any) -> Optional[UserRole]:
- """User role strings returned from db converted to enum integers."""
- if not role:
- return None
- if isinstance(role, str):
- return UserRole[role]
- return role
-
- @field_validator("project_roles", mode="before")
- @classmethod
- def convert_project_role_str_to_ints(
- cls, roles: dict[int, Any]
- ) -> Optional[dict[int, ProjectRole]]:
- """User project strings returned from db converted to enum integers."""
- if not roles:
- return {}
-
- first_value = next(iter(roles.values()), None)
- if isinstance(first_value, str):
- return {id: ProjectRole[role] for id, role in roles.items()}
-
- return roles
+ project_roles: Optional[dict[int, ProjectRole]] = None
+ orgs_managed: Optional[list[int]] = None
diff --git a/src/backend/app/auth/roles.py b/src/backend/app/auth/roles.py
index bd9d66a57e..74099db1eb 100644
--- a/src/backend/app/auth/roles.py
+++ b/src/backend/app/auth/roles.py
@@ -86,38 +86,59 @@ async def check_access(
user_id = await get_uid(user)
sql = """
+ WITH role_hierarchy AS (
+ SELECT 'MAPPER' AS role, 0 AS level
+ UNION ALL SELECT 'VALIDATOR', 1
+ UNION ALL SELECT 'FIELD_MANAGER', 2
+ UNION ALL SELECT 'ASSOCIATE_PROJECT_MANAGER', 3
+ UNION ALL SELECT 'PROJECT_MANAGER', 4
+ )
+
SELECT *
FROM users
WHERE id = %(user_id)s
AND (
CASE
- WHEN role = 'ADMIN' THEN true
- WHEN role = 'READ_ONLY' THEN false
+ -- Simple check to see if ADMIN or blocked (READ_ONLY)
+ WHEN role = 'ADMIN'::public.userrole THEN true
+ WHEN role = 'READ_ONLY'::public.userrole THEN false
ELSE
+
+ -- Check to see if user is org admin
EXISTS (
SELECT 1
FROM organisation_managers
WHERE organisation_managers.user_id = %(user_id)s
- AND
- organisation_managers.organisation_id = %(org_id)s
+ AND organisation_managers.organisation_id = %(org_id)s
)
+
+ -- Check to see if user has equal or greater than project role
OR EXISTS (
SELECT 1
FROM user_roles
+ JOIN role_hierarchy AS user_role_h
+ ON user_roles.role::public.projectrole
+ = user_role_h.role::public.projectrole
+ JOIN role_hierarchy AS required_role_h
+ ON %(role)s::public.projectrole
+ = required_role_h.role::public.projectrole
WHERE user_roles.user_id = %(user_id)s
- AND user_roles.project_id = %(project_id)s
- AND user_roles.role >= %(role)s
+ AND user_roles.project_id = %(project_id)s
+ AND user_role_h.level >= required_role_h.level
)
+
+ -- Extract organisation id from project,
+ -- then check to see if user is org admin
OR (
%(org_id)s IS NULL
AND EXISTS (
SELECT 1
FROM organisation_managers
- JOIN projects ON
- projects.organisation_id
- = organisation_managers.organisation_id
+ JOIN projects
+ ON projects.organisation_id =
+ organisation_managers.organisation_id
WHERE organisation_managers.user_id = %(user_id)s
- AND projects.id = %(project_id)s
+ AND projects.id = %(project_id)s
)
)
END
diff --git a/src/backend/app/config.py b/src/backend/app/config.py
index b1df2afdbf..1f9742429b 100644
--- a/src/backend/app/config.py
+++ b/src/backend/app/config.py
@@ -234,7 +234,7 @@ def assemble_db_connection(cls, v: Optional[str], info: ValidationInfo) -> Any:
# https://github.com/openstreetmap/operations/issues/951#issuecomment-1748717154
OSM_URL: HttpUrlStr = "https://www.openstreetmap.org"
OSM_SCOPE: list[str] = ["read_prefs", "send_messages"]
- OSM_LOGIN_REDIRECT_URI: str = "http://127.0.0.1:7051/osmauth/"
+ OSM_LOGIN_REDIRECT_URI: str = "http://127.0.0.1:7051/osmauth"
S3_ENDPOINT: str = "http://s3:9000"
S3_ACCESS_KEY: Optional[str] = ""
diff --git a/src/backend/app/db/database.py b/src/backend/app/db/database.py
index 603f27a23e..f60573a7ff 100644
--- a/src/backend/app/db/database.py
+++ b/src/backend/app/db/database.py
@@ -57,7 +57,7 @@ async def db_conn(request: Request) -> Connection:
To use the connection in endpoints:
-----------------------------------
- @app.get("/something/")
+ @app.get("/something")
async def do_stuff(db = Depends(db_conn)):
async with db.cursor() as cursor:
await cursor.execute("SELECT * FROM items")
diff --git a/src/backend/app/db/models.py b/src/backend/app/db/models.py
index fdabbabea9..6f074c116e 100644
--- a/src/backend/app/db/models.py
+++ b/src/backend/app/db/models.py
@@ -158,7 +158,7 @@ class DbUser(BaseModel):
registered_at: Optional[AwareDatetime] = None
# Relationships
- project_roles: Optional[list[DbUserRole]] = None
+ project_roles: Optional[dict[int, ProjectRole]] = None # project:role pairs
orgs_managed: Optional[list[int]] = None
@classmethod
@@ -168,13 +168,18 @@ async def one(cls, db: Connection, user_identifier: int | str) -> Self:
sql = """
SELECT
u.*,
- array_agg(
- DISTINCT om.organisation_id
- ) FILTER (WHERE om.organisation_id IS NOT NULL) AS orgs_managed,
- jsonb_object_agg(
- ur.project_id,
- COALESCE(ur.role, 'MAPPER')
- ) FILTER (WHERE ur.project_id IS NOT NULL) AS project_roles
+
+ -- Aggregate organisation IDs managed by the user
+ array_agg(
+ DISTINCT om.organisation_id
+ ) FILTER (WHERE om.organisation_id IS NOT NULL) AS orgs_managed,
+
+ -- Aggregate project roles for the user, as project:role pairs
+ jsonb_object_agg(
+ ur.project_id,
+ COALESCE(ur.role, 'MAPPER')
+ ) FILTER (WHERE ur.project_id IS NOT NULL) AS project_roles
+
FROM users u
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN organisation_managers om ON u.id = om.user_id
diff --git a/src/backend/app/main.py b/src/backend/app/main.py
index 0958d992c9..c506867bec 100644
--- a/src/backend/app/main.py
+++ b/src/backend/app/main.py
@@ -121,6 +121,8 @@ def get_application() -> FastAPI:
debug=settings.DEBUG,
lifespan=lifespan,
root_path=settings.API_PREFIX,
+ # NOTE REST APIs should not have trailing slashes
+ redirect_slashes=False,
)
# Set custom logger
diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py
index 748cdbd5dc..76a60f53ea 100644
--- a/src/backend/app/organisations/organisation_routes.py
+++ b/src/backend/app/organisations/organisation_routes.py
@@ -52,7 +52,7 @@
)
-@router.get("/", response_model=list[OrganisationOut])
+@router.get("", response_model=list[OrganisationOut])
async def get_organisations(
db: Annotated[Connection, Depends(db_conn)],
current_user: Annotated[AuthUser, Depends(login_required)],
@@ -61,34 +61,7 @@ async def get_organisations(
return await DbOrganisation.all(db, current_user.id)
-@router.get("/my-organisations", response_model=list[OrganisationOut])
-async def get_my_organisations(
- db: Annotated[Connection, Depends(db_conn)],
- current_user: Annotated[AuthUser, Depends(login_required)],
-) -> list[DbOrganisation]:
- """Get a list of all organisations."""
- return await organisation_crud.get_my_organisations(db, current_user)
-
-
-@router.get("/unapproved/", response_model=list[OrganisationOut])
-async def list_unapproved_organisations(
- db: Annotated[Connection, Depends(db_conn)],
- current_user: Annotated[AuthUser, Depends(login_required)],
-) -> list[DbOrganisation]:
- """Get a list of unapproved organisations."""
- return await DbOrganisation.unapproved(db)
-
-
-@router.get("/{org_id}", response_model=OrganisationOut)
-async def get_organisation_detail(
- organisation: Annotated[DbOrganisation, Depends(org_exists)],
- current_user: Annotated[AuthUser, Depends(login_required)],
-):
- """Get a specific organisation by id or name."""
- return organisation
-
-
-@router.post("/", response_model=OrganisationOut)
+@router.post("", response_model=OrganisationOut)
async def create_organisation(
db: Annotated[Connection, Depends(db_conn)],
current_user: Annotated[AuthUser, Depends(login_required)],
@@ -109,33 +82,22 @@ async def create_organisation(
return await DbOrganisation.create(db, org_in, current_user.id, logo)
-@router.patch("/{org_id}/", response_model=OrganisationOut)
-async def update_organisation(
+@router.get("/my-organisations", response_model=list[OrganisationOut])
+async def get_my_organisations(
db: Annotated[Connection, Depends(db_conn)],
- org_user_dict: Annotated[AuthUser, Depends(org_admin)],
- new_values: OrganisationUpdate = Depends(parse_organisation_input),
- logo: UploadFile = File(None),
-):
- """Partial update for an existing organisation."""
- org_id = org_user_dict.get("org").id
- return await DbOrganisation.update(db, org_id, new_values, logo)
+ current_user: Annotated[AuthUser, Depends(login_required)],
+) -> list[DbOrganisation]:
+ """Get a list of all organisations."""
+ return await organisation_crud.get_my_organisations(db, current_user)
-@router.delete("/{org_id}")
-async def delete_org(
+@router.get("/unapproved", response_model=list[OrganisationOut])
+async def list_unapproved_organisations(
db: Annotated[Connection, Depends(db_conn)],
- org_user_dict: Annotated[AuthUser, Depends(org_admin)],
-):
- """Delete an organisation."""
- org = org_user_dict.get("org")
- org_deleted = await DbOrganisation.delete(db, org.id)
- if not org_deleted:
- log.error(f"Failed deleting org ({org.name}).")
- raise HTTPException(
- status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
- detail=f"Failed deleting org ({org.name}).",
- )
- return Response(status_code=HTTPStatus.NO_CONTENT)
+ current_user: Annotated[AuthUser, Depends(login_required)],
+) -> list[DbOrganisation]:
+ """Get a list of unapproved organisations."""
+ return await DbOrganisation.unapproved(db)
@router.delete("/unapproved/{org_id}")
@@ -164,7 +126,7 @@ async def delete_unapproved_org(
return Response(status_code=HTTPStatus.NO_CONTENT)
-@router.post("/approve/", response_model=OrganisationOut)
+@router.post("/approve", response_model=OrganisationOut)
async def approve_organisation(
org_id: int,
db: Annotated[Connection, Depends(db_conn)],
@@ -189,7 +151,7 @@ async def approve_organisation(
return approved_org
-@router.post("/new-admin/")
+@router.post("/new-admin")
async def add_new_organisation_admin(
db: Annotated[Connection, Depends(db_conn)],
org_user_dict: Annotated[OrgUserDict, Depends(org_admin)],
@@ -204,3 +166,41 @@ async def add_new_organisation_admin(
org_user_dict.get("org").id,
user_id,
)
+
+
+@router.get("/{org_id}", response_model=OrganisationOut)
+async def get_organisation_detail(
+ organisation: Annotated[DbOrganisation, Depends(org_exists)],
+ current_user: Annotated[AuthUser, Depends(login_required)],
+):
+ """Get a specific organisation by id or name."""
+ return organisation
+
+
+@router.patch("/{org_id}", response_model=OrganisationOut)
+async def update_organisation(
+ db: Annotated[Connection, Depends(db_conn)],
+ org_user_dict: Annotated[AuthUser, Depends(org_admin)],
+ new_values: OrganisationUpdate = Depends(parse_organisation_input),
+ logo: UploadFile = File(None),
+):
+ """Partial update for an existing organisation."""
+ org_id = org_user_dict.get("org").id
+ return await DbOrganisation.update(db, org_id, new_values, logo)
+
+
+@router.delete("/{org_id}")
+async def delete_org(
+ db: Annotated[Connection, Depends(db_conn)],
+ org_user_dict: Annotated[AuthUser, Depends(org_admin)],
+):
+ """Delete an organisation."""
+ org = org_user_dict.get("org")
+ org_deleted = await DbOrganisation.delete(db, org.id)
+ if not org_deleted:
+ log.error(f"Failed deleting org ({org.name}).")
+ raise HTTPException(
+ status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
+ detail=f"Failed deleting org ({org.name}).",
+ )
+ return Response(status_code=HTTPStatus.NO_CONTENT)
diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py
index db829f316f..54ae0614bd 100644
--- a/src/backend/app/projects/project_routes.py
+++ b/src/backend/app/projects/project_routes.py
@@ -98,7 +98,7 @@ async def read_projects_to_featcol(
return await project_crud.get_projects_featcol(db, bbox)
-@router.get("/", response_model=list[project_schemas.ProjectOut])
+@router.get("", response_model=list[project_schemas.ProjectOut])
async def read_projects(
current_user: Annotated[AuthUser, Depends(login_required)],
db: Annotated[Connection, Depends(db_conn)],
@@ -295,7 +295,7 @@ async def set_odk_entities_mapping_status(
@router.get(
- "/{project_id}/tiles/",
+ "/{project_id}/tiles",
response_model=list[project_schemas.BasemapOut],
)
async def tiles_list(
@@ -316,7 +316,7 @@ async def tiles_list(
@router.get(
- "/{project_id}/tiles/{tile_id}/",
+ "/{project_id}/tiles/{tile_id}",
response_model=project_schemas.BasemapOut,
)
async def download_tiles(
@@ -353,15 +353,7 @@ async def download_tiles(
)
-@router.get("/{project_id}", response_model=project_schemas.ProjectOut)
-async def read_project(
- project_user: Annotated[ProjectUserDict, Depends(mapper)],
-):
- """Get a specific project by ID."""
- return project_user.get("project")
-
-
-@router.get("/categories/")
+@router.get("/categories")
async def get_categories(current_user: Annotated[AuthUser, Depends(login_required)]):
"""Get api for fetching all the categories.
@@ -378,7 +370,7 @@ async def get_categories(current_user: Annotated[AuthUser, Depends(login_require
return categories
-@router.get("/download-form/{project_id}/")
+@router.get("/download-form/{project_id}")
async def download_form(
project_user: Annotated[ProjectUserDict, Depends(mapper)],
):
@@ -392,50 +384,7 @@ async def download_form(
return Response(content=project.xlsform_content, headers=headers)
-@router.get("/{project_id}/download")
-async def download_project_boundary(
- project_user: Annotated[ProjectUserDict, Depends(mapper)],
-) -> StreamingResponse:
- """Downloads the boundary of a project as a GeoJSON file."""
- project = project_user.get("project")
- geojson = json.dumps(project.outline).encode("utf-8")
- return StreamingResponse(
- BytesIO(geojson),
- headers={
- "Content-Disposition": (f"attachment; filename={project.slug}.geojson"),
- "Content-Type": "application/media",
- },
- )
-
-
-@router.get("/{project_id}/download_tasks")
-async def download_task_boundaries(
- project_id: int,
- db: Annotated[Connection, Depends(db_conn)],
- project_user: Annotated[ProjectUserDict, Depends(mapper)],
-):
- """Downloads the boundary of the tasks for a project as a GeoJSON file.
-
- Args:
- project_id (int): Project ID path param.
- db (Connection): The database connection.
- project_user (ProjectUserDict): Check if user has MAPPER permission.
-
- Returns:
- Response: The HTTP response object containing the downloaded file.
- """
- project_id = project_user.get("project").id
- out = await project_crud.get_task_geometry(db, project_id)
-
- headers = {
- "Content-Disposition": "attachment; filename=project_outline.geojson",
- "Content-Type": "application/media",
- }
-
- return Response(content=out, headers=headers)
-
-
-@router.get("/features/download/")
+@router.get("/features/download")
async def download_features(
db: Annotated[Connection, Depends(db_conn)],
project_user: Annotated[ProjectUserDict, Depends(mapper)],
@@ -460,7 +409,7 @@ async def download_features(
return Response(content=json.dumps(feature_collection), headers=headers)
-@router.get("/convert-fgb-to-geojson/")
+@router.get("/convert-fgb-to-geojson")
async def convert_fgb_to_geojson(
url: str,
db: Annotated[Connection, Depends(db_conn)],
@@ -537,44 +486,6 @@ async def get_contributors(
##################
-@router.post("/{project_id}/tiles-generate")
-async def generate_project_basemap(
- # NOTE we do not set the correct role on this endpoint yet
- # FIXME once sub project creation implemented, this should be manager only
- project_user: Annotated[ProjectUserDict, Depends(mapper)],
- background_tasks: BackgroundTasks,
- db: Annotated[Connection, Depends(db_conn)],
- basemap_in: project_schemas.BasemapGenerate,
-):
- """Returns basemap tiles for a project."""
- project_id = project_user.get("project").id
-
- # Create task in db and return uuid
- log.debug(
- "Creating generate_project_basemap background task "
- f"for project ID: {project_id}"
- )
- background_task_id = await DbBackgroundTask.create(
- db,
- project_schemas.BackgroundTaskIn(
- project_id=project_id,
- name="generate_basemap",
- ),
- )
-
- background_tasks.add_task(
- project_crud.generate_project_basemap,
- db,
- project_id,
- background_task_id,
- basemap_in.tile_source,
- basemap_in.file_format,
- basemap_in.tms_url,
- )
-
- return {"Message": "Tile generation started"}
-
-
@router.post("/task-split")
async def task_split(
# NOTE we do not set any roles on this endpoint yet
@@ -666,7 +577,7 @@ async def validate_form(
)
-@router.post("/preview-split-by-square/", response_model=FeatureCollection)
+@router.post("/preview-split-by-square", response_model=FeatureCollection)
async def preview_split_by_square(
# NOTE we do not set any roles on this endpoint yet
# FIXME once sub project creation implemented, this should be manager only
@@ -712,7 +623,7 @@ async def preview_split_by_square(
)
-@router.post("/generate-data-extract/")
+@router.post("/generate-data-extract")
async def get_data_extract(
# config_file: Optional[str] = Form(None),
# NOTE we do not set any roles on this endpoint yet
@@ -745,116 +656,7 @@ async def get_data_extract(
return JSONResponse(status_code=HTTPStatus.OK, content={"url": fgb_url})
-@router.post("/{project_id}/generate-project-data")
-async def generate_files(
- db: Annotated[Connection, Depends(db_conn)],
- project_user_dict: Annotated[ProjectUserDict, Depends(project_manager)],
- xlsform_upload: Annotated[
- Optional[BytesIO], Depends(central_deps.read_optional_xlsform)
- ],
- additional_entities: Optional[list[str]] = None,
-):
- """Generate additional content to initialise the project.
-
- Boundary, ODK Central forms, QR codes, etc.
-
- Accepts a project ID, category, custom form flag, and an uploaded file as inputs.
- The generated files are associated with the project ID and stored in the database.
- This api generates odk appuser tokens, forms. This api also creates an app user for
- each task and provides the required roles.
- Some of the other functionality of this api includes converting a xls file
- provided by the user to the xform, generates osm data extracts and uploads
- it to the form.
-
- Args:
- xlsform_upload (UploadFile, optional): A custom XLSForm to use in the project.
- A file should be provided if user wants to upload a custom xls form.
- additional_entities (list[str]): If additional Entity lists need to be
- created (i.e. the project form references multiple geometries).
- db (Connection): The database connection.
- project_user_dict (ProjectUserDict): Project admin role.
-
- Returns:
- json (JSONResponse): A success message containing the project ID.
- """
- project = project_user_dict.get("project")
- project_id = project.id
- form_category = project.xform_category
-
- log.debug(f"Generating additional files for project: {project.id}")
-
- if xlsform_upload:
- log.debug("User provided custom XLSForm")
-
- # Validate uploaded form
- await central_crud.validate_and_update_user_xlsform(
- xlsform=xlsform_upload,
- form_category=form_category,
- additional_entities=additional_entities,
- )
- xlsform = xlsform_upload
-
- else:
- log.debug(f"Using default XLSForm for category: '{form_category}'")
-
- form_filename = XLSFormType(form_category).name
- xlsform_path = f"{xlsforms_path}/{form_filename}.xls"
- with open(xlsform_path, "rb") as f:
- xlsform = BytesIO(f.read())
-
- xform_id, project_xlsform = await central_crud.append_fields_to_user_xlsform(
- xlsform=xlsform,
- form_category=form_category,
- additional_entities=additional_entities,
- )
- # Write XLS form content to db
- xlsform_bytes = project_xlsform.getvalue()
- if len(xlsform_bytes) == 0 or not xform_id:
- raise HTTPException(
- status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
- detail="There was an error modifying the XLSForm!",
- )
- log.debug(f"Setting project XLSForm db data for xFormId: {xform_id}")
- sql = """
- UPDATE public.projects
- SET
- odk_form_id = %(odk_form_id)s,
- xlsform_content = %(xlsform_content)s
- WHERE id = %(project_id)s;
- """
- async with db.cursor() as cur:
- await cur.execute(
- sql,
- {
- "project_id": project_id,
- "odk_form_id": xform_id,
- "xlsform_content": xlsform_bytes,
- },
- )
-
- success = await project_crud.generate_project_files(
- db,
- project_id,
- )
-
- if not success:
- return JSONResponse(
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
- content={
- "message": (
- f"Failed project ({project_id}) creation. "
- "Please contact the server admin."
- )
- },
- )
-
- return JSONResponse(
- status_code=HTTPStatus.OK,
- content={"message": "success"},
- )
-
-
-@router.get("/data-extract-url/")
+@router.get("/data-extract-url")
async def get_or_set_data_extract(
db: Annotated[Connection, Depends(db_conn)],
project_user_dict: Annotated[ProjectUserDict, Depends(project_manager)],
@@ -870,7 +672,7 @@ async def get_or_set_data_extract(
return JSONResponse(status_code=HTTPStatus.OK, content={"url": fgb_url})
-@router.post("/upload-custom-extract/")
+@router.post("/upload-custom-extract")
async def upload_custom_extract(
db: Annotated[Connection, Depends(db_conn)],
project_user_dict: Annotated[ProjectUserDict, Depends(project_manager)],
@@ -921,46 +723,7 @@ async def upload_custom_extract(
return JSONResponse(status_code=HTTPStatus.OK, content={"url": fgb_url})
-@router.post("/{project_id}/additional-entity")
-async def add_additional_entity_list(
- db: Annotated[Connection, Depends(db_conn)],
- project_user_dict: Annotated[ProjectUserDict, Depends(project_manager)],
- geojson: UploadFile = File(...),
-):
- """Add an additional Entity list for the project in ODK.
-
- Note that the Entity list will be named from the filename
- of the GeoJSON uploaded.
- """
- project = project_user_dict.get("project")
- project_id = project.id
- project_odk_id = project.odkid
- project_odk_creds = project.odk_credentials
- # NOTE the Entity name is extracted from the filename (without extension)
- entity_name = Path(geojson.filename).stem
-
- # Parse geojson + divide by task
- # (not technically required, but also appends properties in correct format)
- featcol = parse_geojson_file_to_featcol(await geojson.read())
- properties = list(featcol.get("features")[0].get("properties").keys())
- feature_split_by_task = await split_geojson_by_task_areas(db, featcol, project_id)
- entities_list = await central_crud.task_geojson_dict_to_entity_values(
- feature_split_by_task
- )
- dataset_name = entity_name.replace(" ", "_")
-
- await central_crud.create_entity_list(
- project_odk_creds,
- project_odk_id,
- properties=properties,
- dataset_name=dataset_name,
- entities_list=entities_list,
- )
-
- return Response(status_code=HTTPStatus.OK)
-
-
-@router.post("/add-manager/")
+@router.post("/add-manager")
async def add_new_project_manager(
db: Annotated[Connection, Depends(db_conn)],
project_user_dict: Annotated[ProjectUserDict, Depends(project_manager)],
@@ -978,17 +741,6 @@ async def add_new_project_manager(
return Response(status_code=HTTPStatus.OK)
-@router.patch("/{project_id}", response_model=project_schemas.ProjectOut)
-async def update_project(
- new_data: project_schemas.ProjectUpdate,
- project_user_dict: Annotated[ProjectUserDict, Depends(project_manager)],
- db: Annotated[Connection, Depends(db_conn)],
-):
- """Partial update an existing project."""
- # NOTE this does not including updating the ODK project name
- return await DbProject.update(db, project_user_dict.get("project").id, new_data)
-
-
@router.post("/update-form")
async def update_project_form(
xlsform: Annotated[BytesIO, Depends(central_deps.read_xlsform)],
@@ -1046,6 +798,203 @@ async def update_project_form(
)
+@router.post("/{project_id}/additional-entity")
+async def add_additional_entity_list(
+ db: Annotated[Connection, Depends(db_conn)],
+ project_user_dict: Annotated[ProjectUserDict, Depends(project_manager)],
+ geojson: UploadFile = File(...),
+):
+ """Add an additional Entity list for the project in ODK.
+
+ Note that the Entity list will be named from the filename
+ of the GeoJSON uploaded.
+ """
+ project = project_user_dict.get("project")
+ project_id = project.id
+ project_odk_id = project.odkid
+ project_odk_creds = project.odk_credentials
+ # NOTE the Entity name is extracted from the filename (without extension)
+ entity_name = Path(geojson.filename).stem
+
+ # Parse geojson + divide by task
+ # (not technically required, but also appends properties in correct format)
+ featcol = parse_geojson_file_to_featcol(await geojson.read())
+ properties = list(featcol.get("features")[0].get("properties").keys())
+ feature_split_by_task = await split_geojson_by_task_areas(db, featcol, project_id)
+ entities_list = await central_crud.task_geojson_dict_to_entity_values(
+ feature_split_by_task
+ )
+ dataset_name = entity_name.replace(" ", "_")
+
+ await central_crud.create_entity_list(
+ project_odk_creds,
+ project_odk_id,
+ properties=properties,
+ dataset_name=dataset_name,
+ entities_list=entities_list,
+ )
+
+ return Response(status_code=HTTPStatus.OK)
+
+
+@router.post("/{project_id}/generate-project-data")
+async def generate_files(
+ db: Annotated[Connection, Depends(db_conn)],
+ project_user_dict: Annotated[ProjectUserDict, Depends(project_manager)],
+ xlsform_upload: Annotated[
+ Optional[BytesIO], Depends(central_deps.read_optional_xlsform)
+ ],
+ additional_entities: Optional[list[str]] = None,
+):
+ """Generate additional content to initialise the project.
+
+ Boundary, ODK Central forms, QR codes, etc.
+
+ Accepts a project ID, category, custom form flag, and an uploaded file as inputs.
+ The generated files are associated with the project ID and stored in the database.
+ This api generates odk appuser tokens, forms. This api also creates an app user for
+ each task and provides the required roles.
+ Some of the other functionality of this api includes converting a xls file
+ provided by the user to the xform, generates osm data extracts and uploads
+ it to the form.
+
+ Args:
+ xlsform_upload (UploadFile, optional): A custom XLSForm to use in the project.
+ A file should be provided if user wants to upload a custom xls form.
+ additional_entities (list[str]): If additional Entity lists need to be
+ created (i.e. the project form references multiple geometries).
+ db (Connection): The database connection.
+ project_user_dict (ProjectUserDict): Project admin role.
+
+ Returns:
+ json (JSONResponse): A success message containing the project ID.
+ """
+ project = project_user_dict.get("project")
+ project_id = project.id
+ form_category = project.xform_category
+
+ log.debug(f"Generating additional files for project: {project.id}")
+
+ if xlsform_upload:
+ log.debug("User provided custom XLSForm")
+
+ # Validate uploaded form
+ await central_crud.validate_and_update_user_xlsform(
+ xlsform=xlsform_upload,
+ form_category=form_category,
+ additional_entities=additional_entities,
+ )
+ xlsform = xlsform_upload
+
+ else:
+ log.debug(f"Using default XLSForm for category: '{form_category}'")
+
+ form_filename = XLSFormType(form_category).name
+ xlsform_path = f"{xlsforms_path}/{form_filename}.xls"
+ with open(xlsform_path, "rb") as f:
+ xlsform = BytesIO(f.read())
+
+ xform_id, project_xlsform = await central_crud.append_fields_to_user_xlsform(
+ xlsform=xlsform,
+ form_category=form_category,
+ additional_entities=additional_entities,
+ )
+ # Write XLS form content to db
+ xlsform_bytes = project_xlsform.getvalue()
+ if len(xlsform_bytes) == 0 or not xform_id:
+ raise HTTPException(
+ status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
+ detail="There was an error modifying the XLSForm!",
+ )
+ log.debug(f"Setting project XLSForm db data for xFormId: {xform_id}")
+ sql = """
+ UPDATE public.projects
+ SET
+ odk_form_id = %(odk_form_id)s,
+ xlsform_content = %(xlsform_content)s
+ WHERE id = %(project_id)s;
+ """
+ async with db.cursor() as cur:
+ await cur.execute(
+ sql,
+ {
+ "project_id": project_id,
+ "odk_form_id": xform_id,
+ "xlsform_content": xlsform_bytes,
+ },
+ )
+
+ success = await project_crud.generate_project_files(
+ db,
+ project_id,
+ )
+
+ if not success:
+ return JSONResponse(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ content={
+ "message": (
+ f"Failed project ({project_id}) creation. "
+ "Please contact the server admin."
+ )
+ },
+ )
+
+ return JSONResponse(
+ status_code=HTTPStatus.OK,
+ content={"message": "success"},
+ )
+
+
+@router.post("/{project_id}/tiles-generate")
+async def generate_project_basemap(
+ # NOTE we do not set the correct role on this endpoint yet
+ # FIXME once sub project creation implemented, this should be manager only
+ project_user: Annotated[ProjectUserDict, Depends(mapper)],
+ background_tasks: BackgroundTasks,
+ db: Annotated[Connection, Depends(db_conn)],
+ basemap_in: project_schemas.BasemapGenerate,
+):
+ """Returns basemap tiles for a project."""
+ project_id = project_user.get("project").id
+
+ # Create task in db and return uuid
+ log.debug(
+ "Creating generate_project_basemap background task "
+ f"for project ID: {project_id}"
+ )
+ background_task_id = await DbBackgroundTask.create(
+ db,
+ project_schemas.BackgroundTaskIn(
+ project_id=project_id,
+ name="generate_basemap",
+ ),
+ )
+
+ background_tasks.add_task(
+ project_crud.generate_project_basemap,
+ db,
+ project_id,
+ background_task_id,
+ basemap_in.tile_source,
+ basemap_in.file_format,
+ basemap_in.tms_url,
+ )
+
+ return {"Message": "Tile generation started"}
+
+
+@router.patch("/{project_id}", response_model=project_schemas.ProjectOut)
+async def update_project(
+ new_data: project_schemas.ProjectUpdate,
+ project_user_dict: Annotated[ProjectUserDict, Depends(project_manager)],
+ db: Annotated[Connection, Depends(db_conn)],
+):
+ """Partial update an existing project."""
+ # NOTE this does not including updating the ODK project name
+ return await DbProject.update(db, project_user_dict.get("project").id, new_data)
+
+
@router.post("/{project_id}/upload-task-boundaries")
async def upload_project_task_boundaries(
project_id: int,
@@ -1082,7 +1031,7 @@ async def upload_project_task_boundaries(
####################
-@router.post("/", response_model=project_schemas.ProjectOut)
+@router.post("", response_model=project_schemas.ProjectOut)
async def create_project(
project_info: project_schemas.ProjectIn,
org_user_dict: Annotated[AuthUser, Depends(org_admin)],
@@ -1156,10 +1105,68 @@ async def delete_project(
await central_crud.delete_odk_project(project.odkid, project.odk_credentials)
# Delete S3 resources
await delete_all_objs_under_prefix(
- settings.S3_BUCKET_NAME, f"/{project.organisation_id}/{project.id}/"
+ settings.S3_BUCKET_NAME, f"/{project.organisation_id}/{project.id}"
)
# Delete FMTM project
await DbProject.delete(db, project.id)
log.info(f"Deletion of project {project.id} successful")
return Response(status_code=HTTPStatus.NO_CONTENT)
+
+
+###############################
+# ENDPOINTS THAT MUST BE LAST #
+###############################
+# NOTE this is due to the /{project_id} which will capture any text
+# NOTE in place of the project_id integer
+
+
+@router.get("/{project_id}", response_model=project_schemas.ProjectOut)
+async def read_project(
+ project_user: Annotated[ProjectUserDict, Depends(mapper)],
+):
+ """Get a specific project by ID."""
+ return project_user.get("project")
+
+
+@router.get("/{project_id}/download")
+async def download_project_boundary(
+ project_user: Annotated[ProjectUserDict, Depends(mapper)],
+) -> StreamingResponse:
+ """Downloads the boundary of a project as a GeoJSON file."""
+ project = project_user.get("project")
+ geojson = json.dumps(project.outline).encode("utf-8")
+ return StreamingResponse(
+ BytesIO(geojson),
+ headers={
+ "Content-Disposition": (f"attachment; filename={project.slug}.geojson"),
+ "Content-Type": "application/media",
+ },
+ )
+
+
+@router.get("/{project_id}/download_tasks")
+async def download_task_boundaries(
+ project_id: int,
+ db: Annotated[Connection, Depends(db_conn)],
+ project_user: Annotated[ProjectUserDict, Depends(mapper)],
+):
+ """Downloads the boundary of the tasks for a project as a GeoJSON file.
+
+ Args:
+ project_id (int): Project ID path param.
+ db (Connection): The database connection.
+ project_user (ProjectUserDict): Check if user has MAPPER permission.
+
+ Returns:
+ Response: The HTTP response object containing the downloaded file.
+ """
+ project_id = project_user.get("project").id
+ out = await project_crud.get_task_geometry(db, project_id)
+
+ headers = {
+ "Content-Disposition": "attachment; filename=project_outline.geojson",
+ "Content-Type": "application/media",
+ }
+
+ return Response(content=out, headers=headers)
diff --git a/src/backend/app/submissions/submission_routes.py b/src/backend/app/submissions/submission_routes.py
index de30151a6c..abf2e379c9 100644
--- a/src/backend/app/submissions/submission_routes.py
+++ b/src/backend/app/submissions/submission_routes.py
@@ -47,7 +47,7 @@
)
-@router.get("/")
+@router.get("")
async def read_submissions(
project_user: Annotated[ProjectUserDict, Depends(mapper)],
) -> list[dict]:
@@ -383,7 +383,7 @@ async def download_submission_geojson(
return Response(submission_data.getvalue(), headers=headers)
-@router.get("/conflate-submission-geojson/")
+@router.get("/conflate-submission-geojson")
async def conflate_geojson(
db_task: Annotated[DbTask, Depends(get_task)],
project_user: Annotated[ProjectUserDict, Depends(mapper)],
diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py
index 7e3f16d225..e2dd110b63 100644
--- a/src/backend/app/tasks/task_routes.py
+++ b/src/backend/app/tasks/task_routes.py
@@ -37,7 +37,7 @@
)
-@router.get("/", response_model=list[task_schemas.TaskOut])
+@router.get("", response_model=list[task_schemas.TaskOut])
async def read_tasks(
project_id: int,
db: Annotated[Connection, Depends(db_conn)],
@@ -84,7 +84,7 @@ async def add_new_task_event(
return await DbTaskEvent.create(db, new_event)
-@router.get("/activity/", response_model=list[task_schemas.TaskEventCount])
+@router.get("/activity", response_model=list[task_schemas.TaskEventCount])
async def task_activity(
project_id: int,
db: Annotated[Connection, Depends(db_conn)],
@@ -105,7 +105,7 @@ async def task_activity(
return await task_crud.get_project_task_activity(db, project_id, days)
-@router.get("/{task_id}/history/", response_model=list[task_schemas.TaskEventOut])
+@router.get("/{task_id}/history", response_model=list[task_schemas.TaskEventOut])
async def get_task_event_history(
task_id: int,
db: Annotated[Connection, Depends(db_conn)],
diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py
index 051b25bbab..b91e7327ac 100644
--- a/src/backend/app/users/user_routes.py
+++ b/src/backend/app/users/user_routes.py
@@ -38,7 +38,7 @@
)
-@router.get("/", response_model=List[user_schemas.UserOut])
+@router.get("", response_model=List[user_schemas.UserOut])
async def get_users(
db: Annotated[Connection, Depends(db_conn)],
current_user: Annotated[DbUser, Depends(super_admin)],
@@ -47,6 +47,15 @@ async def get_users(
return await DbUser.all(db)
+@router.get("/user-role-options")
+async def get_user_roles(current_user: Annotated[DbUser, Depends(mapper)]):
+ """Check for available user role options."""
+ user_roles = {}
+ for role in UserRoleEnum:
+ user_roles[role.name] = role.value
+ return user_roles
+
+
@router.get("/{id}", response_model=user_schemas.UserOut)
async def get_user_by_identifier(
user: Annotated[DbUser, Depends(get_user)],
@@ -61,15 +70,6 @@ async def get_user_by_identifier(
return user
-@router.get("/user-role-options/")
-async def get_user_roles(current_user: Annotated[DbUser, Depends(mapper)]):
- """Check for available user role options."""
- user_roles = {}
- for role in UserRoleEnum:
- user_roles[role.name] = role.value
- return user_roles
-
-
@router.delete("/{id}")
async def delete_user_by_identifier(
user: Annotated[DbUser, Depends(get_user)],
diff --git a/src/backend/tests/test_organisation_routes.py b/src/backend/tests/test_organisation_routes.py
index e8199cb9ca..bb42a4f6da 100644
--- a/src/backend/tests/test_organisation_routes.py
+++ b/src/backend/tests/test_organisation_routes.py
@@ -22,7 +22,7 @@
async def test_get_organisation(client, organisation):
"""Test get list of organisations."""
- response = await client.get("/organisation/")
+ response = await client.get("/organisation")
assert response.status_code == 200
data = response.json()[-1]
diff --git a/src/backend/tests/test_projects_routes.py b/src/backend/tests/test_projects_routes.py
index 3a9fe4d00c..9c836477ce 100644
--- a/src/backend/tests/test_projects_routes.py
+++ b/src/backend/tests/test_projects_routes.py
@@ -275,14 +275,14 @@ async def test_upload_data_extracts(client, project):
)
}
response = await client.post(
- f"/projects/upload-custom-extract/?project_id={project.id}",
+ f"/projects/upload-custom-extract?project_id={project.id}",
files=fgb_file,
)
assert response.status_code == 200
response = await client.get(
- f"/projects/data-extract-url/?project_id={project.id}",
+ f"/projects/data-extract-url?project_id={project.id}",
)
assert "url" in response.json()
@@ -294,14 +294,14 @@ async def test_upload_data_extracts(client, project):
)
}
response = await client.post(
- f"/projects/upload-custom-extract/?project_id={project.id}",
+ f"/projects/upload-custom-extract?project_id={project.id}",
files=geojson_file,
)
assert response.status_code == 200
response = await client.get(
- f"/projects/data-extract-url/?project_id={project.id}",
+ f"/projects/data-extract-url?project_id={project.id}",
)
assert "url" in response.json()
diff --git a/src/backend/tests/test_users.py b/src/backend/tests/test_users.py
index 129bb2f518..c649e85844 100644
--- a/src/backend/tests/test_users.py
+++ b/src/backend/tests/test_users.py
@@ -44,18 +44,18 @@ async def test_nothing():
# async def test_create_users(client):
-# response = await client.post("/users/", json={
+# response = await client.post("/users", json={
# "username": "test3", "password": "test1"})
# assert response.status_code == status.HTTP_200_OK
-# response = await client.post("/users/", json={
+# response = await client.post("/users", json={
# "username": "niraj", "password": "niraj"})
# assert response.status_code == status.HTTP_200_OK
-# response = await client.post("/users/", json={"username": "niraj"})
+# response = await client.post("/users", json={"username": "niraj"})
# assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
-# response = await client.post("/users/", json={
+# response = await client.post("/users", json={
# "username": "niraj", "password": "niraj"})
# assert response.status_code == status.HTTP_400_BAD_REQUEST
# assert response.json() == {"detail": "Username already registered"}
diff --git a/src/frontend/src/api/CreateProjectService.ts b/src/frontend/src/api/CreateProjectService.ts
index bf26e0ee65..f17c13f98a 100755
--- a/src/frontend/src/api/CreateProjectService.ts
+++ b/src/frontend/src/api/CreateProjectService.ts
@@ -55,7 +55,7 @@ const CreateProjectService = (
if (isOsmExtract) {
// Generated extract from raw-data-api
extractResponse = await API.get(
- `${import.meta.env.VITE_API_URL}/projects/data-extract-url/?project_id=${projectId}&url=${
+ `${import.meta.env.VITE_API_URL}/projects/data-extract-url?project_id=${projectId}&url=${
projectData.data_extract_url
}`,
);
@@ -64,7 +64,7 @@ const CreateProjectService = (
const dataExtractFormData = new FormData();
dataExtractFormData.append('custom_extract_file', dataExtractFile);
extractResponse = await API.post(
- `${import.meta.env.VITE_API_URL}/projects/upload-custom-extract/?project_id=${projectId}`,
+ `${import.meta.env.VITE_API_URL}/projects/upload-custom-extract?project_id=${projectId}`,
dataExtractFormData,
);
}
diff --git a/src/frontend/src/api/Project.ts b/src/frontend/src/api/Project.ts
index f3ac9648db..c994ff8fd2 100755
--- a/src/frontend/src/api/Project.ts
+++ b/src/frontend/src/api/Project.ts
@@ -157,7 +157,7 @@ export const GenerateProjectTiles = (url: string, projectId: string, data: objec
const generateProjectTiles = async (url: string, projectId: string) => {
try {
await CoreModules.axios.post(url, data);
- dispatch(GetTilesList(`${import.meta.env.VITE_API_URL}/projects/${projectId}/tiles/`));
+ dispatch(GetTilesList(`${import.meta.env.VITE_API_URL}/projects/${projectId}/tiles`));
dispatch(ProjectActions.SetGenerateProjectTilesLoading(false));
} catch (error) {
dispatch(ProjectActions.SetGenerateProjectTilesLoading(false));
diff --git a/src/frontend/src/components/ApproveOrganization/OrganizationForm.tsx b/src/frontend/src/components/ApproveOrganization/OrganizationForm.tsx
index 794a97c729..58747db0b1 100644
--- a/src/frontend/src/components/ApproveOrganization/OrganizationForm.tsx
+++ b/src/frontend/src/components/ApproveOrganization/OrganizationForm.tsx
@@ -38,7 +38,7 @@ const OrganizationForm = () => {
if (organizationId) {
dispatch(
ApproveOrganizationService(
- `${import.meta.env.VITE_API_URL}/organisation/approve/?org_id=${parseInt(organizationId)}`,
+ `${import.meta.env.VITE_API_URL}/organisation/approve?org_id=${parseInt(organizationId)}`,
),
);
}
diff --git a/src/frontend/src/components/CreateEditOrganization/CreateEditOrganizationForm.tsx b/src/frontend/src/components/CreateEditOrganization/CreateEditOrganizationForm.tsx
index 9cb889eaf1..5ac90baf8b 100644
--- a/src/frontend/src/components/CreateEditOrganization/CreateEditOrganizationForm.tsx
+++ b/src/frontend/src/components/CreateEditOrganization/CreateEditOrganizationForm.tsx
@@ -42,16 +42,13 @@ const CreateEditOrganizationForm = ({ organizationId }: { organizationId: string
const submission = () => {
if (!organizationId) {
const { fillODKCredentials, ...filteredValues } = values;
- dispatch(PostOrganisationDataService(`${import.meta.env.VITE_API_URL}/organisation/`, filteredValues));
+ dispatch(PostOrganisationDataService(`${import.meta.env.VITE_API_URL}/organisation`, filteredValues));
} else {
const { fillODKCredentials, ...filteredValues } = values;
const changedValues = diffObject(organisationFormData, filteredValues);
if (Object.keys(changedValues).length > 0) {
dispatch(
- PatchOrganizationDataService(
- `${import.meta.env.VITE_API_URL}/organisation/${organizationId}/`,
- changedValues,
- ),
+ PatchOrganizationDataService(`${import.meta.env.VITE_API_URL}/organisation/${organizationId}`, changedValues),
);
}
}
diff --git a/src/frontend/src/components/DialogTaskActions.tsx b/src/frontend/src/components/DialogTaskActions.tsx
index 8f356d487a..e44b203134 100755
--- a/src/frontend/src/components/DialogTaskActions.tsx
+++ b/src/frontend/src/components/DialogTaskActions.tsx
@@ -55,7 +55,7 @@ export default function Dialog({ taskId, feature }: dialogPropType) {
if (taskId) {
dispatch(
GetProjectTaskActivity(
- `${import.meta.env.VITE_API_URL}/tasks/${selectedTask?.id}/history/?project_id=${currentProjectId}&comments=false`,
+ `${import.meta.env.VITE_API_URL}/tasks/${selectedTask?.id}/history?project_id=${currentProjectId}&comments=false`,
),
);
}
diff --git a/src/frontend/src/components/GenerateBasemap.tsx b/src/frontend/src/components/GenerateBasemap.tsx
index fcfd81b02f..dde23eeedc 100644
--- a/src/frontend/src/components/GenerateBasemap.tsx
+++ b/src/frontend/src/components/GenerateBasemap.tsx
@@ -36,7 +36,7 @@ const GenerateBasemap = ({ projectInfo }: { projectInfo: Partial