2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-09 16:58:49 +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:
Jacob Felknor 2025-04-03 16:22:39 -06:00 committed by GitHub
parent 2712f30382
commit 6021035e3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 133 additions and 11 deletions

View File

@ -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") }}

View File

@ -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,
} }

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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'),

View File

@ -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>

View File

@ -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',