diff --git a/backend/api/projects/resources.py b/backend/api/projects/resources.py index ffbc168d7a..7cab8165c3 100644 --- a/backend/api/projects/resources.py +++ b/backend/api/projects/resources.py @@ -520,6 +520,9 @@ def setup_search_dto(self): search_dto.mapping_types = map( str, mapping_types_str.split(",") ) # Extract list from string + search_dto.mapping_types_exact = strtobool( + request.args.get("mappingTypesExact", "false") + ) project_statuses_str = request.args.get("projectStatuses") if project_statuses_str: search_dto.project_statuses = map(str, project_statuses_str.split(",")) @@ -569,6 +572,11 @@ def get(self): - in: query name: mappingTypes type: string + - in: query + name: mappingTypesExact + type: boolean + default: false + description: if true, limits projects to match the exact mapping types requested - in: query name: organisationName description: Organisation name to search for diff --git a/backend/models/dtos/project_dto.py b/backend/models/dtos/project_dto.py index 2445a8157e..51ba001f73 100644 --- a/backend/models/dtos/project_dto.py +++ b/backend/models/dtos/project_dto.py @@ -291,6 +291,7 @@ class ProjectSearchDTO(Model): preferred_locale = StringType(default="en") mapper_level = StringType(validators=[is_known_mapping_level]) mapping_types = ListType(StringType, validators=[is_known_mapping_type]) + mapping_types_exact = BooleanType(required=False) project_statuses = ListType(StringType, validators=[is_known_project_status]) organisation_name = StringType() organisation_id = IntType() diff --git a/backend/services/project_search_service.py b/backend/services/project_search_service.py index 6c0fe23704..897c98c0bc 100644 --- a/backend/services/project_search_service.py +++ b/backend/services/project_search_service.py @@ -263,12 +263,21 @@ def _filter_projects(search_dto: ProjectSearchDTO, user): if search_dto.mapping_types: # Construct array of mapping types for query mapping_type_array = [] - mapping_type_array = [ - MappingTypes[mapping_type].value - for mapping_type in search_dto.mapping_types - ] - query = query.filter(Project.mapping_types.contains(mapping_type_array)) + if search_dto.mapping_types_exact: + mapping_type_array = [ + { + MappingTypes[mapping_type].value + for mapping_type in search_dto.mapping_types + } + ] + query = query.filter(Project.mapping_types.in_(mapping_type_array)) + else: + mapping_type_array = [ + MappingTypes[mapping_type].value + for mapping_type in search_dto.mapping_types + ] + query = query.filter(Project.mapping_types.overlap(mapping_type_array)) if search_dto.text_search: # We construct an OR search, so any projects that contain or more of the search terms should be returned diff --git a/frontend/src/components/projects/messages.js b/frontend/src/components/projects/messages.js index e216a37517..8c6e24c424 100644 --- a/frontend/src/components/projects/messages.js +++ b/frontend/src/components/projects/messages.js @@ -32,6 +32,10 @@ export default defineMessages({ id: 'project.navFilters.typesOfMapping', defaultMessage: 'Types of mapping', }, + exactMatch: { + id: 'project.navFilters.typesOfMapping.exactMatch', + defaultMessage: 'Exact match', + }, campaigns: { id: 'project.navFilters.campaigns', defaultMessage: 'All campaigns', diff --git a/frontend/src/components/projects/moreFiltersForm.js b/frontend/src/components/projects/moreFiltersForm.js index a681475eb1..af27eb9d21 100644 --- a/frontend/src/components/projects/moreFiltersForm.js +++ b/frontend/src/components/projects/moreFiltersForm.js @@ -1,10 +1,11 @@ import React from 'react'; import { Link } from '@reach/router'; -import { useQueryParam } from 'use-query-params'; +import { useQueryParam, BooleanParam } from 'use-query-params'; import { FormattedMessage } from 'react-intl'; import messages from './messages'; import { Button } from '../button'; +import { SwitchToggle } from '../formInputs'; import { useTagAPI } from '../../hooks/UseTagAPI'; import { useExploreProjectsQueryParams } from '../../hooks/UseProjectsQueryAPI'; import { MappingTypeFilterPicker } from './mappingTypeFilterPicker'; @@ -20,8 +21,9 @@ export const MoreFiltersForm = (props) => { const target = event.target; let value = target.type === 'checkbox' ? target.checked : target.value; const name = target.name; - if (name === 'types') { - //handle mappingTypes toggles in its separate fn inside that component + if (name === 'types' || !name) { + // handle mappingTypes toggles in its separate fn inside that component + // exactTypes doesn't have a name and is handled in a separate fn return; } setFormQuery( @@ -44,15 +46,15 @@ export const MoreFiltersForm = (props) => { const [orgAPIState] = useTagAPI([], 'organisations'); const [countriesAPIState] = useTagAPI([], 'countries', formatFilterCountriesData); - /* another useQueryParam for the second form */ const [mappingTypesInQuery, setMappingTypes] = useQueryParam('types', CommaArrayParam); + const [exactTypes, setExactTypes] = useQueryParam('exactTypes', BooleanParam); const fieldsetStyle = 'w-100 bn'; const titleStyle = 'w-100 db ttu fw5 blue-grey'; return (
-
+
@@ -62,6 +64,19 @@ export const MoreFiltersForm = (props) => { />
+
+ {mappingTypesInQuery && mappingTypesInQuery.length ? ( + } + isChecked={Boolean(exactTypes)} + onChange={() => setExactTypes(!exactTypes || undefined)} + labelPosition="right" + /> + ) : ( + <> + )} +
+