diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_crud.py index bd5cf5946..e2d472b31 100644 --- a/src/backend/app/projects/project_crud.py +++ b/src/backend/app/projects/project_crud.py @@ -263,7 +263,7 @@ async def partial_update_project_info( return await convert_to_app_project(db_project) -async def update_project_info( +async def update_project_with_project_info( db: Session, project_metadata: project_schemas.ProjectUpdate, db_project: db_models.DbProject, @@ -278,24 +278,20 @@ async def update_project_info( detail="No project info passed in", ) - # Project meta information - project_info = project_metadata.project_info - - # Update author of the project - db_project.author_id = db_user.id - db_project.project_name_prefix = project_metadata.project_name_prefix + for key, value in project_metadata.model_dump( + exclude=["project_info", "outline_geojson"] + ).items(): + setattr(db_project, key, value) - # get project info db_project_info = await get_project_info_by_id(db, db_project.id) - - # Update projects meta information (name, descriptions) - if db_project and db_project_info: - db_project_info.name = project_info.name - db_project_info.short_description = project_info.short_description - db_project_info.description = project_info.description + # Update project's meta information (name, descriptions) + if db_project_info: + for key, value in project_info.model_dump(exclude_unset=True).items(): + setattr(db_project_info, key, value) db.commit() db.refresh(db_project) + db.refresh(db_project_info) return await convert_to_app_project(db_project) diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index 5d9ae27ef..6a1a89a9c 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -557,7 +557,7 @@ async def update_project( Raises: - HTTPException with 404 status code if project not found """ - project = await project_crud.update_project_info( + project = await project_crud.update_project_with_project_info( db, project_info, project_user_dict["project"], project_user_dict["user"] ) if not project: diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 1005a9ad1..605d10dae 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -143,7 +143,7 @@ class ProjectIn(BaseModel): xform_category: str custom_tms_url: Optional[str] = None organisation_id: Optional[int] = None - hashtags: Optional[List[str]] = None + hashtags: Optional[str] = None task_split_type: Optional[TaskSplitType] = None task_split_dimension: Optional[int] = None task_num_buildings: Optional[int] = None @@ -177,11 +177,24 @@ def project_name_prefix(self) -> str: @field_validator("hashtags", mode="after") @classmethod - def prepend_hash_to_tags(cls, hashtags: List[str]) -> Optional[List[str]]: - """Add '#' to hashtag if missing. Also added default '#FMTM'.""" + def validate_hashtags(cls, hashtags: Optional[str]) -> List[str]: + """Validate hashtags. + + - Receives a string and parsed as a list of tags. + - Commas or semicolons are replaced with spaces before splitting. + - Add '#' to hashtag if missing. + - Also add default '#FMTM' tag. + """ + if hashtags is None: + return ["#FMTM"] + + hashtags = hashtags.replace(",", " ").replace(";", " ") + hashtags_list = hashtags.split() + + # Add '#' to hashtag strings if missing hashtags_with_hash = [ f"#{hashtag}" if hashtag and not hashtag.startswith("#") else hashtag - for hashtag in hashtags + for hashtag in hashtags_list ] if "#FMTM" not in hashtags_with_hash: @@ -233,7 +246,7 @@ def project_name_prefix(self) -> str: return self.name.replace(" ", "_").lower() -class ProjectUpdate(ProjectIn): +class ProjectUpdate(ProjectUpload): """Update project.""" pass diff --git a/src/backend/tests/conftest.py b/src/backend/tests/conftest.py index fd5299be5..27564c3e3 100644 --- a/src/backend/tests/conftest.py +++ b/src/backend/tests/conftest.py @@ -134,7 +134,7 @@ async def project(db, admin_user, organisation): odk_central_url=os.getenv("ODK_CENTRAL_URL"), odk_central_user=os.getenv("ODK_CENTRAL_USER"), odk_central_password=os.getenv("ODK_CENTRAL_PASSWD"), - hashtags=["hot-fmtm"], + hashtags="hashtag1 hashtag2", outline_geojson=Polygon( type="Polygon", coordinates=[ diff --git a/src/backend/tests/test_projects_routes.py b/src/backend/tests/test_projects_routes.py index c10121682..722cd2488 100644 --- a/src/backend/tests/test_projects_routes.py +++ b/src/backend/tests/test_projects_routes.py @@ -57,7 +57,7 @@ async def test_create_project(client, admin_user, organisation): "description": "test", }, "xform_category": "buildings", - "hashtags": ["#FMTM"], + "hashtags": "#FMTM", "outline_geojson": { "coordinates": [ [ @@ -283,8 +283,8 @@ async def test_update_project(client, admin_user, project): "short_description": "updated short description", "description": "updated description", }, - "xform_category": "buildings", - "hashtags": ["#FMTM"], + "xform_category": "healthcare", + "hashtags": "#FMTM anothertag", "outline_geojson": { "coordinates": [ [ @@ -320,6 +320,9 @@ async def test_update_project(client, admin_user, project): == updated_project_data["project_info"]["description"] ) + assert response_data["xform_category"] == "healthcare" + assert response_data["hashtags"] == ["#FMTM", "#anothertag"] + async def test_project_summaries(client, project): """Test read project summaries.""" diff --git a/src/frontend/src/components/createnewproject/ProjectDetailsForm.tsx b/src/frontend/src/components/createnewproject/ProjectDetailsForm.tsx index dd0f1afd9..8c1fdfa3f 100644 --- a/src/frontend/src/components/createnewproject/ProjectDetailsForm.tsx +++ b/src/frontend/src/components/createnewproject/ProjectDetailsForm.tsx @@ -58,12 +58,6 @@ const ProjectDetailsForm = ({ flag }) => { }; }, []); - // Checks if hashtag value starts with hotosm-fmtm' - const handleHashtagOnChange = (e) => { - let enteredText = e.target.value; - handleCustomChange('hashtags', enteredText); - }; - const handleInputChanges = (e) => { handleChange(e); dispatch(CreateProjectActions.SetIsUnsavedChanges(true)); @@ -240,7 +234,7 @@ const ProjectDetailsForm = ({ flag }) => { label="Hashtags" value={values?.hashtags} onChange={(e) => { - handleHashtagOnChange(e); + handleCustomChange('hashtags', e.target.value); }} fieldType="text" errorMsg={errors.hashtag} diff --git a/src/frontend/src/components/createnewproject/SplitTasks.tsx b/src/frontend/src/components/createnewproject/SplitTasks.tsx index 292247190..4a5875d96 100644 --- a/src/frontend/src/components/createnewproject/SplitTasks.tsx +++ b/src/frontend/src/components/createnewproject/SplitTasks.tsx @@ -92,12 +92,6 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customDataExtractUpload dispatch(CreateProjectActions.SetIsUnsavedChanges(false)); dispatch(CreateProjectActions.SetIndividualProjectDetailsData(formValues)); - const hashtags = projectDetails.hashtags; - const arrayHashtag = hashtags - ?.split('#') - .map((item) => item.trim()) - .filter(Boolean); - // Project POST data let projectData = { project_info: { @@ -116,7 +110,7 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customDataExtractUpload task_split_type: splitTasksSelection, form_ways: projectDetails.formWays, // "uploaded_form": projectDetails.uploaded_form, - hashtags: arrayHashtag, + hashtags: projectDetails.hashtags, data_extract_url: projectDetails.data_extract_url, custom_tms_url: projectDetails.custom_tms_url, }; diff --git a/src/frontend/src/store/types/ICreateProject.ts b/src/frontend/src/store/types/ICreateProject.ts index 76f3fb4d4..013b791c4 100644 --- a/src/frontend/src/store/types/ICreateProject.ts +++ b/src/frontend/src/store/types/ICreateProject.ts @@ -83,7 +83,7 @@ type EditProjectResponseTypes = { outline_geojson: GeoJSONFeatureTypes; tasks: ProjectTaskTypes[]; xform_category: string; - hashtags: string[]; + hashtags: string; }; export type EditProjectDetailsTypes = { name: string;