From 941fdde9e8aca84da0d91946e50e1a4a8d12970a Mon Sep 17 00:00:00 2001 From: Sujan Adhikari <109404840+Sujanadh@users.noreply.github.com> Date: Tue, 13 Feb 2024 22:32:35 +0545 Subject: [PATCH] feat: add community_type for organisations, add unapproved org list endpoint (#1197) * create organisation permission updated to login_required * api to list unapproved organisations * Feat: Added email and community_type field in organisation * fix: changed org_id to mandatory to create project * fix: added organisation_id in payload of create project * fix: await check crs, added email and community type in test organisation * refactor: proper enum field community type in test organisation * build: update community_type migration number & logic * refactor: remove email field from organisation * refactor: remove organisation_id from ProjectUpload model * refactor: remove email from dborg model for conftest * refactor: remove organisation_id from create_project POST json * test: fix remove organisation_id from ProjectUpload * fix: add optional organisation_id to ProjectUpload * feat: add project to org_user_dict if present * fix: extract project from org_user_dict on deletion * test: fix tests to include organisation_id extracted from fixture --------- Co-authored-by: Niraj Adhikari Co-authored-by: sujanadh Co-authored-by: spwoodcock --- src/backend/app/auth/roles.py | 7 ++++- src/backend/app/db/db_models.py | 8 +++++ src/backend/app/models/enums.py | 10 +++++++ .../app/organisations/organisation_crud.py | 7 +++++ .../app/organisations/organisation_routes.py | 11 ++++++- .../app/organisations/organisation_schemas.py | 3 +- src/backend/app/projects/project_routes.py | 10 ++----- src/backend/app/projects/project_schemas.py | 3 +- .../001-project-split-type-fields.sql | 2 +- .../migrations/009-add-community-type.sql | 29 +++++++++++++++++++ .../revert/009-add-community-type.sql | 11 +++++++ src/backend/tests/conftest.py | 5 ++-- src/backend/tests/test_projects_routes.py | 3 +- 13 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 src/backend/migrations/009-add-community-type.sql create mode 100644 src/backend/migrations/revert/009-add-community-type.sql diff --git a/src/backend/app/auth/roles.py b/src/backend/app/auth/roles.py index 240773cad9..926fa50241 100644 --- a/src/backend/app/auth/roles.py +++ b/src/backend/app/auth/roles.py @@ -207,12 +207,17 @@ async def org_admin( detail="org_id must be provided to check organisation admin role", ) - return await check_org_admin( + org_user_dict = await check_org_admin( db, user_data, org_id=org_id, ) + if project: + org_user_dict["project"] = project + + return org_user_dict + async def project_admin( project: DbProject = Depends(get_project_by_id), diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py index d6346be80b..7580d867cf 100644 --- a/src/backend/app/db/db_models.py +++ b/src/backend/app/db/db_models.py @@ -52,6 +52,7 @@ from app.db.postgis_utils import timestamp from app.models.enums import ( BackgroundTaskStatus, + CommunityType, MappingLevel, MappingPermission, OrganisationType, @@ -168,6 +169,13 @@ class DbOrganisation(Base): odk_central_user = cast(str, Column(String)) odk_central_password = cast(str, Column(String)) + community_type = cast( + CommunityType, + Column( + Enum(CommunityType), default=CommunityType.OSM_COMMUNITY, nullable=False + ), + ) + managers = relationship( DbUser, secondary=organisation_managers, diff --git a/src/backend/app/models/enums.py b/src/backend/app/models/enums.py index 0b0525f5be..01defea55f 100644 --- a/src/backend/app/models/enums.py +++ b/src/backend/app/models/enums.py @@ -286,3 +286,13 @@ class ProjectVisibility(IntEnum, Enum): PUBLIC = 0 PRIVATE = 1 INVITE_ONLY = 2 + + +class CommunityType(IntEnum, Enum): + """Enum describing community type.""" + + OSM_COMMUNITY = 0 + COMPANY = 1 + NON_PROFIT = 2 + UNIVERSITY = 3 + OTHER = 4 diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index d43d857bb5..7725cf78dc 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -51,6 +51,13 @@ async def get_organisations( return db.query(db_models.DbOrganisation).filter_by(approved=True).all() +async def get_unapproved_organisations( + db: Session, +) -> list[db_models.DbOrganisation]: + """Get unapproved orgs.""" + return db.query(db_models.DbOrganisation).filter_by(approved=False) + + async def upload_logo_to_s3( db_org: db_models.DbOrganisation, logo_file: UploadFile(None) ) -> str: diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py index ff55591afc..b61207f473 100644 --- a/src/backend/app/organisations/organisation_routes.py +++ b/src/backend/app/organisations/organisation_routes.py @@ -49,6 +49,15 @@ async def get_organisations( return await organisation_crud.get_organisations(db, current_user) +@router.get("/unapproved/", response_model=list[organisation_schemas.OrganisationOut]) +async def list_unapproved_organisations( + db: Session = Depends(database.get_db), + current_user: AuthUser = Depends(super_admin), +) -> list[DbOrganisation]: + """Get a list of all organisations.""" + return await organisation_crud.get_unapproved_organisations(db) + + @router.get("/{org_id}", response_model=organisation_schemas.OrganisationOut) async def get_organisation_detail( organisation: DbOrganisation = Depends(org_exists), @@ -64,7 +73,7 @@ async def create_organisation( org: organisation_schemas.OrganisationIn = Depends(), logo: UploadFile = File(None), db: Session = Depends(database.get_db), - current_user: DbUser = Depends(super_admin), + current_user: DbUser = Depends(login_required), ) -> organisation_schemas.OrganisationOut: """Create an organisation with the given details.""" return await organisation_crud.create_organisation(db, org, logo) diff --git a/src/backend/app/organisations/organisation_schemas.py b/src/backend/app/organisations/organisation_schemas.py index 440443e2ba..ab38d59c8e 100644 --- a/src/backend/app/organisations/organisation_schemas.py +++ b/src/backend/app/organisations/organisation_schemas.py @@ -23,7 +23,7 @@ from pydantic import BaseModel, computed_field from app.config import HttpUrlStr -from app.models.enums import OrganisationType +from app.models.enums import CommunityType, OrganisationType from app.projects.project_schemas import ODKCentralIn # class OrganisationBase(BaseModel): @@ -36,6 +36,7 @@ class OrganisationIn(ODKCentralIn): name: str description: Optional[str] = None url: Optional[HttpUrlStr] = None + community_type: Optional[CommunityType] = None @computed_field @property diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index 8ab59d61be..25bc64ab83 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -222,12 +222,12 @@ async def read_project(project_id: int, db: Session = Depends(database.get_db)): @router.delete("/{project_id}") async def delete_project( - project: db_models.DbProject = Depends(project_deps.get_project_by_id), - current_user: AuthUser = Depends(org_admin), db: Session = Depends(database.get_db), org_user_dict: db_models.DbUser = Depends(org_admin), ): """Delete a project from both ODK Central and the local database.""" + project = org_user_dict.get("project") + log.info( f"User {org_user_dict.get('user').username} attempting " f"deletion of project {project.id}" @@ -614,11 +614,7 @@ async def generate_files( """ log.debug(f"Generating media files tasks for project: {project_id}") - project = await project_crud.get_project(db, project_id) - if not project: - raise HTTPException( - status_code=428, detail=f"Project with id {project_id} does not exist" - ) + project = org_user_dict.get("project") form_category = project.xform_title custom_xls_form = None diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index f3a8d9d819..efc18dda40 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -134,8 +134,8 @@ class ProjectIn(BaseModel): """Upload new project.""" project_info: ProjectInfo - organisation_id: Optional[int] = None xform_title: str + organisation_id: Optional[int] = None hashtags: Optional[List[str]] = None task_split_type: Optional[TaskSplitType] = None task_split_dimension: Optional[int] = None @@ -202,7 +202,6 @@ class ProjectUpdate(ProjectIn): name: Optional[str] = None short_description: Optional[str] = None description: Optional[str] = None - organisation_id: Optional[int] = None class GeojsonFeature(BaseModel): diff --git a/src/backend/migrations/001-project-split-type-fields.sql b/src/backend/migrations/001-project-split-type-fields.sql index 48b230526a..31c281c8af 100644 --- a/src/backend/migrations/001-project-split-type-fields.sql +++ b/src/backend/migrations/001-project-split-type-fields.sql @@ -16,7 +16,7 @@ BEGIN ); END IF; END $$; -ALTER TYPE public.mappinglevel OWNER TO fmtm; +ALTER TYPE public.tasksplittype OWNER TO fmtm; -- Update task_split_type DO $$ diff --git a/src/backend/migrations/009-add-community-type.sql b/src/backend/migrations/009-add-community-type.sql new file mode 100644 index 0000000000..09f348bd69 --- /dev/null +++ b/src/backend/migrations/009-add-community-type.sql @@ -0,0 +1,29 @@ +-- ## Migration to: +-- * Add public.communitytype enum. +-- * Add public.organisation.community_type field. + +-- Start a transaction +BEGIN; + +-- Create communitytype enum if it doesn't exist +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'communitytype') THEN + CREATE TYPE public.communitytype AS ENUM ( + 'OSM_COMMUNITY', + 'COMPANY', + 'NON_PROFIT', + 'UNIVERSITY', + 'OTHER' + ); + END IF; +END $$; +ALTER TYPE public.communitytype OWNER TO fmtm; + +-- Add the community_type column to organisations table +ALTER TABLE IF EXISTS public.organisations + ADD COLUMN IF NOT EXISTS community_type public.communitytype + DEFAULT 'OSM_COMMUNITY'; + +-- Commit the transaction +COMMIT; diff --git a/src/backend/migrations/revert/009-add-community-type.sql b/src/backend/migrations/revert/009-add-community-type.sql new file mode 100644 index 0000000000..82122217bf --- /dev/null +++ b/src/backend/migrations/revert/009-add-community-type.sql @@ -0,0 +1,11 @@ +BEGIN; + +-- Remove the community_type column from organisations table +ALTER TABLE public.organisations + DROP COLUMN IF EXISTS community_type; + +-- Drop the communitytype enum +DROP TYPE IF EXISTS public.communitytype; + +-- Commit the transaction +COMMIT; diff --git a/src/backend/tests/conftest.py b/src/backend/tests/conftest.py index 6db87f86e4..1d9b4139b9 100644 --- a/src/backend/tests/conftest.py +++ b/src/backend/tests/conftest.py @@ -36,7 +36,7 @@ from app.db.database import Base, get_db from app.db.db_models import DbOrganisation from app.main import get_application -from app.models.enums import UserRole +from app.models.enums import CommunityType, UserRole from app.projects import project_crud from app.projects.project_schemas import ODKCentralDecrypted, ProjectInfo, ProjectUpload @@ -113,6 +113,7 @@ def organisation(db): url="https://test.org", logo="none", approved=True, + community_type=CommunityType.OSM_COMMUNITY, ) db.add(db_org) db.commit() @@ -133,7 +134,6 @@ async def project(db, admin_user, organisation): odk_central_user=os.getenv("ODK_CENTRAL_USER"), odk_central_password=os.getenv("ODK_CENTRAL_PASSWD"), hashtags=["hot-fmtm"], - organisation_id=organisation.id, outline_geojson={ "type": "Feature", "properties": {}, @@ -150,6 +150,7 @@ async def project(db, admin_user, organisation): "type": "Polygon", }, }, + organisation_id=organisation.id, ) odk_creds_decrypted = ODKCentralDecrypted( diff --git a/src/backend/tests/test_projects_routes.py b/src/backend/tests/test_projects_routes.py index 6e9fae24ec..54be08e35c 100644 --- a/src/backend/tests/test_projects_routes.py +++ b/src/backend/tests/test_projects_routes.py @@ -85,8 +85,9 @@ async def test_create_project(client, admin_user, organisation): assert "id" in response_data -async def test_delete_project(client, project): +async def test_delete_project(client, admin_user, project): """Test deleting a FMTM project, plus ODK Central project.""" + log.warning(project) response = client.delete(f"/projects/{project.id}") assert response.status_code == 204