diff --git a/public/language/en_GB/search.json b/public/language/en_GB/search.json index 23519b038b..7ae707185c 100644 --- a/public/language/en_GB/search.json +++ b/public/language/en_GB/search.json @@ -3,5 +3,7 @@ "no-matches": "No matches found", "in": "In", "by": "By", - "posted-by": "Posted by" + "posted-by": "Posted by", + "in-categories": "In Categories", + "search-child-categories": "Search child categories" } diff --git a/public/src/client/search.js b/public/src/client/search.js index e536aaa160..d6b0566e56 100644 --- a/public/src/client/search.js +++ b/public/src/client/search.js @@ -10,17 +10,25 @@ define('forum/search', ['search'], function(searchModule) { $('#advanced-search #search-input').val(searchQuery); var params = utils.params(); - var select = $('#advanced-search select'); + var searchIn = $('#advanced-search #search-in'); if (params && params.in) { - select.val(params.in); + searchIn.val(params.in); } if (params && params.by) { - $('.by-container #posted-by-input').val(params.by); + $('.by-container #posted-by-user').val(params.by); } - select.on('change', function() { - $('.by-container').toggleClass('hide', select.val() !== 'posts'); + if (params && params['categories[]']) { + $('#posted-in-categories').val(params['categories[]']); + } + + if (params && params.searchChildren) { + $('#search-children').prop('checked', true); + } + + searchIn.on('change', function() { + $('.by-container').toggleClass('hide', searchIn.val() !== 'posts'); }); highlightMatches(searchQuery); @@ -28,13 +36,13 @@ define('forum/search', ['search'], function(searchModule) { $('#advanced-search').off('submit').on('submit', function(e) { e.preventDefault(); var input = $(this).find('#search-input'); - var searchIn = $(this).find('select'); - var postedBy = $(this).find('#posted-by-input'); searchModule.query({ term: input.val(), - in: searchIn.val(), - by: postedBy.val() + in: $(this).find('#search-in').val(), + by: $(this).find('#posted-by-user').val(), + categories: $(this).find('#posted-in-categories').val(), + searchChildren: $(this).find('#search-children').is(':checked') }, function() { input.val(''); }); @@ -63,7 +71,7 @@ define('forum/search', ['search'], function(searchModule) { function enableAutoComplete() { - var input = $('.by-container #posted-by-input'); + var input = $('.by-container #posted-by-user'); input.autocomplete({ delay: 100, source: function(request, response) { diff --git a/public/src/modules/search.js b/public/src/modules/search.js index 73ff828673..e8cb85b31f 100644 --- a/public/src/modules/search.js +++ b/public/src/modules/search.js @@ -27,6 +27,13 @@ define('search', ['navigator'], function(nav) { if (postedBy && searchIn === 'posts') { query.by = postedBy; } + + if (data.categories && data.categories.length) { + query.categories = data.categories; + if (data.searchChildren) { + query.searchChildren = data.searchChildren; + } + } ajaxify.go('search/' + term + '?' + decodeURIComponent($.param(query))); callback(); } else { diff --git a/public/src/utils.js b/public/src/utils.js index b9658593cc..b58984a20e 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -256,7 +256,14 @@ value = options.skipToType[key] ? decodeURI(val[1]) : utils.toType(decodeURI(val[1])); if (key) { - hash[key] = value; + if (!hash[key]) { + hash[key] = value; + } else { + if (!$.isArray(hash[key])) { + hash[key] = [hash[key]]; + } + hash[key].push(value); + } } }); return hash; diff --git a/src/controllers/search.js b/src/controllers/search.js index 355fddeced..f974cb3e94 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -5,6 +5,7 @@ var searchController = {}, validator = require('validator'), plugins = require('../plugins'), search = require('../search'), + categories = require('../categories'), helpers = require('./helpers'); @@ -12,35 +13,46 @@ searchController.search = function(req, res, next) { if (!plugins.hasListeners('filter:search.query')) { return helpers.notFound(req, res); } - var breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:search]]'}]); - if (!req.params.term) { - return res.render('search', { - time: 0, - search_query: '', - posts: [], - topics: [], - users: [], - tags: [], - breadcrumbs: breadcrumbs - }); - } var uid = req.user ? req.user.uid : 0; + var breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:search]]'}]); - req.params.term = validator.escape(req.params.term); - - search.search({ - query: req.params.term, - searchIn: req.query.in, - postedBy: req.query.by, - uid: uid - }, function(err, results) { + categories.getCategoriesByPrivilege(uid, 'read', function(err, categories) { if (err) { return next(err); } - results.breadcrumbs = breadcrumbs; - res.render('search', results); + if (!req.params.term) { + return res.render('search', { + time: 0, + search_query: '', + posts: [], + topics: [], + users: [], + tags: [], + categories: categories, + breadcrumbs: breadcrumbs + }); + } + + req.params.term = validator.escape(req.params.term); + + search.search({ + query: req.params.term, + searchIn: req.query.in, + postedBy: req.query.by, + categories: req.query.categories, + searchChildren: req.query.searchChildren, + uid: uid + }, function(err, results) { + if (err) { + return next(err); + } + + results.breadcrumbs = breadcrumbs; + results.categories = categories; + res.render('search', results); + }); }); }; diff --git a/src/search.js b/src/search.js index c4a830f41a..800aea308c 100644 --- a/src/search.js +++ b/src/search.js @@ -3,6 +3,7 @@ var async = require('async'), posts = require('./posts'), topics = require('./topics'), + categories = require('./categories'), user = require('./user'), plugins = require('./plugins'), privileges = require('./privileges'); @@ -29,7 +30,6 @@ search.search = function(data, callback) { var query = data.query; var searchIn = data.searchIn || 'posts'; - var uid = data.uid || 0; var result = { posts: [], @@ -38,7 +38,7 @@ search.search = function(data, callback) { }; if (searchIn === 'posts') { - searchInPosts(query, data.postedBy, uid, done); + searchInPosts(query, data, done); } else if (searchIn === 'users') { searchInUsers(query, done); } else if (searchIn === 'tags') { @@ -48,7 +48,9 @@ search.search = function(data, callback) { } }; -function searchInPosts(query, postedBy, uid, callback) { +function searchInPosts(query, data, callback) { + data.uid = data.uid || 0; + var postedBy = data.postedBy || ''; async.parallel({ pids: function(next) { searchQuery('post', query, next); @@ -56,12 +58,8 @@ function searchInPosts(query, postedBy, uid, callback) { tids: function(next) { searchQuery('topic', query, next); }, - postedByUid: function(next) { - if (postedBy) { - user.getUidByUsername(postedBy, next); - } else { - next(null, 0); - } + searchCategories: function(next) { + getSearchCategories(data, next); } }, function (err, results) { if (err) { @@ -82,15 +80,18 @@ function searchInPosts(query, postedBy, uid, callback) { mainPids.push(pid); } }); - privileges.posts.filter('read', mainPids, uid, next); + privileges.posts.filter('read', mainPids, data.uid, next); }, function(pids, next) { - posts.getPostSummaryByPids(pids, uid, {stripTags: true, parse: false}, next); + posts.getPostSummaryByPids(pids, data.uid, {stripTags: true, parse: false}, next); }, function(posts, next) { - if (postedBy) { + var searchCategories = results.searchCategories; + if (postedBy || searchCategories.length) { posts = posts.filter(function(post) { - return post && parseInt(post.uid, 10) === parseInt(results.postedByUid, 10); + return post && + (postedBy ? post.user.username === postedBy : true) && + (searchCategories.length ? searchCategories.indexOf(post.category.cid) !== -1 : true); }); } next(null, posts); @@ -99,6 +100,56 @@ function searchInPosts(query, postedBy, uid, callback) { }); } +function getSearchCategories(data, callback) { + if (!Array.isArray(data.categories) || !data.categories.length || data.categories.indexOf('all') !== -1) { + return callback(null, []); + } + + async.parallel({ + watchedCids: function(next) { + if (data.categories.indexOf('watched') !== -1) { + user.getWatchedCategories(data.uid, next); + } else { + next(null, []); + } + }, + childrenCids: function(next) { + if (data.searchChildren) { + getChildrenCids(data.categories, data.uid, next); + } else { + next(null, []); + } + } + }, function(err, results) { + if (err) { + return callback(err); + } + + var cids = results.watchedCids.concat(results.childrenCids).concat(data.categories).filter(function(cid, index, array) { + return cid && array.indexOf(cid) === index; + }); + + callback(null, cids); + }); +} + +function getChildrenCids(cids, uid, callback) { + categories.getChildren(cids, uid, function(err, childrenCategories) { + if (err) { + return callback(err); + } + + var childrenCids = []; + childrenCategories.forEach(function(childrens) { + childrenCids = childrenCids.concat(childrens.map(function(category) { + return category && category.cid; + })); + }); + + callback(null, childrenCids); + }); +} + function searchInUsers(query, callback) { user.search({query: query}, function(err, results) { callback(err, results ? results.users : null); diff --git a/src/user.js b/src/user.js index 79060f020b..b320c3f594 100644 --- a/src/user.js +++ b/src/user.js @@ -435,6 +435,26 @@ var async = require('async'), db.getSortedSetRange('uid:' + uid + ':ignored:cids', 0, -1, callback); }; + User.getWatchedCategories = function(uid, callback) { + async.parallel({ + ignored: function(next) { + User.getIgnoredCategories(uid, next); + }, + all: function(next) { + db.getSortedSetRange('categories:cid', 0, -1, next); + } + }, function(err, results) { + if (err) { + return callback(err); + } + + var watched = results.all.filter(function(cid) { + return cid && results.ignored.indexOf(cid) === -1; + }); + callback(null, watched); + }); + }; + User.ignoreCategory = function(uid, cid, callback) { if (!uid) { return callback();