diff --git a/core/server/api/users.js b/core/server/api/users.js index f9c408bcc9..e6eba10ec5 100644 --- a/core/server/api/users.js +++ b/core/server/api/users.js @@ -255,9 +255,22 @@ users = { destroy: function destroy(options) { return canThis(options.context).destroy.user(options.id).then(function () { return users.read(options).then(function (result) { - return dataProvider.User.destroy(options).then(function () { + return dataProvider.Base.transaction(function (t) { + options.transacting = t; + dataProvider.Post.destroyByAuthor(options).then(function () { + return dataProvider.User.destroy(options); + }).then(function () { + t.commit(); + }).catch(function (error) { + t.rollback(error); + }); + }).then(function () { return result; + }, function (error) { + return when.reject(new errors.InternalServerError(error)); }); + }, function (error) { + return errors.handleAPIError(error); }); }).catch(function (error) { return errors.handleAPIError(error); diff --git a/core/server/models/post.js b/core/server/models/post.js index 125da72b8e..2b3481c9b2 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -521,6 +521,31 @@ Post = ghostBookshelf.Model.extend({ }); }, + + /** + * ### destroyByAuthor + * @param {[type]} options has context and id. Context is the user doing the destroy, id is the user to destroy + */ + destroyByAuthor: function (options) { + var postCollection = Posts.forge(), + authorId = options.id; + + options = this.filterOptions(options, 'destroyByAuthor'); + if (authorId) { + return postCollection.query('where', 'author_id', '=', authorId).fetch(options).then(function (results) { + return when.map(results.models, function (post) { + return post.related('tags').detach(null, options).then(function () { + return post.destroy(options); + }); + }); + }, function (error) { + return when.reject(new errors.InternalServerError(error.message || error)); + }); + } + return when.reject(new errors.NotFoundError('No user found')); + }, + + permissible: function (postModelOrId, action, context, loadedPermissions, hasUserPermission, hasAppPermission) { var self = this, postModel = postModelOrId, diff --git a/core/server/models/user.js b/core/server/models/user.js index cbc327801d..bcbd85142c 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -745,7 +745,7 @@ User = ghostBookshelf.Model.extend({ contextUser = ctxUser; return User.findOne({id: object.id}); }).then(function (user) { - + var currentRoles = user.toJSON().roles; if (!_.contains(currentRoles, adminRole.id)) { return when.reject(new errors.ValidationError('Only administrators can be assigned the owner role.')); diff --git a/core/test/integration/model/model_posts_spec.js b/core/test/integration/model/model_posts_spec.js index 314842755d..d9a7a3a860 100644 --- a/core/test/integration/model/model_posts_spec.js +++ b/core/test/integration/model/model_posts_spec.js @@ -12,70 +12,57 @@ var testUtils = require('../../utils'), describe('Post Model', function () { // Keep the DB clean - before(testUtils.teardown); - afterEach(testUtils.teardown); - beforeEach(testUtils.setup('owner', 'posts', 'apps')); - should.exist(PostModel); + describe('Single author posts', function () { - function checkFirstPostData(firstPost) { - should.not.exist(firstPost.author_id); - firstPost.author.should.be.an.Object; - firstPost.fields.should.be.an.Array; - firstPost.tags.should.be.an.Array; - firstPost.author.name.should.equal(DataGenerator.Content.users[0].name); - firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key); - firstPost.created_at.should.be.an.instanceof(Date); - firstPost.created_by.should.be.an.Object; - firstPost.updated_by.should.be.an.Object; - firstPost.published_by.should.be.an.Object; - firstPost.created_by.name.should.equal(DataGenerator.Content.users[0].name); - firstPost.updated_by.name.should.equal(DataGenerator.Content.users[0].name); - firstPost.published_by.name.should.equal(DataGenerator.Content.users[0].name); - firstPost.tags[0].name.should.equal(DataGenerator.Content.tags[0].name); - } + before(testUtils.teardown); + afterEach(testUtils.teardown); + beforeEach(testUtils.setup('owner', 'posts', 'apps')); - it('can findAll', function (done) { - PostModel.findAll().then(function (results) { - should.exist(results); - results.length.should.be.above(1); + should.exist(PostModel); - done(); - }).catch(done); - }); + function checkFirstPostData(firstPost) { + should.not.exist(firstPost.author_id); + firstPost.author.should.be.an.Object; + firstPost.fields.should.be.an.Array; + firstPost.tags.should.be.an.Array; + firstPost.author.name.should.equal(DataGenerator.Content.users[0].name); + firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key); + firstPost.created_at.should.be.an.instanceof(Date); + firstPost.created_by.should.be.an.Object; + firstPost.updated_by.should.be.an.Object; + firstPost.published_by.should.be.an.Object; + firstPost.created_by.name.should.equal(DataGenerator.Content.users[0].name); + firstPost.updated_by.name.should.equal(DataGenerator.Content.users[0].name); + firstPost.published_by.name.should.equal(DataGenerator.Content.users[0].name); + firstPost.tags[0].name.should.equal(DataGenerator.Content.tags[0].name); + } - it('can findAll, returning all related data', function (done) { - var firstPost; - - PostModel.findAll({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']}) - .then(function (results) { + it('can findAll', function (done) { + PostModel.findAll().then(function (results) { should.exist(results); - results.length.should.be.above(0); - firstPost = results.models[0].toJSON(); - checkFirstPostData(firstPost); + results.length.should.be.above(1); done(); }).catch(done); - }); + }); - it('can findPage (default)', function (done) { - PostModel.findPage().then(function (results) { - should.exist(results); + it('can findAll, returning all related data', function (done) { + var firstPost; - results.meta.pagination.page.should.equal(1); - results.meta.pagination.limit.should.equal(15); - results.meta.pagination.pages.should.equal(1); - results.posts.length.should.equal(4); + PostModel.findAll({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']}) + .then(function (results) { + should.exist(results); + results.length.should.be.above(0); + firstPost = results.models[0].toJSON(); + checkFirstPostData(firstPost); - done(); - }).catch(done); - }); + done(); + }).catch(done); + }); - it('can findPage, returning all related data', function (done) { - var firstPost; - - PostModel.findPage({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']}) - .then(function (results) { + it('can findPage (default)', function (done) { + PostModel.findPage().then(function (results) { should.exist(results); results.meta.pagination.page.should.equal(1); @@ -83,397 +70,444 @@ describe('Post Model', function () { results.meta.pagination.pages.should.equal(1); results.posts.length.should.equal(4); + done(); + }).catch(done); + }); + + it('can findPage, returning all related data', function (done) { + var firstPost; + + PostModel.findPage({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']}) + .then(function (results) { + should.exist(results); + + results.meta.pagination.page.should.equal(1); + results.meta.pagination.limit.should.equal(15); + results.meta.pagination.pages.should.equal(1); + results.posts.length.should.equal(4); + + firstPost = results.posts[0]; + + checkFirstPostData(firstPost); + + done(); + }).catch(done); + }); + + + it('can findOne', function (done) { + var firstPost; + + PostModel.findPage().then(function (results) { + should.exist(results); + should.exist(results.posts); + results.posts.length.should.be.above(0); firstPost = results.posts[0]; - checkFirstPostData(firstPost); + return PostModel.findOne({slug: firstPost.slug}); + }).then(function (found) { + should.exist(found); + found.attributes.title.should.equal(firstPost.title); done(); }).catch(done); - }); + }); + it('can findOne, returning all related data', function (done) { + var firstPost; + // TODO: should take author :-/ + PostModel.findOne({}, {include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']}) + .then(function (result) { + should.exist(result); + firstPost = result.toJSON(); - it('can findOne', function (done) { - var firstPost; + checkFirstPostData(firstPost); - PostModel.findPage().then(function (results) { - should.exist(results); - should.exist(results.posts); - results.posts.length.should.be.above(0); - firstPost = results.posts[0]; + done(); + }).catch(done); + }); - return PostModel.findOne({slug: firstPost.slug}); - }).then(function (found) { - should.exist(found); - found.attributes.title.should.equal(firstPost.title); + it('can edit', function (done) { + var firstPost = 1; - done(); - }).catch(done); - }); + PostModel.findOne({id: firstPost}).then(function (results) { + var post; + should.exist(results); + post = results.toJSON(); + post.id.should.equal(firstPost); + post.title.should.not.equal('new title'); - it('can findOne, returning all related data', function (done) { - var firstPost; - // TODO: should take author :-/ - PostModel.findOne({}, {include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']}) - .then(function (result) { - should.exist(result); - firstPost = result.toJSON(); - - checkFirstPostData(firstPost); + return PostModel.edit({title: 'new title'}, _.extend(context, {id: firstPost})); + }).then(function (edited) { + should.exist(edited); + edited.attributes.title.should.equal('new title'); done(); }).catch(done); - }); - - it('can edit', function (done) { - var firstPost = 1; - - PostModel.findOne({id: firstPost}).then(function (results) { - var post; - should.exist(results); - post = results.toJSON(); - post.id.should.equal(firstPost); - post.title.should.not.equal('new title'); - - return PostModel.edit({title: 'new title'}, _.extend(context, {id: firstPost})); - }).then(function (edited) { - should.exist(edited); - edited.attributes.title.should.equal('new title'); - - done(); - }).catch(done); - }); + }); - it('can add, defaults are all correct', function (done) { - var createdPostUpdatedDate, - newPost = testUtils.DataGenerator.forModel.posts[2], - newPostDB = testUtils.DataGenerator.Content.posts[2]; + it('can add, defaults are all correct', function (done) { + var createdPostUpdatedDate, + newPost = testUtils.DataGenerator.forModel.posts[2], + newPostDB = testUtils.DataGenerator.Content.posts[2]; - PostModel.add(newPost, context).then(function (createdPost) { - return new PostModel({id: createdPost.id}).fetch(); - }).then(function (createdPost) { - should.exist(createdPost); - createdPost.has('uuid').should.equal(true); - createdPost.get('status').should.equal('draft'); - createdPost.get('title').should.equal(newPost.title, 'title is correct'); - createdPost.get('markdown').should.equal(newPost.markdown, 'markdown is correct'); - createdPost.has('html').should.equal(true); - createdPost.get('html').should.equal(newPostDB.html); - createdPost.get('slug').should.equal(newPostDB.slug + '-2'); - (!!createdPost.get('featured')).should.equal(false); - (!!createdPost.get('page')).should.equal(false); - createdPost.get('language').should.equal('en_US'); - // testing for nulls - (createdPost.get('image') === null).should.equal(true); - (createdPost.get('meta_title') === null).should.equal(true); - (createdPost.get('meta_description') === null).should.equal(true); + PostModel.add(newPost, context).then(function (createdPost) { + return new PostModel({id: createdPost.id}).fetch(); + }).then(function (createdPost) { + should.exist(createdPost); + createdPost.has('uuid').should.equal(true); + createdPost.get('status').should.equal('draft'); + createdPost.get('title').should.equal(newPost.title, 'title is correct'); + createdPost.get('markdown').should.equal(newPost.markdown, 'markdown is correct'); + createdPost.has('html').should.equal(true); + createdPost.get('html').should.equal(newPostDB.html); + createdPost.get('slug').should.equal(newPostDB.slug + '-2'); + (!!createdPost.get('featured')).should.equal(false); + (!!createdPost.get('page')).should.equal(false); + createdPost.get('language').should.equal('en_US'); + // testing for nulls + (createdPost.get('image') === null).should.equal(true); + (createdPost.get('meta_title') === null).should.equal(true); + (createdPost.get('meta_description') === null).should.equal(true); - createdPost.get('created_at').should.be.above(new Date(0).getTime()); - createdPost.get('created_by').should.equal(1); - createdPost.get('author_id').should.equal(1); - createdPost.has('author').should.equal(false); - createdPost.get('created_by').should.equal(createdPost.get('author_id')); - createdPost.get('updated_at').should.be.above(new Date(0).getTime()); - createdPost.get('updated_by').should.equal(1); - should.equal(createdPost.get('published_at'), null); - should.equal(createdPost.get('published_by'), null); + createdPost.get('created_at').should.be.above(new Date(0).getTime()); + createdPost.get('created_by').should.equal(1); + createdPost.get('author_id').should.equal(1); + createdPost.has('author').should.equal(false); + createdPost.get('created_by').should.equal(createdPost.get('author_id')); + createdPost.get('updated_at').should.be.above(new Date(0).getTime()); + createdPost.get('updated_by').should.equal(1); + should.equal(createdPost.get('published_at'), null); + should.equal(createdPost.get('published_by'), null); - createdPostUpdatedDate = createdPost.get('updated_at'); + createdPostUpdatedDate = createdPost.get('updated_at'); - // Set the status to published to check that `published_at` is set. - return createdPost.save({status: 'published'}, context); - }).then(function (publishedPost) { - publishedPost.get('published_at').should.be.instanceOf(Date); - publishedPost.get('published_by').should.equal(1); - publishedPost.get('updated_at').should.be.instanceOf(Date); - publishedPost.get('updated_by').should.equal(1); - publishedPost.get('updated_at').should.not.equal(createdPostUpdatedDate); + // Set the status to published to check that `published_at` is set. + return createdPost.save({status: 'published'}, context); + }).then(function (publishedPost) { + publishedPost.get('published_at').should.be.instanceOf(Date); + publishedPost.get('published_by').should.equal(1); + publishedPost.get('updated_at').should.be.instanceOf(Date); + publishedPost.get('updated_by').should.equal(1); + publishedPost.get('updated_at').should.not.equal(createdPostUpdatedDate); - done(); - }).catch(done); + done(); + }).catch(done); - }); + }); - it('can add, with previous published_at date', function (done) { - var previousPublishedAtDate = new Date(2013, 8, 21, 12); + it('can add, with previous published_at date', function (done) { + var previousPublishedAtDate = new Date(2013, 8, 21, 12); - PostModel.add({ - status: 'published', - published_at: previousPublishedAtDate, - title: 'published_at test', - markdown: 'This is some content' - }, context).then(function (newPost) { + PostModel.add({ + status: 'published', + published_at: previousPublishedAtDate, + title: 'published_at test', + markdown: 'This is some content' + }, context).then(function (newPost) { - should.exist(newPost); - new Date(newPost.get('published_at')).getTime().should.equal(previousPublishedAtDate.getTime()); + should.exist(newPost); + new Date(newPost.get('published_at')).getTime().should.equal(previousPublishedAtDate.getTime()); - done(); + done(); - }).catch(done); - }); + }).catch(done); + }); - it('can trim title', function (done) { - var untrimmedCreateTitle = ' test trimmed create title ', - untrimmedUpdateTitle = ' test trimmed update title ', - newPost = { - title: untrimmedCreateTitle, - markdown: 'Test Content' + it('can trim title', function (done) { + var untrimmedCreateTitle = ' test trimmed create title ', + untrimmedUpdateTitle = ' test trimmed update title ', + newPost = { + title: untrimmedCreateTitle, + markdown: 'Test Content' + }; + + PostModel.add(newPost, context).then(function (createdPost) { + return new PostModel({ id: createdPost.id }).fetch(); + }).then(function (createdPost) { + should.exist(createdPost); + createdPost.get('title').should.equal(untrimmedCreateTitle.trim()); + + return createdPost.save({ title: untrimmedUpdateTitle }, context); + }).then(function (updatedPost) { + updatedPost.get('title').should.equal(untrimmedUpdateTitle.trim()); + + done(); + }).catch(done); + }); + + it('can generate a non conflicting slug', function (done) { + // Create 12 posts with the same title + sequence(_.times(12, function (i) { + return function () { + return PostModel.add({ + title: 'Test Title', + markdown: 'Test Content ' + (i+1) + }, context); + }; + })).then(function (createdPosts) { + // Should have created 12 posts + createdPosts.length.should.equal(12); + + // Should have unique slugs and contents + _(createdPosts).each(function (post, i) { + var num = i + 1; + + // First one has normal title + if (num === 1) { + post.get('slug').should.equal('test-title'); + return; + } + + post.get('slug').should.equal('test-title-' + num); + post.get('markdown').should.equal('Test Content ' + num); + }); + + done(); + }).catch(done); + }); + + it('can generate slugs without duplicate hyphens', function (done) { + var newPost = { + title: 'apprehensive titles have too many spaces—and m-dashes — – and also n-dashes ', + markdown: 'Test Content 1' }; - PostModel.add(newPost, context).then(function (createdPost) { - return new PostModel({ id: createdPost.id }).fetch(); - }).then(function (createdPost) { - should.exist(createdPost); - createdPost.get('title').should.equal(untrimmedCreateTitle.trim()); + PostModel.add(newPost, context).then(function (createdPost) { - return createdPost.save({ title: untrimmedUpdateTitle }, context); - }).then(function (updatedPost) { - updatedPost.get('title').should.equal(untrimmedUpdateTitle.trim()); + createdPost.get('slug').should.equal('apprehensive-titles-have-too-many-spaces-and-m-dashes-and-also-n-dashes'); - done(); - }).catch(done); - }); + done(); + }).catch(done); + }); - it('can generate a non conflicting slug', function (done) { - // Create 12 posts with the same title - sequence(_.times(12, function (i) { - return function () { - return PostModel.add({ - title: 'Test Title', - markdown: 'Test Content ' + (i+1) - }, context); + it('can generate a safe slug when a reserved keyword is used', function(done) { + var newPost = { + title: 'rss', + markdown: 'Test Content 1' }; - })).then(function (createdPosts) { - // Should have created 12 posts - createdPosts.length.should.equal(12); - // Should have unique slugs and contents - _(createdPosts).each(function (post, i) { - var num = i + 1; - - // First one has normal title - if (num === 1) { - post.get('slug').should.equal('test-title'); - return; - } - - post.get('slug').should.equal('test-title-' + num); - post.get('markdown').should.equal('Test Content ' + num); + PostModel.add(newPost, context).then(function (createdPost) { + createdPost.get('slug').should.not.equal('rss'); + done(); }); + }); - done(); - }).catch(done); - }); + it('can generate slugs without non-ascii characters', function (done) { + var newPost = { + title: 'भुते धडकी भरवणारा आहेत', + markdown: 'Test Content 1' + }; - it('can generate slugs without duplicate hyphens', function (done) { - var newPost = { - title: 'apprehensive titles have too many spaces—and m-dashes — – and also n-dashes ', - markdown: 'Test Content 1' - }; + PostModel.add(newPost, context).then(function (createdPost) { + createdPost.get('slug').should.equal('bhute-dhddkii-bhrvnnaaraa-aahet'); + done(); + }).catch(done); + }); - PostModel.add(newPost, context).then(function (createdPost) { + it('detects duplicate slugs before saving', function (done) { + var firstPost = { + title: 'First post', + markdown: 'First content 1' + }, + secondPost = { + title: 'Second post', + markdown: 'Second content 1' + }; - createdPost.get('slug').should.equal('apprehensive-titles-have-too-many-spaces-and-m-dashes-and-also-n-dashes'); + // Create the first post + PostModel.add(firstPost, context) + .then(function (createdFirstPost) { + // Store the slug for later + firstPost.slug = createdFirstPost.get('slug'); - done(); - }).catch(done); - }); + // Create the second post + return PostModel.add(secondPost, context); + }).then(function (createdSecondPost) { + // Store the slug for comparison later + secondPost.slug = createdSecondPost.get('slug'); - it('can generate a safe slug when a reserved keyword is used', function(done) { - var newPost = { - title: 'rss', - markdown: 'Test Content 1' - }; + // Update with a conflicting slug from the first post + return createdSecondPost.save({ + slug: firstPost.slug + }, context); + }).then(function (updatedSecondPost) { - PostModel.add(newPost, context).then(function (createdPost) { - createdPost.get('slug').should.not.equal('rss'); - done(); + // Should have updated from original + updatedSecondPost.get('slug').should.not.equal(secondPost.slug); + // Should not have a conflicted slug from the first + updatedSecondPost.get('slug').should.not.equal(firstPost.slug); + + return PostModel.findOne({ + id: updatedSecondPost.id, + status: 'all' + }); + }).then(function (foundPost) { + + // Should have updated from original + foundPost.get('slug').should.not.equal(secondPost.slug); + // Should not have a conflicted slug from the first + foundPost.get('slug').should.not.equal(firstPost.slug); + + done(); + }).catch(done); + }); + + it('can destroy', function (done) { + // We're going to try deleting post id 1 which also has tag id 1 + var firstItemData = {id: 1}; + + // Test that we have the post we expect, with exactly one tag + PostModel.findOne(firstItemData).then(function (results) { + var post; + should.exist(results); + post = results.toJSON(); + post.id.should.equal(firstItemData.id); + post.tags.should.have.length(2); + post.tags[0].should.equal(firstItemData.id); + + // Destroy the post + return PostModel.destroy(firstItemData); + }).then(function (response) { + var deleted = response.toJSON(); + + deleted.tags.should.be.empty; + should.equal(deleted.author, undefined); + + // Double check we can't find the post again + return PostModel.findOne(firstItemData); + }).then(function (newResults) { + should.equal(newResults, null); + + done(); + }).catch(done); + }); + + + it('can findPage, with various options', function (done) { + testUtils.fixtures.insertMorePosts().then(function () { + + return testUtils.fixtures.insertMorePostsTags(); + }).then(function () { + return PostModel.findPage({page: 2}); + }).then(function (paginationResult) { + paginationResult.meta.pagination.page.should.equal(2); + paginationResult.meta.pagination.limit.should.equal(15); + paginationResult.meta.pagination.pages.should.equal(4); + paginationResult.posts.length.should.equal(15); + + return PostModel.findPage({page: 5}); + }).then(function (paginationResult) { + paginationResult.meta.pagination.page.should.equal(5); + paginationResult.meta.pagination.limit.should.equal(15); + paginationResult.meta.pagination.pages.should.equal(4); + paginationResult.posts.length.should.equal(0); + + return PostModel.findPage({limit: 30}); + }).then(function (paginationResult) { + paginationResult.meta.pagination.page.should.equal(1); + paginationResult.meta.pagination.limit.should.equal(30); + paginationResult.meta.pagination.pages.should.equal(2); + paginationResult.posts.length.should.equal(30); + + // Test both boolean formats + return PostModel.findPage({limit: 10, staticPages: true}); + }).then(function (paginationResult) { + paginationResult.meta.pagination.page.should.equal(1); + paginationResult.meta.pagination.limit.should.equal(10); + paginationResult.meta.pagination.pages.should.equal(1); + paginationResult.posts.length.should.equal(1); + + // Test both boolean formats + return PostModel.findPage({limit: 10, staticPages: '1'}); + }).then(function (paginationResult) { + paginationResult.meta.pagination.page.should.equal(1); + paginationResult.meta.pagination.limit.should.equal(10); + paginationResult.meta.pagination.pages.should.equal(1); + paginationResult.posts.length.should.equal(1); + + return PostModel.findPage({limit: 10, page: 2, status: 'all'}); + }).then(function (paginationResult) { + paginationResult.meta.pagination.pages.should.equal(11); + + done(); + }).catch(done); + }); + it('can findPage for tag, with various options', function (done) { + testUtils.fixtures.insertMorePosts().then(function () { + + return testUtils.fixtures.insertMorePostsTags(); + }).then(function () { + // Test tag filter + return PostModel.findPage({page: 1, tag: 'bacon'}); + }).then(function (paginationResult) { + paginationResult.meta.pagination.page.should.equal(1); + paginationResult.meta.pagination.limit.should.equal(15); + paginationResult.meta.pagination.pages.should.equal(1); + paginationResult.meta.filters.tags[0].name.should.equal('bacon'); + paginationResult.meta.filters.tags[0].slug.should.equal('bacon'); + paginationResult.posts.length.should.equal(2); + + return PostModel.findPage({page: 1, tag: 'kitchen-sink'}); + }).then(function (paginationResult) { + paginationResult.meta.pagination.page.should.equal(1); + paginationResult.meta.pagination.limit.should.equal(15); + paginationResult.meta.pagination.pages.should.equal(1); + paginationResult.meta.filters.tags[0].name.should.equal('kitchen sink'); + paginationResult.meta.filters.tags[0].slug.should.equal('kitchen-sink'); + paginationResult.posts.length.should.equal(2); + + return PostModel.findPage({page: 1, tag: 'injection'}); + }).then(function (paginationResult) { + paginationResult.meta.pagination.page.should.equal(1); + paginationResult.meta.pagination.limit.should.equal(15); + paginationResult.meta.pagination.pages.should.equal(2); + paginationResult.meta.filters.tags[0].name.should.equal('injection'); + paginationResult.meta.filters.tags[0].slug.should.equal('injection'); + paginationResult.posts.length.should.equal(15); + + return PostModel.findPage({page: 2, tag: 'injection'}); + }).then(function (paginationResult) { + paginationResult.meta.pagination.page.should.equal(2); + paginationResult.meta.pagination.limit.should.equal(15); + paginationResult.meta.pagination.pages.should.equal(2); + paginationResult.meta.filters.tags[0].name.should.equal('injection'); + paginationResult.meta.filters.tags[0].slug.should.equal('injection'); + paginationResult.posts.length.should.equal(10); + + done(); + }).catch(done); }); }); - it('can generate slugs without non-ascii characters', function (done) { - var newPost = { - title: 'भुते धडकी भरवणारा आहेत', - markdown: 'Test Content 1' - }; - PostModel.add(newPost, context).then(function (createdPost) { - createdPost.get('slug').should.equal('bhute-dhddkii-bhrvnnaaraa-aahet'); - done(); - }).catch(done); - }); + describe('Multiauthor Posts', function () { + before(testUtils.teardown); + afterEach(testUtils.teardown); + beforeEach(testUtils.setup('posts:mu')); - it('detects duplicate slugs before saving', function (done) { - var firstPost = { - title: 'First post', - markdown: 'First content 1' - }, - secondPost = { - title: 'Second post', - markdown: 'Second content 1' - }; + should.exist(PostModel); - // Create the first post - PostModel.add(firstPost, context) - .then(function (createdFirstPost) { - // Store the slug for later - firstPost.slug = createdFirstPost.get('slug'); + it('can destroy multiple posts by author', function (done) { - // Create the second post - return PostModel.add(secondPost, context); - }).then(function (createdSecondPost) { - // Store the slug for comparison later - secondPost.slug = createdSecondPost.get('slug'); - - // Update with a conflicting slug from the first post - return createdSecondPost.save({ - slug: firstPost.slug - }, context); - }).then(function (updatedSecondPost) { - - // Should have updated from original - updatedSecondPost.get('slug').should.not.equal(secondPost.slug); - // Should not have a conflicted slug from the first - updatedSecondPost.get('slug').should.not.equal(firstPost.slug); - - return PostModel.findOne({ - id: updatedSecondPost.id, - status: 'all' - }); - }).then(function (foundPost) { - - // Should have updated from original - foundPost.get('slug').should.not.equal(secondPost.slug); - // Should not have a conflicted slug from the first - foundPost.get('slug').should.not.equal(firstPost.slug); + // We're going to delete all posts by user 1 + var authorData = {id: 1}; + PostModel.findAll().then(function (found) { + // There are 50 posts to begin with + found.length.should.equal(50); + return PostModel.destroyByAuthor(authorData); + }).then(function (results) { + // User 1 has 13 posts in the database + results.length.should.equal(13); + return PostModel.findAll(); + }).then(function (found) { + // Only 37 should remain + found.length.should.equal(37); done(); }).catch(done); - }); - - it('can destroy', function (done) { - // We're going to try deleting post id 1 which also has tag id 1 - var firstItemData = {id: 1}; - - // Test that we have the post we expect, with exactly one tag - PostModel.findOne(firstItemData).then(function (results) { - var post; - should.exist(results); - post = results.toJSON(); - post.id.should.equal(firstItemData.id); - post.tags.should.have.length(2); - post.tags[0].should.equal(firstItemData.id); - - // Destroy the post - return PostModel.destroy(firstItemData); - }).then(function (response) { - var deleted = response.toJSON(); - - deleted.tags.should.be.empty; - should.equal(deleted.author, undefined); - - // Double check we can't find the post again - return PostModel.findOne(firstItemData); - }).then(function (newResults) { - should.equal(newResults, null); - - done(); - }).catch(done); - }); - - it('can findPage, with various options', function (done) { - testUtils.fixtures.insertMorePosts().then(function () { - - return testUtils.fixtures.insertMorePostsTags(); - }).then(function () { - return PostModel.findPage({page: 2}); - }).then(function (paginationResult) { - paginationResult.meta.pagination.page.should.equal(2); - paginationResult.meta.pagination.limit.should.equal(15); - paginationResult.meta.pagination.pages.should.equal(4); - paginationResult.posts.length.should.equal(15); - - return PostModel.findPage({page: 5}); - }).then(function (paginationResult) { - paginationResult.meta.pagination.page.should.equal(5); - paginationResult.meta.pagination.limit.should.equal(15); - paginationResult.meta.pagination.pages.should.equal(4); - paginationResult.posts.length.should.equal(0); - - return PostModel.findPage({limit: 30}); - }).then(function (paginationResult) { - paginationResult.meta.pagination.page.should.equal(1); - paginationResult.meta.pagination.limit.should.equal(30); - paginationResult.meta.pagination.pages.should.equal(2); - paginationResult.posts.length.should.equal(30); - - // Test both boolean formats - return PostModel.findPage({limit: 10, staticPages: true}); - }).then(function (paginationResult) { - paginationResult.meta.pagination.page.should.equal(1); - paginationResult.meta.pagination.limit.should.equal(10); - paginationResult.meta.pagination.pages.should.equal(1); - paginationResult.posts.length.should.equal(1); - - // Test both boolean formats - return PostModel.findPage({limit: 10, staticPages: '1'}); - }).then(function (paginationResult) { - paginationResult.meta.pagination.page.should.equal(1); - paginationResult.meta.pagination.limit.should.equal(10); - paginationResult.meta.pagination.pages.should.equal(1); - paginationResult.posts.length.should.equal(1); - - return PostModel.findPage({limit: 10, page: 2, status: 'all'}); - }).then(function (paginationResult) { - paginationResult.meta.pagination.pages.should.equal(11); - - done(); - }).catch(done); - }); - it('can findPage for tag, with various options', function (done) { - testUtils.fixtures.insertMorePosts().then(function () { - - return testUtils.fixtures.insertMorePostsTags(); - }).then(function () { - // Test tag filter - return PostModel.findPage({page: 1, tag: 'bacon'}); - }).then(function (paginationResult) { - paginationResult.meta.pagination.page.should.equal(1); - paginationResult.meta.pagination.limit.should.equal(15); - paginationResult.meta.pagination.pages.should.equal(1); - paginationResult.meta.filters.tags[0].name.should.equal('bacon'); - paginationResult.meta.filters.tags[0].slug.should.equal('bacon'); - paginationResult.posts.length.should.equal(2); - - return PostModel.findPage({page: 1, tag: 'kitchen-sink'}); - }).then(function (paginationResult) { - paginationResult.meta.pagination.page.should.equal(1); - paginationResult.meta.pagination.limit.should.equal(15); - paginationResult.meta.pagination.pages.should.equal(1); - paginationResult.meta.filters.tags[0].name.should.equal('kitchen sink'); - paginationResult.meta.filters.tags[0].slug.should.equal('kitchen-sink'); - paginationResult.posts.length.should.equal(2); - - return PostModel.findPage({page: 1, tag: 'injection'}); - }).then(function (paginationResult) { - paginationResult.meta.pagination.page.should.equal(1); - paginationResult.meta.pagination.limit.should.equal(15); - paginationResult.meta.pagination.pages.should.equal(2); - paginationResult.meta.filters.tags[0].name.should.equal('injection'); - paginationResult.meta.filters.tags[0].slug.should.equal('injection'); - paginationResult.posts.length.should.equal(15); - - return PostModel.findPage({page: 2, tag: 'injection'}); - }).then(function (paginationResult) { - paginationResult.meta.pagination.page.should.equal(2); - paginationResult.meta.pagination.limit.should.equal(15); - paginationResult.meta.pagination.pages.should.equal(2); - paginationResult.meta.filters.tags[0].name.should.equal('injection'); - paginationResult.meta.filters.tags[0].slug.should.equal('injection'); - paginationResult.posts.length.should.equal(10); - - done(); - }).catch(done); + }); }); // disabling sanitization until we can implement a better version diff --git a/core/test/utils/fixtures/data-generator.js b/core/test/utils/fixtures/data-generator.js index b79e75bde6..6e233049db 100644 --- a/core/test/utils/fixtures/data-generator.js +++ b/core/test/utils/fixtures/data-generator.js @@ -268,14 +268,16 @@ DataGenerator.forKnex = (function () { }); } - function createGenericPost(uniqueInteger, status, language) { + function createGenericPost(uniqueInteger, status, language, author_id) { status = status || 'draft'; language = language || 'en_US'; + author_id = author_id || 1; return createPost({ uuid: uuid.v4(), title: 'Test Post ' + uniqueInteger, slug: 'ghost-from-fiction-to-function-' + uniqueInteger, + author_id: author_id, markdown: "Three days ago I released a concept page<\/a> for a lite version of WordPress that I've been thinking about for a long time, called Ghost. I think it's fair to say that I didn't quite anticipate how strong the reaction would be - and I've hardly had time to catch my breath in the last 72 hours.\n\nThe response was overwhelming, and overwhelmingly positive. In the first 6 hours my site got 35,000 page views after hitting the number 1 slot on Hacker News<\/a>. As of right now, the traffic count is just over 91,000 page views<\/a> - and Ghost has been featured all over the place. Notable mentions so far include Christina Warren from Mashable, who wrote about it<\/a>. Michael Carney from PandoDaily interviewed me about it<\/a>. Someone even wrote about it in Chinese<\/a>. That's pretty cool.\n\n\nThe feedback has been amazing, and while it's impossible to reply to all of the messages individually, I'm getting to as many of them as I can and I want to thank each and every one of you who took the time to send me a message or share the concept because you liked it. Now that the initial storm has died down a bit, I wanted to take some time to answer some of the more common questions and talk about what's next.\n

FAQ - Continued...<\/h2>\n\nThe most common question, bizarrely:\n

Oh my god, why is that whole page made of images? What's wrong with you? \/\/ I can't take you seriously \/\/ Don't you know anything about the web? \/\/ You are literally Satan re-incarnate.<\/strong><\/em><\/h5>\n\nThis was really the only negativity I got in response to the post, and it surprised me. I put together the concept page as... just that... a concept. It was a way for me to get the ideas out of my head and \"down on paper\" - or so to speak. I used photoshop as a tool<\/em> to write down my idea with text and images. If I used a sketchbook as a tool <\/em>to create images and handwritten notes, then uploaded scans of it, I doubt anyone would complain. The concept page was never supposed to be a finished product because I had no idea if there would be any interest in it. I had no motivation to waste hours coding a custom layout for something might only ever be read by a few people and then forgotten.\n\nHardware manufacturers make hundreds of foam cutout prototypes of products before they build one with working buttons and screens. I'm aware of all the usability problems with a web page made of images, and equally, foam cutouts without buttons or screens aren't particularly user friendly either. They're not supposed to be.\n\nLet's move on.\n
What? Why no comments? I need comments.<\/strong><\/em><\/h5>\n\nBecause comments add a layer of complexity that is beyond the core focus of this platform, which is publishing. Again, that's not to say you couldn't have any comments. This could easily be added with a dedicated plugin where you own the data or (as mentioned) there are third party providers such as Disqus, IntenseDebate, Livefyre and Facebook who all have great platforms. The point of this isn't to say \"you can't have comments\" - it's to say \"comments aren't on by default\". It's about simplicity, more than anything else.\n
Yeah, but WordPress are already going to revise their dashboard, WordPress.com is experimenting with a potential simplified version... so why bother with this?<\/strong><\/em><\/h5>\n\n\"\"<\/a>\n\nSorry, but Tumblr already did this - it's not the future of blogging, it's the past.\n\nGhost isn't about sharing \"Fuck Yeah [Dogs<\/a>\/Sharks<\/a>\/Girls with Tattoos<\/a>]\" - it's about publishing - which means writing - rather than mashing a few buttons to make sure that everyone can see and appreciate your latest funny picture\/status, which is surely the most funny picture\/status you've ever posted.\n\nTumblr, Pinterest and Facebook already have this locked down. It's not the future.\n
So... are you actually going to build this thing?<\/strong><\/em><\/h5>\n\nThe concept page was a way for me to test demand and interest. To see if anyone actually agreed with my frustrations and, more importantly, my solutions. I plucked a random figure of \"10,000 pageviews\" out of the air before I hit the publish button. If it got less than 10,000 pageviews, I would surrender to the fact that it would only ever be an idea. I've now exceeded that goal 9 times over, so yes, I'm looking at how Ghost can now be made into a reality.\n
How can I find out when it's done? \/\/ SHUT UP AND TAKE MY MONEY<\/strong><\/em><\/h5>\n\nOk, ok - there's a holding page up on http:\/\/TryGhost.org<\/a> - put your email address in.\n
\n

How are you going to do this?<\/h3>\n\nThere's three main ways of going about this, each has merits as well as drawbacks.\n\n1.) Build it from scratch<\/strong><\/em> - Many people (particularly the Hacker News crowd) expressed the sentiment that there was little point in forking WordPress. When you're going to strip out so much, you get to a point where you might as well start from scratch anyway. Take away the crutches of being stuck with older technologies and put together something which is as sophisticated in code as it is in UI\/UX.\n