diff --git a/lms/lib/courseware_search/lms_filter_generator.py b/lms/lib/courseware_search/lms_filter_generator.py index 5b2592e4cd19..d8e295c540d0 100644 --- a/lms/lib/courseware_search/lms_filter_generator.py +++ b/lms/lib/courseware_search/lms_filter_generator.py @@ -66,4 +66,4 @@ def exclude_dictionary(self, **kwargs): if not getattr(settings, "SEARCH_SKIP_SHOW_IN_CATALOG_FILTERING", True): exclude_dictionary['catalog_visibility'] = [CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE] - return exclude_dictionary + return exclude_dictionary \ No newline at end of file diff --git a/lms/static/js/discovery/collections/filters.js b/lms/static/js/discovery/collections/filters.js index 9bb7681fa016..ad7f1145752c 100644 --- a/lms/static/js/discovery/collections/filters.js +++ b/lms/static/js/discovery/collections/filters.js @@ -5,10 +5,12 @@ return Backbone.Collection.extend({ model: Filter, getTerms: function() { - return this.reduce(function(terms, filter) { - terms[filter.id] = filter.get('query'); - return terms; - }, {}); + return this.reduce(function(memo, filter) { + const type = filter.get('type'); + if (!memo[type]) memo[type] = []; + memo[type].push(filter.get('query')); + return memo; + }, {}); } }); }); diff --git a/lms/static/js/discovery/discovery_factory.js b/lms/static/js/discovery/discovery_factory.js index d26841f86fca..5f66741153ef 100644 --- a/lms/static/js/discovery/discovery_factory.js +++ b/lms/static/js/discovery/discovery_factory.js @@ -13,7 +13,8 @@ var filterBar = new FilterBar({collection: filters}); var refineSidebar = new RefineSidebar({ collection: search.discovery.facetOptions, - meanings: meanings + meanings: meanings || {}, + filtersCollection: filters }); var listing; var courseListingModel = search.discovery; @@ -37,12 +38,19 @@ dispatcher.listenTo(refineSidebar, 'selectOption', function(type, query, name) { form.showLoadingIndicator(); - if (filters.get(type)) { - removeFilter(type); + const exist = filters.findWhere({ type: type, query: query }); + if (exist) { + filters.remove(exist); } else { - filters.add({type: type, query: query, name: name}); - search.refineSearch(filters.getTerms()); + filters.add({ type: type, query: query, name: name }); + refineSidebar.render(); } + + const terms = groupTerms(filters.toJSON()); + const queryString = flattenTermsToQuery(terms); + Backbone.history.navigate('search?' + queryString, { trigger: true }); + + search.refineSearch(terms); }); dispatcher.listenTo(filterBar, 'clearFilter', removeFilter); @@ -95,9 +103,31 @@ search.refineSearch(filters.getTerms()); } } + // Group flat list of terms into { type: [queries...] } + function groupTerms(termsList) { + const grouped = {}; + _.each(termsList, function(termObj) { + if (!grouped[termObj.type]) { + grouped[termObj.type] = []; + } + grouped[termObj.type].push(termObj.query); + }); + return grouped; + } + + // Flatten grouped terms into query string + function flattenTermsToQuery(terms) { + const pairs = []; + _.each(terms, function(values, key) { + _.each(values, function(val) { + pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(val)); + }); + }); + return pairs.join('&'); + } - function quote(string) { - return '"' + string + '"'; + function quote(str) { + return '"' + str + '"'; } }; }); diff --git a/lms/static/js/discovery/models/filter.js b/lms/static/js/discovery/models/filter.js index 54bc3ead8f0d..e353cf75717f 100644 --- a/lms/static/js/discovery/models/filter.js +++ b/lms/static/js/discovery/models/filter.js @@ -1,13 +1,16 @@ (function(define) { define(['backbone'], function(Backbone) { 'use strict'; - return Backbone.Model.extend({ - idAttribute: 'type', + defaults: { type: 'search_query', query: '', name: '' + }, + initialize: function() { + // Manually set model ID used by collection.get() + this.set('id', this.get('type') + '|' + this.get('query')); } }); }); diff --git a/lms/static/js/discovery/models/search_state.js b/lms/static/js/discovery/models/search_state.js index 93dd7cb6475c..465b97c7970f 100644 --- a/lms/static/js/discovery/models/search_state.js +++ b/lms/static/js/discovery/models/search_state.js @@ -6,6 +6,36 @@ 'js/discovery/collections/filters' ], function(_, Backbone, CourseDiscovery, Filters) { 'use strict'; + // ✅ Add the conversion function inside the define scope + function convertAggsToFacets(aggs) { + const facets = {}; + + for (const facetName in aggs) { + if (!aggs.hasOwnProperty(facetName)) continue; + + const facetAgg = aggs[facetName]; + const terms = facetAgg.terms || {}; + const termsList = []; + + for (const term in terms) { + if (!terms.hasOwnProperty(term)) continue; + + if (['total', 'other'].includes(term)) continue; // skip metadata + termsList.push({ + name: term, + count: terms[term], + }); + } + + facets[facetName] = { + displayName: facetName, + name: facetName, + terms: termsList, + }; + } + + return facets; + } return Backbone.Model.extend({ @@ -32,33 +62,44 @@ refineSearch: function(terms) { this.reset(); - this.terms = terms; - this.sendQuery(this.buildQuery(0)); - }, + if (terms) { + this.terms = terms; + + } else { + this.terms = {}; + } + + const data = this.buildQuery(0); + console.log('sending data:', data); + this.sendQuery(data); + }, loadNextPage: function() { if (this.hasNextPage()) { this.sendQuery(this.buildQuery(this.page + 1)); } }, - // private - hasNextPage: function() { var total = this.discovery.get('totalCount'); return total - ((this.page + 1) * this.pageSize) > 0; }, sendQuery: function(data) { - // eslint-disable-next-line no-unused-expressions - this.jqhxr && this.jqhxr.abort(); + if (this.jqhxr) { + this.jqhxr.abort(); + } + console.log('Sending data to backend:', data); + this.jqhxr = this.discovery.fetch({ type: 'POST', - data: data + data: JSON.stringify(data), + contentType: 'application/json', + dataType: 'json' }); + return this.jqhxr; }, - buildQuery: function(pageIndex) { var data = { search_string: this.searchTerm, @@ -68,7 +109,17 @@ _.extend(data, this.terms); return data; }, - + // this groupTerms added to group the search terms and send to refineSearch + groupTerms: function(termsList) { + const grouped = {}; + _.each(termsList, function(termObj) { + if (!grouped[termObj.type]) { + grouped[termObj.type] = []; + } + grouped[termObj.type].push(termObj.query); + }); + return grouped; + }, reset: function() { this.discovery.reset(); this.page = 0; @@ -81,11 +132,24 @@ this.trigger('error'); } }, - onSync: function(collection, response, options) { + // ✅ Convert aggs to facets + if (!response.facets && response.aggs) { + response.facets = convertAggsToFacets(response.aggs); + console.log('✅ Converted facets:', response.facets); + } + + // ✅ Safely parse options.data + let pageIndex = 0; + try { + const parsedData = typeof options.data === 'string' ? JSON.parse(options.data) : options.data; + pageIndex = parsedData.page_index || 0; + } catch (e) { + console.warn('Failed to parse options.data:', e); + } var total = this.discovery.get('totalCount'); var originalSearchTerm = this.searchTerm; - if (options.data.page_index === 0) { + if (pageIndex === 0) { if (total === 0) { // list all courses this.cachedDiscovery().done(function(cached) { @@ -111,7 +175,7 @@ this.trigger('search', this.searchTerm, total); } } else { - this.page = options.data.page_index; + this.page = pageIndex; this.trigger('next'); } }, diff --git a/lms/static/js/discovery/views/courses_listing.js b/lms/static/js/discovery/views/courses_listing.js index 65413c62dbdf..31a0467654bc 100644 --- a/lms/static/js/discovery/views/courses_listing.js +++ b/lms/static/js/discovery/views/courses_listing.js @@ -39,6 +39,8 @@ var item = new CourseCardView({model: result}); return item.render().el; }, this); + console.log("Rendering", items.length, "course cards"); + HtmlUtils.append( this.$list, HtmlUtils.HTML(items) diff --git a/lms/static/js/discovery/views/filter_bar.js b/lms/static/js/discovery/views/filter_bar.js index 2bc0561075a9..afc4b20f1894 100644 --- a/lms/static/js/discovery/views/filter_bar.js +++ b/lms/static/js/discovery/views/filter_bar.js @@ -55,12 +55,44 @@ this.hide(); }, + // clearFilter: function(event) { + // var $target = $(event.currentTarget); + // var filter = this.collection.get($target.data('type')); + // this.trigger('clearFilter', filter.id); + // }, clearFilter: function(event) { - var $target = $(event.currentTarget); - var filter = this.collection.get($target.data('type')); - this.trigger('clearFilter', filter.id); - }, + var $target = $(event.currentTarget); // event.currentTarget is .discovery-button + var type = $target.data('type'); + var query = $target.data('query'); + + if (!type || !query) { + console.warn('Missing data-type or data-query'); + return; + } + + var uid = type + '|' + query; + var filter = this.collection.get(uid); + + if (!filter) { + console.warn('Filter not found for uid:', uid); + return; + } + + this.collection.remove(filter); + // ✅ Uncheck the matching checkbox + // Unselect the corresponding filter button in the facet list + var selector = 'button.facet-option.discovery-button[data-facet="' + type + '"][data-value="' + query + '"]'; + var $button = $(selector); + if ($button.length) { + $button.removeClass('selected'); + } else { + console.warn('Filter button not found to unselect:', selector); + } + + // ✅ Trigger new search with updated filter state + this.trigger('clearFilter'); + }, clearAll: function(event) { this.trigger('clearAll'); }, diff --git a/lms/static/js/discovery/views/refine_sidebar.js b/lms/static/js/discovery/views/refine_sidebar.js index e3e01d069d13..30864e787f7a 100644 --- a/lms/static/js/discovery/views/refine_sidebar.js +++ b/lms/static/js/discovery/views/refine_sidebar.js @@ -18,9 +18,11 @@ initialize: function(options) { this.meanings = options.meanings || {}; + this.filtersCollection = options.filtersCollection || null; //initialize filterCollection colled in discovery_factory this.$container = this.$el.find('.search-facets-lists'); this.facetTpl = HtmlUtils.template($('#facet-tpl').html()); this.facetOptionTpl = HtmlUtils.template($('#facet_option-tpl').html()); + this.fullOptions = {}; }, facetName: function(key) { @@ -39,6 +41,11 @@ return HtmlUtils.joinHtml.apply(this, _.map(options, function(option) { var data = _.clone(option.attributes); data.name = this.termName(data.facet, data.term); + // this added to handle selected orgs as selected // replaced with returns + data.selected = this.filtersCollection && this.filtersCollection.any(function(filter) { + return filter.get('type') === data.facet && filter.get('query') === data.term; + }); + return this.facetOptionTpl(data); }, this)); }, @@ -54,6 +61,26 @@ render: function() { var grouped = this.collection.groupBy('facet'); + // added tho render for each selected orgs + _.each(grouped, function(currentOptions, facetKey) { + var termMap = {}; + _.each(currentOptions, function(model) { + termMap[model.get('term')] = model; + }); + + if (this.fullOptions[facetKey]) { + _.each(this.fullOptions[facetKey], function(model) { + var term = model.get('term'); + if (!termMap[term]) { + termMap[term] = model; + } + }); + } + + this.fullOptions[facetKey] = _.values(termMap); + grouped[facetKey] = this.fullOptions[facetKey]; + }, this); + var htmlSnippet = HtmlUtils.joinHtml.apply( this, _.map(grouped, function(options, facetKey) { if (options.length > 0) { diff --git a/lms/templates/discovery/filter.underscore b/lms/templates/discovery/filter.underscore index e73f50fa56ed..96c2e298850d 100644 --- a/lms/templates/discovery/filter.underscore +++ b/lms/templates/discovery/filter.underscore @@ -1,4 +1,4 @@ -