2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-09 08:48:48 +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_REGEX") }}
{{ usersetting("SEARCH_NOTES") }}
{{ usersetting("SEARCH_PREVIEW_RESULTS") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_PARTS") }}
{{ usersetting("SEARCH_HIDE_INACTIVE_PARTS") }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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