From c628af4f4907e73e2bc7b1e434b590435452f1e6 Mon Sep 17 00:00:00 2001 From: Roland Date: Tue, 21 Nov 2023 14:46:55 -0700 Subject: [PATCH] Removing result categories when no results exist in those categories. --- app/src/api.js | 5 +++ app/src/components/Search.js | 85 +++++++++++++++++++++++++----------- wrolpi/root_api.py | 29 +++++++++++- wrolpi/schema.py | 1 + wrolpi/test/test_root_api.py | 15 +++++-- 5 files changed, 106 insertions(+), 29 deletions(-) diff --git a/app/src/api.js b/app/src/api.js index 21d8236e..12506aee 100644 --- a/app/src/api.js +++ b/app/src/api.js @@ -1024,10 +1024,15 @@ export async function searchSuggestions(search_str) { const response = await apiPost(`${API_URI}/search_suggestions`, body); if (response.ok) { const content = await response.json(); + + const fileGroups = content['file_groups']; + const zimsEstimates = content['zims_estimates']; const channels = content['channels']; const domains = content['domains']; const tags = content['tags']; return { + fileGroups, + zimsEstimates, channels, domains, tags, diff --git a/app/src/components/Search.js b/app/src/components/Search.js index 34906fee..0f7d2d1e 100644 --- a/app/src/components/Search.js +++ b/app/src/components/Search.js @@ -15,6 +15,7 @@ const suggestedApps = [ {location: '/more/vin', title: 'Vin Decoder', description: 'Decode and analyze vehicle VIN numbers'}, {location: '/admin', title: 'Downloads', description: 'View your downloads'}, {location: '/admin/settings', title: 'Settings', description: 'View and modify your settings'}, + {location: '/admin/status', title: 'Status', description: 'View the status of this WROLPi'}, {location: '/admin/wrol', title: 'WROL Mode', description: 'Enable or disable WROL Mode'}, ]; @@ -31,29 +32,55 @@ export function useSearchSuggestions() { if (searchStr && searchStr.length > 0) { const lowerSearchStr = searchStr.toLowerCase(); try { - const i = await searchSuggestions(searchStr); + const newSuggestions = await searchSuggestions(searchStr); const matchingApps = suggestedApps.filter(i => i.title.toLowerCase().includes(lowerSearchStr) || fuzzyMatch(i.title.toLowerCase(), lowerSearchStr)); - const matchingSuggestions = { - channels: { - name: 'Channels', results: i.channels.map(i => { - return {title: i['name'], id: i['id'], type: 'channel'} + + const zimSum = newSuggestions.zimsEstimates.reduce((i, j) => i + j['estimate'], 0); + + // Suggested results are ordered. + let matchingSuggestions = {}; + if (newSuggestions.fileGroups > 0) { + matchingSuggestions.fileGroups = { + name: 'Files', results: [ + {description: newSuggestions.fileGroups, type: 'files'} + ] + }; + } + if (zimSum > 0) { + matchingSuggestions.zimsSum = { + name: 'Zims', results: [ + {description: zimSum, type: 'zims'} + ], + } + } + if (newSuggestions.channels && newSuggestions.channels.length > 0) { + matchingSuggestions.channels = { + name: 'Channels', results: newSuggestions.channels.map(i => { + return {type: 'channel', description: i['name'], id: i['id']} }) - }, - domains: { - name: 'Domains', results: i.domains.map(i => { - return {title: i['domain'], id: i['id'], type: 'domain'} + } + } + if (newSuggestions.domains && newSuggestions.domains.length > 0) { + matchingSuggestions.domains = { + name: 'Domains', results: newSuggestions.domains.map(i => { + return {type: 'domain', description: i.domain, id: i['id'], domain: i.domain} }) - }, - tags: { - name: 'Tags', results: i.tags.map(i => { - return {title: i['name'], type: 'tag'} + } + } + if (newSuggestions.tags && newSuggestions.tags.length > 0) { + matchingSuggestions.tags = { + name: 'Tags', results: newSuggestions.tags.map(i => { + return {type: 'tag', description: i['name']} }) - }, - apps: {name: 'Apps', results: matchingApps}, - }; - console.debug('newSuggestions', matchingSuggestions); + } + } + if (matchingApps && matchingApps.length > 0) { + matchingSuggestions.apps = {name: 'Apps', results: matchingApps}; + } + + console.debug('matchingSuggestions', matchingSuggestions); setSuggestions(matchingSuggestions); } catch (e) { console.error(e); @@ -75,14 +102,19 @@ export function useSearchSuggestions() { } if (result.location) { - navigate(result.location); + return navigate(result.location); } else if (result['type'] === 'channel') { - navigate(`/videos/channel/${result['id']}/video`); + return navigate(`/videos/channel/${result['id']}/video`); } else if (result['type'] === 'domain') { - navigate(`/archive?domain=${result['domain']}`); + return navigate(`/archive?domain=${result['domain']}`); } else if (result['type'] === 'tag') { - navigate(`/search?tag=${encodeURIComponent(result['title'])}`); + return navigate(`/search?tag=${encodeURIComponent(result['title'])}`); + } else if (result['type'] === 'files') { + return navigate(`/search?q=${encodeURIComponent(searchStr)}`); + } else if (result['type'] === 'zims') { + return navigate(`/search/zim?q=${encodeURIComponent(searchStr)}`); } + console.error('No handleResultSelect defined:', result); }; const resultRenderer = ({type, title, description}) => { @@ -92,10 +124,13 @@ export function useSearchSuggestions() { } // No specific renderer, use the generic. - return <> - {title} - {description} - + if (title) { + return <> + {title} + {description} + + } + return {description} }; return {suggestions, searchStr, handleResultSelect, resultRenderer} diff --git a/wrolpi/root_api.py b/wrolpi/root_api.py index 2626609f..9bf294c4 100644 --- a/wrolpi/root_api.py +++ b/wrolpi/root_api.py @@ -551,13 +551,40 @@ async def post_search_suggestions(_: Request, body: schema.SearchSuggestionsRequ """Used by the Global search to suggest links to the user.""" from modules.videos.channel.lib import search_channels_by_name from modules.archive.lib import search_domains_by_name + from modules.zim.models import Zims - channels, domains, tags_ = await asyncio.gather( + if not body.search_str and not body.tag_names: + return response.empty(HTTPStatus.BAD_REQUEST) + + file_groups, channels, domains, tags_ = await asyncio.gather( + estimate_search(body.search_str, body.tag_names), search_channels_by_name(body.search_str), search_domains_by_name(body.search_str), tags.search_tags_by_name(body.search_str), ) + + if body.tag_names: + # Get actual count of entries tagged with the tag names. + zims_estimates = list() + for zim, count in Zims.entries_with_tags(body.tag_names).items(): + d = dict( + estimate=count, + **zim.__json__(), + ) + zims_estimates.append(d) + else: + # Get estimates using libzim. + zims_estimates = list() + for zim, estimate in Zims.estimate(body.search_str).items(): + d = dict( + estimate=estimate, + **zim.__json__(), + ) + zims_estimates.append(d) + ret = dict( + file_groups=file_groups, + zims_estimates=zims_estimates, channels=channels, domains=domains, tags=tags_, diff --git a/wrolpi/schema.py b/wrolpi/schema.py index baa3b18c..0655f384 100644 --- a/wrolpi/schema.py +++ b/wrolpi/schema.py @@ -146,3 +146,4 @@ class SearchEstimateRequest: @dataclass class SearchSuggestionsRequest: search_str: Optional[str] = None + tag_names: List[str] = field(default_factory=lambda: list()) diff --git a/wrolpi/test/test_root_api.py b/wrolpi/test/test_root_api.py index e9b15166..e5921e9c 100644 --- a/wrolpi/test/test_root_api.py +++ b/wrolpi/test/test_root_api.py @@ -488,12 +488,14 @@ def test_search_str_estimate(test_session, test_client, test_directory, test_zim @pytest.mark.asyncio -async def test_search_suggestions(test_session, test_async_client, channel_factory, archive_factory, tag_factory): +async def test_search_suggestions(test_session, test_async_client, channel_factory, archive_factory, tag_factory, + zim_factory): channel_factory(name='Foo') channel_factory(name='Fool') channel_factory(name='Bar') - archive_factory(domain='foo.com') - archive_factory(domain='bar.com') + archive_factory(domain='foo.com', contents='contents of foo') + archive_factory(domain='bar.com', contents='contents of bar') + zim_factory('test zim') tag_factory() test_session.commit() @@ -506,12 +508,17 @@ async def test_search_suggestions(test_session, test_async_client, channel_facto 'name': 'Fool', 'url': 'https://example.com/Fool'}] assert response.json['domains'] == [{'directory': 'archive/foo.com', 'domain': 'foo.com', 'id': 1}] + assert response.json['file_groups'] == 1 + assert 'zims_estimates' in response.json and len(response.json['zims_estimates']) == 1 + assert response.json['zims_estimates'][0]['estimate'] == 0 body = dict(search_str='bar') request, response = await test_async_client.post('/api/search_suggestions', json=body) assert response.status_code == HTTPStatus.OK assert response.json['channels'] == [{'directory': 'Bar', 'id': 3, 'name': 'Bar', 'url': 'https://example.com/Bar'}] assert response.json['domains'] == [{'directory': 'archive/bar.com', 'domain': 'bar.com', 'id': 2}] + assert response.json['file_groups'] == 1 + assert response.json['zims_estimates'][0]['estimate'] == 0 body = dict(search_str='one') request, response = await test_async_client.post('/api/search_suggestions', json=body) @@ -519,6 +526,8 @@ async def test_search_suggestions(test_session, test_async_client, channel_facto assert response.json['channels'] == [] assert response.json['domains'] == [] assert response.json['tags'] == [{'color': '#111111', 'id': 1, 'name': 'one'}] + assert response.json['file_groups'] == 0 + assert response.json['zims_estimates'][0]['estimate'] == 2 def test_recursive_errors():