diff --git a/ghost/core/core/server/models/post.js b/ghost/core/core/server/models/post.js index 7280bcd5a0..1a04b6aa2f 100644 --- a/ghost/core/core/server/models/post.js +++ b/ghost/core/core/server/models/post.js @@ -1367,6 +1367,24 @@ Post = ghostBookshelf.Model.extend({ return editPost(); }, + bulkEdit: async function bulkEdit(ids, tableName, options) { + if (tableName === this.prototype.tableName) { + const result = await ghostBookshelf.Model.bulkEdit.call(this, ids, tableName, options); + + if (labs.isSet('collectionsCard')) { + // reset all page HTML so collection cards can be re-rendered with updated posts + // NOTE: we can't check for only published edits here as we don't have access to previous values + // to see if a previously published post has been unpublished, so we just reset all pages + const pageResetQuery = ghostBookshelf.knex.raw('UPDATE posts set html = NULL WHERE type = "page" AND lexical IS NOT NULL'); + await (options.transacting ? pageResetQuery.transacting(options.transacting) : pageResetQuery); + } + + return result; + } else { + return ghostBookshelf.Model.bulkEdit.call(this, ids, tableName, options); + } + }, + /** * ### Add * @extends ghostBookshelf.Model.add to handle returning the full object @@ -1413,6 +1431,34 @@ Post = ghostBookshelf.Model.extend({ return destroyPost(); }, + bulkDestroy: async function bulkDestroy(ids, tableName, options) { + if (tableName === this.prototype.tableName) { + if (labs.isSet('collectionsCard')) { + // get count of published posts to be destroyed before they no longer exist to count + const deletedPublishedCount = await this.query((qb) => { + qb.where('type', 'post') + .where('status', 'published') + .whereIn('id', ids); + }).count({transacting: options.transacting}); + + const result = await ghostBookshelf.Model.bulkDestroy.call(this, ids, tableName, options); + + // if we've deleted any published posts, we need to reset the html for all pages so dynamic collection + // card content can be re-rendered + if (deletedPublishedCount > 0) { + const pageResetQuery = ghostBookshelf.knex.raw('UPDATE posts set html = NULL WHERE type = "page" AND lexical IS NOT NULL'); + await (options.transacting ? pageResetQuery.transacting(options.transacting) : pageResetQuery); + } + + return result; + } else { + return ghostBookshelf.Model.bulkDestroy.call(this, ids, tableName, options); + } + } else { + return ghostBookshelf.Model.bulkDestroy.call(this, ids, tableName, options); + } + }, + // NOTE: the `authors` extension is the parent of the post model. It also has a permissible function. permissible: async function permissible(postModel, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) { let isContributor; diff --git a/ghost/core/test/e2e-api/admin/posts-bulk.test.js b/ghost/core/test/e2e-api/admin/posts-bulk.test.js index 3714dc007e..ff23a72056 100644 --- a/ghost/core/test/e2e-api/admin/posts-bulk.test.js +++ b/ghost/core/test/e2e-api/admin/posts-bulk.test.js @@ -1,4 +1,6 @@ +const should = require('should'); const DomainEvents = require('@tryghost/domain-events'); +const {mobiledocToLexical} = require('@tryghost/kg-converters'); const {agentProvider, fixtureManager, mockManager} = require('../../utils/e2e-framework'); const models = require('../../../core/server/models'); const assert = require('assert/strict'); @@ -8,15 +10,26 @@ describe('Posts Bulk API', function () { before(async function () { mockManager.mockLabsEnabled('collections'); + mockManager.mockLabsEnabled('collectionsCard'); agent = await agentProvider.getAdminAPIAgent(); // Note that we generate lots of fixtures here to test the bulk deletion correctly await fixtureManager.init('posts', 'newsletters', 'members:newsletters', 'emails', 'redirects', 'clicks', 'comments', 'feedback', 'links', 'mentions'); await agent.loginAsOwner(); + + // convert inserted pages to lexical so we can test page.html reset/re-render + const pages = await models.Post.where('type', 'page').fetchAll(); + for (const page of pages) { + const lexical = mobiledocToLexical(page.get('mobiledoc')); + await models.Base.knex.raw('UPDATE posts SET mobiledoc=NULL, lexical=? where id=?', [lexical, page.id]); + } }); - afterEach(function () { + afterEach(async function () { + // give pages some HTML back to alleviate test interdependence when pages are reset on create/update/delete + await models.Base.knex.raw('UPDATE posts SET html = "

Testing

" WHERE type = \'page\' AND html IS NULL'); + mockManager.restore(); }); @@ -48,6 +61,13 @@ describe('Posts Bulk API', function () { assert.equal(response.body.bulk.meta.stats.successful, amount, `Expect all matching posts (${amount}) to be changed`); + // Check page HTML was reset to enable re-render of collection cards + // Must be done before fetch all posts otherwise they will be re-rendered + const totalPageCount = await models.Post.where({type: 'page'}).count(); + const emptyPageCount = await models.Post.where({html: null, type: 'page'}).count(); + should.exist(emptyPageCount); + emptyPageCount.should.equal(totalPageCount, 'no. of render-queued pages after bulk edit'); + // Fetch all posts and check if they are featured const posts = await models.Post.findAll({filter, status: 'all'}); assert.equal(posts.length, amount, `Expect all matching posts (${amount}) to be changed`); @@ -281,7 +301,7 @@ describe('Posts Bulk API', function () { it('Can unpublish posts', async function () { const filter = 'status:[published]'; - const changedPosts = await models.Post.findPage({filter, limit: 1, status: 'all'}); + const changedPosts = await models.Post.findPage({filter, status: 'published'}); const amount = changedPosts.meta.pagination.total; assert(amount > 0, 'Expect at least one post to be affected for this test to work'); @@ -301,6 +321,10 @@ describe('Posts Bulk API', function () { // Fetch all posts and check if they are unpublished const posts = await models.Post.findAll({filter, status: 'all'}); assert.equal(posts.length, 0, `Expect all matching posts (${amount}) to be unpublished`); + + // Re-publish the posts so we don't affect later tests + const postIds = changedPosts.data.map(post => post.id); + await models.Base.knex.raw(`UPDATE posts SET status = \'published\' WHERE id IN (${postIds.map(() => '?').join(',')})`, [...postIds]); }); }); @@ -332,6 +356,12 @@ describe('Posts Bulk API', function () { // Check if all posts were deleted const posts = await models.Post.findPage({filter, status: 'all'}); assert.equal(posts.meta.pagination.total, 0, `Expect all matching posts (${amount}) to be deleted`); + + // Check page HTML was reset to enable re-render of collection cards + const totalPageCount = await models.Post.where({type: 'page'}).count(); + const emptyPageCount = await models.Post.where({html: null, type: 'page'}).count(); + should.exist(emptyPageCount); + emptyPageCount.should.equal(totalPageCount, 'no. of render-queued pages after bulk delete'); }); it('Can delete all posts', async function () {