diff --git a/arches/app/media/css/arches.scss b/arches/app/media/css/arches.scss index 3ea94577d3..b76fa37ffa 100644 --- a/arches/app/media/css/arches.scss +++ b/arches/app/media/css/arches.scss @@ -10025,7 +10025,7 @@ table.table.dataTable { .search-type-btn { height: 50px; - padding: 0px; + padding: 0px 4px; font-size: 1.2rem; font-weight: 600; color: #888; diff --git a/arches/app/media/js/views/components/search/advanced-search.js b/arches/app/media/js/views/components/search/advanced-search.js index e41be9db45..f32fa394d9 100644 --- a/arches/app/media/js/views/components/search/advanced-search.js +++ b/arches/app/media/js/views/components/search/advanced-search.js @@ -14,7 +14,6 @@ define([ initialize: function(options) { var self = this; options.name = 'Advanced Search Filter'; - BaseFilter.prototype.initialize.call(this, options); this.urls = arches.urls; this.tagId = "Advanced Search"; @@ -179,7 +178,7 @@ define([ options.loading(false); }); - this.filters[componentName](this); + this.searchFilterVms[componentName](this); }, updateQuery: function() { @@ -194,12 +193,12 @@ define([ }); queryObj[componentName] = JSON.stringify(advanced); - if (this.getFilter('term-filter').hasTag(this.tagId) === false) { - this.getFilter('term-filter').addTag(this.tagId, this.name, ko.observable(false)); + if (this.getFilterByType('term-filter-type').hasTag(this.tagId) === false) { + this.getFilterByType('term-filter-type').addTag(this.tagId, this.name, ko.observable(false)); } } else { delete queryObj[componentName]; - this.getFilter('term-filter').removeTag(this.tagId); + this.getFilterByType('term-filter-type').removeTag(this.tagId); } this.query(queryObj); }, @@ -223,7 +222,7 @@ define([ var facets = JSON.parse(query[componentName]); if (facets.length > 0) { - this.getFilter('term-filter').addTag("Advanced Search", this.name, ko.observable(false)); + this.getFilterByType('term-filter-type').addTag("Advanced Search", this.name, ko.observable(false)); } _.each(facets, function(facet) { var nodeIds = _.filter(Object.keys(facet), function(key) { diff --git a/arches/app/media/js/views/components/search/base-filter.js b/arches/app/media/js/views/components/search/base-filter.js index c17ebb9cb8..651c0302ce 100644 --- a/arches/app/media/js/views/components/search/base-filter.js +++ b/arches/app/media/js/views/components/search/base-filter.js @@ -8,23 +8,12 @@ define([ this.name = 'Base Filter'; // the various filters managed by this widget this.filter = {}; - this.requiredFilters = []; // Call the original constructor Backbone.View.apply(this, arguments); }, initialize: function(options) { $.extend(this, options); - this.requiredFiltersLoaded = ko.computed(function() { - var self = this; - var res = this.requiredFilters.every(function(f){return self.getFilter(f) !== null;}); - return res; - }, this); }, - - - getFilter: function(filterName) { - return ko.unwrap(this.filters[filterName]); - } }); }); diff --git a/arches/app/media/js/views/components/search/base-search-view.js b/arches/app/media/js/views/components/search/base-search-view.js new file mode 100644 index 0000000000..ee5d1e2bdb --- /dev/null +++ b/arches/app/media/js/views/components/search/base-search-view.js @@ -0,0 +1,102 @@ +define([ + 'jquery', + 'underscore', + 'knockout', + 'backbone', + 'arches', + 'viewmodels/alert', +], function($, _, ko, BackBone, arches, AlertViewModel) { + return Backbone.View.extend({ + constructor: function() { + this.name = 'Base Search View'; + this.filter = {}; + this.defaultQuery = {}; + Backbone.View.apply(this, arguments); + }, + + initialize: function(sharedStateObject) { + const self = this; + $.extend(this, sharedStateObject); + this.query = sharedStateObject.query; + this.queryString = sharedStateObject.queryString; + this.updateRequest = sharedStateObject.updateRequest; + this.userIsReviewer = sharedStateObject.userIsReviewer; + this.total = sharedStateObject.total; + this.userid = sharedStateObject.userid; + this.hits = sharedStateObject.hits; + this.alert = sharedStateObject.alert; + this.sharedStateObject = sharedStateObject; + this.queryString.subscribe(function() { + if (this.searchViewFiltersLoaded()) { + this.doQuery(); + } else { + this.searchViewFiltersLoaded.subscribe(function() { + this.doQuery(); + }, this); + } + }, this); + // init query + if (self.updateRequest === undefined) { + if (this.searchViewFiltersLoaded()) { + this.doQuery(); + } else { + this.searchViewFiltersLoaded.subscribe(function() { + this.doQuery(); + }, this); + } + } + }, + + doQuery: function() { + const queryObj = JSON.parse(this.queryString()); + if (self.updateRequest) { self.updateRequest.abort(); } + self.updateRequest = $.ajax({ + type: "GET", + url: arches.urls.search_results, + data: queryObj, + context: this, + success: function(response) { + _.each(this.sharedStateObject.searchResults, function(value, key, results) { + if (key !== 'timestamp') { + delete this.sharedStateObject.searchResults[key]; + } + }, this); + _.each(response, function(value, key, response) { + if (key !== 'timestamp') { + this.sharedStateObject.searchResults[key] = value; + } + }, this); + this.sharedStateObject.searchResults.timestamp(response.timestamp); + this.sharedStateObject.userIsReviewer(response.reviewer); + this.sharedStateObject.userid(response.userid); + this.sharedStateObject.total(response.total_results); + this.sharedStateObject.hits(response.results.hits.hits.length); + this.sharedStateObject.alert(false); + }, + error: function(response, status, error) { + const alert = new AlertViewModel('ep-alert-red', arches.translations.requestFailed.title, response.responseJSON?.message); + if(self.updateRequest.statusText !== 'abort'){ + self.alert(alert); + } + this.sharedStateObject.loading(false); + }, + complete: function(request, status) { + self.updateRequest = undefined; + window.history.pushState({}, '', '?' + $.param(queryObj).split('+').join('%20')); + this.sharedStateObject.loading(false); + } + }); + }, + + clearQuery: function(){ + Object.values(this.searchFilterVms).forEach(function(value){ + if (value()){ + if (value().clear){ + value().clear(); + } + } + }, this); + this.query(this.defaultQuery); + }, + }); +}); diff --git a/arches/app/media/js/views/components/search/map-filter.js b/arches/app/media/js/views/components/search/map-filter.js index 3295ab7131..8355290b6d 100644 --- a/arches/app/media/js/views/components/search/map-filter.js +++ b/arches/app/media/js/views/components/search/map-filter.js @@ -337,7 +337,7 @@ define([ updateSearchResultPointLayer(); }; - this.filters[componentName](this); + this.searchFilterVms[componentName](this); this.map.subscribe(function(){ this.setupDraw(); this.restoreState(); @@ -514,8 +514,8 @@ define([ var self = this; var queryObj = this.query(); if (this.filter.feature_collection().features.length > 0) { - if (this.getFilter('term-filter').hasTag(this.type) === false) { - this.getFilter('term-filter').addTag('Map Filter Enabled', this.name, this.filter.inverted); + if (this.getFilterByType('term-filter-type').hasTag(this.type) === false) { + this.getFilterByType('term-filter-type').addTag('Map Filter Enabled', this.name, this.filter.inverted); } this.filter.feature_collection().features[0].properties['inverted'] = this.filter.inverted(); queryObj[componentName] = ko.toJSON(this.filter.feature_collection()); @@ -552,7 +552,7 @@ define([ this.bufferUnit = ko.observable(bufferUnit).extend({ deferred: true }); this.filter.inverted = ko.observable(inverted).extend({ deferred: true }); if (hasSpatialFilter) { - this.getFilter('term-filter').addTag('Map Filter Enabled', this.name, this.filter.inverted); + this.getFilterByType('term-filter-type').addTag('Map Filter Enabled', this.name, this.filter.inverted); } this.updateResults(); this.pageLoaded = true; @@ -581,7 +581,7 @@ define([ "type": "FeatureCollection", "features": [] }); - this.getFilter('term-filter').removeTag('Map Filter Enabled'); + this.getFilterByType('term-filter-type').removeTag('Map Filter Enabled'); this.draw.deleteAll(); this.searchGeometries([]); }, diff --git a/arches/app/media/js/views/components/search/paging-filter.js b/arches/app/media/js/views/components/search/paging-filter.js index 038b09bc5e..b217a82fbd 100644 --- a/arches/app/media/js/views/components/search/paging-filter.js +++ b/arches/app/media/js/views/components/search/paging-filter.js @@ -5,7 +5,7 @@ define([ 'utils/aria', 'templates/views/components/search/paging-filter.htm', ], function(BaseFilter, ko, koMapping, ariaUtils, pagingFilterTemplate) { - var componentName = 'paging-filter'; + const componentName = 'paging-filter'; const viewModel = BaseFilter.extend({ initialize: function(options) { options.name = 'Paging Filter'; @@ -45,7 +45,7 @@ define([ this.updateResults(); }, this); - this.filters[componentName](this); + this.searchFilterVms[componentName](this); this.restoreState(); this.pageInitialized = true; }, @@ -60,7 +60,7 @@ define([ if(page){ this.userRequestedNewPage = true; this.page(page); - this.shiftFocus('#search-results-list'); + this.shiftFocus('#search-results-list-type'); } }, diff --git a/arches/app/media/js/views/components/search/provisional-filter.js b/arches/app/media/js/views/components/search/provisional-filter.js index 68bdc872cd..94b86ba2fd 100644 --- a/arches/app/media/js/views/components/search/provisional-filter.js +++ b/arches/app/media/js/views/components/search/provisional-filter.js @@ -4,24 +4,14 @@ define([ 'views/components/search/base-filter', 'templates/views/components/search/provisional-filter.htm', ], function(ko, arches, BaseFilter, provisionalFilterTemplate) { - var componentName = 'provisional-filter'; + const componentName = 'provisional-filter'; const viewModel = BaseFilter.extend({ initialize: function(options) { options.name = 'Provisional Filter'; - this.translations = arches. translations; - this.requiredFilters = ['term-filter']; + this.translations = arches.translations; BaseFilter.prototype.initialize.call(this, options); this.filter = ko.observableArray(); this.provisionalOptions = [{'name': 'Authoritative'},{'name': 'Provisional'}]; - - if (this.requiredFiltersLoaded() === false) { - this.requiredFiltersLoaded.subscribe(function() { - this.restoreState(); - }, this); - } else { - this.restoreState(); - } - var filterUpdated = ko.computed(function() { return JSON.stringify(ko.toJS(this.filter())); }, this); @@ -29,7 +19,15 @@ define([ this.updateQuery(); }, this); - this.filters[componentName](this); + this.searchFilterVms[componentName](this); + + if (this.searchViewFiltersLoaded() === false) { + this.searchViewFiltersLoaded.subscribe(function() { + this.restoreState(); + }, this); + } else { + this.restoreState(); + } }, updateQuery: function() { @@ -49,7 +47,7 @@ define([ if (provisionalQuery.length > 0) { provisionalQuery.forEach(function(type){ type.inverted = ko.observable(!!type.inverted); - this.getFilter('term-filter').addTag(type.provisionaltype, this.name, type.inverted); + this.getFilterByType('term-filter-type').addTag(type.provisionaltype, this.name, type.inverted); }, this); this.filter(provisionalQuery); } @@ -58,12 +56,12 @@ define([ selectProvisional: function(item) { this.filter().forEach(function(val){ - this.getFilter('term-filter').removeTag(val.provisionaltype); + this.getFilterByType('term-filter-type').removeTag(val.provisionaltype); }, this); if(!!item){ var inverted = ko.observable(false); - this.getFilter('term-filter').addTag(item.name, this.name, inverted); + this.getFilterByType('term-filter-type').addTag(item.name, this.name, inverted); this.filter([{provisionaltype: item.name, inverted: inverted}]); }else{ diff --git a/arches/app/media/js/views/components/search/related-resources-filter.js b/arches/app/media/js/views/components/search/related-resources-filter.js index 168523c02a..01bd81bce4 100644 --- a/arches/app/media/js/views/components/search/related-resources-filter.js +++ b/arches/app/media/js/views/components/search/related-resources-filter.js @@ -10,30 +10,28 @@ define([ const viewModel = BaseFilter.extend ({ initialize: function(options) { options.name = 'Related Resources Filter'; - - - this.requiredFilters = ['search-results']; BaseFilter.prototype.initialize.call(this, options); this.ready = ko.observable(false); this.options = options; this.urls = arches.urls; var self = this; - // this component is just a light weight wrapper around the relatd resources manager - // need to wait for the search-resutls filter to be ready - // before we can load the realated-resources-filter - // because we need to pass the entire rsearch results filter into the - // related resources filter var setSearchResults = function(){ - options.searchResultsVm = self.getFilter('search-results'); - options.searchResultsVm.relatedResourcesManager = self; - options.filters[componentName](self); + self.searchResultsVm.relatedResourcesManager = self; self.ready(true); }; + this.searchFilterVms[componentName](this); - if (this.requiredFiltersLoaded() === false) { - this.requiredFiltersLoaded.subscribe(setSearchResults, this); - } else { + this.searchResultsVm = self.getFilterByType('search-results-type', false); + if (ko.unwrap(this.searchResultsVm)) { + this.searchResultsVm = this.searchResultsVm(); setSearchResults(); + } else { + this.searchResultsVm.subscribe(searchResultsFilter => { + if (searchResultsFilter) { + this.searchResultsVm = searchResultsFilter; + setSearchResults(); + } + }, this); } } }); diff --git a/arches/app/media/js/views/components/search/resource-type-filter.js b/arches/app/media/js/views/components/search/resource-type-filter.js index 99bbbaae27..eb8b635503 100644 --- a/arches/app/media/js/views/components/search/resource-type-filter.js +++ b/arches/app/media/js/views/components/search/resource-type-filter.js @@ -8,9 +8,6 @@ define([ const viewModel = BaseFilter.extend({ initialize: async function(options) { options.name = 'Resource Type Filter'; - - - this.requiredFilters = ['term-filter']; BaseFilter.prototype.initialize.call(this, options); this.resourceModels = ko.observableArray(); this.filter = ko.observableArray(); @@ -39,10 +36,10 @@ define([ this.updateQuery(); }, this); - this.filters[componentName](this); + this.searchFilterVms[componentName](this); - if (this.requiredFiltersLoaded() === false) { - this.requiredFiltersLoaded.subscribe(function() { + if (this.searchViewFiltersLoaded() === false) { + this.searchViewFiltersLoaded.subscribe(function() { this.restoreState(); }, this); } else { @@ -67,7 +64,7 @@ define([ if (resourceTypeQuery.length > 0) { resourceTypeQuery.forEach(function(type){ type.inverted = ko.observable(!!type.inverted); - this.getFilter('term-filter').addTag(type.name, this.name, type.inverted); + this.getFilterByType('term-filter-type').addTag(type.name, this.name, type.inverted); }, this); this.filter(resourceTypeQuery); } @@ -80,11 +77,11 @@ define([ selectModelType: function(item){ this.filter().forEach(function(item){ - this.getFilter('term-filter').removeTag(item.name); + this.getFilterByType('term-filter-type').removeTag(item.name); }, this); if(!!item){ var inverted = ko.observable(false); - this.getFilter('term-filter').addTag(item.name, this.name, inverted); + this.getFilterByType('term-filter-type').addTag(item.name, this.name, inverted); this.filter([{graphid:item.graphid, name: item.name, inverted: inverted}]); }else{ this.clear(); diff --git a/arches/app/media/js/views/components/search/saved-searches.js b/arches/app/media/js/views/components/search/saved-searches.js index 4a7fc102b9..f25be9b387 100644 --- a/arches/app/media/js/views/components/search/saved-searches.js +++ b/arches/app/media/js/views/components/search/saved-searches.js @@ -5,9 +5,10 @@ define([ 'templates/views/components/search/saved-searches.htm', 'bindings/smartresize', ], function($, ko, arches, savedSearchesTemplate) { - var componentName = 'saved-searches'; + const componentName = 'saved-searches'; const viewModel = function(params) { var self = this; + self.searchFilterVms = params.searchFilterVms; self.urls = arches.urls; @@ -18,7 +19,7 @@ define([ url: arches.urls.api_search_component_data + componentName, context: this }).done(function(response) { - response.saved_searches.forEach(function(search) { + response[componentName].forEach(function(search) { let searchImageUrl = arches.urls.url_subpath + ((search.IMAGE && search.IMAGE.length > 0) ? search.IMAGE[0].url : ''); searchImageUrl = searchImageUrl.replace('//', '/'); self.items.push({ @@ -28,6 +29,7 @@ define([ searchUrl: search.SEARCH_URL[arches.activeLanguage].value }); }); + self.searchFilterVms[componentName](self); }); self.options = { diff --git a/arches/app/media/js/views/components/search/search-export.js b/arches/app/media/js/views/components/search/search-export.js index 37ad468f94..dcae056a5a 100644 --- a/arches/app/media/js/views/components/search/search-export.js +++ b/arches/app/media/js/views/components/search/search-export.js @@ -7,14 +7,14 @@ define([ 'bindings/clipboard', 'views/components/simple-switch', ], function($, ko, arches, searchExportTemplate) { - var componentName = 'search-export'; - const viewModel = function(params) { + const componentName = 'search-export'; + const viewModel = function(sharedStateObject) { var self = this; - this.total = params.total; - this.query = params.query; - this.selectedPopup = params.selectedPopup; + this.total = sharedStateObject.total; + this.query = sharedStateObject.query; + this.selectedPopup = sharedStateObject.selectedPopup; this.downloadStarted = ko.observable(false); this.reportlink = ko.observable(false); this.format = ko.observable('tilecsv'); @@ -25,7 +25,7 @@ define([ this.celeryRunning = ko.observable(arches.celeryRunning); this.hasExportHtmlTemplates = ko.observable(arches.exportHtmlTemplates.length > 0); this.downloadPending = ko.observable(false); - this.hasResourceTypeFilter = ko.observable(!!params.query()['resource-type-filter']); + this.hasResourceTypeFilter = ko.observable(!!sharedStateObject.query()['resource-type-filter']); this.exportSystemValues = ko.observable(false); this.query.subscribe(function(val) { @@ -98,6 +98,7 @@ define([ } }; + sharedStateObject.searchFilterVms[componentName](this); }; return ko.components.register(componentName, { diff --git a/arches/app/media/js/views/components/search/search-result-details.js b/arches/app/media/js/views/components/search/search-result-details.js index 53503dc3d3..113e68e158 100644 --- a/arches/app/media/js/views/components/search/search-result-details.js +++ b/arches/app/media/js/views/components/search/search-result-details.js @@ -15,11 +15,7 @@ define([ const viewModel = BaseFilter.extend({ initialize: function(options) { var self = this; - - options.name = 'Search Result Details'; - this.requiredFilters = ['search-results']; - BaseFilter.prototype.initialize.call(this, options); this.options = options; @@ -27,17 +23,21 @@ define([ this.report = ko.observable(); this.loading = ko.observable(false); this.reportExpanded = ko.observable(); + this.searchFilterVms[componentName](this); var setSearchResults = function(){ - options.searchResultsVm = self.getFilter('search-results'); - options.searchResultsVm.details = self; - options.filters[componentName](self); + self.searchResultsVm().details = self; }; - if (this.requiredFiltersLoaded() === false) { - this.requiredFiltersLoaded.subscribe(setSearchResults, this); - } else { + this.searchResultsVm = this.getFilterByType('search-results-type', false); + if (ko.unwrap(this.searchResultsVm)) { setSearchResults(); + } else { + this.searchResultsVm.subscribe(searchResultsFilter => { + if (searchResultsFilter) { + setSearchResults(); + } + }, this); } var query = this.query(); diff --git a/arches/app/media/js/views/components/search/search-results.js b/arches/app/media/js/views/components/search/search-results.js index 6b9057db11..698533be6d 100644 --- a/arches/app/media/js/views/components/search/search-results.js +++ b/arches/app/media/js/views/components/search/search-results.js @@ -37,9 +37,7 @@ define([ }, initialize: function (options) { - options.name = "Search Results"; - - this.requiredFilters = ["map-filter"]; + options.name = 'Search Results'; BaseFilter.prototype.initialize.call(this, options); this.results = ko.observableArray(); this.showRelationships = ko.observable(); @@ -54,20 +52,19 @@ define([ this.updateResults(); }, this); - this.filters[componentName](this); + this.searchFilterVms[componentName](this); this.restoreState(); - if (this.requiredFiltersLoaded() === false) { - this.requiredFiltersLoaded.subscribe(function () { - this.mapFilter = this.getFilter("map-filter"); - }, this); - } else { - this.mapFilter = this.getFilter("map-filter"); - } + + this.mapFilter = this.getFilterByType("map-filter-type", false); + this.mapFilter.subscribe(mapFilter => { + if (mapFilter) { + this.mapFilter = mapFilter; + } + }, this); this.selectedTab.subscribe(function (tab) { - var self = this; - if (tab === "map-filter") { + if (tab === "map-filter-type") { if (ko.unwrap(this.mapFilter.map)) { - self.mapFilter.map().resize(); + this.mapFilter.map().resize(); } } }, this); @@ -112,10 +109,10 @@ define([ } } self.showRelationships(resourceinstance); - if (self.selectedTab() !== "related-resources-filter") { - self.selectedTab("related-resources-filter"); + if (self.selectedTab() !== "related-resources-filter-type") { + self.selectedTab("related-resources-filter-type"); } - self.shiftFocus("#related-resources-filter-tabpanel"); + self.shiftFocus("#related-resources-filter-type-tabpanel"); }; }, @@ -168,8 +165,8 @@ define([ } }); - if (self.selectedTab() !== "search-result-details") { - self.selectedTab("search-result-details"); + if (self.selectedTab() !== "search-result-details-type") { + self.selectedTab("search-result-details-type"); } }; }, @@ -327,8 +324,8 @@ define([ self.selectedResourceId( result._source.resourceinstanceid, ); - if (self.selectedTab() !== "map-filter") { - self.selectedTab("map-filter"); + if (self.selectedTab() !== "map-filter-type") { + self.selectedTab("map-filter-type"); } self.mapLinkData({ properties: result._source, diff --git a/arches/app/media/js/views/components/search/sort-results.js b/arches/app/media/js/views/components/search/sort-results.js index 131796535f..fe4f6c6736 100644 --- a/arches/app/media/js/views/components/search/sort-results.js +++ b/arches/app/media/js/views/components/search/sort-results.js @@ -10,12 +10,10 @@ define([ const viewModel = BaseFilter.extend({ initialize: function(options) { options.name = 'Sort Results'; - - BaseFilter.prototype.initialize.call(this, options); this.filter = ko.observable(''); - this.filters[componentName](this); + this.searchFilterVms[componentName](this); this.filter.subscribe(function(){ this.updateQuery(); diff --git a/arches/app/media/js/views/components/search/standard-search-view.js b/arches/app/media/js/views/components/search/standard-search-view.js new file mode 100644 index 0000000000..e311bcf438 --- /dev/null +++ b/arches/app/media/js/views/components/search/standard-search-view.js @@ -0,0 +1,60 @@ +define([ + 'jquery', + 'underscore', + 'knockout', + 'arches', + 'viewmodels/alert', + 'views/components/search/base-search-view', + 'templates/views/components/search/standard-search-view.htm', +], function($, _, ko, arches, AlertViewModel, BaseSearchViewComponent, standardSearchViewTemplate) { + const componentName = 'standard-search-view'; + const viewModel = BaseSearchViewComponent.extend({ + initialize: function(sharedStateObject) { + const self = this; + BaseSearchViewComponent.prototype.initialize.call(this, sharedStateObject); + + this.selectedPopup = ko.observable(''); + this.sharedStateObject.selectedPopup = this.selectedPopup; + var firstEnabledFilter = _.find(this.sharedStateObject.searchFilterConfigs, function(filter) { + return filter.config.layoutType === 'tabbed'; + }, this); + this.selectedTab = ko.observable(firstEnabledFilter.type); + this.sharedStateObject.selectedTab = this.selectedTab; + this.resultsExpanded = ko.observable(true); + this.isResourceRelatable = function(graphId) { + var relatable = false; + if (this.graph) { + relatable = _.contains(this.graph.relatable_resource_model_ids, graphId); + } + return relatable; + }; + this.sharedStateObject.isResourceRelatable = this.isResourceRelatable; + this.toggleRelationshipCandidacy = function() { + return function(resourceinstanceid){ + var candidate = _.contains(sharedStateObject.relationshipCandidates(), resourceinstanceid); + if (candidate) { + sharedStateObject.relationshipCandidates.remove(resourceinstanceid); + } else { + sharedStateObject.relationshipCandidates.push(resourceinstanceid); + } + }; + }; + this.sharedStateObject.toggleRelationshipCandidacy = this.toggleRelationshipCandidacy; + + this.selectPopup = function(component_type) { + if(this.selectedPopup() !== '' && component_type === this.selectedPopup()) { + this.selectedPopup(''); + } else { + this.selectedPopup(component_type); + } + }; + this.searchFilterVms[componentName](this); + }, + + }); + + return ko.components.register(componentName, { + viewModel: viewModel, + template: standardSearchViewTemplate, + }); +}); diff --git a/arches/app/media/js/views/components/search/term-filter.js b/arches/app/media/js/views/components/search/term-filter.js index 9394606a4c..9394d7c3ed 100644 --- a/arches/app/media/js/views/components/search/term-filter.js +++ b/arches/app/media/js/views/components/search/term-filter.js @@ -11,7 +11,6 @@ define([ const viewModel = BaseFilter.extend({ initialize: function(options) { options.name = 'Term Filter'; - BaseFilter.prototype.initialize.call(this, options); this.filter.terms = ko.observableArray(); @@ -42,7 +41,7 @@ define([ return tag.value.type === currentTag.type; }, this); if(!found){ - _.each(this.filters, function(filter){ + _.each(this.searchFilterVms, function(filter){ if(!!filter() && filter().name === tag.value.type){ filter().clear(); } @@ -52,7 +51,7 @@ define([ }, this); }, this, "arrayChange"); - this.filters[componentName](this); + this.searchFilterVms[componentName](this); this.restoreState(); }, diff --git a/arches/app/media/js/views/components/search/time-filter.js b/arches/app/media/js/views/components/search/time-filter.js index aa861ee86c..35367eb9e8 100644 --- a/arches/app/media/js/views/components/search/time-filter.js +++ b/arches/app/media/js/views/components/search/time-filter.js @@ -105,7 +105,7 @@ define([ this.filterChanged = ko.computed(function(){ if(!!this.filter.fromDate() || !!this.filter.toDate()){ - this.getFilter('term-filter').addTag(this.name, this.name, this.filter.inverted); + this.getFilterByType('term-filter-type').addTag(this.name, this.name, this.filter.inverted); } return ko.toJSON(this.filter); }, this).extend({ deferred: true }); @@ -113,7 +113,7 @@ define([ this.filterChanged.subscribe(function() { this.updateQuery(); }, this); - this.filters[componentName](this); + this.searchFilterVms[componentName](this); }, updateQuery: function() { @@ -147,7 +147,7 @@ define([ if (componentName in query) { var timeQuery = JSON.parse(query[componentName]); this.filter.inverted(!!timeQuery.inverted); - this.getFilter('term-filter').addTag(this.name, this.name, this.filter.inverted); + this.getFilterByType('term-filter-type').addTag(this.name, this.name, this.filter.inverted); ['fromDate', 'toDate', 'dateNodeId'].forEach(function(key) { if (key in timeQuery) { this.filter[key](timeQuery[key]); @@ -179,7 +179,7 @@ define([ this.filter.dateNodeId(null); this.filter.inverted(false); this.dateRangeType('custom'); - this.getFilter('term-filter').removeTag(this.name); + this.getFilterByType('term-filter-type').removeTag(this.name); this.selectedPeriod(null); return; } diff --git a/arches/app/media/js/views/search.js b/arches/app/media/js/views/search.js index 2706e03276..31ce38548c 100644 --- a/arches/app/media/js/views/search.js +++ b/arches/app/media/js/views/search.js @@ -3,13 +3,11 @@ define([ 'underscore', 'knockout', 'knockout-mapping', - 'arches', - 'viewmodels/alert', 'search-components', 'views/base-manager', 'utils/aria', 'datatype-config-components' -], function($, _, ko, koMapping, arches, AlertViewModel, SearchComponents, BaseManagerView, ariaUtils) { +], function($, _, ko, koMapping, SearchComponents, BaseManagerView, ariaUtils) { // a method to track the old and new values of a subscribable // from https://github.com/knockout/knockout/issues/914 // @@ -45,71 +43,49 @@ define([ }; var CommonSearchViewModel = function() { - this.filters = {}; - this.filtersList = _.sortBy(Object.values(SearchComponents), function(filter) { - return filter.sortorder; - }, this); + this.searchFilterVms = {}; + this.searchFilterConfigs = Object.values(SearchComponents); + this.defaultSearchViewConfig = this.searchFilterConfigs.find(filter => filter.type == "search-view"); + this.searchViewComponentName = ko.observable(false); + this.getFilter = function(filterName, unwrap=true) { + if (unwrap) + return ko.unwrap(this.searchFilterVms[filterName]); + return this.searchFilterVms[filterName]; + }; + this.getFilterByType = function(type, unwrap=true) { + const filter = this.searchFilterConfigs.find(component => component.type == type); + if (!filter) + return null; + if (unwrap) + return ko.unwrap(this.searchFilterVms[filter.componentname]); + return this.searchFilterVms[filter.componentname]; + }; Object.values(SearchComponents).forEach(function(component) { - this.filters[component.componentname] = ko.observable(null); + this.searchFilterVms[component.componentname] = ko.observable(null); + // uncomment below to test for any filters that don't load as expected + // this.searchFilterVms[component.componentname].subscribe(vm => {console.log(component.componentname+" loaded");}) }, this); - var firstEnabledFilter = _.find(this.filtersList, function(filter) { - return filter.type === 'filter' && filter.enabled === true; + this.searchViewFiltersLoaded = ko.computed(function() { + let res = true; + Object.entries(this.searchFilterVms).forEach(function([componentName, filter]) { + res = res && ko.unwrap(filter); + }); + return res; }, this); - this.selectedTab = ko.observable(firstEnabledFilter.componentname); - this.selectedPopup = ko.observable(''); - this.resultsExpanded = ko.observable(true); this.query = ko.observable(getQueryObject()); - this.clearQuery = function(){ - Object.values(this.filters).forEach(function(value){ - if (value()){ - if (value().clear){ - value().clear(); - } - } - }, this); - this.query({"paging-filter": "1", tiles: "true"}); - }; - this.filterApplied = ko.pureComputed(function(){ - var self = this; - var filterNames = Object.keys(this.filters); - return filterNames.some(function(filterName){ - if (ko.unwrap(self.filters[filterName]) && filterName !== 'paging-filter') { - return !!ko.unwrap(self.filters[filterName]).query()[filterName]; - } else { - return false; - } - }); + if (this.query()["search-view"] !== undefined) { + this.searchViewComponentName(this.query()["search-view"]); + } else { + this.searchViewComponentName(this.defaultSearchViewConfig.componentname); + } + this.queryString = ko.computed(function() { + return JSON.stringify(this.query()); }, this); this.mouseoverInstanceId = ko.observable(); this.mapLinkData = ko.observable(null); - this.userIsReviewer = ko.observable(false); + this.userIsReviewer = ko.observable(null); this.userid = ko.observable(null); this.searchResults = {'timestamp': ko.observable()}; - this.selectPopup = function(componentname) { - if(this.selectedPopup() !== '' && componentname === this.selectedPopup()) { - this.selectedPopup(''); - } else { - this.selectedPopup(componentname); - } - }; - this.isResourceRelatable = function(graphId) { - var relatable = false; - if (this.graph) { - relatable = _.contains(this.graph.relatable_resource_model_ids, graphId); - } - return relatable; - }; - this.toggleRelationshipCandidacy = function() { - var self = this; - return function(resourceinstanceid){ - var candidate = _.contains(self.relationshipCandidates(), resourceinstanceid); - if (candidate) { - self.relationshipCandidates.remove(resourceinstanceid); - } else { - self.relationshipCandidates.push(resourceinstanceid); - } - }; - }; }; var SearchView = BaseManagerView.extend({ @@ -119,66 +95,18 @@ define([ this.viewModel.hits = ko.observable(); _.extend(this, this.viewModel.sharedStateObject); this.viewModel.sharedStateObject.total = this.viewModel.total; + this.viewModel.sharedStateObject.hits = this.viewModel.hits; + this.viewModel.sharedStateObject.alert = this.viewModel.alert; this.viewModel.sharedStateObject.loading = this.viewModel.loading; this.viewModel.sharedStateObject.resources = this.viewModel.resources; this.viewModel.sharedStateObject.userCanEditResources = this.viewModel.userCanEditResources; this.viewModel.sharedStateObject.userCanReadResources = this.viewModel.userCanReadResources; this.shiftFocus = ariaUtils.shiftFocus; - this.queryString = ko.computed(function() { - return JSON.stringify(this.query()); - }, this); - - this.queryString.subscribe(function() { - this.doQuery(); - }, this); - this.viewModel.loading(true); BaseManagerView.prototype.initialize.call(this, options); + this.viewModel.sharedStateObject.menuActive = this.viewModel.menuActive; }, - - doQuery: function() { - var queryString = JSON.parse(this.queryString()); - - if (this.updateRequest) { - this.updateRequest.abort(); - } - - this.updateRequest = $.ajax({ - type: "GET", - url: arches.urls.search_results, - data: queryString, - context: this, - success: function(response) { - _.each(this.viewModel.sharedStateObject.searchResults, function(value, key, results) { - if (key !== 'timestamp') { - delete this.viewModel.sharedStateObject.searchResults[key]; - } - }, this); - _.each(response, function(value, key, response) { - if (key !== 'timestamp') { - this.viewModel.sharedStateObject.searchResults[key] = value; - } - }, this); - this.viewModel.sharedStateObject.searchResults.timestamp(response.timestamp); - this.viewModel.sharedStateObject.userIsReviewer(response.reviewer); - this.viewModel.sharedStateObject.userid(response.userid); - this.viewModel.total(response.total_results); - this.viewModel.hits(response.results.hits.hits.length); - this.viewModel.alert(false); - }, - error: function(response, status, error) { - const alert = new AlertViewModel('ep-alert-red', arches.translations.requestFailed.title, response.responseJSON?.message); - if(this.updateRequest.statusText !== 'abort'){ - this.viewModel.alert(alert); - } - }, - complete: function(request, status) { - this.updateRequest = undefined; - window.history.pushState({}, '', '?' + $.param(queryString).split('+').join('%20')); - } - }); - } }); return new SearchView(); diff --git a/arches/app/models/migrations/10804_core_search_filters.py b/arches/app/models/migrations/10804_core_search_filters.py new file mode 100644 index 0000000000..9fff410bd7 --- /dev/null +++ b/arches/app/models/migrations/10804_core_search_filters.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("models", "11179_file_and_geom_search"), + ] + + sql = """ + INSERT INTO search_component ( + searchcomponentid, + name, + icon, + modulename, + classname, + type, + componentpath, + componentname, + sortorder, + enabled, + config + ) VALUES ( + '69695d63-6f03-4536-8da9-841b07116381', + 'Standard Search View', + '', + 'standard_search_view.py', + 'StandardSearchView', + 'search-view', + 'views/components/search/standard-search-view', + 'standard-search-view', + 0, + true, + '{"default":true,"linkedSearchFilters":[{"componentname":"map-filter","searchcomponentid":"09d97fc6-8c83-4319-9cef-3aaa08c3fbec","layoutSortorder":1},{"componentname":"advanced-search","searchcomponentid":"f0e56205-acb5-475b-9c98-f5e44f1dbd2c","layoutSortorder":2},{"componentname":"related-resources-filter","searchcomponentid":"59f28272-d1f1-4805-af51-227771739aed","layoutSortorder":3},{"componentname":"provisional-filter","searchcomponentid":"073406ed-93e5-4b5b-9418-b61c26b3640f","layoutSortorder":4},{"componentname":"resource-type-filter","searchcomponentid":"f1c46b7d-0132-421b-b1f3-95d67f9b3980","layoutSortorder":5},{"componentname":"saved-searches","searchcomponentid":"6dc29637-43a1-4fba-adae-8d9956dcd3b9","layoutSortorder":6},{"componentname":"search-export","searchcomponentid":"9c6a5a9c-a7ec-48d2-8a25-501b55b8eff6","layoutSortorder":7},{"componentname":"search-result-details","searchcomponentid":"f5986dae-8b01-11ea-b65a-77903936669c","layoutSortorder":8},{"componentname":"sort-results","searchcomponentid":"6a2fe122-de54-4e44-8e93-b6a0cda7955c","layoutSortorder":9},{"componentname":"term-filter","searchcomponentid":"1f42f501-ed70-48c5-bae1-6ff7d0d187da","layoutSortorder":10},{"componentname":"time-filter","searchcomponentid":"7497ed4f-2085-40da-bee5-52076a48bcb1","layoutSortorder":11},{"componentname":"paging-filter","searchcomponentid":"7aff5819-651c-4390-9b9a-a61221ba52c6","required":true,"layoutSortorder":12,"executionSortorder":2},{"componentname":"search-results","searchcomponentid":"00673743-8c1c-4cc0-bd85-c073a52e03ec","required":true,"layoutSortorder":13,"executionSortorder":1}]}' + ); + UPDATE search_component SET config = '{"layoutType":"tabbed"}' where componentname in ('advanced-search', 'related-resources-filter', 'search-result-details', 'map-filter'); + UPDATE search_component SET config = '{"layoutType":"popup"}' where componentname in ('time-filter', 'saved-searches', 'search-export'); + UPDATE search_component SET componentpath = null where componentpath = ''; + UPDATE search_component SET type = componentname || '-type' + WHERE componentname IN ( + 'advanced-search', + 'map-filter', + 'paging-filter', + 'provisional-filter', + 'related-resources-filter', + 'resource-type-filter', + 'saved-searches', + 'search-export', + 'search-results', + 'search-result-details', + 'sort-results', + 'term-filter', + 'time-filter' + ); + + """ + reverse_sql = """ + delete from search_component where searchcomponentid = '69695d63-6f03-4536-8da9-841b07116381'; + UPDATE search_component SET enabled = true, sortorder = 2 where type != 'search-view'; + UPDATE search_component SET type = 'filter' where type like '%-type'; + UPDATE search_component SET type = 'filter', sortorder = 1 where componentname = 'map-filter'; + UPDATE search_component SET type = 'results-list' where componentname = 'search-results'; + UPDATE search_component SET type = 'text-input' where componentname = 'term-filter'; + UPDATE search_component SET componentpath = '' where componentpath is null; + UPDATE search_component SET type = 'inline-filter' + where componentname in ( + 'sort-results', + 'provisional-filter', + 'paging-filter', + 'resource-type-filter' + ); + UPDATE search_component SET type = 'popup' where componentname in ( + 'time-filter', + 'search-export', + 'saved-searches' + ); + + """ + + operations = [ + migrations.AlterField( + model_name="searchcomponent", + name="componentpath", + field=models.TextField(null=True, unique=True), + ), + migrations.AddField( + model_name="searchcomponent", + name="config", + field=models.JSONField(default=dict), + ), + migrations.RunSQL( + sql, + reverse_sql, + ), + migrations.RemoveField( + model_name="searchcomponent", + name="enabled", + ), + migrations.RemoveField( + model_name="searchcomponent", + name="sortorder", + ), + ] diff --git a/arches/app/models/models.py b/arches/app/models/models.py index da10365840..3d49ab209b 100644 --- a/arches/app/models/models.py +++ b/arches/app/models/models.py @@ -29,6 +29,7 @@ from django.db import connection from django.db.models import JSONField from django.core.cache import caches +from django.core.exceptions import ValidationError from django.core.mail import EmailMultiAlternatives from django.core.serializers.json import DjangoJSONEncoder from django.template.loader import get_template, render_to_string @@ -1203,10 +1204,9 @@ class SearchComponent(models.Model): modulename = models.TextField(blank=True, null=True) classname = models.TextField(blank=True, null=True) type = models.TextField() - componentpath = models.TextField(unique=True) + componentpath = models.TextField(unique=True, null=True) componentname = models.TextField(unique=True) - sortorder = models.IntegerField(blank=True, null=True, default=None) - enabled = models.BooleanField(default=False) + config = models.JSONField(default=dict) def __str__(self): return self.name @@ -1234,6 +1234,18 @@ def toJSON(self): return JSONSerializer().serialize(self) +@receiver(pre_save, sender=SearchComponent) +def ensure_single_default_searchview(sender, instance, **kwargs): + if instance.config.get("default", False) and instance.type == "search-view": + existing_default = SearchComponent.objects.filter( + config__default=True, type="search-view" + ).exclude(searchcomponentid=instance.searchcomponentid) + if existing_default.exists(): + raise ValidationError( + "Only one search logic component can be default at a time." + ) + + class SearchExportHistory(models.Model): searchexportid = models.UUIDField(primary_key=True) user = models.ForeignKey(User, on_delete=models.CASCADE) diff --git a/arches/app/search/components/advanced_search.py b/arches/app/search/components/advanced_search.py index 2f74ced021..756e9a25e8 100644 --- a/arches/app/search/components/advanced_search.py +++ b/arches/app/search/components/advanced_search.py @@ -1,8 +1,14 @@ -from arches.app.models import models +from arches.app.models.models import ( + Node, + DDataType, + GraphModel, + CardModel, + CardXNodeXWidget, +) from arches.app.models.system_settings import settings from arches.app.datatypes.datatypes import DataTypeFactory from arches.app.utils.betterJSONSerializer import JSONDeserializer -from arches.app.search.elasticsearch_dsl_builder import Bool, Nested, Terms +from arches.app.search.elasticsearch_dsl_builder import Bool, Nested from arches.app.search.components.base import BaseSearchFilter details = { @@ -11,19 +17,16 @@ "icon": "fa fa-check-circle-o", "modulename": "advanced_search.py", "classname": "AdvancedSearch", - "type": "filter", + "type": "advanced-search-type", "componentpath": "views/components/search/advanced-search", "componentname": "advanced-search", - "sortorder": "3", - "enabled": True, + "config": {}, } class AdvancedSearch(BaseSearchFilter): - def append_dsl( - self, search_results_object, permitted_nodegroups, include_provisional - ): - querysting_params = self.request.GET.get(details["componentname"], "") + def append_dsl(self, search_query_object, **kwargs): + querysting_params = self.request.GET.get(self.componentname, "") advanced_filters = JSONDeserializer().deserialize(querysting_params) datatype_factory = DataTypeFactory() search_query = Bool() @@ -35,7 +38,7 @@ def append_dsl( null_query = Bool() for key, val in advanced_filter.items(): if key != "op": - node = models.Node.objects.get(pk=key) + node = Node.objects.get(pk=key) if self.request.user.has_perm("read_nodegroup", node.nodegroup): datatype = datatype_factory.get_instance(node.datatype) try: @@ -67,32 +70,30 @@ def append_dsl( for grouped_query in grouped_queries: advanced_query.should(grouped_query) search_query.must(advanced_query) - search_results_object["query"].add_query(search_query) + search_query_object["query"].add_query(search_query) def view_data(self): ret = {} resource_graphs = ( - models.GraphModel.objects.exclude( - pk=settings.SYSTEM_SETTINGS_RESOURCE_MODEL_ID - ) + GraphModel.objects.exclude(pk=settings.SYSTEM_SETTINGS_RESOURCE_MODEL_ID) .exclude(isresource=False) .exclude(publication=None) ) searchable_datatypes = [ - d.pk for d in models.DDataType.objects.filter(issearchable=True) + d.pk for d in DDataType.objects.filter(issearchable=True) ] - searchable_nodes = models.Node.objects.filter( + searchable_nodes = Node.objects.filter( graph__isresource=True, graph__publication__isnull=False, datatype__in=searchable_datatypes, issearchable=True, ) - resource_cards = models.CardModel.objects.filter( + resource_cards = CardModel.objects.filter( graph__isresource=True, graph__publication__isnull=False ).select_related("nodegroup") - cardwidgets = models.CardXNodeXWidget.objects.filter(node__in=searchable_nodes) - datatypes = models.DDataType.objects.all() + cardwidgets = CardXNodeXWidget.objects.filter(node__in=searchable_nodes) + datatypes = DDataType.objects.all() # only allow cards that the user has permission to read searchable_cards = [] diff --git a/arches/app/search/components/base.py b/arches/app/search/components/base.py index d9a87b1154..b0b6e06cf4 100644 --- a/arches/app/search/components/base.py +++ b/arches/app/search/components/base.py @@ -1,31 +1,20 @@ +from typing import List, Tuple, Any from arches.app.const import ExtensionType -from arches.app.models import models -from arches.app.models.system_settings import settings +from arches.app.models.models import SearchComponent from arches.app.utils.module_importer import get_class_from_modulename details = {} -# details = { -# "searchcomponentid": "", # leave blank for the system to generate a uuid -# "name": "", # the name that shows up in the UI -# "icon": "", # the icon class to use -# "modulename": "base.py", # the name of this file -# "classname": "BaseSearchFilter", # the classname below", -# "type": "filter", # 'filter' if you want the component to show up dynamically -# "componentpath": "views/components/search/...", # path to ko component -# "componentname": "advanced-search", # lowercase unique name -# "sortorder": "0", # order in which to display dynamically added filters to the UI -# "enabled": True # True to enable in the system -# } +# see docs for more info on developing search components: +# https://arches.readthedocs.io/en/latest/developing/reference/search-components/ class BaseSearchFilter: - def __init__(self, request=None, user=None): + def __init__(self, request=None, user=None, componentname=None): self.request = request self.user = user + self.componentname = componentname - def append_dsl( - self, search_results_object, permitted_nodegroups, include_provisional - ): + def append_dsl(self, search_query_object, **kwargs): """ used to append ES query dsl to the search request @@ -41,7 +30,13 @@ def view_data(self): pass - def post_search_hook(self, search_results_object, results, permitted_nodegroups): + def execute_query(self, search_query_object, response_object, **kwargs): + """ + code responsible for execution of the search query logic and mutation of the response object + """ + pass + + def post_search_hook(self, search_query_object, response_object, **kwargs): """ code to run after the search results have been retrieved @@ -56,7 +51,7 @@ def __init__(self, request=None, user=None): self.user = user self.search_filters = { search_filter.componentname: search_filter - for search_filter in models.SearchComponent.objects.all() + for search_filter in SearchComponent.objects.all() } self.search_filters_instances = {} @@ -75,10 +70,44 @@ def get_filter(self, componentname): ExtensionType.SEARCH_COMPONENTS, ) if class_method: - filter_instance = class_method(self.request, self.user) + filter_instance = class_method( + self.request, self.user, componentname + ) self.search_filters_instances[search_filter.componentname] = ( filter_instance ) return filter_instance else: return None + + def get_searchview_name(self): + if not self.request: + searchview_component_name = None + elif self.request.method == "POST": + searchview_component_name = self.request.POST.get("search-view", None) + else: + searchview_component_name = self.request.GET.get("search-view", None) + + if not searchview_component_name: + # get default search_view component + searchview_component = list( + filter( + lambda x: x.config.get("default", False) + and x.type == "search-view", + list(self.search_filters.values()), + ) + )[0] + searchview_component_name = searchview_component.componentname + + return searchview_component_name + + def get_searchview_instance(self): + searchview_component_name = self.get_searchview_name() + return self.get_filter(searchview_component_name) + + def create_search_query_dict(self, key_value_pairs: List[Tuple[str, Any]]): + # handles list of key,value tuples so that dict-like data from POST and GET + # requests can be concatenated into single method call + searchview_component_name = self.get_searchview_name() + searchview_instance = self.get_filter(searchview_component_name) + return searchview_instance.create_query_dict(dict(key_value_pairs)) diff --git a/arches/app/search/components/base_search_view.py b/arches/app/search/components/base_search_view.py new file mode 100644 index 0000000000..a72b0af096 --- /dev/null +++ b/arches/app/search/components/base_search_view.py @@ -0,0 +1,171 @@ +from arches.app.search.components.base import BaseSearchFilter +from arches.app.models.models import SearchComponent + +details = {} +# details = { +# "searchcomponentid": "", # leave blank for the system to generate a uuid +# "name": "", # the name that shows up in the UI +# "icon": "", # the icon class to use +# "modulename": "custom_search_view.py", # the name of this file +# "classname": "BaseSearchView", # the classname below", +# "type": "search-view", # 'search-view' if you want this to govern the search +# "componentpath": "views/components/search/...", # path to ko component +# "componentname": "custom-search-view", # lowercase unique name +# "config": { +# "default": True, # set for search-view components; only 1 can be the default +# "linkedSearchFilters": [ +# { +# "componentname":"map-filter","searchcomponentid":"09d97fc6-8c83-4319-9cef-3aaa08c3fbec","layoutSortorder":1 +# }, +# { +# "componentname":"advanced-search","searchcomponentid":"f0e56205-acb5-475b-9c98-f5e44f1dbd2c","layoutSortorder":2 +# }, +# { +# "componentname":"related-resources-filter","searchcomponentid":"59f28272-d1f1-4805-af51-227771739aed","layoutSortorder":3 +# }, +# { +# "componentname":"provisional-filter","searchcomponentid":"073406ed-93e5-4b5b-9418-b61c26b3640f","layoutSortorder":4 +# }, +# { +# "componentname":"term-filter","searchcomponentid":"1f42f501-ed70-48c5-bae1-6ff7d0d187da","layoutSortorder":10 +# }, +# { +# "required": True, "componentname":"paging-filter","searchcomponentid":"7aff5819-651c-4390-9b9a-a61221ba52c6","layoutSortorder":12, "executionSortorder":2 +# }, +# { +# "required": True, "componentname":"search-results","searchcomponentid":"00673743-8c1c-4cc0-bd85-c073a52e03ec","layoutSortorder":13, "executionSortorder":1 +# } +# ] +# } +# } + + +class BaseSearchView(BaseSearchFilter): + """ + Special type of component that specifies which other components to be used, + how to execute a search in the search_results method + """ + + def __init__(self, request=None, user=None, componentname=None): + super().__init__(request=request, user=user, componentname=componentname) + self.searchview_component = SearchComponent.objects.get( + componentname=componentname + ) + required_filter_sort_order = { + item["componentname"]: int(item.get("executionSortorder", 99)) + for item in self.searchview_component.config["linkedSearchFilters"] + } + self._required_search_filters = list( + SearchComponent.objects.filter( + searchcomponentid__in=[ + linked_filter["searchcomponentid"] + for linked_filter in self.searchview_component.config[ + "linkedSearchFilters" + ] + if linked_filter.get("required", False) + ] + ) + ) + self._required_search_filters = sorted( + self._required_search_filters, + key=lambda item: required_filter_sort_order.get( + item.componentname, float("inf") + ), + ) + available_filter_sort_order = { + item["componentname"]: int(item.get("layoutSortorder", 99)) + for item in self.searchview_component.config["linkedSearchFilters"] + } + self._available_search_filters = list( + SearchComponent.objects.filter( + searchcomponentid__in=[ + available_filter["searchcomponentid"] + for available_filter in self.searchview_component.config[ + "linkedSearchFilters" + ] + ], + componentpath__isnull=False, + ) + ) + self._available_search_filters = sorted( + self._available_search_filters, + key=lambda item: available_filter_sort_order.get( + item.componentname, float("inf") + ), + ) + + @property + def required_search_filters(self): + return self._required_search_filters + + @property + def available_search_filters(self): + return self._available_search_filters + + def get_searchview_filters(self): + return self.available_search_filters + [self.searchview_component] + + def sort_query_dict(self, query_dict): + filter_sort_order = { + item["componentname"]: int(item.get("executionSortorder", 99)) + for item in self.searchview_component.config["linkedSearchFilters"] + } + sorted_items = sorted( + query_dict.items(), + key=lambda item: filter_sort_order.get(item[0], float("inf")), + ) + + return dict(sorted_items) + + def create_query_dict(self, query_dict): + # check that all searchview required linkedSearchFilters are present + query_dict[self.searchview_component.componentname] = True + for linked_filter in self.searchview_component.config["linkedSearchFilters"]: + if ( + linked_filter.get("required", False) + and linked_filter["componentname"] not in query_dict + ): + query_dict[linked_filter["componentname"]] = {} + return self.sort_query_dict(query_dict) + + def handle_search_results_query( + self, search_query_object, response_object, search_filter_factory, returnDsl + ): + """ + returns response_object, search_query_object + See arches.app.search.components.arches_core_search for example implementation + """ + + sorted_query_obj = search_filter_factory.create_search_query_dict( + list(self.request.GET.items()) + list(self.request.POST.items()) + ) + + for filter_type, querystring in list(sorted_query_obj.items()): + search_filter = search_filter_factory.get_filter(filter_type) + if search_filter: + search_filter.append_dsl(search_query_object) + + if returnDsl: + dsl = search_query_object.pop("query", None) + return dsl, search_query_object + + for filter_type, querystring in list(sorted_query_obj.items()): + search_filter = search_filter_factory.get_filter(filter_type) + if search_filter: + search_filter.execute_query(search_query_object, response_object) + + if response_object["results"] is not None: + # allow filters to modify the results + for filter_type, querystring in list(sorted_query_obj.items()): + search_filter = search_filter_factory.get_filter(filter_type) + if search_filter: + search_filter.post_search_hook(search_query_object, response_object) + + search_query_object.pop("query") + # ensure that if a search filter modified the query in some way + # that the modification is set on the response_object + for key, value in list(search_query_object.items()): + if key not in response_object: + response_object[key] = value + + return response_object, search_query_object diff --git a/arches/app/search/components/map_filter.py b/arches/app/search/components/map_filter.py index 51ca68ba35..dd48fa23e1 100644 --- a/arches/app/search/components/map_filter.py +++ b/arches/app/search/components/map_filter.py @@ -15,20 +15,19 @@ "icon": "fa fa-map-marker", "modulename": "map_filter.py", "classname": "MapFilter", - "type": "filter", + "type": "map-filter-type", "componentpath": "views/components/search/map-filter", "componentname": "map-filter", - "sortorder": "0", - "enabled": True, + "config": {}, } class MapFilter(BaseSearchFilter): - def append_dsl( - self, search_results_object, permitted_nodegroups, include_provisional - ): + def append_dsl(self, search_query_object, **kwargs): + permitted_nodegroups = kwargs.get("permitted_nodegroups") + include_provisional = kwargs.get("include_provisional") search_query = Bool() - querysting_params = self.request.GET.get(details["componentname"], "") + querysting_params = self.request.GET.get(self.componentname, "") spatial_filter = JSONDeserializer().deserialize(querysting_params) if "features" in spatial_filter: if len(spatial_filter["features"]) > 0: @@ -74,15 +73,13 @@ def append_dsl( search_query.filter(Nested(path="geometries", query=spatial_query)) - search_results_object["query"].add_query(search_query) + search_query_object["query"].add_query(search_query) - if details["componentname"] not in search_results_object: - search_results_object[details["componentname"]] = {} + if self.componentname not in search_query_object: + search_query_object[self.componentname] = {} try: - search_results_object[details["componentname"]][ - "search_buffer" - ] = feature_geom + search_query_object[self.componentname]["search_buffer"] = feature_geom except NameError: logger.info(_("Feature geometry is not defined")) diff --git a/arches/app/search/components/paging_filter.py b/arches/app/search/components/paging_filter.py index 8d8cea0b70..216208be12 100644 --- a/arches/app/search/components/paging_filter.py +++ b/arches/app/search/components/paging_filter.py @@ -8,24 +8,21 @@ "icon": "", "modulename": "paging_filter.py", "classname": "PagingFilter", - "type": "paging", + "type": "paging-filter-type", "componentpath": "views/components/search/paging-filter", "componentname": "paging-filter", - "sortorder": "0", - "enabled": True, + "config": {}, } class PagingFilter(BaseSearchFilter): - def append_dsl( - self, search_results_object, permitted_nodegroups, include_provisional - ): + def append_dsl(self, search_query_object, **kwargs): export = self.request.GET.get("export", None) mobile_download = self.request.GET.get("mobiledownload", None) page = ( 1 - if self.request.GET.get(details["componentname"]) == "" - else int(self.request.GET.get(details["componentname"], 1)) + if self.request.GET.get(self.componentname) == "" + else int(self.request.GET.get(self.componentname, 1)) ) if export is not None: @@ -35,23 +32,28 @@ def append_dsl( else: limit = settings.SEARCH_ITEMS_PER_PAGE limit = int(self.request.GET.get("limit", limit)) - search_results_object["query"].start = limit * int(page - 1) - search_results_object["query"].limit = limit + search_query_object["query"].start = limit * int(page - 1) + search_query_object["query"].limit = limit - def post_search_hook(self, search_results_object, results, permitted_nodegroups): + def post_search_hook(self, search_query_object, response_object, **kwargs): total = ( - results["hits"]["total"]["value"] - if results["hits"]["total"]["value"] <= settings.SEARCH_RESULT_LIMIT + response_object["results"]["hits"]["total"]["value"] + if response_object["results"]["hits"]["total"]["value"] + <= settings.SEARCH_RESULT_LIMIT else settings.SEARCH_RESULT_LIMIT ) page = ( 1 - if self.request.GET.get(details["componentname"]) == "" - else int(self.request.GET.get(details["componentname"], 1)) + if self.request.GET.get(self.componentname) == "" + else int(self.request.GET.get(self.componentname, 1)) ) paginator, pages = get_paginator( - self.request, results, total, page, settings.SEARCH_ITEMS_PER_PAGE + self.request, + response_object["results"], + total, + page, + settings.SEARCH_ITEMS_PER_PAGE, ) page = paginator.page(page) @@ -68,6 +70,6 @@ def post_search_hook(self, search_results_object, results, permitted_nodegroups) ret["end_index"] = page.end_index() ret["pages"] = pages - if details["componentname"] not in search_results_object: - search_results_object[details["componentname"]] = {} - search_results_object[details["componentname"]]["paginator"] = ret + if self.componentname not in response_object: + response_object[self.componentname] = {} + response_object[self.componentname]["paginator"] = ret diff --git a/arches/app/search/components/provisional_filter.py b/arches/app/search/components/provisional_filter.py index 66aaa10f4e..9f83de6dd5 100644 --- a/arches/app/search/components/provisional_filter.py +++ b/arches/app/search/components/provisional_filter.py @@ -7,18 +7,16 @@ "icon": "", "modulename": "provisional_filter.py", "classname": "ProvisionalFilter", - "type": "", + "type": "provisional-filter-type", "componentpath": "views/components/search/provisional-filter", "componentname": "provisional-filter", - "sortorder": "0", - "enabled": True, + "config": {}, } class ProvisionalFilter(BaseSearchFilter): - def append_dsl( - self, search_results_object, permitted_nodegroups, include_provisional - ): + def append_dsl(self, search_query_object, **kwargs): + include_provisional = kwargs.get("include_provisional") search_query = Bool() if include_provisional is not True: @@ -35,4 +33,4 @@ def append_dsl( ) search_query.must(provisional_resource_filter) - search_results_object["query"].add_query(search_query) + search_query_object["query"].add_query(search_query) diff --git a/arches/app/search/components/related_resources_filter.py b/arches/app/search/components/related_resources_filter.py index 2791afb73d..8caa96e437 100644 --- a/arches/app/search/components/related_resources_filter.py +++ b/arches/app/search/components/related_resources_filter.py @@ -4,9 +4,8 @@ "icon": "fa fa-code-fork", "modulename": "", "classname": "", - "type": "filter", + "type": "related-resources-filter-type", "componentpath": "views/components/search/related-resources-filter", "componentname": "related-resources-filter", - "sortorder": "4", - "enabled": True, + "config": {}, } diff --git a/arches/app/search/components/resource_type_filter.py b/arches/app/search/components/resource_type_filter.py index ee7aa3d540..f7afa928d2 100644 --- a/arches/app/search/components/resource_type_filter.py +++ b/arches/app/search/components/resource_type_filter.py @@ -1,4 +1,3 @@ -from arches.app.models.graph import Graph from arches.app.utils.betterJSONSerializer import JSONDeserializer from arches.app.search.elasticsearch_dsl_builder import Bool, Terms from arches.app.search.components.base import BaseSearchFilter @@ -11,11 +10,10 @@ "icon": "", "modulename": "resource_type_filter.py", "classname": "ResourceTypeFilter", - "type": "resource-type-filter", + "type": "resource-type-filter-type", "componentpath": "views/components/search/resource-type-filter", "componentname": "resource-type-filter", - "sortorder": "0", - "enabled": True, + "config": {}, } @@ -27,11 +25,10 @@ def get_permitted_graphids(permitted_nodegroups): class ResourceTypeFilter(BaseSearchFilter): - def append_dsl( - self, search_results_object, permitted_nodegroups, include_provisional - ): + def append_dsl(self, search_query_object, **kwargs): + permitted_nodegroups = kwargs.get("permitted_nodegroups") search_query = Bool() - querystring_params = self.request.GET.get(details["componentname"], "") + querystring_params = self.request.GET.get(self.componentname, "") graph_ids = [] permitted_graphids = get_permitted_graphids(permitted_nodegroups) @@ -53,7 +50,7 @@ def append_dsl( search_query.filter(terms) - search_results_object["query"].add_query(search_query) + search_query_object["query"].add_query(search_query) def view_data(self): return { diff --git a/arches/app/search/components/saved_searches.py b/arches/app/search/components/saved_searches.py index 04d88b443c..9ab61448da 100644 --- a/arches/app/search/components/saved_searches.py +++ b/arches/app/search/components/saved_searches.py @@ -1,5 +1,4 @@ from arches.app.models.system_settings import settings -from arches.app.utils.betterJSONSerializer import JSONSerializer from arches.app.search.components.base import BaseSearchFilter details = { @@ -8,17 +7,16 @@ "icon": "fa fa-bookmark", "modulename": "saved_searches.py", "classname": "SavedSearches", - "type": "popup", + "type": "saved-searches-type", "componentpath": "views/components/search/saved-searches", "componentname": "saved-searches", - "sortorder": "2", - "enabled": True, + "config": {}, } class SavedSearches(BaseSearchFilter): def view_data(self): ret = {} - ret["saved_searches"] = settings.SAVED_SEARCHES + ret[self.componentname] = settings.SAVED_SEARCHES return ret diff --git a/arches/app/search/components/search_export.py b/arches/app/search/components/search_export.py index 07e0a1d05b..e2c0e702ae 100644 --- a/arches/app/search/components/search_export.py +++ b/arches/app/search/components/search_export.py @@ -1,5 +1,3 @@ -from arches.app.models.system_settings import settings -from arches.app.utils.betterJSONSerializer import JSONSerializer from arches.app.search.components.base import BaseSearchFilter details = { @@ -8,11 +6,10 @@ "icon": "fa fa-download", "modulename": "search_export.py", "classname": "SearchExport", - "type": "popup", + "type": "search-export-type", "componentpath": "views/components/search/search-export", "componentname": "search-export", - "sortorder": "3", - "enabled": True, + "config": {}, } diff --git a/arches/app/search/components/search_results.py b/arches/app/search/components/search_results.py index 224fb72dcd..94d72fecad 100644 --- a/arches/app/search/components/search_results.py +++ b/arches/app/search/components/search_results.py @@ -1,5 +1,5 @@ import uuid -from arches.app.models import models +from arches.app.models.models import Node from arches.app.models.system_settings import settings from arches.app.search.elasticsearch_dsl_builder import ( Bool, @@ -13,6 +13,8 @@ from arches.app.search.components.base import BaseSearchFilter from arches.app.search.components.resource_type_filter import get_permitted_graphids from arches.app.utils.permission_backend import user_is_resource_reviewer +from arches.app.utils import permission_backend +from django.utils.translation import get_language, gettext as _ details = { "searchcomponentid": "", @@ -20,24 +22,23 @@ "icon": "", "modulename": "search_results.py", "classname": "SearchResultsFilter", - "type": "results-list", + "type": "search-results-type", "componentpath": "views/components/search/search-results", "componentname": "search-results", - "sortorder": "0", - "enabled": True, + "config": {}, } class SearchResultsFilter(BaseSearchFilter): - def append_dsl( - self, search_results_object, permitted_nodegroups, include_provisional - ): + def append_dsl(self, search_query_object, **kwargs): + permitted_nodegroups = kwargs.get("permitted_nodegroups") + include_provisional = kwargs.get("include_provisional") nested_agg = NestedAgg(path="points", name="geo_aggs") nested_agg_filter = FiltersAgg(name="inner") geo_agg_filter = Bool() try: - search_results_object["query"].dsl["query"]["bool"]["filter"][0]["terms"][ + search_query_object["query"].dsl["query"]["bool"]["filter"][0]["terms"][ "graph_id" ] # check if resource_type filter is already applied except (KeyError, IndexError): @@ -45,7 +46,7 @@ def append_dsl( permitted_graphids = get_permitted_graphids(permitted_nodegroups) terms = Terms(field="graph_id", terms=list(permitted_graphids)) resource_model_filter.filter(terms) - search_results_object["query"].add_query(resource_model_filter) + search_query_object["query"].add_query(resource_model_filter) if include_provisional is True: geo_agg_filter.filter( @@ -88,19 +89,30 @@ def append_dsl( ) ) search_query.must(subsearch_query) - search_results_object["query"].add_query(search_query) + search_query_object["query"].add_query(search_query) - search_results_object["query"].add_aggregation(nested_agg) + search_query_object["query"].add_aggregation(nested_agg) - def post_search_hook(self, search_results_object, results, permitted_nodegroups): + def post_search_hook(self, search_query_object, response_object, **kwargs): + permitted_nodegroups = kwargs.get("permitted_nodegroups") user_is_reviewer = user_is_resource_reviewer(self.request.user) + descriptor_types = ("displaydescription", "displayname") + active_and_default_language_codes = (get_language(), settings.LANGUAGE_CODE) + groups = [group.id for group in self.request.user.groups.all()] + response_object["groups"] = groups + # only reuturn points and geometries a user is allowed to view geojson_nodes = get_nodegroups_by_datatype_and_perm( self.request, "geojson-feature-collection", "read_nodegroup" ) - for result in results["hits"]["hits"]: + for result in response_object["results"]["hits"]["hits"]: + result.update( + permission_backend.get_search_ui_permissions( + self.request.user, result, groups + ) + ) result["_source"]["points"] = select_geoms_for_results( result["_source"]["points"], geojson_nodes, user_is_reviewer ) @@ -116,10 +128,23 @@ def post_search_hook(self, search_results_object, results, permitted_nodegroups) except KeyError: pass + for descriptor_type in descriptor_types: + descriptor = get_localized_descriptor( + result, descriptor_type, active_and_default_language_codes + ) + if descriptor: + result["_source"][descriptor_type] = descriptor["value"] + if descriptor_type == "displayname": + result["_source"]["displayname_language"] = descriptor[ + "language" + ] + else: + result["_source"][descriptor_type] = _("Undefined") + def get_nodegroups_by_datatype_and_perm(request, datatype, permission): nodes = [] - for node in models.Node.objects.filter(datatype=datatype): + for node in Node.objects.filter(datatype=datatype): if request.user.has_perm(permission, node.nodegroup): nodes.append(str(node.nodegroup_id)) return nodes @@ -139,3 +164,13 @@ def select_geoms_for_results(features, geojson_nodes, user_is_reviewer): res.append(feature) return res + + +def get_localized_descriptor(resource, descriptor_type, language_codes): + descriptor = resource["_source"][descriptor_type] + result = descriptor[0] if len(descriptor) > 0 else None + for language_code in language_codes: + for entry in descriptor: + if entry["language"] == language_code and entry["value"] != "": + return entry + return result diff --git a/arches/app/search/components/sort_results.py b/arches/app/search/components/sort_results.py index 128a73e6be..4682998560 100644 --- a/arches/app/search/components/sort_results.py +++ b/arches/app/search/components/sort_results.py @@ -1,5 +1,4 @@ from arches.app.search.components.base import BaseSearchFilter -from arches.app.search.elasticsearch_dsl_builder import Nested from django.utils.translation import get_language details = { @@ -8,22 +7,19 @@ "icon": "", "modulename": "sort_results.py", "classname": "SortResults", - "type": "", + "type": "sort-results-type", "componentpath": "views/components/search/sort-results", "componentname": "sort-results", - "sortorder": "0", - "enabled": True, + "config": {}, } class SortResults(BaseSearchFilter): - def append_dsl( - self, search_results_object, permitted_nodegroups, include_provisional - ): - sort_param = self.request.GET.get(details["componentname"], None) + def append_dsl(self, search_query_object, **kwargs): + sort_param = self.request.GET.get(self.componentname, None) if sort_param is not None and sort_param != "": - search_results_object["query"].sort( + search_query_object["query"].sort( field="displayname.value", dsl={ "order": sort_param, diff --git a/arches/app/search/components/standard_search_view.py b/arches/app/search/components/standard_search_view.py new file mode 100644 index 0000000000..24c0bb1439 --- /dev/null +++ b/arches/app/search/components/standard_search_view.py @@ -0,0 +1,242 @@ +from arches.app.models.system_settings import settings +from arches.app.search.components.base_search_view import BaseSearchView +from arches.app.search.mappings import RESOURCES_INDEX +from arches.app.views.search import ( + append_instance_permission_filter_dsl, + get_permitted_nodegroups, + get_provisional_type, +) +from arches.app.utils.permission_backend import ( + user_is_resource_reviewer, + user_is_resource_exporter, +) +from arches.app.utils.response import JSONErrorResponse +from arches.app.utils.string_utils import get_str_kwarg_as_bool +from datetime import datetime +import logging + + +details = { + "searchcomponentid": "69695d63-6f03-4536-8da9-841b07116381", + "name": "Standard Search View", + "icon": "", + "modulename": "standard_search_view.py", + "classname": "StandardSearchView", + "type": "search-view", + "componentpath": "views/components/search/standard-search-view", + "componentname": "standard-search-view", + "config": { + "default": True, + "linkedSearchFilters": [ + { + "componentname": "paging-filter", + "searchcomponentid": "7aff5819-651c-4390-9b9a-a61221ba52c6", + "layoutSortorder": 1, + "required": True, + "executionSortorder": 2, + }, + { + "componentname": "search-results", + "searchcomponentid": "00673743-8c1c-4cc0-bd85-c073a52e03ec", + "layoutSortorder": 2, + }, + { + "componentname": "map-filter", + "searchcomponentid": "09d97fc6-8c83-4319-9cef-3aaa08c3fbec", + "layoutSortorder": 1, + }, + { + "componentname": "advanced-search", + "searchcomponentid": "f0e56205-acb5-475b-9c98-f5e44f1dbd2c", + "layoutSortorder": 2, + }, + { + "componentname": "related-resources-filter", + "searchcomponentid": "59f28272-d1f1-4805-af51-227771739aed", + "layoutSortorder": 3, + }, + { + "componentname": "provisional-filter", + "searchcomponentid": "073406ed-93e5-4b5b-9418-b61c26b3640f", + "layoutSortorder": 4, + }, + { + "componentname": "resource-type-filter", + "searchcomponentid": "f1c46b7d-0132-421b-b1f3-95d67f9b3980", + "layoutSortorder": 5, + }, + { + "componentname": "saved-searches", + "searchcomponentid": "6dc29637-43a1-4fba-adae-8d9956dcd3b9", + "layoutSortorder": 6, + }, + { + "componentname": "search-export", + "searchcomponentid": "9c6a5a9c-a7ec-48d2-8a25-501b55b8eff6", + "layoutSortorder": 7, + }, + { + "componentname": "search-result-details", + "searchcomponentid": "f5986dae-8b01-11ea-b65a-77903936669c", + "layoutSortorder": 8, + }, + { + "componentname": "sort-results", + "searchcomponentid": "6a2fe122-de54-4e44-8e93-b6a0cda7955c", + "layoutSortorder": 9, + }, + { + "componentname": "term-filter", + "searchcomponentid": "1f42f501-ed70-48c5-bae1-6ff7d0d187da", + "layoutSortorder": 10, + }, + { + "componentname": "time-filter", + "searchcomponentid": "7497ed4f-2085-40da-bee5-52076a48bcb1", + "layoutSortorder": 11, + }, + { + "componentname": "paging-filter", + "searchcomponentid": "7aff5819-651c-4390-9b9a-a61221ba52c6", + "layoutSortorder": 12, + }, + { + "componentname": "search-results", + "searchcomponentid": "00673743-8c1c-4cc0-bd85-c073a52e03ec", + "layoutSortorder": 13, + "required": True, + "executionSortorder": 1, + }, + ], + }, +} + +logger = logging.getLogger(__name__) + + +class StandardSearchView(BaseSearchView): + + def append_dsl(self, search_query_object, **kwargs): + search_query_object["query"].include("graph_id") + search_query_object["query"].include("root_ontology_class") + search_query_object["query"].include("resourceinstanceid") + search_query_object["query"].include("points") + search_query_object["query"].include("geometries") + search_query_object["query"].include("displayname") + search_query_object["query"].include("displaydescription") + search_query_object["query"].include("map_popup") + search_query_object["query"].include("provisional_resource") + search_query_object["query"].include("permissions") + load_tiles = get_str_kwarg_as_bool("tiles", self.request.GET) + if load_tiles: + search_query_object["query"].include("tiles") + + def execute_query(self, search_query_object, response_object, **kwargs): + for_export = get_str_kwarg_as_bool("export", self.request.GET) + pages = self.request.GET.get("pages", None) + total = int(self.request.GET.get("total", "0")) + resourceinstanceid = self.request.GET.get("id", None) + dsl = search_query_object["query"] + if for_export or pages: + results = dsl.search(index=RESOURCES_INDEX, scroll="1m") + scroll_id = results["_scroll_id"] + if not pages: + if total <= settings.SEARCH_EXPORT_LIMIT: + pages = (total // settings.SEARCH_RESULT_LIMIT) + 1 + else: + pages = ( + int( + settings.SEARCH_EXPORT_LIMIT // settings.SEARCH_RESULT_LIMIT + ) + - 1 + ) + for page in range(int(pages)): + results_scrolled = dsl.se.es.scroll(scroll_id=scroll_id, scroll="1m") + results["hits"]["hits"] += results_scrolled["hits"]["hits"] + else: + results = dsl.search(index=RESOURCES_INDEX, id=resourceinstanceid) + + if results is not None: + if "hits" not in results: + if "docs" in results: + results = {"hits": {"hits": results["docs"]}} + else: + results = {"hits": {"hits": [results]}} + response_object["results"] = results + + def post_search_hook(self, search_query_object, response_object, **kwargs): + dsl = search_query_object["query"] + response_object["reviewer"] = user_is_resource_reviewer(self.request.user) + response_object["timestamp"] = datetime.now() + response_object["total_results"] = dsl.count(index=RESOURCES_INDEX) + response_object["userid"] = self.request.user.id + + def get_searchview_filters(self): + search_filters = [ + available_filter + for available_filter in self.available_search_filters + if available_filter.componentname != "search-export" + ] + if user_is_resource_exporter(self.request.user): + search_filters.extend( + [ + available_filter + for available_filter in self.available_search_filters + if available_filter.componentname == "search-export" + ] + ) + + search_filters.append(self.searchview_component) + + return search_filters + + def handle_search_results_query( + self, search_query_object, response_object, search_filter_factory, returnDsl + ): + sorted_query_obj = search_filter_factory.create_search_query_dict( + list(self.request.GET.items()) + list(self.request.POST.items()) + ) + permitted_nodegroups = get_permitted_nodegroups(self.request.user) + include_provisional = get_provisional_type(self.request) + try: + for filter_type, querystring in list(sorted_query_obj.items()): + search_filter = search_filter_factory.get_filter(filter_type) + if search_filter: + search_filter.append_dsl( + search_query_object, + permitted_nodegroups=permitted_nodegroups, + include_provisional=include_provisional, + ) + append_instance_permission_filter_dsl(self.request, search_query_object) + except Exception as err: + logger.exception(err) + return JSONErrorResponse(message=str(err)) + + if returnDsl: + dsl = search_query_object.pop("query", None) + return dsl, search_query_object + + for filter_type, querystring in list(sorted_query_obj.items()): + search_filter = search_filter_factory.get_filter(filter_type) + if search_filter: + search_filter.execute_query(search_query_object, response_object) + + if response_object["results"] is not None: + # allow filters to modify the results + for filter_type, querystring in list(sorted_query_obj.items()): + search_filter = search_filter_factory.get_filter(filter_type) + if search_filter: + search_filter.post_search_hook( + search_query_object, + response_object, + permitted_nodegroups=permitted_nodegroups, + ) + + search_query_object.pop("query") + # ensure that if a search filter modified the query in some way + # that the modification is set on the response_object + for key, value in list(search_query_object.items()): + if key not in response_object: + response_object[key] = value + + return response_object, search_query_object diff --git a/arches/app/search/components/term_filter.py b/arches/app/search/components/term_filter.py index 4202b69a36..043f50a2ef 100644 --- a/arches/app/search/components/term_filter.py +++ b/arches/app/search/components/term_filter.py @@ -20,20 +20,19 @@ "icon": "", "modulename": "term_filter.py", "classname": "TermFilter", - "type": "text-input", + "type": "term-filter-type", "componentpath": "views/components/search/term-filter", "componentname": "term-filter", - "sortorder": "0", - "enabled": True, + "config": {}, } class TermFilter(BaseSearchFilter): - def append_dsl( - self, search_results_object, permitted_nodegroups, include_provisional - ): + def append_dsl(self, search_query_object, **kwargs): + permitted_nodegroups = kwargs.get("permitted_nodegroups") + include_provisional = kwargs.get("include_provisional") search_query = Bool() - querysting_params = self.request.GET.get(details["componentname"], "") + querysting_params = self.request.GET.get(self.componentname, "") language = self.request.GET.get("language", "*") for term in JSONDeserializer().deserialize(querysting_params): if term["type"] == "term" or term["type"] == "string": @@ -124,7 +123,7 @@ def append_dsl( else: search_query.must(nested_string_filter) # need to set min_score because the query returns results with score 0 and those have to be removed, which I don't think it should be doing - search_results_object["query"].min_score("0.01") + search_query_object["query"].min_score("0.01") elif term["type"] == "concept": concept_ids = _get_child_concepts(term["value"]) conceptid_filter = Bool() @@ -150,7 +149,7 @@ def append_dsl( else: search_query.filter(nested_conceptid_filter) - search_results_object["query"].add_query(search_query) + search_query_object["query"].add_query(search_query) def _get_child_concepts(conceptid): diff --git a/arches/app/search/components/time_filter.py b/arches/app/search/components/time_filter.py index b70ec4033a..a77b901172 100644 --- a/arches/app/search/components/time_filter.py +++ b/arches/app/search/components/time_filter.py @@ -1,11 +1,8 @@ -from datetime import datetime -from arches.app.models import models -from arches.app.models.system_settings import settings +from arches.app.models.models import CardModel, CardXNodeXWidget, GraphModel, Node from arches.app.utils.date_utils import ExtendedDateFormat from arches.app.utils.betterJSONSerializer import JSONDeserializer from arches.app.search.elasticsearch_dsl_builder import Bool, Nested, Term, Terms, Range from arches.app.search.components.base import BaseSearchFilter -from django.db.models import Q details = { "searchcomponentid": "", @@ -13,20 +10,19 @@ "icon": "fa fa-calendar", "modulename": "time_filter.py", "classname": "TimeFilter", - "type": "popup", + "type": "time-filter-type", "componentpath": "views/components/search/time-filter", "componentname": "time-filter", - "sortorder": "1", - "enabled": True, + "config": {}, } class TimeFilter(BaseSearchFilter): - def append_dsl( - self, search_results_object, permitted_nodegroups, include_provisional - ): + def append_dsl(self, search_query_object, **kwargs): + permitted_nodegroups = kwargs.get("permitted_nodegroups") + include_provisional = kwargs.get("include_provisional") search_query = Bool() - querysting_params = self.request.GET.get(details["componentname"], "") + querysting_params = self.request.GET.get(self.componentname, "") temporal_filter = JSONDeserializer().deserialize(querysting_params) if "fromDate" in temporal_filter and "toDate" in temporal_filter: # now = str(datetime.utcnow()) @@ -153,12 +149,12 @@ def append_dsl( search_query.filter(temporal_query) - search_results_object["query"].add_query(search_query) + search_query_object["query"].add_query(search_query) def view_data(self): ret = {} date_datatypes = ["date", "edtf"] - date_nodes = models.Node.objects.filter( + date_nodes = Node.objects.filter( datatype__in=date_datatypes, graph__isresource=True, graph__publication__isnull=False, @@ -169,11 +165,11 @@ def view_data(self): if self.request.user.has_perm("read_nodegroup", node.nodegroup) } - date_cardxnodesxwidgets = models.CardXNodeXWidget.objects.filter( + date_cardxnodesxwidgets = CardXNodeXWidget.objects.filter( node_id__in=list(node_graph_dict.keys()) ) card_ids = [cnw.card_id for cnw in date_cardxnodesxwidgets] - cards = models.CardModel.objects.filter(cardid__in=card_ids) + cards = CardModel.objects.filter(cardid__in=card_ids) card_name_dict = {str(card.cardid): card.name for card in cards} node_obj_list = [] for cnw in date_cardxnodesxwidgets: @@ -184,7 +180,7 @@ def view_data(self): node_obj_list.append(node_obj) ret["date_nodes"] = node_obj_list - ret["graph_models"] = models.GraphModel.objects.filter( + ret["graph_models"] = GraphModel.objects.filter( graphid__in=list(node_graph_dict.values()) ) return ret diff --git a/arches/app/search/search_export.py b/arches/app/search/search_export.py index 3bd2a7ab39..2a3fc0d475 100644 --- a/arches/app/search/search_export.py +++ b/arches/app/search/search_export.py @@ -34,7 +34,7 @@ from arches.app.utils.betterJSONSerializer import JSONDeserializer from arches.app.utils.data_management.resources.exporter import ResourceExporter from arches.app.utils.geo_utils import GeoUtils -from arches.app.utils.string_utils import str_to_bool +from arches.app.utils.string_utils import get_str_kwarg_as_bool import arches.app.utils.zip as zip_utils from arches.app.models.system_settings import settings @@ -50,9 +50,9 @@ def __init__(self, search_request=None): search_request.GET["export"] = True self.report_link = search_request.GET.get("reportlink", False) self.format = search_request.GET.get("format", "tilecsv") - self.export_system_values = search_request.GET.get("exportsystemvalues", False) - if not isinstance(self.export_system_values, bool): - self.export_system_values = str_to_bool(self.export_system_values) + self.export_system_values = get_str_kwarg_as_bool( + "exportsystemvalues", search_request.GET + ) self.compact = search_request.GET.get("compact", True) self.precision = int(search_request.GET.get("precision", 5)) if self.format == "shp" and self.compact is not True: diff --git a/arches/app/templates/views/components/search/related-resources-filter.htm b/arches/app/templates/views/components/search/related-resources-filter.htm index 991253c0ca..bb6feba6bf 100644 --- a/arches/app/templates/views/components/search/related-resources-filter.htm +++ b/arches/app/templates/views/components/search/related-resources-filter.htm @@ -1,7 +1,7 @@ {% load webpack_static from webpack_loader %}