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_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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user