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:
parent
2712f30382
commit
6021035e3f
@ -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',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user