From 6021035e3f866c099a57d1ccf8c8e9d28d429b60 Mon Sep 17 00:00:00 2001 From: Jacob Felknor Date: Thu, 3 Apr 2025 16:22:39 -0600 Subject: [PATCH] User Setting To Search Notes (#9393) * add search notes and tie user search settings to checkboxes in search drawer * add user setting to optionally search the notes of objects * add search filter test * add PR link * add limit parameter * typo, meant to check part result * resolve api_version.py conflict * don't use search_whole and search_regex together --------- Co-authored-by: Matthias Mair --- docs/docs/settings/user.md | 1 + src/backend/InvenTree/InvenTree/api.py | 2 + .../InvenTree/InvenTree/api_version.py | 5 +- src/backend/InvenTree/InvenTree/filters.py | 13 +++- src/backend/InvenTree/InvenTree/test_api.py | 77 +++++++++++++++++++ src/backend/InvenTree/common/setting/user.py | 8 ++ .../src/components/nav/SearchDrawer.tsx | 37 +++++++-- .../src/pages/Index/Settings/UserSettings.tsx | 1 + 8 files changed, 133 insertions(+), 11 deletions(-) diff --git a/docs/docs/settings/user.md b/docs/docs/settings/user.md index dc69339d59..999cb0536a 100644 --- a/docs/docs/settings/user.md +++ b/docs/docs/settings/user.md @@ -38,6 +38,7 @@ Customize settings for search results: | ---- | ----------- | ------- | ----- | {{ usersetting("SEARCH_WHOLE") }} {{ usersetting("SEARCH_REGEX") }} +{{ usersetting("SEARCH_NOTES") }} {{ usersetting("SEARCH_PREVIEW_RESULTS") }} {{ usersetting("SEARCH_PREVIEW_SHOW_PARTS") }} {{ usersetting("SEARCH_HIDE_INACTIVE_PARTS") }} diff --git a/src/backend/InvenTree/InvenTree/api.py b/src/backend/InvenTree/InvenTree/api.py index f76b3bea3f..8a0f4e0f68 100644 --- a/src/backend/InvenTree/InvenTree/api.py +++ b/src/backend/InvenTree/InvenTree/api.py @@ -582,6 +582,7 @@ class APISearchViewSerializer(serializers.Serializer): search = serializers.CharField() search_regex = serializers.BooleanField(default=False, required=False) search_whole = serializers.BooleanField(default=False, required=False) + search_notes = serializers.BooleanField(default=False, required=False) limit = serializers.IntegerField(default=1, required=False) offset = serializers.IntegerField(default=0, required=False) @@ -643,6 +644,7 @@ class APISearchView(GenericAPIView): 'search': '', 'search_regex': False, 'search_whole': False, + 'search_notes': False, 'limit': 1, 'offset': 0, } diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 0c6f105590..f82170449b 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 331 +INVENTREE_API_VERSION = 332 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v332 - 2025-04-02 : https://github.com/inventree/InvenTree/pull/9393 + - Adds 'search_notes' parameter to all searchable API endpoints + v331 - 2025-04-01 : https://github.com/inventree/InvenTree/pull/9437 - Set correct types on various formerly-string PK fields as well permissions - Include metadata request and response types diff --git a/src/backend/InvenTree/InvenTree/filters.py b/src/backend/InvenTree/InvenTree/filters.py index b5dafa181a..30745679a0 100644 --- a/src/backend/InvenTree/InvenTree/filters.py +++ b/src/backend/InvenTree/InvenTree/filters.py @@ -32,14 +32,23 @@ class InvenTreeSearchFilter(filters.SearchFilter): """Return a set of search fields for the request, adjusted based on request params. The following query params are available to 'augment' the search (in decreasing order of priority) + - search_notes: If True, 'notes' is added to the search_fields if it isn't already present - search_regex: If True, search is performed on 'regex' comparison """ - regex = InvenTree.helpers.str2bool( - request.query_params.get('search_regex', False) + search_notes = InvenTree.helpers.str2bool( + request.query_params.get('search_notes', False) ) search_fields = super().get_search_fields(view, request) + if search_notes and 'notes' not in search_fields: + # don't modify existing list, create a new object so further queries aren't affected + search_fields = [*search_fields, 'notes'] + + regex = InvenTree.helpers.str2bool( + request.query_params.get('search_regex', False) + ) + fields = [] if search_fields: diff --git a/src/backend/InvenTree/InvenTree/test_api.py b/src/backend/InvenTree/InvenTree/test_api.py index f6defd64e5..8d3f9c4086 100644 --- a/src/backend/InvenTree/InvenTree/test_api.py +++ b/src/backend/InvenTree/InvenTree/test_api.py @@ -297,6 +297,7 @@ class SearchTests(InvenTreeAPITestCase): 'stock', 'order', 'sales_order', + 'build', ] roles = ['build.view', 'part.view'] @@ -353,6 +354,82 @@ class SearchTests(InvenTreeAPITestCase): self.assertNotIn('stockitem', response.data) self.assertNotIn('build', response.data) + def test_search_filters(self): + """Test that the regex, whole word, and notes filters are handled correctly.""" + SEARCH_TERM = 'some note' + RE_SEARCH_TERM = 'some (.*) note' + + response = self.post( + reverse('api-search'), + {'search': SEARCH_TERM, 'limit': 10, 'part': {}, 'build': {}}, + expected_code=200, + ) + # No build or part results + self.assertEqual(response.data['build']['count'], 0) + self.assertEqual(response.data['part']['count'], 0) + + # add the search_notes param + response = self.post( + reverse('api-search'), + { + 'search': SEARCH_TERM, + 'limit': 10, + 'search_notes': True, + 'part': {}, + 'build': {}, + }, + expected_code=200, + ) + # now should have some build results + self.assertEqual(response.data['build']['count'], 4) + + # use the regex term + response = self.post( + reverse('api-search'), + { + 'search': RE_SEARCH_TERM, + 'limit': 10, + 'search_notes': True, + 'part': {}, + 'build': {}, + }, + expected_code=200, + ) + # No results again + self.assertEqual(response.data['build']['count'], 0) + + # add the regex_search param + response = self.post( + reverse('api-search'), + { + 'search': RE_SEARCH_TERM, + 'limit': 10, + 'search_notes': True, + 'search_regex': True, + 'part': {}, + 'build': {}, + }, + expected_code=200, + ) + # we get our results back! + self.assertEqual(response.data['build']['count'], 4) + + # add the search_whole param + response = self.post( + reverse('api-search'), + { + 'search': RE_SEARCH_TERM, + 'limit': 10, + 'search_notes': True, + 'search_whole': True, + 'part': {}, + 'build': {}, + }, + expected_code=200, + ) + # No results again + self.assertEqual(response.data['build']['count'], 0) + def test_permissions(self): """Test that users with insufficient permissions are handled correctly.""" # First, remove all roles diff --git a/src/backend/InvenTree/common/setting/user.py b/src/backend/InvenTree/common/setting/user.py index 99eb41bfbb..bfb6e6956a 100644 --- a/src/backend/InvenTree/common/setting/user.py +++ b/src/backend/InvenTree/common/setting/user.py @@ -165,6 +165,14 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = { 'default': False, 'validator': bool, }, + 'SEARCH_NOTES': { + 'name': _('Search Notes'), + 'description': _( + "Search queries return results for matches from the item's notes" + ), + 'default': False, + 'validator': bool, + }, 'PART_SHOW_QUANTITY_IN_FORMS': { 'name': _('Show Quantity in Forms'), 'description': _('Display available part quantity in some forms'), diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx index c8fd710be5..cc9e0b8c99 100644 --- a/src/frontend/src/components/nav/SearchDrawer.tsx +++ b/src/frontend/src/components/nav/SearchDrawer.tsx @@ -189,12 +189,23 @@ export function SearchDrawer({ const [value, setValue] = useState(''); const [searchText] = useDebouncedValue(value, 500); - const [searchRegex, setSearchRegex] = useState(false); - const [searchWhole, setSearchWhole] = useState(false); - const user = useUserState(); const userSettings = useUserSettingsState(); + const [searchRegex, setSearchRegex] = useState(false); + const [searchWhole, setSearchWhole] = useState(false); + const [searchNotes, setSearchNotes] = useState(false); + + useEffect(() => { + setSearchRegex(userSettings.isSet('SEARCH_REGEX', false)); + setSearchWhole(userSettings.isSet('SEARCH_WHOLE', false)); + setSearchNotes(userSettings.isSet('SEARCH_NOTES', false)); + }, [ + userSettings.isSet('SEARCH_REGEX', false), + userSettings.isSet('SEARCH_WHOLE', false), + userSettings.isSet('SEARCH_NOTES', false) + ]); + // Build out search queries based on user permissions and preferences const searchQueryList: SearchQuery[] = useMemo(() => { return [ @@ -373,7 +384,8 @@ export function SearchDrawer({ limit: userSettings.getSetting('SEARCH_PREVIEW_RESULTS', '10'), search: searchText, search_regex: searchRegex, - search_whole: searchWhole + search_whole: searchWhole, + search_notes: searchNotes }; // Add in custom query parameters @@ -393,7 +405,7 @@ export function SearchDrawer({ // Search query manager const searchQuery = useQuery({ - queryKey: ['search', searchText, searchRegex, searchWhole], + queryKey: ['search', searchText, searchRegex, searchWhole, searchNotes], queryFn: performSearch }); @@ -495,6 +507,15 @@ export function SearchDrawer({ {t`Search Options`} + + + setSearchWhole(event.currentTarget.checked) + } + /> + - setSearchWhole(event.currentTarget.checked) + setSearchNotes(event.currentTarget.checked) } /> diff --git a/src/frontend/src/pages/Index/Settings/UserSettings.tsx b/src/frontend/src/pages/Index/Settings/UserSettings.tsx index f38eb67bea..84e8e2f808 100644 --- a/src/frontend/src/pages/Index/Settings/UserSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/UserSettings.tsx @@ -71,6 +71,7 @@ export default function UserSettings() { keys={[ 'SEARCH_WHOLE', 'SEARCH_REGEX', + 'SEARCH_NOTES', 'SEARCH_PREVIEW_RESULTS', 'SEARCH_PREVIEW_SHOW_PARTS', 'SEARCH_HIDE_INACTIVE_PARTS',