doortts doortts 2017-01-14
watcher-list: Make watcher list to work as ajax
@64c2f1b58f0d8cdbb908fa20161b0ed2802951c4
app/actions/AbstractProjectCheckAction.java
--- app/actions/AbstractProjectCheckAction.java
+++ app/actions/AbstractProjectCheckAction.java
@@ -30,10 +30,7 @@
 import play.mvc.Http.Context;
 import play.mvc.Result;
 import play.libs.F.Promise;
-import utils.AccessControl;
-import utils.AccessLogger;
-import utils.ErrorViews;
-import utils.RedirectUtil;
+import utils.*;
 
 import static play.mvc.Controller.flash;
 import static play.mvc.Http.Context.current;
@@ -54,12 +51,11 @@
         String projectName = null;
 
         PathParser parser = new PathParser(context);
-        if (current().request().getHeader(UserApp.USER_TOKEN_HEADER) != null) {
+        PathVariable pathVariable = new PathVariable(current().request().path());
+        if (pathVariable.isApiCall()) {
             // eg. context.request().path() : /-_-api/v1/owners/doortts/projects/Test/posts
-            String[] base = context.request().path().split("/owners/");
-            String[] partial = base[1].split("/");
-            ownerLoginId = partial[0];
-            projectName = partial[2];
+            ownerLoginId = pathVariable.getPathVariable("owners");
+            projectName = pathVariable.getPathVariable("projects");
         } else {
             ownerLoginId = parser.getOwnerLoginId();
             projectName = parser.getProjectName();
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -6494,7 +6494,7 @@
 }
 
 .show-watchers {
-    display: inline-block;
+    display: none;
     margin-left: 3px;
     &:hover {
         cursor: pointer;
app/controllers/api/ProjectApi.java
--- app/controllers/api/ProjectApi.java
+++ app/controllers/api/ProjectApi.java
@@ -9,7 +9,6 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import controllers.MigrationApp;
-import controllers.annotation.AnonymousCheck;
 import controllers.annotation.IsAllowed;
 import models.*;
 import models.enumeration.Operation;
@@ -19,7 +18,8 @@
 import play.mvc.Controller;
 import play.mvc.Result;
 
-import java.util.*;
+import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 import static controllers.MigrationApp.composePlainCommentsJson;
 
app/controllers/api/WatcherApi.java (added)
+++ app/controllers/api/WatcherApi.java
@@ -0,0 +1,64 @@
+/**
+ * Yona, 21st Century Project Hosting SW
+ *
+ * Copyright 2016 the original author or authors.
+ */
+
+package controllers.api;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import models.*;
+import org.apache.commons.lang3.StringUtils;
+import play.db.ebean.Transactional;
+import play.libs.Json;
+import play.mvc.Controller;
+import play.mvc.Result;
+import utils.RouteUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import static play.libs.Json.toJson;
+
+public class WatcherApi extends Controller {
+
+    @Transactional
+    public static Result getWatchers(String owner, String projectName, Long number) {
+        final int LIMIT = 100;
+        int counter = 0;
+
+        Project project = Project.findByOwnerAndProjectName(owner, projectName);
+        AbstractPosting posting = null;
+        String type = request().getQueryString("type");
+        if (StringUtils.isNotEmpty(type) && type.equals("issues")) {
+            posting = AbstractPosting.findByNumber(Issue.finder, project, number);
+        } else if (StringUtils.isNotEmpty(type) && type.equals("posts")){
+            posting = AbstractPosting.findByNumber(Posting.finder, project, number);
+        } else {
+            return ok();
+        }
+
+        Set<User> watchers = posting.getWatchers();
+        ObjectNode json = Json.newObject();
+        List<ObjectNode> watcherList = new ArrayList<>();
+
+        for(User user: watchers){
+            counter++;
+
+            ObjectNode watcher = Json.newObject();
+            watcher.put("name", user.name);
+            watcher.put("url", RouteUtil.getUrl(user));
+            watcherList.add(watcher);
+            if (counter == LIMIT) {
+                break;
+            }
+        }
+
+        json.put("totalWatchers", watchers.size());
+        json.put("watchersInList", counter);
+        json.put("watchers", toJson(watcherList));
+        return ok(json);
+    }
+
+}
 
app/utils/PathVariable.java (added)
+++ app/utils/PathVariable.java
@@ -0,0 +1,56 @@
+/**
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp.
+ * https://yona.io
+ **/
+package utils;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class PathVariable {
+    String url;
+    public static final String rootPath = play.Configuration.root().getString("application.context", "");
+    public static final String API_PREFIX = "/-_-api/v1/";
+    private Map<String, String> pathVariable = new HashMap<>();
+    private boolean isApiPathCall = false;
+
+    public PathVariable(String url) {
+        String refinedUrl  = url;
+        if(StringUtils.isNotEmpty(rootPath)){
+            refinedUrl = refinedUrl.replaceFirst(rootPath, "");
+        }
+        if(refinedUrl.startsWith(API_PREFIX)){
+            refinedUrl = refinedUrl.replaceFirst(API_PREFIX, "");
+            this.isApiPathCall = true;
+            decomposeToPathVariable(refinedUrl);
+        }
+    }
+
+    /**
+     * ex> if url path is /-_-api/v1/owners/doortts/projects/test/issue/21
+     * getPathVariable("owners") returns "doortts"
+     * getPathVariable("projects") returns "test"
+     */
+    public String getPathVariable(String pathName) {
+        return this.pathVariable.get(pathName);
+    }
+
+    public boolean isApiCall() {
+        return this.isApiPathCall;
+    }
+
+    private void decomposeToPathVariable(String refinedUrl) {
+        String[] decomposed = refinedUrl.split("/");
+        for (int i = 0; i < decomposed.length; i = i + 2) {
+            if (i + 1 > decomposed.length - 1) {
+                pathVariable.put(decomposed[i], "");
+            } else {
+                pathVariable.put(decomposed[i], decomposed[i + 1]);
+            }
+        }
+    }
+}
app/utils/TemplateHelper.scala
--- app/utils/TemplateHelper.scala
+++ app/utils/TemplateHelper.scala
@@ -29,34 +29,10 @@
 
 object TemplateHelper {
 
-  def watcherList(posting: AbstractPosting): String = {
-    val LIMIT = 100
-    var counter = 0
-    var str =  ""
-    val watchers = posting.getWatchers
-    breakable {
-      for(watcher <- watchers){
-        counter += 1
-        if(counter > LIMIT) break
-        var dummy = watcher.toString  // ebean eagerly loading hack
-        str += "<a href='" + userInfo(watcher.loginId) + "' class='watcher-name'>" + watcher.name + "</a>"
-      }
-    }
-
-    if( watchers.size > LIMIT ) {
-      str += Messages.get("watchers.more", (watchers.size - LIMIT).toString)
-    }
-    str
-  }
-
   def showWatchers(posting: AbstractPosting): String = {
-    if(posting.getWatchers.size > 1 && UserApp.currentUser() != User.anonymous){
       "<div class='show-watchers' data-toggle='tooltip' data-placement='top' data-trigger='hover' data-html='true' title='" + Messages.get("watchers") + "'>" +
-      "<button id='watch-button' type='button' class='ybtn'><i class='yobicon-emo-coffee'></i> " + posting.getWatchers.size.toString + "</button>" +
+      "<button id='watcher-list-button' type='button' class='ybtn'><i class='yobicon-emo-coffee'></i><span class='watcherCount'></span></button>" +
       "</div>"
-    } else {
-      ""
-    }
   }
 
   def buildQueryString(call: Call, queryMap: Map[String, String]): String = {
app/views/board/view.scala.html
--- app/views/board/view.scala.html
+++ app/views/board/view.scala.html
@@ -102,7 +102,7 @@
             }
     	</div>
 
-        <div class="watcher-list">@Html(watcherList(post))</div>
+        <div class="watcher-list"></div>
     	@** Comment **@
     	<div id="comments" class="board-comment-wrap">
             @partial_comments(project, post)
@@ -142,6 +142,7 @@
 <link rel="stylesheet" type="text/css" media="screen" href="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.css")">
 <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.caret.min.js")"></script>
 <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.js")"></script>
+<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yobi.WatcherList.js")"></script>
 <script type="text/javascript">
 	$(document).ready(function(){
 		$yobi.loadModule("board.View", {
@@ -181,9 +182,14 @@
         // detect comment which contains mention at me
         $(".comment-body:contains('@UserApp.currentUser().loginId')").closest(".comment").addClass("mentioned");
 
-        $(".show-watchers").on("click", function(){
-            $(".watcher-list").toggle();
-        })
+        // Watcher List
+        var watcherApiUrl = "@api.routes.WatcherApi.getWatchers(project.owner, project.name, post.getNumber)?type=posts";
+        $("#watch-button").on('click', function () {
+            setTimeout(function () {
+                watcherListApi(watcherApiUrl);
+            }, 1000);
+        });
+        watcherListApi(watcherApiUrl);
 	});
 </script>
 @common.select2()
app/views/issue/view.scala.html
--- app/views/issue/view.scala.html
+++ app/views/issue/view.scala.html
@@ -151,7 +151,7 @@
                         <a href="@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)" class="ybtn">@Messages("button.edit")</a>
                     }
                 </div>
-                <div class="watcher-list">@Html(watcherList(issue))</div>
+                <div class="watcher-list"></div>
                 @** Comment **@
                 <div id="comments" class="board-comment-wrap">
                     <div id="timeline">
@@ -332,6 +332,7 @@
 <link rel="stylesheet" type="text/css" media="screen" href="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.css")">
 <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.caret.min.js")"></script>
 <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.js")"></script>
+<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yobi.WatcherList.js")"></script>
 <script type="text/javascript">
     $(function(){
         // yobi.issue.View
@@ -367,9 +368,14 @@
         // detect comment which contains mention at me
         $(".comment-body:contains('@UserApp.currentUser().loginId')").closest(".comment").addClass("mentioned");
 
-        $(".show-watchers").on("click", function(){
-            $(".watcher-list").toggle();
-        })
+        // Watcher List
+        var watcherApiUrl = "@api.routes.WatcherApi.getWatchers(project.owner, project.name, issue.getNumber)?type=issues";
+        $("#watch-button").on('click', function () {
+            setTimeout(function () {
+                watcherListApi(watcherApiUrl);
+            }, 1000);
+        });
+        watcherListApi(watcherApiUrl);
     });
 </script>
 }
app/views/layout.scala.html
--- app/views/layout.scala.html
+++ app/views/layout.scala.html
@@ -29,11 +29,8 @@
 <link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("javascripts/lib/pikaday/pikaday.css")" />
 <link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("stylesheets/yobi.css")">
 <link rel='stylesheet' href="@routes.Assets.at("javascripts/lib/nprogress/nprogress.css")"/>
-
 <script type="text/javascript" src="@routes.Assets.at("javascripts/yona-layout.js")"></script>
-<script type="text/javascript">
-    NProgress.configure({ minimum: 0.7 });
-</script>
+
 </head>
 
 <body class="@theme">
@@ -43,5 +40,9 @@
 @partial_update_notification()
 @content
 @common.scripts()
+
+<script type="text/javascript">
+        NProgress.configure({ minimum: 0.7 });
+</script>
 </body>
 </html>
app/views/projectMenu.scala.html
--- app/views/projectMenu.scala.html
+++ app/views/projectMenu.scala.html
@@ -55,7 +55,8 @@
                     <span class="menu-name">@Messages("title.projectHome")</span><span class="short-menu">H</span>
                 </a>
             </li>
-            @if(project.menuSetting.code) {
+            @defining(project.menuSetting){ menuSetting =>
+            @if(menuSetting.code) {
                 @if(!project.isCodeAccessibleMemberOnly || project.hasMember(UserApp.currentUser())) {
                     <li class="@isActiveMenu(MenuType.CODE)">
                         <a href="@routes.CodeApp.codeBrowser(project.owner, project.name)">
@@ -64,14 +65,14 @@
                     </li>
                 }
             }
-            @if(project.menuSetting.issue) {
+            @if(menuSetting.issue) {
                 <li class="@isActiveMenu(MenuType.ISSUE)">
                     <a href="@routes.IssueApp.issues(project.owner, project.name, "open")">
                         <span class="menu-name">@Messages("menu.issue")</span><span class="short-menu">I</span> @countingBadge(Issue.countIssues(project.id, State.OPEN))
                     </a>
                 </li>
             }
-            @if(project.menuSetting.pullRequest && project.vcs.equals("GIT")) {
+            @if(menuSetting.pullRequest && project.vcs.equals("GIT")) {
                 @if(!project.isCodeAccessibleMemberOnly || project.hasMember(UserApp.currentUser())) {
                     <li class="@isActiveMenu(MenuType.PULL_REQUEST)">
                     <a href="@getPullRequestURL(project)">
@@ -80,7 +81,7 @@
                 </li>
                 }
             }
-            @if(project.menuSetting.review) {
+            @if(menuSetting.review) {
                 @if(!project.isCodeAccessibleMemberOnly || project.hasMember(UserApp.currentUser())) {
                     <li class="@isActiveMenu(MenuType.PROJECT_REVIEW)">
                         <a href="@routes.ReviewThreadApp.reviewThreads(project.owner, project.name)">
@@ -90,14 +91,14 @@
                     </li>
                 }
             }
-            @if(project.menuSetting.milestone) {
+            @if(menuSetting.milestone) {
                 <li class="@isActiveMenu(MenuType.MILESTONE)">
                     <a href="@routes.MilestoneApp.milestones(project.owner, project.name)">
                         <span class="menu-name">@Messages("milestone")</span><span class="short-menu">M</span>
                     </a>
                 </li>
             }
-            @if(project.menuSetting.board) {
+            @if(menuSetting.board) {
                 <li class="@isActiveMenu(MenuType.BOARD)">
                     <a href="@routes.BoardApp.posts(project.owner, project.name)">
                         <span class="menu-name">@Messages("menu.board")</span><span class="short-menu">B</span>
@@ -107,6 +108,7 @@
                     </a>
                 </li>
             }
+        }
         </ul>
         @if(AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.UPDATE)){
         <div class="project-setting">
@@ -125,21 +127,22 @@
 </div>
 <script type="text/javascript">
 $(document).ready(function(){
+    @defining(project.menuSetting) { menuSetting =>
     var htKeyMap = {
         "H": "@routes.ProjectApp.project(project.owner, project.name)"
-        @if(project.menuSetting.board) {
+        @if(menuSetting.board) {
             ,"B": "@routes.BoardApp.posts(project.owner, project.name)"
         }
-        @if(project.menuSetting.code) {
+        @if(menuSetting.code) {
             ,"C": "@routes.CodeApp.codeBrowser(project.owner, project.name)"
         }
-        @if(project.menuSetting.issue) {
+        @if(menuSetting.issue) {
             ,"I": "@routes.IssueApp.issues(project.owner, project.name,"open")"
         }
-        @if(project.menuSetting.milestone) {
+        @if(menuSetting.milestone) {
             ,"M": "@routes.MilestoneApp.milestones(project.owner, project.name)"
         }
-        @if(project.menuSetting.pullRequest && project.vcs.equals("GIT")){
+        @if(menuSetting.pullRequest && project.vcs.equals("GIT")){
            ,"F": "@getPullRequestURL(project)"
         }
         @requestHeader.session.get("loginId") match {
@@ -147,6 +150,7 @@
             case _ => { }
         }
     };
+    }
 
     $yobi.loadModule("project.Global", {
         "htKeyMap": htKeyMap,
conf/routes
--- conf/routes
+++ conf/routes
@@ -35,6 +35,7 @@
 GET            /-_-api                                                                 controllers.Application.index()
 GET            /-_-api/v1/                                                             controllers.Application.index()
 GET            /-_-api/v1/owners/:owner/projects/:projectName/exports                  controllers.api.ProjectApi.exports(owner:String, projectName:String)
+GET            /-_-api/v1/owners/:owner/projects/:projectName/posts/$number<[0-9]+>/watchers   controllers.api.WatcherApi.getWatchers(owner:String, projectName:String, number:Long)
 POST           /-_-api/v1/owners/:owner/projects/:projectName/posts                    controllers.api.BoardApi.newPostByJson(owner:String, projectName:String)
 POST           /-_-api/v1/owners/:owner/projects/:projectName/postlabel/:number        controllers.api.BoardApi.updatePostLabel(owner:String, projectName:String, number:Long)
 GET            /-_-api/v1/hello                                                        controllers.api.GlobalApi.hello()
 
public/javascripts/common/yobi.WatcherList.js (added)
+++ public/javascripts/common/yobi.WatcherList.js
@@ -0,0 +1,28 @@
+var apiUrlMemo;
+$(".show-watchers").on("click", function () {
+    $(".watcher-list").toggle();
+});
+function watcherListApi(apiUrl){
+
+    if (apiUrl) {
+        apiUrlMemo = apiUrl;
+    }
+
+    $.get(apiUrl || apiUrlMemo)
+        .done(function (data) {
+            var watcherList = "";
+            if( data.watchersInList > 0 ){
+                $(".watcherCount").text(" " + data.totalWatchers);
+                $(".show-watchers").css("display", "inline-block");
+                data.watchers.forEach(function (watcher) {
+                    watcherList += '<a href="' + watcher.url + '" class="watcher-name">' + watcher.name + "</a>";
+                });
+                if(data.totalWatchers > data.watchersInList) {
+                    watcherList += Messages("watchers.more", (data.totalWatchers - data.watchersInList))
+                }
+                $(".watcher-list").html(watcherList);
+            } else {
+                $(".show-watchers").css("display", "none");
+            }
+        });
+}(파일 끝에 줄바꿈 문자 없음)
Add a comment
List