diff --git a/core/server/helpers/foreach.js b/core/server/helpers/foreach.js index 96d554f836..dcd3df5515 100644 --- a/core/server/helpers/foreach.js +++ b/core/server/helpers/foreach.js @@ -6,20 +6,48 @@ var hbs = require('express-hbs'), _ = require('lodash'), errors = require('../errors'), i18n = require('../i18n'), + labs = require('../utils/labs'), + utils = require('./utils'), hbsUtils = hbs.handlebars.Utils, foreach; -foreach = function (itemType, options) { +function filterItemsByVisibility(items, options) { + var visibility = utils.parseVisibility(options); + + if (!labs.isSet('internalTags') || _.includes(visibility, 'all')) { + return items; + } + + function visibilityFilter(item) { + // If the item doesn't have a visibility property && options.hash.visibility wasn't set + // We return the item, else we need to be sure that this item has the property + if (!item.visibility && !options.hash.visibility || _.includes(visibility, item.visibility)) { + return item; + } + } + + // We don't want to change the structure of what is returned + return _.isArray(items) ? _.filter(items, visibilityFilter) : _.pickBy(items, visibilityFilter); +} + +foreach = function (items, options) { if (!options) { errors.logWarn(i18n.t('warnings.helpers.foreach.iteratorNeeded')); } + if (hbsUtils.isFunction(items)) { + items = items.call(this); + } + + // Exclude items which should not be visible in the theme + items = filterItemsByVisibility(items, options); + // Initial values set based on parameters sent through. If nothing sent, set to defaults var fn = options.fn, inverse = options.inverse, columns = options.hash.columns, - length = _.size(itemType), + length = _.size(items), limit = parseInt(options.hash.limit, 10) || length, from = parseInt(options.hash.from, 10) || 1, to = parseInt(options.hash.to, 10) || length, @@ -37,10 +65,6 @@ foreach = function (itemType, options) { contextPath = hbsUtils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; } - if (hbsUtils.isFunction(itemType)) { - itemType = itemType.call(this); - } - if (options.data) { data = hbs.handlebars.createFrame(options.data); } @@ -61,9 +85,9 @@ foreach = function (itemType, options) { } } - output = output + fn(itemType[field], { + output = output + fn(items[field], { data: data, - blockParams: hbsUtils.blockParams([itemType[field], field], [contextPath + field, null]) + blockParams: hbsUtils.blockParams([items[field], field], [contextPath + field, null]) }); } @@ -88,8 +112,8 @@ foreach = function (itemType, options) { }); } - if (itemType && typeof itemType === 'object') { - iterateCollection(itemType); + if (items && typeof items === 'object') { + iterateCollection(items); } if (length === 0) { diff --git a/core/server/helpers/tags.js b/core/server/helpers/tags.js index ff4b87d6a6..eacecbfc33 100644 --- a/core/server/helpers/tags.js +++ b/core/server/helpers/tags.js @@ -17,37 +17,51 @@ tags = function (options) { options = options || {}; options.hash = options.hash || {}; - var autolink = !(_.isString(options.hash.autolink) && options.hash.autolink === 'false'), - separator = _.isString(options.hash.separator) ? options.hash.separator : ', ', - prefix = _.isString(options.hash.prefix) ? options.hash.prefix : '', - suffix = _.isString(options.hash.suffix) ? options.hash.suffix : '', - limit = options.hash.limit ? parseInt(options.hash.limit, 10) : undefined, - from = options.hash.from ? parseInt(options.hash.from, 10) : 1, - to = options.hash.to ? parseInt(options.hash.to, 10) : undefined, - output = ''; + var autolink = !(_.isString(options.hash.autolink) && options.hash.autolink === 'false'), + separator = _.isString(options.hash.separator) ? options.hash.separator : ', ', + prefix = _.isString(options.hash.prefix) ? options.hash.prefix : '', + suffix = _.isString(options.hash.suffix) ? options.hash.suffix : '', + limit = options.hash.limit ? parseInt(options.hash.limit, 10) : undefined, + from = options.hash.from ? parseInt(options.hash.from, 10) : 1, + to = options.hash.to ? parseInt(options.hash.to, 10) : undefined, + visibility = utils.parseVisibility(options), + output = ''; function createTagList(tags) { - if (labs.isSet('internalTags')) { - tags = _.filter(tags, ['visibility', 'public']); - } + return _.reduce(tags, function (tagArray, tag) { + // If labs.internalTags is set && visibility is not set to all + // Then, if tag has a visibility property, and that visibility property is also not explicitly allowed, skip tag + // or if there is no visibility property, and options.hash.visibility was set, skip tag + if (labs.isSet('internalTags') && !_.includes(visibility, 'all')) { + if ( + (tag.visibility && !_.includes(visibility, tag.visibility) && !_.includes(visibility, 'all')) || + (!!options.hash.visibility && !_.includes(visibility, 'all') && !tag.visibility) + ) { + // Skip this tag + return tagArray; + } + } - if (autolink) { - return _.map(tags, function (tag) { - return utils.linkTemplate({ - url: config.urlFor('tag', {tag: tag}), - text: _.escape(tag.name) - }); - }); - } - return _(tags).map('name').each(_.escape); + var tagOutput = autolink ? utils.linkTemplate({ + url: config.urlFor('tag', {tag: tag}), + text: _.escape(tag.name) + }) : _.escape(tag.name); + + tagArray.push(tagOutput); + + return tagArray; + }, []); } if (this.tags && this.tags.length) { output = createTagList(this.tags); from -= 1; // From uses 1-indexed, but array uses 0-indexed. - to = to || limit + from || this.tags.length; + to = to || limit + from || output.length; + output = output.slice(from, to).join(separator); + } - output = prefix + output.slice(from, to).join(separator) + suffix; + if (output) { + output = prefix + output + suffix; } return new hbs.handlebars.SafeString(output); diff --git a/core/server/helpers/utils.js b/core/server/helpers/utils.js index e51ea905fe..6fbc42f63a 100644 --- a/core/server/helpers/utils.js +++ b/core/server/helpers/utils.js @@ -18,6 +18,13 @@ utils = { } return null; + }, + parseVisibility: function parseVisibility(options) { + if (!options.hash.visibility) { + return ['public']; + } + + return _.map(options.hash.visibility.split(','), _.trim); } }; diff --git a/core/test/unit/server_helpers/foreach_spec.js b/core/test/unit/server_helpers/foreach_spec.js index 8734955201..f6c1fe9af3 100644 --- a/core/test/unit/server_helpers/foreach_spec.js +++ b/core/test/unit/server_helpers/foreach_spec.js @@ -7,6 +7,7 @@ var should = require('should'), // Stuff we are testing handlebars = hbs.handlebars, + labs = require('../../../server/utils/labs'), helpers = require('../../../server/helpers'); describe('{{#foreach}} helper', function () { @@ -496,5 +497,115 @@ describe('{{#foreach}} helper', function () { shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); + + describe('Internal Tags', function () { + var tagArrayHash = { + tags: [ + {name: 'first', visibility: 'public'}, + {name: 'second', visibility: 'public'}, + {name: 'third', visibility: 'internal'}, + {name: 'fourth', visibility: 'public'}, + {name: 'fifth'} + ] + }, + tagObjectHash = { + tags: { + first: {name: 'first', visibility: 'public'}, + second: {name: 'second', visibility: 'public'}, + third: {name: 'third', visibility: 'internal'}, + fourth: {name: 'fourth', visibility: 'public'}, + fifth: {name: 'fifth'} + } + }; + + // @TODO: remove these once internal tags are out of beta + describe('Labs flag', function () { + it('will output internal tags when the labs flag IS NOT set', function () { + sandbox.stub(labs, 'isSet').returns(false); + + var templateString = '', + expected = ''; + + shouldCompileToExpected(templateString, tagObjectHash, expected); + shouldCompileToExpected(templateString, tagArrayHash, expected); + }); + + it('will NOT output internal tags when the labs flag IS set', function () { + sandbox.stub(labs, 'isSet').returns(true); + + var templateString = '', + expected = ''; + + shouldCompileToExpected(templateString, tagObjectHash, expected); + shouldCompileToExpected(templateString, tagArrayHash, expected); + }); + }); + + describe('Enabled', function () { + beforeEach(function () { + sandbox.stub(labs, 'isSet').returns(true); + }); + + it('will not output internal tags by default', function () { + var templateString = '', + expected = ''; + + shouldCompileToExpected(templateString, tagObjectHash, expected); + shouldCompileToExpected(templateString, tagArrayHash, expected); + }); + + it('should still correctly apply from & limit tags', function () { + var templateString = '', + expected = ''; + + shouldCompileToExpected(templateString, tagObjectHash, expected); + shouldCompileToExpected(templateString, tagArrayHash, expected); + }); + + it('should output all tags with visibility="all"', function () { + var templateString = '', + expected = ''; + + shouldCompileToExpected(templateString, tagObjectHash, expected); + shouldCompileToExpected(templateString, tagArrayHash, expected); + }); + + it('should output all tags with visibility property set with visibility="public,internal"', function () { + var templateString = '', + expected = ''; + + shouldCompileToExpected(templateString, tagObjectHash, expected); + shouldCompileToExpected(templateString, tagArrayHash, expected); + }); + + it('should output all tags with visibility="internal"', function () { + var templateString = '', + expected = ''; + + shouldCompileToExpected(templateString, tagObjectHash, expected); + shouldCompileToExpected(templateString, tagArrayHash, expected); + }); + + it('should output nothing if all tags are internal', function () { + var tagArrayHash = { + tags: [ + {name: 'first', visibility: 'internal'}, + {name: 'second', visibility: 'internal'} + ] + }, + tagObjectHash = { + tags: { + first: {name: 'first', visibility: 'internal'}, + second: {name: 'second', visibility: 'internal'} + } + }, + templateString = '', + expected = ''; + + shouldCompileToExpected(templateString, tagObjectHash, expected); + shouldCompileToExpected(templateString, tagArrayHash, expected); + }); + }); + }); }); }); diff --git a/core/test/unit/server_helpers/tags_spec.js b/core/test/unit/server_helpers/tags_spec.js index ebbebfeb97..931ade013b 100644 --- a/core/test/unit/server_helpers/tags_spec.js +++ b/core/test/unit/server_helpers/tags_spec.js @@ -1,4 +1,4 @@ -/*globals describe, afterEach, before, it*/ +/*globals describe, afterEach, before, beforeEach, it*/ var should = require('should'), sinon = require('sinon'), @@ -184,29 +184,132 @@ describe('{{tags}} helper', function () { String(rendered).should.equal('foo, bar, baz'); }); - describe('Hidden/Internal tags', function () { + describe('Internal tags', function () { + var tags = [ + {name: 'foo', slug: 'foo-bar', visibility: 'public'}, + {name: '#bar', slug: 'hash-bar', visibility: 'internal'}, + {name: 'bar', slug: 'bar', visibility: 'public'}, + {name: 'baz', slug: 'baz', visibility: 'public'}, + {name: 'buzz', slug: 'buzz'} + ]; + // @TODO: remove these once internal tags are out of beta - it('Should output internal tags when the labs flag IS NOT set', function () { - sandbox.stub(labs, 'isSet').returns(false); - var tags = [{name: 'foo', slug: 'foo-bar', visibility: 'public'}, {name: 'bar', slug: 'bar', visibility: 'public'}], - rendered = helpers.tags.call( + describe('Labs Flag', function () { + it('will output internal tags when the labs flag IS NOT set', function () { + sandbox.stub(labs, 'isSet').returns(false); + + var rendered = helpers.tags.call( {tags: tags} ); - should.exist(rendered); - String(rendered).should.equal('foo, bar'); + String(rendered).should.equal( + 'foo, ' + + '#bar, ' + + 'bar, ' + + 'baz, ' + + 'buzz' + ); + }); + + it('will NOT output internal tags when the labs flag IS set', function () { + sandbox.stub(labs, 'isSet').returns(true); + + var rendered = helpers.tags.call( + {tags: tags} + ); + + String(rendered).should.equal( + 'foo, ' + + 'bar, ' + + 'baz, ' + + 'buzz' + ); + }); }); - it('Should NOT output internal tags when the labs flag IS set', function () { - sandbox.stub(labs, 'isSet').returns(true); + describe('Enabled', function () { + beforeEach(function () { + sandbox.stub(labs, 'isSet').returns(true); + }); - var tags = [{name: 'foo', slug: 'foo-bar', visibility: 'public'}, {name: 'bar', slug: 'bar', visibility: 'internal'}], - rendered = helpers.tags.call( + it('will not output internal tags by default', function () { + var rendered = helpers.tags.call( {tags: tags} ); - should.exist(rendered); - String(rendered).should.equal('foo'); + String(rendered).should.equal( + 'foo, ' + + 'bar, ' + + 'baz, ' + + 'buzz' + ); + }); + + it('should still correctly apply from & limit tags', function () { + var rendered = helpers.tags.call( + {tags: tags}, + {hash: {from: '2', limit: '2'}} + ); + + String(rendered).should.equal( + 'bar, ' + + 'baz' + ); + }); + + it('should output all tags with visibility="all"', function () { + var rendered = helpers.tags.call( + {tags: tags}, + {hash: {visibility: 'all'}} + ); + + String(rendered).should.equal( + 'foo, ' + + '#bar, ' + + 'bar, ' + + 'baz, ' + + 'buzz' + ); + }); + + it('should output all tags with visibility property set with visibility="public,internal"', function () { + var rendered = helpers.tags.call( + {tags: tags}, + {hash: {visibility: 'public,internal'}} + ); + should.exist(rendered); + + String(rendered).should.equal( + 'foo, ' + + '#bar, ' + + 'bar, ' + + 'baz' + ); + }); + + it('Should output only internal tags with visibility="internal"', function () { + var rendered = helpers.tags.call( + {tags: tags}, + {hash: {visibility: 'internal'}} + ); + should.exist(rendered); + + String(rendered).should.equal('#bar'); + }); + + it('should output nothing if all tags are internal', function () { + var tags = [ + {name: '#foo', slug: 'hash-foo-bar', visibility: 'internal'}, + {name: '#bar', slug: 'hash-bar', visibility: 'internal'} + ], + rendered = helpers.tags.call( + {tags: tags}, + {hash: {prefix: 'stuff'}} + ); + should.exist(rendered); + + String(rendered).should.equal(''); + }); }); }); }); diff --git a/package.json b/package.json index bfd819b3ff..d9faf55353 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "jsonpath": "0.2.4", "knex": "0.10.0", "lodash": "4.13.1", + "lodash.pickby": "4.4.0", "moment": "2.13.0", "moment-timezone": "0.5.4", "morgan": "1.7.0",