doortts doortts 2016-12-03
contents-history: Support change history of issue/board contents
@8103bf15ab40e99d6c9151523d1d760d13d1656a
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -2695,8 +2695,12 @@
 
 .board-body {
     .author-info {
-        display:block;
+        display:inline-block;
         margin:10px 20px;
+    }
+
+    .posting-history {
+        display: inline-block;
     }
 
     .content {
@@ -3852,6 +3856,32 @@
     vertical-align:middle;
     overflow: hidden;
 }
+
+#-yona-posting-history{
+    .history-made-by{
+        font-size: 16px;
+        background-color: #eee;
+        line-height: 40px;
+    }
+    .added {
+        background-color: #abdd52;
+        padding: 5px;
+    }
+    .deleted {
+        background-color: #fda9a6;
+        padding: 5px;
+    }
+    .diff-added {
+        background-color: #abdd52;
+        padding: 2px;
+    }
+    .diff-deleted {
+        background-color: #fda9a6;
+        text-decoration: line-through;
+        padding: 2px;
+    }
+}
+
 .voter-list {
     display:block;
     height:30px;
app/controllers/AbstractPostingApp.java
--- app/controllers/AbstractPostingApp.java
+++ app/controllers/AbstractPostingApp.java
@@ -22,7 +22,10 @@
 
 import com.fasterxml.jackson.databind.JsonNode;
 import controllers.annotation.AnonymousCheck;
-import models.*;
+import models.AbstractPosting;
+import models.Attachment;
+import models.Comment;
+import models.IssueLabel;
 import models.enumeration.Direction;
 import models.enumeration.Operation;
 import models.resource.Resource;
@@ -31,10 +34,17 @@
 import play.data.Form;
 import play.db.ebean.Model;
 import play.i18n.Messages;
-import play.mvc.*;
+import play.mvc.Call;
+import play.mvc.Controller;
+import play.mvc.Http;
+import play.mvc.Result;
 import utils.*;
 
+import java.util.LinkedList;
 import java.util.Map;
+
+import static utils.JodaDateUtil.getDateString;
+import static utils.diff_match_patch.Diff;
 
 @AnonymousCheck
 public class AbstractPostingApp extends Controller {
@@ -107,6 +117,9 @@
         posting.authorName = original.authorName;
         posting.project = original.project;
         posting.setNumber(original.getNumber());
+        if (!StringUtils.defaultString(original.body, "").equals(StringUtils.defaultString(posting.body, ""))) {
+            posting.history = addToHistory(original, posting) + StringUtils.defaultString(original.history, "");
+        }
         preUpdateHook.run();
 
         try {
@@ -124,6 +137,77 @@
         return redirect(redirectTo);
     }
 
+    private static String addToHistory(AbstractPosting original, AbstractPosting posting) {
+        diff_match_patch dmp = new diff_match_patch();
+        LinkedList<diff_match_patch.Diff> diffs = dmp.diff_main(original.body, posting.body);
+        dmp.diff_cleanupSemanticLossless(diffs);
+
+        return (getHistoryMadeBy(posting, diffs) + getDiffText(original.body, posting.body) + "\n").replaceAll("\n", "</br>\n");
+    }
+
+    private static String getHistoryMadeBy(AbstractPosting posting, LinkedList<diff_match_patch.Diff> diffs) {
+        int insertions = 0;
+        int deletions = 0;
+        for (Diff diff : diffs) {
+            switch (diff.operation) {
+                case DELETE:
+                    deletions++;
+                    break;
+                case INSERT:
+                    insertions++;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("<div class='history-made-by'>").append(UserApp.currentUser().name)
+                .append("(").append(UserApp.currentUser().loginId).append(") ");
+        if (insertions > 0) {
+            sb.append("<span class='added'> ")
+                    .append(" + ")
+                    .append(insertions).append(" </span>");
+        }
+        if (deletions > 0) {
+            sb.append("<span class='deleted'> ")
+                    .append(" - ")
+                    .append(deletions).append(" </span>");
+        }
+        sb.append(" at ").append(getDateString(posting.updatedDate, "yyyy-MM-dd h:mm:ss a")).append("</div><hr/>\n");
+
+        return sb.toString();
+    }
+
+    private static String getDiffText(String oldValue, String newValue) {
+        diff_match_patch dmp = new diff_match_patch();
+        StringBuilder sb = new StringBuilder();
+        if (oldValue != null) {
+            LinkedList<diff_match_patch.Diff> diffs = dmp.diff_main(oldValue, newValue);
+            dmp.diff_cleanupSemanticLossless(diffs);
+            for(Diff diff: diffs){
+                switch (diff.operation) {
+                    case DELETE:
+                        sb.append("<span class='diff-deleted'>");
+                        sb.append(diff.text.replaceAll("\n", "&nbsp;\n"));
+                        sb.append("</span>");
+                        break;
+                    case EQUAL:
+                        sb.append(diff.text);
+                        break;
+                    case INSERT:
+                        sb.append("<span class='diff-added'>");
+                        sb.append(diff.text.replaceAll("\n", "&nbsp;\n"));
+                        sb.append("</span>");
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+        return sb.toString();
+    }
+
     public static void attachUploadFilesToPost(Resource resource) {
         final String[] temporaryUploadFiles = getTemporaryFileListFromHiddenForm();
         if(isTemporaryFilesExist(temporaryUploadFiles)){
app/models/AbstractPosting.java
--- app/models/AbstractPosting.java
+++ app/models/AbstractPosting.java
@@ -42,6 +42,9 @@
     @Lob
     public String body;
 
+    @Lob
+    public String history;
+
     @Constraints.Required
     @Formats.DateTime(pattern = "YYYY/MM/DD/hh/mm/ss")
     public Date createdDate;
app/views/board/view.scala.html
--- app/views/board/view.scala.html
+++ app/views/board/view.scala.html
@@ -1,26 +1,12 @@
 @**
-* Yobi, Project Hosting SW
+* Yona, 21st Century Project Hosting SW
 *
-* Copyright 2012 NAVER Corp.
-* http://yobi.io
-*
-* @author Ahn Hyeok Jun
-* @author Suwon Chae
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*   http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
 **@
 @(post:Posting, commentForm: play.data.Form[PostingComment],  project:Project)
 
+@import org.apache.commons.lang.StringUtils
 @import utils.JodaDateUtil
 @import utils.TemplateHelper._
 @import utils.AccessControl._
@@ -72,6 +58,12 @@
               <strong class="name">@Messages("common.noAuthor")</strong>
             }
           </a>
+            @if(post.history != null){
+                <div class="posting-history">
+                    <a href="#-yona-posting-history" data-toggle="modal">@Messages("change.history")</a>
+                    @common.partial_history(post)
+                </div>
+            }
           <div class="board-labels pull-right">
             @if(!IssueLabel.findByProject(project).isEmpty){
                 @if(isAllowed(UserApp.currentUser(), post.asResource(), Operation.UPDATE)){
@@ -82,7 +74,11 @@
             }
             </div>
         </div>
-        <div class="content markdown-wrap">@Html(Markdown.render(post.body, post.asResource().getProject()))</div>
+        @if(StringUtils.isEmpty(post.body)){
+            <div class="content empty-content"></div>
+        } else {
+            <div class="content markdown-wrap">@Html(Markdown.render(post.body, post.asResource().getProject()))</div>
+        }
         <div class="attachments" id="attachments" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.BOARD_POST.toString(), post.id.toString()))"></div>
     	</div>
     	<div class="board-footer board-actrow">
 
app/views/common/partial_history.scala.html (added)
+++ app/views/common/partial_history.scala.html
@@ -0,0 +1,24 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
+**@
+@(posting:models.AbstractPosting)
+
+@import utils.TemplateHelper._
+
+<div id="-yona-posting-history" class="modal hide">
+    <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+        <h5 class="nm">@Messages("change.history")</h5>
+    </div>
+    <div class="modal-body">
+        <p>
+            @Html(posting.history)
+        </p>
+    </div>
+    <div class="modal-footer">
+        <button class="ybtn ybtn-info ybtn-small" data-dismiss="modal" aria-hidden="true">@Messages("button.confirm")</button>
+    </div>
+</div>
app/views/issue/view.scala.html
--- app/views/issue/view.scala.html
+++ app/views/issue/view.scala.html
@@ -1,22 +1,8 @@
 @**
-* Yobi, Project Hosting SW
+* Yona, 21st Century Project Hosting SW
 *
-* Copyright 2012 NAVER Corp.
-* http://yobi.io
-*
-* @author Ahn Hyeok Jun
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*   http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
 **@
 @(title:String, issue:Issue, issueForm: play.data.Form[Issue], commentForm: play.data.Form[Comment],project:Project)
 @import org.apache.commons.lang.StringUtils
@@ -103,7 +89,12 @@
                             }
                         </a>
                 </div>
-
+                @if(issue.history != null){
+                <div class="posting-history">
+                    <a href="#-yona-posting-history" data-toggle="modal">@Messages("change.history")</a>
+                    @common.partial_history(issue)
+                </div>
+                }
                 @if(StringUtils.isEmpty(issue.body)){
                     <div class="content empty-content"></div>
                 } else {
 
conf/evolutions/default/7.sql (added)
+++ conf/evolutions/default/7.sql
@@ -0,0 +1,7 @@
+# --- !Ups
+ALTER TABLE issue ADD COLUMN history longtext;
+ALTER TABLE posting ADD COLUMN history longtext;
+
+# --- !Downs
+ALTER TABLE issue DROP COLUMN history;
+ALTER TABLE posting DROP COLUMN history;
conf/messages
--- conf/messages
+++ conf/messages
@@ -73,6 +73,9 @@
 button.user.makeAccountUnlock.false = Lock account
 button.user.makeAccountUnlock.true = Unlock account
 button.yes = Yes
+change.added = Added
+change.deleted = Deleted
+change.history = Change history
 code.addedPath = {0} (added)
 code.author = Author
 code.authorDate = Author Date
conf/messages.ko-KR
--- conf/messages.ko-KR
+++ conf/messages.ko-KR
@@ -73,6 +73,9 @@
 button.user.makeAccountUnlock.false = 계정잠그기
 button.user.makeAccountUnlock.true = 잠김해제
 button.yes = 예
+change.added = 추가
+change.deleted = 삭제
+change.history = 변경 이력
 code.addedPath = {0} (추가됨)
 code.author = 작성자
 code.authorDate = 작성된 날짜
Add a comment
List