From 30a0b1794a565263d0de9399c300b3fcde23b0b7 Mon Sep 17 00:00:00 2001 From: Katharina Irrgang Date: Fri, 4 Jan 2019 19:00:45 +0100 Subject: [PATCH] Added calculated excerpt field to Content API v2 (#10326) closes #10062 - return `post.excerpt` for Content API v2 - do not use `downsize`, because we might want to get rid of it if we drop v0.1 (downsize does not create good excerpts) - simple substring of the plaintext field --- .../serializers/output/utils/extra-attrs.js | 13 +++++++ .../utils/serializers/output/utils/mapper.js | 2 + core/server/data/meta/index.js | 4 +- core/server/helpers/excerpt.js | 9 +++++ core/test/functional/api/v2/content/utils.js | 2 + .../serializers/output/utils/extra-attrs.js | 38 +++++++++++++++++++ .../serializers/output/utils/mapper_spec.js | 9 ++++- 7 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 core/server/api/v2/utils/serializers/output/utils/extra-attrs.js create mode 100644 core/test/unit/api/v2/utils/serializers/output/utils/extra-attrs.js diff --git a/core/server/api/v2/utils/serializers/output/utils/extra-attrs.js b/core/server/api/v2/utils/serializers/output/utils/extra-attrs.js new file mode 100644 index 0000000000..9daabebd61 --- /dev/null +++ b/core/server/api/v2/utils/serializers/output/utils/extra-attrs.js @@ -0,0 +1,13 @@ +module.exports.forPost = (frame, model, attrs) => { + const _ = require('lodash'); + + if (!frame.options.hasOwnProperty('columns') || + (frame.options.columns.includes('excerpt') && frame.options.formats && frame.options.formats.includes('plaintext'))) { + if (_.isEmpty(attrs.custom_excerpt)) { + const plaintext = model.get('plaintext'); + attrs.excerpt = plaintext.substring(0, 500); + } else { + attrs.excerpt = attrs.custom_excerpt; + } + } +}; diff --git a/core/server/api/v2/utils/serializers/output/utils/mapper.js b/core/server/api/v2/utils/serializers/output/utils/mapper.js index bcd36290eb..0d0eeef6c1 100644 --- a/core/server/api/v2/utils/serializers/output/utils/mapper.js +++ b/core/server/api/v2/utils/serializers/output/utils/mapper.js @@ -3,6 +3,7 @@ const url = require('./url'); const date = require('./date'); const members = require('./members'); const clean = require('./clean'); +const extraAttrs = require('./extra-attrs'); const mapUser = (model, frame) => { const jsonModel = model.toJSON ? model.toJSON(frame.options) : model; @@ -36,6 +37,7 @@ const mapPost = (model, frame) => { if (utils.isContentAPI(frame)) { date.forPost(jsonModel); members.forPost(jsonModel, frame); + extraAttrs.forPost(frame, model, jsonModel); clean.post(jsonModel); } diff --git a/core/server/data/meta/index.js b/core/server/data/meta/index.js index 60b74a932b..a2d8fb3b37 100644 --- a/core/server/data/meta/index.js +++ b/core/server/data/meta/index.js @@ -83,7 +83,9 @@ function getMetaData(data, root) { // 1. CASE: custom_excerpt is populated via the UI // 2. CASE: no custom_excerpt, but meta_description is poplated via the UI // 3. CASE: fall back to automated excerpt of 50 words if neither custom_excerpt nor meta_description is provided - customExcerpt = data.post.custom_excerpt; + // @NOTE: v2 returns a calculated `post.excerpt`. v0.1 does not + // @TODO: simplify or remove if we drop v0.1 + customExcerpt = data.post.excerpt || data.post.custom_excerpt; metaDescription = data.post.meta_description; fallbackExcerpt = data.post.html ? getExcerpt(data.post.html, {words: 50}) : ''; diff --git a/core/server/helpers/excerpt.js b/core/server/helpers/excerpt.js index 817587e74d..3d072331ce 100644 --- a/core/server/helpers/excerpt.js +++ b/core/server/helpers/excerpt.js @@ -10,6 +10,15 @@ var proxy = require('./proxy'), SafeString = proxy.SafeString, getMetaDataExcerpt = proxy.metaData.getMetaDataExcerpt; +/** + * @NOTE: + * + * Content API v2 returns a calculated `post.excerpt` field. + * See https://github.com/TryGhost/Ghost/issues/10062. + * We have not touched this helper yet, we will revisit later. + * + * @TODO: remove or change if we drop v0.1 + */ module.exports = function excerpt(options) { var truncateOptions = (options || {}).hash || {}, excerptText = this.custom_excerpt ? String(this.custom_excerpt) : String(this.html); diff --git a/core/test/functional/api/v2/content/utils.js b/core/test/functional/api/v2/content/utils.js index 6a453a5e19..5ed676b015 100644 --- a/core/test/functional/api/v2/content/utils.js +++ b/core/test/functional/api/v2/content/utils.js @@ -23,6 +23,8 @@ const expectedProperties = { .without('locale') // These fields aren't useful as they always have known values .without('page', 'status') + // v2 returns a calculated excerpt field + .concat('excerpt') , author: _(schema.users) .keys() diff --git a/core/test/unit/api/v2/utils/serializers/output/utils/extra-attrs.js b/core/test/unit/api/v2/utils/serializers/output/utils/extra-attrs.js new file mode 100644 index 0000000000..812a84b158 --- /dev/null +++ b/core/test/unit/api/v2/utils/serializers/output/utils/extra-attrs.js @@ -0,0 +1,38 @@ +const should = require('should'); +const sinon = require('sinon'); +const extraAttrsUtil = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/extra-attrs'); +const sandbox = sinon.sandbox.create(); + +describe('Unit: v2/utils/serializers/output/utils/extra-attrs', () => { + const frame = { + options: {} + }; + + let model; + + beforeEach(function () { + model = sandbox.stub(); + model.get = sandbox.stub(); + model.get.withArgs('plaintext').returns(new Array(5000).join('A')); + }); + + describe('for post', function () { + it('respects custom excerpt', () => { + const attrs = {custom_excerpt: 'custom excerpt'}; + + extraAttrsUtil.forPost(frame, model, attrs); + model.get.called.should.be.false(); + + attrs.excerpt.should.eql(attrs.custom_excerpt); + }); + + it('no custom excerpt', () => { + const attrs = {}; + + extraAttrsUtil.forPost(frame, model, attrs); + model.get.called.should.be.true(); + + attrs.excerpt.should.eql(new Array(501).join('A')); + }); + }); +}); diff --git a/core/test/unit/api/v2/utils/serializers/output/utils/mapper_spec.js b/core/test/unit/api/v2/utils/serializers/output/utils/mapper_spec.js index 4ede8da469..04006244ae 100644 --- a/core/test/unit/api/v2/utils/serializers/output/utils/mapper_spec.js +++ b/core/test/unit/api/v2/utils/serializers/output/utils/mapper_spec.js @@ -4,6 +4,7 @@ const testUtils = require('../../../../../../../utils'); const dateUtil = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/date'); const urlUtil = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/url'); const cleanUtil = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/clean'); +const extraAttrsUtils = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/extra-attrs'); const mapper = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/mapper'); const sandbox = sinon.sandbox.create(); @@ -16,6 +17,8 @@ describe('Unit: v2/utils/serializers/output/utils/mapper', () => { sandbox.stub(urlUtil, 'forTag').returns({}); sandbox.stub(urlUtil, 'forUser').returns({}); + sandbox.stub(extraAttrsUtils, 'forPost').returns({}); + sandbox.stub(cleanUtil, 'post').returns({}); sandbox.stub(cleanUtil, 'tag').returns({}); sandbox.stub(cleanUtil, 'author').returns({}); @@ -30,7 +33,9 @@ describe('Unit: v2/utils/serializers/output/utils/mapper', () => { beforeEach(() => { postModel = (data) => { - return Object.assign(data, {toJSON: sandbox.stub().returns(data)}); + return Object.assign(data, { + toJSON: sandbox.stub().returns(data) + }); }; }); @@ -62,6 +67,8 @@ describe('Unit: v2/utils/serializers/output/utils/mapper', () => { dateUtil.forPost.callCount.should.equal(1); + extraAttrsUtils.forPost.callCount.should.equal(1); + cleanUtil.post.callCount.should.eql(1); cleanUtil.tag.callCount.should.eql(1); cleanUtil.author.callCount.should.eql(1);