|
|
@@ -8,6 +8,11 @@ module.exports = function(crowi) {
|
|
|
, GRANT_OWNER = 4
|
|
|
, PAGE_GRANT_ERROR = 1
|
|
|
|
|
|
+ , STATUS_WIP = 'wip'
|
|
|
+ , STATUS_PUBLISHED = 'published'
|
|
|
+ , STATUS_DELETED = 'deleted'
|
|
|
+ , STATUS_DEPRECATED = 'deprecated'
|
|
|
+
|
|
|
, pageEvent = crowi.event('page')
|
|
|
|
|
|
, pageSchema;
|
|
|
@@ -21,12 +26,16 @@ module.exports = function(crowi) {
|
|
|
}
|
|
|
|
|
|
pageSchema = new mongoose.Schema({
|
|
|
- path: { type: String, required: true, index: true },
|
|
|
+ path: { type: String, required: true, index: true, unique: true },
|
|
|
revision: { type: ObjectId, ref: 'Revision' },
|
|
|
redirectTo: { type: String, index: true },
|
|
|
+ status: { type: String, default: STATUS_PUBLISHED, index: true },
|
|
|
grant: { type: Number, default: GRANT_PUBLIC, index: true },
|
|
|
grantedUsers: [{ type: ObjectId, ref: 'User' }],
|
|
|
creator: { type: ObjectId, ref: 'User', index: true },
|
|
|
+ // lastUpdateUser: this schema is from 1.5.x (by deletion feature), and null is default.
|
|
|
+ // the last update user on the screen is by revesion.author for B.C.
|
|
|
+ lastUpdateUser: { type: ObjectId, ref: 'User', index: true },
|
|
|
liker: [{ type: ObjectId, ref: 'User', index: true }],
|
|
|
seenUsers: [{ type: ObjectId, ref: 'User', index: true }],
|
|
|
commentCount: { type: Number, default: 0 },
|
|
|
@@ -54,6 +63,23 @@ module.exports = function(crowi) {
|
|
|
pageEvent.on('create', pageEvent.onCreate);
|
|
|
pageEvent.on('update', pageEvent.onUpdate);
|
|
|
|
|
|
+ pageSchema.methods.isWIP = function() {
|
|
|
+ return this.status === STATUS_WIP;
|
|
|
+ };
|
|
|
+
|
|
|
+ pageSchema.methods.isPublished = function() {
|
|
|
+ // null: this is for B.C.
|
|
|
+ return this.status === null || this.status === STATUS_PUBLISHED;
|
|
|
+ };
|
|
|
+
|
|
|
+ pageSchema.methods.isDeleted = function() {
|
|
|
+ return this.status === STATUS_DELETED;
|
|
|
+ };
|
|
|
+
|
|
|
+ pageSchema.methods.isDeprecated = function() {
|
|
|
+ return this.status === STATUS_DEPRECATED;
|
|
|
+ };
|
|
|
+
|
|
|
pageSchema.methods.isPublic = function() {
|
|
|
if (!this.grant || this.grant == GRANT_PUBLIC) {
|
|
|
return true;
|
|
|
@@ -232,6 +258,7 @@ module.exports = function(crowi) {
|
|
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
pageData.populate([
|
|
|
+ {path: 'lastUpdateUser', model: 'User', select: User.USER_PUBLIC_FIELDS},
|
|
|
{path: 'creator', model: 'User', select: User.USER_PUBLIC_FIELDS},
|
|
|
{path: 'revision', model: 'Revision'},
|
|
|
//{path: 'liker', options: { limit: 11 }},
|
|
|
@@ -288,10 +315,10 @@ module.exports = function(crowi) {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
- pageSchema.statics.hasPortalPage = function (path, user) {
|
|
|
+ pageSchema.statics.hasPortalPage = function (path, user, revisionId) {
|
|
|
var self = this;
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
- self.findPage(path, user)
|
|
|
+ self.findPage(path, user, revisionId)
|
|
|
.then(function(page) {
|
|
|
resolve(page);
|
|
|
}).catch(function(err) {
|
|
|
@@ -322,6 +349,32 @@ module.exports = function(crowi) {
|
|
|
return '/user/' + user.username;
|
|
|
};
|
|
|
|
|
|
+ pageSchema.statics.getDeletedPageName = function(path) {
|
|
|
+ if (path.match('\/')) {
|
|
|
+ path = path.substr(1);
|
|
|
+ }
|
|
|
+ return '/trash/' + path;
|
|
|
+ };
|
|
|
+
|
|
|
+ pageSchema.statics.getRevertDeletedPageName = function(path) {
|
|
|
+ return path.replace('\/trash', '');
|
|
|
+ };
|
|
|
+
|
|
|
+ pageSchema.statics.isDeletableName = function(path) {
|
|
|
+ var notDeletable = [
|
|
|
+ /^\/user\/[^\/]+$/, // user page
|
|
|
+ ];
|
|
|
+
|
|
|
+ for (var i = 0; i < notDeletable.length; i++) {
|
|
|
+ var pattern = notDeletable[i];
|
|
|
+ if (path.match(pattern)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+
|
|
|
pageSchema.statics.isCreatableName = function(name) {
|
|
|
var forbiddenPages = [
|
|
|
/\^|\$|\*|\+|\#/,
|
|
|
@@ -533,6 +586,7 @@ module.exports = function(crowi) {
|
|
|
var Page = this;
|
|
|
var User = crowi.model('User');
|
|
|
var pathCondition = [];
|
|
|
+ var includeDeletedPage = option.includeDeletedPage || false
|
|
|
|
|
|
if (!option) {
|
|
|
option = {sort: 'updatedAt', desc: -1, offset: 0, limit: 50};
|
|
|
@@ -573,6 +627,15 @@ module.exports = function(crowi) {
|
|
|
.skip(opt.offset)
|
|
|
.limit(opt.limit);
|
|
|
|
|
|
+ if (!includeDeletedPage) {
|
|
|
+ q.and({
|
|
|
+ $or: [
|
|
|
+ {status: null},
|
|
|
+ {status: STATUS_PUBLISHED},
|
|
|
+ ],
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
q.exec()
|
|
|
.then(function(pages) {
|
|
|
Page.populate(pages, {path: 'revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS})
|
|
|
@@ -652,6 +715,7 @@ module.exports = function(crowi) {
|
|
|
|
|
|
debug('Successfully saved new revision', newRevision);
|
|
|
pageData.revision = newRevision;
|
|
|
+ pageData.lastUpdateUser = user;
|
|
|
pageData.updatedAt = Date.now();
|
|
|
pageData.save(function(err, data) {
|
|
|
if (err) {
|
|
|
@@ -690,10 +754,12 @@ module.exports = function(crowi) {
|
|
|
var newPage = new Page();
|
|
|
newPage.path = path;
|
|
|
newPage.creator = user;
|
|
|
+ newPage.lastUpdateUser = user;
|
|
|
newPage.createdAt = Date.now();
|
|
|
newPage.updatedAt = Date.now();
|
|
|
newPage.redirectTo = redirectTo;
|
|
|
newPage.grant = grant;
|
|
|
+ newPage.status = STATUS_PUBLISHED;
|
|
|
newPage.grantedUsers = [];
|
|
|
newPage.grantedUsers.push(user);
|
|
|
|
|
|
@@ -743,6 +809,108 @@ module.exports = function(crowi) {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
+ pageSchema.statics.deletePage = function(pageData, user, options) {
|
|
|
+ var Page = this
|
|
|
+ , newPath = Page.getDeletedPageName(pageData.path)
|
|
|
+ ;
|
|
|
+ if (Page.isDeletableName(pageData.path)) {
|
|
|
+ return new Promise(function(resolve, reject) {
|
|
|
+ Page.updatePageProperty(pageData, {status: STATUS_DELETED, lastUpdateUser: user})
|
|
|
+ .then(function(data) {
|
|
|
+ pageData.status = STATUS_DELETED;
|
|
|
+
|
|
|
+ // ページ名が /trash/ 以下に存在する場合、おかしなことになる
|
|
|
+ // が、 /trash 以下にページが有るのは、個別に作っていたケースのみ。
|
|
|
+ // 一応しばらく前から uncreatable pages になっているのでこれでいいことにする
|
|
|
+ debug('Deleted the page, and rename it', pageData.path, newPath);
|
|
|
+ return Page.rename(pageData, newPath, user, {createRedirectPage: true})
|
|
|
+ }).then(function(pageData) {
|
|
|
+ resolve(pageData);
|
|
|
+ }).catch(reject);
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ return Promise.reject('Page is not deletable.');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ pageSchema.statics.revertDeletedPage = function(pageData, user, options) {
|
|
|
+ var Page = this
|
|
|
+ , newPath = Page.getRevertDeletedPageName(pageData.path)
|
|
|
+ ;
|
|
|
+
|
|
|
+ // 削除時、元ページの path には必ず redirectTo 付きで、ページが作成される。
|
|
|
+ // そのため、そいつは削除してOK
|
|
|
+ // が、redirectTo ではないページが存在している場合それは何かがおかしい。(データ補正が必要)
|
|
|
+ return new Promise(function(resolve, reject) {
|
|
|
+ Page.findPageByPath(newPath)
|
|
|
+ .then(function(originPageData) {
|
|
|
+ if (originPageData.redirectTo !== pageData.path) {
|
|
|
+ throw new Error('The new page of to revert is exists and the redirect path of the page is not the deleted page.');
|
|
|
+ }
|
|
|
+
|
|
|
+ return Page.completelyDeletePage(originPageData);
|
|
|
+ }).then(function(done) {
|
|
|
+ return Page.updatePageProperty(pageData, {status: STATUS_PUBLISHED, lastUpdateUser: user})
|
|
|
+ }).then(function(done) {
|
|
|
+ pageData.status = STATUS_PUBLISHED;
|
|
|
+
|
|
|
+ debug('Revert deleted the page, and rename again it', pageData, newPath);
|
|
|
+ return Page.rename(pageData, newPath, user, {})
|
|
|
+ }).then(function(done) {
|
|
|
+ pageData.path = newPath;
|
|
|
+ resolve(pageData);
|
|
|
+ }).catch(reject);
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This is danger.
|
|
|
+ */
|
|
|
+ pageSchema.statics.completelyDeletePage = function(pageData, user, options) {
|
|
|
+ // Delete Bookmarks, Attachments, Revisions, Pages and emit delete
|
|
|
+ var Bookmark = crowi.model('Bookmark')
|
|
|
+ , Attachment = crowi.model('Attachment')
|
|
|
+ , Comment = crowi.model('Comment')
|
|
|
+ , Revision = crowi.model('Revision')
|
|
|
+ , Page = this
|
|
|
+ , pageId = pageData._id
|
|
|
+ ;
|
|
|
+
|
|
|
+ debug('Completely delete', pageData.path);
|
|
|
+
|
|
|
+ return new Promise(function(resolve, reject) {
|
|
|
+ Bookmark.removeBookmarksByPageId(pageId)
|
|
|
+ .then(function(done) {
|
|
|
+ }).then(function(done) {
|
|
|
+ return Attachment.removeAttachmentsByPageId(pageId);
|
|
|
+ }).then(function(done) {
|
|
|
+ return Comment.removeCommentsByPageId(pageId);
|
|
|
+ }).then(function(done) {
|
|
|
+ return Revision.removeRevisionsByPath(pageData.path);
|
|
|
+ }).then(function(done) {
|
|
|
+ return Page.removePageById(pageId);
|
|
|
+ }).then(function(done) {
|
|
|
+ pageEvent.emit('delete', pageData, user); // update as renamed page
|
|
|
+ resolve();
|
|
|
+ }).catch(reject);
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ pageSchema.statics.removePageById = function(pageId) {
|
|
|
+ var Page = this;
|
|
|
+
|
|
|
+ return new Promise(function(resolve, reject) {
|
|
|
+ Page.remove({_id: pageId}, function(err, done) {
|
|
|
+ debug('Remove phisiaclly, the page', pageId, err, done);
|
|
|
+ if (err) {
|
|
|
+ return reject(err);
|
|
|
+ }
|
|
|
+
|
|
|
+ resolve(done);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
pageSchema.statics.rename = function(pageData, newPagePath, user, options) {
|
|
|
var Page = this
|
|
|
, Revision = crowi.model('Revision')
|
|
|
@@ -752,13 +920,11 @@ module.exports = function(crowi) {
|
|
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
// pageData の path を変更
|
|
|
- Page.updatePageProperty(pageData, {updatedAt: Date.now(), path: newPagePath})
|
|
|
+ Page.updatePageProperty(pageData, {updatedAt: Date.now(), path: newPagePath, lastUpdateUser: user})
|
|
|
.then(function(data) {
|
|
|
- debug('Before ', pageData);
|
|
|
// reivisions の path を変更
|
|
|
return Revision.updateRevisionListByPath(path, {path: newPagePath}, {})
|
|
|
}).then(function(data) {
|
|
|
- debug('After ', pageData);
|
|
|
pageData.path = newPagePath;
|
|
|
|
|
|
if (createRedirectPage) {
|