mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	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 <code@mjmair.com>
This commit is contained in:
		| @@ -38,6 +38,7 @@ Customize settings for search results: | |||||||
| | ---- | ----------- | ------- | ----- | | | ---- | ----------- | ------- | ----- | | ||||||
| {{ usersetting("SEARCH_WHOLE") }} | {{ usersetting("SEARCH_WHOLE") }} | ||||||
| {{ usersetting("SEARCH_REGEX") }} | {{ usersetting("SEARCH_REGEX") }} | ||||||
|  | {{ usersetting("SEARCH_NOTES") }} | ||||||
| {{ usersetting("SEARCH_PREVIEW_RESULTS") }} | {{ usersetting("SEARCH_PREVIEW_RESULTS") }} | ||||||
| {{ usersetting("SEARCH_PREVIEW_SHOW_PARTS") }} | {{ usersetting("SEARCH_PREVIEW_SHOW_PARTS") }} | ||||||
| {{ usersetting("SEARCH_HIDE_INACTIVE_PARTS") }} | {{ usersetting("SEARCH_HIDE_INACTIVE_PARTS") }} | ||||||
|   | |||||||
| @@ -582,6 +582,7 @@ class APISearchViewSerializer(serializers.Serializer): | |||||||
|     search = serializers.CharField() |     search = serializers.CharField() | ||||||
|     search_regex = serializers.BooleanField(default=False, required=False) |     search_regex = serializers.BooleanField(default=False, required=False) | ||||||
|     search_whole = 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) |     limit = serializers.IntegerField(default=1, required=False) | ||||||
|     offset = serializers.IntegerField(default=0, required=False) |     offset = serializers.IntegerField(default=0, required=False) | ||||||
|  |  | ||||||
| @@ -643,6 +644,7 @@ class APISearchView(GenericAPIView): | |||||||
|             'search': '', |             'search': '', | ||||||
|             'search_regex': False, |             'search_regex': False, | ||||||
|             'search_whole': False, |             'search_whole': False, | ||||||
|  |             'search_notes': False, | ||||||
|             'limit': 1, |             'limit': 1, | ||||||
|             'offset': 0, |             'offset': 0, | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,12 +1,15 @@ | |||||||
| """InvenTree API version information.""" | """InvenTree API version information.""" | ||||||
|  |  | ||||||
| # InvenTree API version | # 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.""" | """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" | ||||||
|  |  | ||||||
|  |  | ||||||
| INVENTREE_API_TEXT = """ | 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 | v331 - 2025-04-01 : https://github.com/inventree/InvenTree/pull/9437 | ||||||
|     - Set correct types on various formerly-string PK fields as well permissions |     - Set correct types on various formerly-string PK fields as well permissions | ||||||
|     - Include metadata request and response types |     - Include metadata request and response types | ||||||
|   | |||||||
| @@ -32,14 +32,23 @@ class InvenTreeSearchFilter(filters.SearchFilter): | |||||||
|         """Return a set of search fields for the request, adjusted based on request params. |         """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) |         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 |         - search_regex: If True, search is performed on 'regex' comparison | ||||||
|         """ |         """ | ||||||
|         regex = InvenTree.helpers.str2bool( |         search_notes = InvenTree.helpers.str2bool( | ||||||
|             request.query_params.get('search_regex', False) |             request.query_params.get('search_notes', False) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         search_fields = super().get_search_fields(view, request) |         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 = [] |         fields = [] | ||||||
|  |  | ||||||
|         if search_fields: |         if search_fields: | ||||||
|   | |||||||
| @@ -297,6 +297,7 @@ class SearchTests(InvenTreeAPITestCase): | |||||||
|         'stock', |         'stock', | ||||||
|         'order', |         'order', | ||||||
|         'sales_order', |         'sales_order', | ||||||
|  |         'build', | ||||||
|     ] |     ] | ||||||
|     roles = ['build.view', 'part.view'] |     roles = ['build.view', 'part.view'] | ||||||
|  |  | ||||||
| @@ -353,6 +354,82 @@ class SearchTests(InvenTreeAPITestCase): | |||||||
|         self.assertNotIn('stockitem', response.data) |         self.assertNotIn('stockitem', response.data) | ||||||
|         self.assertNotIn('build', 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): |     def test_permissions(self): | ||||||
|         """Test that users with insufficient permissions are handled correctly.""" |         """Test that users with insufficient permissions are handled correctly.""" | ||||||
|         # First, remove all roles |         # First, remove all roles | ||||||
|   | |||||||
| @@ -165,6 +165,14 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = { | |||||||
|         'default': False, |         'default': False, | ||||||
|         'validator': bool, |         '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': { |     'PART_SHOW_QUANTITY_IN_FORMS': { | ||||||
|         'name': _('Show Quantity in Forms'), |         'name': _('Show Quantity in Forms'), | ||||||
|         'description': _('Display available part quantity in some forms'), |         'description': _('Display available part quantity in some forms'), | ||||||
|   | |||||||
| @@ -189,12 +189,23 @@ export function SearchDrawer({ | |||||||
|   const [value, setValue] = useState<string>(''); |   const [value, setValue] = useState<string>(''); | ||||||
|   const [searchText] = useDebouncedValue(value, 500); |   const [searchText] = useDebouncedValue(value, 500); | ||||||
|  |  | ||||||
|   const [searchRegex, setSearchRegex] = useState<boolean>(false); |  | ||||||
|   const [searchWhole, setSearchWhole] = useState<boolean>(false); |  | ||||||
|  |  | ||||||
|   const user = useUserState(); |   const user = useUserState(); | ||||||
|   const userSettings = useUserSettingsState(); |   const userSettings = useUserSettingsState(); | ||||||
|  |  | ||||||
|  |   const [searchRegex, setSearchRegex] = useState<boolean>(false); | ||||||
|  |   const [searchWhole, setSearchWhole] = useState<boolean>(false); | ||||||
|  |   const [searchNotes, setSearchNotes] = useState<boolean>(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 |   // Build out search queries based on user permissions and preferences | ||||||
|   const searchQueryList: SearchQuery[] = useMemo(() => { |   const searchQueryList: SearchQuery[] = useMemo(() => { | ||||||
|     return [ |     return [ | ||||||
| @@ -373,7 +384,8 @@ export function SearchDrawer({ | |||||||
|       limit: userSettings.getSetting('SEARCH_PREVIEW_RESULTS', '10'), |       limit: userSettings.getSetting('SEARCH_PREVIEW_RESULTS', '10'), | ||||||
|       search: searchText, |       search: searchText, | ||||||
|       search_regex: searchRegex, |       search_regex: searchRegex, | ||||||
|       search_whole: searchWhole |       search_whole: searchWhole, | ||||||
|  |       search_notes: searchNotes | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     // Add in custom query parameters |     // Add in custom query parameters | ||||||
| @@ -393,7 +405,7 @@ export function SearchDrawer({ | |||||||
|  |  | ||||||
|   // Search query manager |   // Search query manager | ||||||
|   const searchQuery = useQuery({ |   const searchQuery = useQuery({ | ||||||
|     queryKey: ['search', searchText, searchRegex, searchWhole], |     queryKey: ['search', searchText, searchRegex, searchWhole, searchNotes], | ||||||
|     queryFn: performSearch |     queryFn: performSearch | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| @@ -495,6 +507,15 @@ export function SearchDrawer({ | |||||||
|             </Menu.Target> |             </Menu.Target> | ||||||
|             <Menu.Dropdown> |             <Menu.Dropdown> | ||||||
|               <Menu.Label>{t`Search Options`}</Menu.Label> |               <Menu.Label>{t`Search Options`}</Menu.Label> | ||||||
|  |               <Menu.Item> | ||||||
|  |                 <Checkbox | ||||||
|  |                   label={t`Whole word search`} | ||||||
|  |                   checked={searchWhole} | ||||||
|  |                   onChange={(event) => | ||||||
|  |                     setSearchWhole(event.currentTarget.checked) | ||||||
|  |                   } | ||||||
|  |                 /> | ||||||
|  |               </Menu.Item> | ||||||
|               <Menu.Item> |               <Menu.Item> | ||||||
|                 <Checkbox |                 <Checkbox | ||||||
|                   label={t`Regex search`} |                   label={t`Regex search`} | ||||||
| @@ -506,10 +527,10 @@ export function SearchDrawer({ | |||||||
|               </Menu.Item> |               </Menu.Item> | ||||||
|               <Menu.Item> |               <Menu.Item> | ||||||
|                 <Checkbox |                 <Checkbox | ||||||
|                   label={t`Whole word search`} |                   label={t`Notes search`} | ||||||
|                   checked={searchWhole} |                   checked={searchNotes} | ||||||
|                   onChange={(event) => |                   onChange={(event) => | ||||||
|                     setSearchWhole(event.currentTarget.checked) |                     setSearchNotes(event.currentTarget.checked) | ||||||
|                   } |                   } | ||||||
|                 /> |                 /> | ||||||
|               </Menu.Item> |               </Menu.Item> | ||||||
|   | |||||||
| @@ -71,6 +71,7 @@ export default function UserSettings() { | |||||||
|             keys={[ |             keys={[ | ||||||
|               'SEARCH_WHOLE', |               'SEARCH_WHOLE', | ||||||
|               'SEARCH_REGEX', |               'SEARCH_REGEX', | ||||||
|  |               'SEARCH_NOTES', | ||||||
|               'SEARCH_PREVIEW_RESULTS', |               'SEARCH_PREVIEW_RESULTS', | ||||||
|               'SEARCH_PREVIEW_SHOW_PARTS', |               'SEARCH_PREVIEW_SHOW_PARTS', | ||||||
|               'SEARCH_HIDE_INACTIVE_PARTS', |               'SEARCH_HIDE_INACTIVE_PARTS', | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user