Sotaro KARASAWA 9 лет назад
Родитель
Сommit
c7b78c418d

+ 16 - 0
lib/models/attachment.js

@@ -88,5 +88,21 @@ module.exports = function(crowi) {
     return 'attachment/' + pageId + '/' + generateFileHash(fileName) + ext;
   };
 
+  attachmentSchema.statics.removeAttachmentsByPageId = function(pageId) {
+    var Attachment = this;
+
+    // todo
+    return Promise.resolve();
+    //return new Promise(function(resolve, reject) {
+    //  // target find
+    //  Attachment.getListByPageId(pageId)
+    //  .then(function(attachments) {
+    //  }).then(function(done) {
+    //  }).catch(function(err) {
+    //  });
+    //});
+
+  };
+
   return mongoose.model('Attachment', attachmentSchema);
 };

+ 16 - 1
lib/models/bookmark.js

@@ -116,7 +116,22 @@ module.exports = function(crowi) {
     });
   };
 
-  bookmarkSchema.statics.remove = function(page, user) {
+  bookmarkSchema.statics.removeBookmarksByPageId = function(pageId) {
+    var Bookmark = this;
+
+    return new Promise(function(resolve, reject) {
+      Bookmark.remove({page: pageId}, function(err, data) {
+        if (err) {
+          debug('Bookmark.remove failed (removeBookmarkByPage)', err);
+          return reject(err);
+        }
+
+        return resolve(data);
+      });
+    });
+  };
+
+  bookmarkSchema.statics.removeBookmark = function(page, user) {
     var Bookmark = this;
 
     return new Promise(function(resolve, reject) {

+ 14 - 0
lib/models/comment.js

@@ -100,6 +100,20 @@ module.exports = function(crowi) {
     });
   };
 
+  commentSchema.statics.removeCommentsByPageId = function(pageId) {
+    var Comment = this;
+
+    return new Promise(function(resolve, reject) {
+      Comment.remove({page: pageId}, function(err, done) {
+        if (err) {
+          return reject(err);
+        }
+
+        resolve(done);
+      });;
+    });
+  };
+
   /**
    * post save hook
    */

+ 88 - 5
lib/models/page.js

@@ -26,13 +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 },
@@ -711,6 +714,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) {
@@ -749,6 +753,7 @@ 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;
@@ -809,9 +814,14 @@ module.exports = function(crowi) {
       ;
     if (Page.isDeletableName(pageData.path)) {
       return new Promise(function(resolve, reject) {
-        Page.updatePageProperty(pageData, {status: STATUS_DELETED})
+        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);
@@ -823,6 +833,81 @@ module.exports = function(crowi) {
   };
 
   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) {
@@ -834,13 +919,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) {

+ 14 - 0
lib/models/revision.js

@@ -112,6 +112,20 @@ module.exports = function(crowi) {
     return newRevision;
   };
 
+  revisionSchema.statics.removeRevisionsByPath = function(path) {
+    var Revision = this;
+
+    return new Promise(function(resolve, reject) {
+      Revision.remove({path: path}, function(err, data) {
+        if (err) {
+          return reject(err);
+        }
+
+        return resolve(data);
+      });
+    });
+  };
+
   revisionSchema.statics.updatePath = function(pathName) {
   };
 

+ 1 - 1
lib/routes/bookmark.js

@@ -75,7 +75,7 @@ module.exports = function(crowi, app) {
   actions.api.remove = function(req, res){
     var pageId = req.body.page_id;
 
-    Bookmark.remove(pageId, req.user)
+    Bookmark.removeBookmark(pageId, req.user)
     .then(function(data) {
       debug('Bookmark removed.', data); // if the bookmark is not exists, this 'data' is null
       return res.json(ApiResponse.success());

+ 1 - 0
lib/routes/index.js

@@ -99,6 +99,7 @@ module.exports = function(crowi, app) {
   app.post('/_api/pages.seen'         , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.seen);
   app.post('/_api/pages.rename'       , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.rename);
   app.post('/_api/pages.remove'       , loginRequired(crowi, app) , page.api.remove); // (Avoid from API Token)
+  app.post('/_api/pages.revertRemove' , loginRequired(crowi, app) , page.api.revertRemove); // (Avoid from API Token)
   app.get('/_api/comments.get'        , accessTokenParser(crowi, app) , loginRequired(crowi, app) , comment.api.get);
   app.post('/_api/comments.add'       , form.comment, accessTokenParser(crowi, app) , loginRequired(crowi, app) , comment.api.add);
   app.get( '/_api/bookmarks.get'      , accessTokenParser(crowi, app) , loginRequired(crowi, app) , bookmark.api.get);

+ 37 - 9
lib/routes/page.js

@@ -282,22 +282,23 @@ module.exports = function(crowi, app) {
     .then(function(page) {
       debug('Page found', page._id, page.path);
 
+      if (isMarkdown) {
+        res.set('Content-Type', 'text/plain');
+        return res.send(page.revision.body);
+      }
+
+      return renderPage(page, req, res);
+    }).catch(function(err) {
+
       // pageShow は /* にマッチしてる最後の砦なので、creatableName でない routing は
       // これ以前に定義されているはずなので、こうしてしまって問題ない。
-      if (!Page.isCreatableName(path) && !page.isDeleted()) {
+      if (!Page.isCreatableName(path)) {
         // 削除済みページの場合 /trash 以下に移動しているので creatableName になっていないので、表示を許可
         debug('Page is not creatable name.', path);
         res.redirect('/');
         return ;
       }
 
-      if (isMarkdown) {
-        res.set('Content-Type', 'text/plain');
-        return res.send(page.revision.body);
-      }
-
-      return renderPage(page, req, res);
-    }).catch(function(err) {
       if (req.query.revision) {
         return res.redirect(encodeURI(path));
       }
@@ -648,7 +649,7 @@ module.exports = function(crowi, app) {
       }
       return Page.deletePage(pageData, req.user);
     }).then(function(data) {
-      debug('Page deleted', data);
+      debug('Page deleted', data.path);
       var result = {};
       result.page = data;
 
@@ -659,6 +660,33 @@ module.exports = function(crowi, app) {
     });
   };
 
+  /**
+   * @api {post} /pages.revertRemove Revert removed page
+   * @apiName RevertRemovePage
+   * @apiGroup Page
+   *
+   * @apiParam {String} page_id Page Id.
+   */
+  api.revertRemove = function(req, res){
+    var pageId = req.body.page_id;
+
+    Page.findPageByIdAndGrantedUser(pageId, req.user)
+    .then(function(pageData) {
+
+      // TODO: これでいいんだっけ
+      return Page.revertDeletedPage(pageData, req.user);
+    }).then(function(data) {
+      debug('Complete to revert deleted page', data.path);
+      var result = {};
+      result.page = data;
+
+      return res.json(ApiResponse.success(result));
+    }).catch(function(err) {
+      debug('Error occured while get setting', err, err.stack);
+      return res.json(ApiResponse.error('Failed to revert deleted page.'));
+    });
+  };
+
   /**
    * @api {post} /pages.rename Rename page
    * @apiName RenamePage

+ 17 - 2
lib/util/search.js

@@ -34,6 +34,7 @@ SearchClient.prototype.registerUpdateEvent = function() {
   var pageEvent = this.crowi.event('page');
   pageEvent.on('create', this.syncPageCreated.bind(this))
   pageEvent.on('update', this.syncPageUpdated.bind(this))
+  pageEvent.on('delete', this.syncPageDeleted.bind(this))
 };
 
 SearchClient.prototype.shouldIndexed = function(page) {
@@ -394,7 +395,7 @@ SearchClient.prototype.searchKeywordUnderPath = function(keyword, path, option)
 
 SearchClient.prototype.syncPageCreated = function(page, user)
 {
-  debug('SearchClient.syncPageCreated', page);
+  debug('SearchClient.syncPageCreated', page.path);
 
   if (!this.shouldIndexed(page)) {
     return ;
@@ -411,7 +412,7 @@ SearchClient.prototype.syncPageCreated = function(page, user)
 
 SearchClient.prototype.syncPageUpdated = function(page, user)
 {
-  debug('SearchClient.syncPageUpdated', page);
+  debug('SearchClient.syncPageUpdated', page.path);
   // TODO delete
   if (!this.shouldIndexed(page)) {
     this.deletePages([page])
@@ -434,5 +435,19 @@ SearchClient.prototype.syncPageUpdated = function(page, user)
   });
 };
 
+SearchClient.prototype.syncPageDeleted = function(page, user)
+{
+  debug('SearchClient.syncPageDeleted', page.path);
+
+  this.deletePages([page])
+  .then(function(res) {
+    debug('deletePages: ES Response', res);
+  })
+  .catch(function(err){
+    debug('deletePages:ES Error', err);
+  });
+
+  return ;
+};
 
 module.exports = SearchClient;

+ 9 - 1
lib/views/page.html

@@ -63,7 +63,15 @@
 
   {% if page.isDeleted() %}
   <div class="alert alert-danger">
-    <i class="fa fa-trash-o" aria-hidden="true"></i> This page is in the trash.
+    <form role="form" class="pull-right" id="revert-delete-page-form" onsubmit="return false;">
+      <input type="hidden" name="path" value="{{ page.path }}">
+      <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
+      <input type="submit" class="btn btn-danger btn-inverse btn-sm" value="Put Back!">
+    </form>
+    <p>
+      <i class="fa fa-trash-o" aria-hidden="true"></i>
+      This page is in the trash.<br>
+    </p>
   </div>
   {% endif %}
 

+ 18 - 0
resource/js/crowi.js

@@ -347,6 +347,24 @@ $(function() {
 
     return false;
   });
+  $('#revert-delete-page-form').submit(function(e) {
+    $.ajax({
+      type: 'POST',
+      url: '/_api/pages.revertRemove',
+      data: $('#revert-delete-page-form').serialize(),
+      dataType: 'json'
+    }).done(function(res) {
+      if (!res.ok) {
+        $('#delete-errors').html('<i class="fa fa-times-circle"></i> ' + res.error);
+        $('#delete-errors').addClass('alert-danger');
+      } else {
+        var page = res.page;
+        top.location.href = page.path;
+      }
+    });
+
+    return false;
+  });
 
   $('#create-portal-button').on('click', function(e) {
     $('.portal').removeClass('hide');