diff --git a/package.json b/package.json index 83c62b7e9e..6bfa27004b 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "start": "node loader.js", "lint": "eslint --cache .", "pretest": "npm run lint", - "test": "istanbul cover node_modules/mocha/bin/_mocha -- -R spec", - "coveralls": "istanbul cover _mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" + "test": "istanbul cover node_modules/mocha/bin/_mocha -- -R dot", + "coveralls": "istanbul cover _mocha --report lcovonly -- -R dot && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, "dependencies": { "async": "~1.5.0", diff --git a/public/language/en-GB/search.json b/public/language/en-GB/search.json index fde9db35f6..98c1afcea2 100644 --- a/public/language/en-GB/search.json +++ b/public/language/en-GB/search.json @@ -8,6 +8,7 @@ "posted-by": "Posted by", "in-categories": "In Categories", "search-child-categories": "Search child categories", + "has-tags": "Has tags", "reply-count": "Reply Count", "at-least": "At least", "at-most": "At most", diff --git a/public/src/client/search.js b/public/src/client/search.js index 20c85c13c2..48cf358c0c 100644 --- a/public/src/client/search.js +++ b/public/src/client/search.js @@ -12,8 +12,6 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc var searchIn = $('#search-in'); - fillOutForm(); - searchIn.on('change', function () { updateFormItemVisiblity(searchIn.val()); }); @@ -31,6 +29,8 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc handleSavePreferences(); enableAutoComplete(); + + fillOutForm(); }; function getSearchData() { @@ -43,6 +43,7 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc searchData.by = form.find('#posted-by-user').val(); searchData.categories = form.find('#posted-in-categories').val(); searchData.searchChildren = form.find('#search-children').is(':checked'); + searchData.hasTags = form.find('#has-tags').tagsinput('items'); searchData.replies = form.find('#reply-count').val(); searchData.repliesFilter = form.find('#reply-count-filter').val(); searchData.timeFilter = form.find('#post-time-filter').val(); @@ -79,7 +80,6 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc $('#posted-by-user').val(formData.by); } - if (formData.categories) { $('#posted-in-categories').val(formData.categories); } @@ -88,6 +88,13 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc $('#search-children').prop('checked', true); } + if (formData.hasTags) { + formData.hasTags = Array.isArray(formData.hasTags) ? formData.hasTags : [formData.hasTags]; + formData.hasTags.forEach(function (tag) { + $('#has-tags').tagsinput('add', tag); + }); + } + if (formData.replies) { $('#reply-count').val(formData.replies); $('#reply-count-filter').val(formData.repliesFilter); @@ -157,6 +164,14 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc function enableAutoComplete() { autocomplete.user($('#posted-by-user')); + + var tagEl = $('#has-tags'); + tagEl.tagsinput({ + confirmKeys: [13, 44], + trimValue: true + }); + + autocomplete.tag($('#has-tags').siblings('.bootstrap-tagsinput').find('input')); } return Search; diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js index c20f804f38..ecaf563d10 100644 --- a/public/src/modules/autocomplete.js +++ b/public/src/modules/autocomplete.js @@ -75,5 +75,40 @@ define('autocomplete', function () { }); }; + module.tag = function (input, onselect) { + app.loadJQueryUI(function () { + input.autocomplete({ + delay: 100, + open: function () { + $(this).autocomplete('widget').css('z-index', 20000); + }, + select: function (event, ui) { + onselect = onselect || function () {}; + var e = jQuery.Event('keypress'); + e.which = 13; + e.keyCode = 13; + setTimeout(function () { + input.trigger(e); + }, 100); + onselect(event, ui); + }, + source: function (request, response) { + socket.emit('topics.autocompleteTags', { + query: request.term, + cid: ajaxify.data.cid || 0 + }, function (err, tags) { + if (err) { + return app.alertError(err.message); + } + if (tags) { + response(tags); + } + $('.ui-autocomplete a').attr('data-ajaxify', 'false'); + }); + } + }); + }); + }; + return module; }); diff --git a/public/src/modules/search.js b/public/src/modules/search.js index 4319de5617..990c9dec97 100644 --- a/public/src/modules/search.js +++ b/public/src/modules/search.js @@ -53,6 +53,10 @@ define('search', ['navigator', 'translator'], function (nav, translator) { } } + if (data.hasTags && data.hasTags.length) { + query.hasTags = data.hasTags; + } + if (parseInt(data.replies, 10) > 0) { query.replies = data.replies; query.repliesFilter = data.repliesFilter || 'atleast'; diff --git a/src/controllers/search.js b/src/controllers/search.js index 5967cfc88e..2261e4f3b7 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -33,6 +33,7 @@ searchController.search = function (req, res, next) { postedBy: req.query.by, categories: req.query.categories, searchChildren: req.query.searchChildren, + hasTags: req.query.hasTags, replies: req.query.replies, repliesFilter: req.query.repliesFilter, timeRange: req.query.timeRange, diff --git a/src/meta/dependencies.js b/src/meta/dependencies.js index 6c5be29e99..5ec596ae5b 100644 --- a/src/meta/dependencies.js +++ b/src/meta/dependencies.js @@ -41,6 +41,7 @@ module.exports = function (Meta) { next(true); } } catch(e) { + console.log(e); process.stdout.write('[' + 'missing'.red + '] ' + module.bold + ' is a required dependency but could not be found\n'); depsMissing = true; next(true); diff --git a/src/search.js b/src/search.js index 7f03634625..d77b1f58f0 100644 --- a/src/search.js +++ b/src/search.js @@ -126,6 +126,7 @@ function filterAndSort(pids, data, callback) { posts = filterByPostcount(posts, data.replies, data.repliesFilter); posts = filterByTimerange(posts, data.timeRange, data.timeFilter); + posts = filterByTags(posts, data.hasTags); sortPosts(posts, data); @@ -166,6 +167,7 @@ function getMatchedPosts(pids, data, callback) { var keys = pids.map(function (pid) { return 'post:' + pid; }); + db.getObjectsFields(keys, postFields, next); }, function (_posts, next) { @@ -185,7 +187,7 @@ function getMatchedPosts(pids, data, callback) { } }, topics: function (next) { - var topics; + var topicsData; async.waterfall([ function (next) { var topicKeys = posts.map(function (post) { @@ -194,12 +196,12 @@ function getMatchedPosts(pids, data, callback) { db.getObjectsFields(topicKeys, topicFields, next); }, function (_topics, next) { - topics = _topics; + topicsData = _topics; async.parallel({ teasers: function (next) { if (topicFields.indexOf('teaserPid') !== -1) { - var teaserKeys = topics.map(function (topic) { + var teaserKeys = topicsData.map(function (topic) { return 'post:' + topic.teaserPid; }); db.getObjectsFields(teaserKeys, ['timestamp'], next); @@ -211,10 +213,20 @@ function getMatchedPosts(pids, data, callback) { if (!categoryFields.length) { return next(); } - var cids = topics.map(function (topic) { + var cids = topicsData.map(function (topic) { return 'category:' + topic.cid; }); db.getObjectsFields(cids, categoryFields, next); + }, + tags: function (next) { + if (data.hasTags && data.hasTags.length) { + var tids = posts.map(function (post) { + return post && post.tid; + }); + topics.getTopicsTags(tids, next); + } else { + setImmediate(next); + } } }, next); } @@ -223,16 +235,19 @@ function getMatchedPosts(pids, data, callback) { return next(err); } - topics.forEach(function (topic, index) { + topicsData.forEach(function (topic, index) { if (topic && results.categories && results.categories[index]) { topic.category = results.categories[index]; } if (topic && results.teasers && results.teasers[index]) { topic.teaser = results.teasers[index]; } + if (topic && results.tags && results.tags[index]) { + topic.tags = results.tags[index]; + } }); - next(null, topics); + next(null, topicsData); }); } }, next); @@ -297,6 +312,21 @@ function filterByTimerange(posts, timeRange, timeFilter) { return posts; } +function filterByTags(posts, hasTags) { + if (hasTags && hasTags.length) { + posts = posts.filter(function (post) { + var hasAllTags = false; + if (post && post.topic && post.topic.tags && post.topic.tags.length) { + hasAllTags = hasTags.every(function (tag) { + return post.topic.tags.indexOf(tag) !== -1; + }); + } + return hasAllTags; + }); + } + return posts; +} + function sortPosts(posts, data) { if (!posts.length || !data.sortBy) { return; diff --git a/src/topics/tags.js b/src/topics/tags.js index db1be50fbe..a360de290e 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -203,6 +203,13 @@ module.exports = function (Topics) { db.getSetMembers('topic:' + tid + ':tags', callback); }; + Topics.getTopicsTags = function (tids, callback) { + var keys = tids.map(function (tid) { + return 'topic:' + tid + ':tags'; + }); + db.getSetsMembers(keys, callback); + }; + Topics.getTopicTagsObjects = function (tid, callback) { Topics.getTopicsTagsObjects([tid], function (err, data) { callback(err, Array.isArray(data) && data.length ? data[0] : []); diff --git a/test/search.js b/test/search.js index b88dcd22fd..7c619d0b6c 100644 --- a/test/search.js +++ b/test/search.js @@ -61,7 +61,7 @@ describe('Search', function () { cid: cid1, title: 'nodebb mongodb bugs', content: 'avocado cucumber apple orange fox', - tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin'] + tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'jquery'] }, next); }, function (results, next) { @@ -73,7 +73,7 @@ describe('Search', function () { cid: cid2, title: 'java mongodb redis', content: 'avocado cucumber carrot armadillo', - tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin'] + tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'javascript'] }, next); }, function (results, next) { @@ -155,6 +155,18 @@ describe('Search', function () { }); }); + it('should search with tags filter', function (done) { + search.search({ + query: 'mongodb', + searchIn: 'titles', + hasTags: ['nodebb', 'javascript'] + }, function (err, data) { + assert.ifError(err); + assert.equal(data.posts[0].tid, topic2Data.tid); + done(); + }); + }); + after(function (done) { db.emptydb(done); });