Просмотр исходного кода

Added minimum function of comment feature

Sotaro KARASAWA 10 лет назад
Родитель
Сommit
871d6ce54c

+ 11 - 0
lib/form/comment.js

@@ -0,0 +1,11 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('commentForm.page_id').trim().required(),
+  field('commentForm.revision_id').trim().required(),
+  field('commentForm.comment').trim().required(),
+  field('commentForm.comment_position').trim().toInt()
+);

+ 1 - 0
lib/form/index.js

@@ -2,6 +2,7 @@ exports.login = require('./login');
 exports.register = require('./register');
 exports.register = require('./register');
 exports.invited = require('./invited');
 exports.invited = require('./invited');
 exports.revision = require('./revision');
 exports.revision = require('./revision');
+exports.comment = require('./comment');
 exports.me = {
 exports.me = {
   user: require('./me/user'),
   user: require('./me/user'),
   password: require('./me/password'),
   password: require('./me/password'),

+ 88 - 0
lib/models/comment.js

@@ -0,0 +1,88 @@
+module.exports = function(crowi) {
+  var debug = require('debug')('crowi:models:comment')
+    , mongoose = require('mongoose')
+    , ObjectId = mongoose.Schema.Types.ObjectId
+  ;
+
+  commentSchema = new mongoose.Schema({
+    page: { type: ObjectId, ref: 'Page', index: true },
+    creator: { type: ObjectId, ref: 'User', index: true  },
+    revision: { type: ObjectId, ref: 'Revision', index: true },
+    comment: { type: String, required: true },
+    commentPosition: { type: Number, default: -1 },
+    createdAt: { type: Date, default: Date.now }
+  });
+
+  commentSchema.statics.create = function(pageId, creatorId, revisionId, comment, position) {
+    var Comment = this,
+      commentPosition = position || -1;
+
+
+    return new Promise(function(resolve, reject) {
+      var newComment = new Comment();
+
+      newComment.page = pageId;
+      newComment.creator = creatorId;
+      newComment.revision = revisionId;
+      newComment.comment = comment;
+      newComment.commentPosition = position;
+
+      newComment.save(function(err, data) {
+        if (err) {
+          debug('Error on saving comment.', err);
+          return reject(err);
+        }
+        debug('Comment saved.', data);
+        return resolve(data);
+      });
+    });
+  };
+
+  commentSchema.statics.getCommentsByPageId = function(id) {
+    var self = this;
+
+    return new Promise(function(resolve, reject) {
+      self
+        .find({page: id})
+        .sort({'createdAt': -1})
+        .populate('creator')
+        .exec(function(err, data) {
+          if (err) {
+            return reject(err);
+          }
+
+          if (data.length < 1) {
+            return resolve([]);
+          }
+
+          debug('Comment loaded', data);
+          return resolve(data);
+        });
+    });
+  };
+
+  commentSchema.statics.getCommentsByRevisionId = function(id) {
+    var self = this;
+
+    return new Promise(function(resolve, reject) {
+      self
+        .find({revision: id})
+        .sort({'createdAt': -1})
+        .populate('creator')
+        .exec(function(err, data) {
+          if (err) {
+            return reject(err);
+          }
+
+          if (data.length < 1) {
+            return resolve([]);
+          }
+
+          debug('Comment loaded', data);
+          return resolve(data);
+        });
+    });
+  };
+
+  return mongoose.model('Comment', commentSchema);
+};

+ 1 - 0
lib/models/index.js

@@ -3,5 +3,6 @@ module.exports = {
   User: require('./user'),
   User: require('./user'),
   Revision: require('./revision'),
   Revision: require('./revision'),
   Bookmark: require('./bookmark'),
   Bookmark: require('./bookmark'),
+  Comment: require('./comment'),
   Attachment: require('./attachment'),
   Attachment: require('./attachment'),
 };
 };

+ 75 - 0
lib/routes/comment.js

@@ -0,0 +1,75 @@
+module.exports = function(crowi, app) {
+  'use strict';
+
+  var debug = require('debug')('crowi:routs:comment')
+    , Comment = crowi.model('Comment')
+    , User = crowi.model('User')
+    , Page = crowi.model('Page')
+    , ApiResponse = require('../util/ApiResponse')
+    , actions = {}
+    , api = {};
+
+  actions.api = api;
+
+  /**
+   * @api {get} /comments.get Get comments of the page of the revision
+   * @apiName GetComments
+   * @apiGroup Comment
+   *
+   * @apiParam {String} page_id Page Id.
+   * @apiParam {String} revision_id Revision Id.
+   */
+  api.get = function(req, res){
+    var pageId = req.query.page_id;
+    var revisionId = req.query.revision_id;
+
+    if (revisionId) {
+      return Comment.getCommentsByRevisionId(revisionId)
+        .then(function(comments) {
+          res.json(ApiResponse.success({comments}));
+        }).catch(function(err) {
+          res.json(ApiResponse.error(err));
+        });
+    }
+
+    return Comment.getCommentsByPageId(pageId)
+      .then(function(comments) {
+        res.json(ApiResponse.success({comments}));
+      }).catch(function(err) {
+        res.json(ApiResponse.error(err));
+      });
+  };
+
+  /**
+   * @api {post} /comments.post Post comment for the page
+   * @apiName PostComment
+   * @apiGroup Comment
+   *
+   * @apiParam {String} page_id Page Id.
+   * @apiParam {String} revision_id Revision Id.
+   * @apiParam {String} comment Comment body
+   * @apiParam {Number} comment_position=-1 Line number of the comment
+   */
+  api.post = function(req, res){
+    var form = req.form.commentForm;
+
+    if (!req.form.isValid) {
+      return res.json(ApiResponse.error('Invalid comment.'));
+    }
+
+    var pageId = form.page_id;
+    var revisionId = form.revision_id;
+    var comment = form.comment;
+    var position = form.comment_position || -1;
+
+    return Comment.create(pageId, req.user._id, revisionId, comment, position)
+      .then(function(createdComment) {
+        createdComment.creator = req.user;
+        return res.json(ApiResponse.success({comment: createdComment}));
+      }).catch(function(err) {
+        return res.json(ApiResponse.error(err));
+      });
+  };
+
+  return actions;
+};

+ 3 - 0
lib/routes/index.js

@@ -9,6 +9,7 @@ module.exports = function(crowi, app) {
     , installer = require('./installer')(crowi, app)
     , installer = require('./installer')(crowi, app)
     , user      = require('./user')(crowi, app)
     , user      = require('./user')(crowi, app)
     , attachment= require('./attachment')(crowi, app)
     , attachment= require('./attachment')(crowi, app)
+    , comment= require('./comment')(crowi, app)
     , loginRequired = middleware.loginRequired
     , loginRequired = middleware.loginRequired
     , accessTokenParser = middleware.accessTokenParser
     , accessTokenParser = middleware.accessTokenParser
     ;
     ;
@@ -77,6 +78,8 @@ module.exports = function(crowi, app) {
 
 
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
   app.get('/_api/pages.get'           , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.get);
   app.get('/_api/pages.get'           , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.get);
+  app.get('/_api/comments.get'        , accessTokenParser(crowi, app) , loginRequired(crowi, app) , comment.api.get);
+  app.post('/_api/comments.post'      , form.comment, accessTokenParser(crowi, app) , loginRequired(crowi, app) , comment.api.post);
   //app.get('/_api/revision/:id'     , user.useUserData()         , revision.api.get);
   //app.get('/_api/revision/:id'     , user.useUserData()         , revision.api.get);
   //app.get('/_api/r/:revisionId'    , user.useUserData()         , page.api.get);
   //app.get('/_api/r/:revisionId'    , user.useUserData()         , page.api.get);
 
 

+ 3 - 8
lib/routes/page.js

@@ -6,6 +6,7 @@ module.exports = function(crowi, app) {
     , User = crowi.model('User')
     , User = crowi.model('User')
     , Revision = crowi.model('Revision')
     , Revision = crowi.model('Revision')
     , Bookmark = crowi.model('Bookmark')
     , Bookmark = crowi.model('Bookmark')
+    , ApiResponse = require('../util/ApiResponse')
     , actions = {};
     , actions = {};
 
 
   function getPathFromRequest(req) {
   function getPathFromRequest(req) {
@@ -217,16 +218,10 @@ module.exports = function(crowi, app) {
     Page.findPage(pagePath, req.user, revision, options, function(err, pageData) {
     Page.findPage(pagePath, req.user, revision, options, function(err, pageData) {
       var result = {};
       var result = {};
       if (err) {
       if (err) {
-        result = {
-          ok: false,
-          message: err.toString()
-        };
+        result = ApiResponse.error(err);
       }
       }
       if (pageData) {
       if (pageData) {
-        result = {
-          ok: true,
-          page: pageData
-        };
+        result = ApiResponse.success(pageData);
       }
       }
 
 
       return res.json(result);
       return res.json(result);

+ 29 - 0
lib/util/apiResponse.js

@@ -0,0 +1,29 @@
+'use strict';
+
+function ApiResponse () {
+};
+
+ApiResponse.error = function (err) {
+  var result = {};
+
+  result = {
+    ok: false
+  };
+
+  if (typeof err == Error) {
+    result.error = err.toString();
+  } else {
+    result.error = err;
+  }
+
+  return result;
+};
+
+ApiResponse.success = function (data) {
+  var result = data;
+
+  result.ok = true;
+  return result;
+};
+
+module.exports = ApiResponse;

+ 37 - 36
lib/views/page.html

@@ -13,7 +13,11 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block content_main %}
 {% block content_main %}
-<div id="content-main" class="content-main {% if not page %}on-edit{% endif %}" data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}">
+<div id="content-main" class="content-main {% if not page %}on-edit{% endif %}"
+  data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
+  data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
+  data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
+  >
 
 
   {% if not page %}
   {% if not page %}
   <ul class="nav nav-tabs hidden-print">
   <ul class="nav nav-tabs hidden-print">
@@ -144,46 +148,43 @@
 
 
 {% block content_footer %}
 {% block content_footer %}
 
 
+<div class="page-comments">
+  <form class="form" id="page-comment-form">
+    <img src="{{ user|picture }}" class="picture picture-rounded">
+    <div class="comment-form">
+      <ul class="nav nav-tabs">
+        <li class="active"><a id="comment-write-tab" href="#comment-write" role="tab" data-toggle="tab">Write</a></li>
+        <li><a id="comment-preview-tab" href="#comment-preview" role="tab" data-toggle="tab">Preview</a></li>
+
+      </ul>
+      <div class="comment-form-main">
+        <div class="tab-content">
+          <div role="tabpanel" class="comment-write tab-pane active" id="comment-write">
+            <textarea class="comment-form-comment form-control" id="comment-form-comment" name="commentForm[comment]"></textarea>
+          </div>
+          <div role="tabpanel" class="comment-preview tab-pane wiki" id="comment-preview">
+          </div>
+
+        </div>
+        <div class="comment-submit">
+          <input type="hidden" name="commentForm[page_id]" value="{{ page._id.toString() }}">
+          <input type="hidden" name="commentForm[revision_id]" value="{{ revision._id.toString() }}">
+          <span class="text-danger" id="comment-form-message"></span>
+          <input type="submit" value="Comment" class="btn btn-primary form-inline">
+        </div>
+      </div>
+    </div>
+  </form>
+
+  <div class="page-comments-list" id="page-comments-list">
+  </div>
+</div>
+
 <div class="page-attachments">
 <div class="page-attachments">
   <p>Attachments</p>
   <p>Attachments</p>
   <ul>
   <ul>
   </ul>
   </ul>
 </div>
 </div>
-<script>
-  (function() {
-    var pageId = $('#content-main').data('page-id');
-    var $pageAttachmentList = $('.page-attachments ul');
-    if (pageId) {
-      $.get('/_api/attachment/page/' + pageId, function(res) {
-        var attachments = res.data.attachments;
-        var urlBase = res.data.fileBaseUrl;
-        if (attachments.length > 0) {
-          $.each(attachments, function(i, file) {
-            $pageAttachmentList.append(
-            '<li><a href="' + urlBase + file.filePath + '">' + (file.originalName || file.fileName) + '</a> <span class="label label-default">' + file.fileFormat + '</span></li>'
-            );
-          })
-        } else {
-          $('.page-attachments').remove();
-        }
-      });
-    }
-  })();
-</script>
-<footer>
-  {% if not page %}
-  {% else %}
-  <p class="meta">
-  Path: <span id="pagePath">{{ page.path }}</span><br />
-  Revision: {{ revision._id.toString() }}<br />
-  {% if author %}
-  Last Updated User: <a href="/user/{{ author.username }}">{{ author.name }}</a><br />
-  {% endif %}
-  Created: {{ page.createdAt|datetz('Y-m-d H:i:s') }}<br />
-  Updated: {{ page.updatedAt|datetz('Y-m-d H:i:s') }}<br />
-  </p>
-  {% endif %}
-</footer>
 
 
 {% endblock %}
 {% endblock %}
 
 

+ 101 - 0
resource/css/_comment.scss

@@ -0,0 +1,101 @@
+.page-comments {
+  margin: 30px 0 0 0;
+  padding: 20px 8px 8px 8px;
+  border-top: solid 1px #ccc;
+
+  form {
+    .picture {
+      float: left;
+      width: 32px;
+      height: 32px;
+    }
+
+    .nav {
+      background: #f0f0f0;
+      border-top: solid 1px #ccc;
+      border-right: solid 1px #ccc;
+      padding: 2px 0 0 7px;
+      border-left: solid 1px #ccc;
+    }
+
+    .comment-form {
+      margin-left: 40px;
+    }
+
+    .comment-form-main {
+      padding: 8px;
+      border: solid 1px #ccc;
+      border-top: none;
+
+      .comment-form-comment {
+        height: 120px;
+      }
+
+      .comment-submit {
+        margin-top: 8px;
+        text-align: right;
+      }
+    }
+  }
+
+  .page-comments-list {
+    .page-comment {
+      margin-top: 8px;
+
+      .picture {
+        float: left;
+        width: 32px;
+        height: 32px;
+      }
+
+      .page-comment-main {
+        border: solid 1px #ccc;
+        margin-left: 40px;
+
+        .page-comment-meta {
+          background: #f0f0f0;
+          padding: 8px;
+
+          .page-comment-creator {
+            font-weight: bold;
+          }
+        }
+
+        .page-comment-body {
+          padding: 8px;
+          font-size: 14px;
+
+          h1, h2, h3, h4, h5 {
+            &:first-child {
+              padding-top: 16px;
+            }
+          }
+        }
+      }
+    }
+
+    .page-comment.page-comment-me {
+      .picture {
+        float: right;
+      }
+      .page-comment-main {
+        border: solid 1px lighten($crowiHeaderBackground, 50%);
+        margin-right: 40px;
+        margin-left: 0px;
+
+        .page-comment-meta {
+          background: lighten($crowiHeaderBackground, 65%);
+        }
+      }
+    }
+
+    .page-comment.page-comment-old {
+      opacity: .7;
+
+      &:hover {
+        opacity: 1;
+      }
+    }
+  }
+}
+

+ 1 - 0
resource/css/crowi.scss

@@ -11,6 +11,7 @@
 @import 'form';
 @import 'form';
 @import 'wiki';
 @import 'wiki';
 @import 'admin';
 @import 'admin';
+@import 'comment';
 
 
 
 
 ul {
 ul {

+ 122 - 0
resource/js/crowi.js

@@ -195,7 +195,27 @@ Crowi.renderer.prototype = {
   }
   }
 };
 };
 
 
+// original: middleware.swigFilter
+Crowi.userPicture = function (user) {
+  if (!user) {
+    return '/images/userpicture.png';
+  }
+
+  if (user.image && user.image != '/images/userpicture.png') {
+    return user.image;
+  } else if (user.fbId) {
+    return '//graph.facebook.com/' + user.fbId + '/picture?size=square';
+  } else {
+    return '/images/userpicture.png';
+  }
+};
+
+
 $(function() {
 $(function() {
+  var pageId = $('#content-main').data('page-id');
+  var revisionId = $('#content-main').data('page-revision-id');
+  var currentUser = $('#content-main').data('current-user');
+
   Crowi.linkPath();
   Crowi.linkPath();
 
 
   $('[data-toggle="tooltip"]').tooltip();
   $('[data-toggle="tooltip"]').tooltip();
@@ -248,5 +268,107 @@ $(function() {
     return false;
     return false;
   });
   });
 
 
+
+  if (pageId) {
+
+    // omg
+    function createCommentHTML(revision, creator, comment, commentedAt) {
+      var $comment = $('<div>');
+      var $commentImage = $('<img class="picture picture-rounded">')
+        .attr('src', Crowi.userPicture(creator));
+      var $commentCreator = $('<span class="page-comment-creator">')
+        .text(creator.username);
+
+      var $commentRevision = $('<a class="page-comment-revision label">')
+        .attr('href', '?revision=' + revision)
+        .text(revision.substr(0,8));
+      if (revision !== revisionId) {
+        $commentRevision.addClass('label-default');
+      } else {
+        $commentRevision.addClass('label-primary');
+      }
+
+      var $commentMeta = $('<div class="page-comment-meta">')
+        .text(' commented at ' + commentedAt + ' ')
+        .prepend($commentCreator)
+        .append($commentRevision);
+      var $commentBody = $('<div class="page-comment-body wiki">');
+      var renderer = new Crowi.renderer(comment, $commentBody);
+        renderer.render();
+
+      var $commentMain = $('<div class="page-comment-main">')
+        .append($commentMeta)
+        .append($commentBody);
+
+      $comment.addClass('page-comment');
+      if (creator._id === currentUser) {
+        $comment.addClass('page-comment-me');
+      }
+      if (revision !== revisionId) {
+        $comment.addClass('page-comment-old');
+      }
+      $comment
+        .append($commentImage)
+        .append($commentMain)
+
+      return $comment;
+    }
+
+    // get comments
+    var $pageCommentList = $('.page-comments-list');
+    $.get('/_api/comments.get', {page_id: pageId}, function(res) {
+      if (res.ok) {
+        var comments = res.comments;
+        $.each(comments, function(i, comment) {
+          $pageCommentList.append(createCommentHTML(comment.revision, comment.creator, comment.comment, comment.createdAt));
+        });
+      }
+    }).fail(function(data) {
+      // console.log(data);
+    });
+
+    // post comment event
+    $('#page-comment-form').on('submit', function() {
+      $.post('/_api/comments.post', $(this).serialize(), function(data) {
+        if (data.ok) {
+          var comment = data.comment;
+
+          $pageCommentList.prepend(createCommentHTML(comment.revision, comment.creator, comment.comment, comment.createdAt));
+          $('#comment-form-comment').val('');
+          $('#comment-form-message').text('');
+          $('#comment-write-tab').tab('show');
+        } else {
+          $('#comment-form-message').text(data.error);
+        }
+      }).fail(function(data) {
+        if (data.status !== 200) {
+          $('#comment-form-message').text(data.statusText);
+        }
+      });
+
+      return false;
+    });
+
+    $('#comment-preview-tab').on('shown.bs.tab', function(e) {
+      var commentPreviewRenderer = new Crowi.renderer($('#comment-form-comment').val(), $('#comment-preview'));
+      commentPreviewRenderer.render();
+    });
+
+    // attachment
+    var $pageAttachmentList = $('.page-attachments ul');
+    $.get('/_api/attachment/page/' + pageId, function(res) {
+      var attachments = res.data.attachments;
+      var urlBase = res.data.fileBaseUrl;
+      if (attachments.length > 0) {
+        $.each(attachments, function(i, file) {
+          $pageAttachmentList.append(
+          '<li><a href="' + urlBase + file.filePath + '">' + (file.originalName || file.fileName) + '</a> <span class="label label-default">' + file.fileFormat + '</span></li>'
+          );
+        })
+      } else {
+        $('.page-attachments').remove();
+      }
+    });
+  }
 });
 });