mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +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_REGEX") }} | ||||
| {{ usersetting("SEARCH_NOTES") }} | ||||
| {{ usersetting("SEARCH_PREVIEW_RESULTS") }} | ||||
| {{ usersetting("SEARCH_PREVIEW_SHOW_PARTS") }} | ||||
| {{ usersetting("SEARCH_HIDE_INACTIVE_PARTS") }} | ||||
|   | ||||
| @@ -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, | ||||
|         } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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'), | ||||
|   | ||||
| @@ -189,12 +189,23 @@ export function SearchDrawer({ | ||||
|   const [value, setValue] = useState<string>(''); | ||||
|   const [searchText] = useDebouncedValue(value, 500); | ||||
|  | ||||
|   const [searchRegex, setSearchRegex] = useState<boolean>(false); | ||||
|   const [searchWhole, setSearchWhole] = useState<boolean>(false); | ||||
|  | ||||
|   const user = useUserState(); | ||||
|   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 | ||||
|   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({ | ||||
|             </Menu.Target> | ||||
|             <Menu.Dropdown> | ||||
|               <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> | ||||
|                 <Checkbox | ||||
|                   label={t`Regex search`} | ||||
| @@ -506,10 +527,10 @@ export function SearchDrawer({ | ||||
|               </Menu.Item> | ||||
|               <Menu.Item> | ||||
|                 <Checkbox | ||||
|                   label={t`Whole word search`} | ||||
|                   checked={searchWhole} | ||||
|                   label={t`Notes search`} | ||||
|                   checked={searchNotes} | ||||
|                   onChange={(event) => | ||||
|                     setSearchWhole(event.currentTarget.checked) | ||||
|                     setSearchNotes(event.currentTarget.checked) | ||||
|                   } | ||||
|                 /> | ||||
|               </Menu.Item> | ||||
|   | ||||
| @@ -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', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user