From f8ed345f5b4a6f10ed362805f67a57850261074a Mon Sep 17 00:00:00 2001 From: Wonjun Hong Date: Fri, 27 Oct 2017 14:11:41 +0900 Subject: [PATCH 01/77] Group: Add pull request menu (#260) --- app/controllers/IssueApp.java | 1 + app/controllers/OrganizationApp.java | 23 ++++ app/controllers/PullRequestApp.java | 9 +- app/models/PullRequest.java | 9 ++ .../group_pullrequest_list.scala.html | 76 +++++++++++++ .../group_pullrequest_list_partial.scala.html | 101 ++++++++++++++++++ app/views/organization/menu.scala.html | 5 + conf/messages | 1 + conf/routes | 1 + 9 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 app/views/organization/group_pullrequest_list.scala.html create mode 100644 app/views/organization/group_pullrequest_list_partial.scala.html diff --git a/app/controllers/IssueApp.java b/app/controllers/IssueApp.java index 91ebc4f4c..d85051ea1 100644 --- a/app/controllers/IssueApp.java +++ b/app/controllers/IssueApp.java @@ -44,6 +44,7 @@ public class IssueApp extends AbstractPostingApp { @AnonymousCheck(requiresLogin = false, displaysFlashMessage = true) public static Result organizationIssues(@Nonnull String organizationName, @Nonnull String state, @Nonnull String format, int pageNum) throws WriteException, IOException { + // SearchCondition from param Form issueParamForm = new Form<>(models.support.SearchCondition.class); models.support.SearchCondition searchCondition = issueParamForm.bindFromRequest().get(); diff --git a/app/controllers/OrganizationApp.java b/app/controllers/OrganizationApp.java index c2c9ff3d2..4dfe64b73 100644 --- a/app/controllers/OrganizationApp.java +++ b/app/controllers/OrganizationApp.java @@ -9,6 +9,8 @@ import com.avaje.ebean.Page; import controllers.annotation.AnonymousCheck; import controllers.annotation.GuestProhibit; +import controllers.PullRequestApp.SearchCondition; +import controllers.PullRequestApp.Category; import models.*; import models.enumeration.Operation; import models.enumeration.RequestState; @@ -30,6 +32,7 @@ import views.html.organization.members; import views.html.organization.setting; import views.html.organization.view; +import views.html.organization.group_pullrequest_list; import javax.servlet.ServletException; import javax.validation.ConstraintViolation; @@ -45,6 +48,26 @@ */ @AnonymousCheck public class OrganizationApp extends Controller { + + @AnonymousCheck(requiresLogin = false, displaysFlashMessage = true) + public static Result organizationPullRequests(String organizationName, String category) { + + Organization organization = Organization.findByName(organizationName); + if (organization == null) { + return notFound(ErrorViews.NotFound.render("error.notfound.organization")); + } + + SearchCondition condition = Form.form(SearchCondition.class).bindFromRequest().get(); + if (category == "open") + condition.setOrganization(organization).setCategory(Category.OPEN); + else + condition.setOrganization(organization).setCategory(Category.CLOSED); + Page page = PullRequest.findPagingList(condition); + + return ok(group_pullrequest_list.render("title.pullrequest", organization, page, condition, category)); + } + + /** * show New Group page * @return {@link Result} diff --git a/app/controllers/PullRequestApp.java b/app/controllers/PullRequestApp.java index 13c50f0c7..4f38aebb0 100755 --- a/app/controllers/PullRequestApp.java +++ b/app/controllers/PullRequestApp.java @@ -343,7 +343,7 @@ private static Result pullRequests(String userName, String projectName, Category // Only members can access code? if(project.isCodeAccessibleMemberOnly && !project.hasMember(UserApp.currentUser())) { - return forbidden(ErrorViews.Forbidden.render("error.forbidden", project)); + return forbidden(ErrorViews.Forbidden.render("error.forbidden", project)); } SearchCondition condition = Form.form(SearchCondition.class).bindFromRequest().get(); @@ -697,6 +697,12 @@ public static class SearchCondition implements Cloneable { public Long contributorId; public int pageNum = Constants.DEFAULT_PAGE; public Category category; + public Organization organization; + + public SearchCondition setOrganization(Organization organization) { + this.organization = organization; + return this; + } public SearchCondition setProject(Project project) { this.project = project; @@ -731,6 +737,7 @@ public SearchCondition clone() throws CloneNotSupportedException { clone.contributorId = this.contributorId; clone.pageNum = this.pageNum; clone.category = this.category; + clone.organization = this.organization; return clone; } diff --git a/app/models/PullRequest.java b/app/models/PullRequest.java index 744f62216..01b26c34a 100644 --- a/app/models/PullRequest.java +++ b/app/models/PullRequest.java @@ -779,6 +779,15 @@ private static ExpressionList createSearchExpressionList(SearchCond if (condition.project != null) { el.eq(condition.category.project(), condition.project); } + if (condition.organization != null) { + List projects = condition.organization.getVisibleProjects(UserApp.currentUser()); + List projectsIds = new ArrayList<>(); + for (Project project : projects) { + projectsIds.add(project.id.toString()); + } + el.in("to_project_id", projectsIds); + el.in("from_project_id", projectsIds); + } Expression state = createStateSearchExpression(condition.category.states()); if (state != null) { el.add(state); diff --git a/app/views/organization/group_pullrequest_list.scala.html b/app/views/organization/group_pullrequest_list.scala.html new file mode 100644 index 000000000..082319336 --- /dev/null +++ b/app/views/organization/group_pullrequest_list.scala.html @@ -0,0 +1,76 @@ +@** +* Yona, 21st Century Project Hosting SW +* +* Copyright Yona & Yobi Authors & NAVER Corp. +* https://yona.io +**@ +@(title: String, organization: Organization, currentPage: com.avaje.ebean.Page[PullRequest], + condition: controllers.PullRequestApp.SearchCondition, requestType: String) + +@import utils.AccessControl +@import controllers.PullRequestApp.Category +@import models.PushedBranch + +@conditionForOpen = @{condition.clone.setCategory(Category.OPEN)} +@conditionForClosed = @{condition.clone.setCategory(Category.CLOSED)} + +@searchFormAction(category: Category) = @{ + category match { + case Category.CLOSED => { + routes.OrganizationApp.organizationPullRequests(organization.name, "closed") + } + case Category.OPEN => { + routes.OrganizationApp.organizationPullRequests(organization.name, "open") + } + } +} + +@organizationLayout(organization.name, utils.MenuType.NONE, organization) { + @header(organization) + @menu(organization) +
+
+
+
+ +
+
+ +
+
+ @views.html.organization.group_pullrequest_list_partial(organization, currentPage) +
+
+
+
+ +
+
+} diff --git a/app/views/organization/group_pullrequest_list_partial.scala.html b/app/views/organization/group_pullrequest_list_partial.scala.html new file mode 100644 index 000000000..7f9897865 --- /dev/null +++ b/app/views/organization/group_pullrequest_list_partial.scala.html @@ -0,0 +1,101 @@ +@** +* Yona, 21st Century Project Hosting SW +* +* Copyright Yona & Yobi Authors & NAVER Corp. +* https://yona.io +**@ +@(organization:Organization, page: com.avaje.ebean.Page[PullRequest]) + +@import utils.JodaDateUtil +@import utils.TemplateHelper._ +@import org.apache.commons.lang3.StringUtils + + + + diff --git a/app/views/organization/menu.scala.html b/app/views/organization/menu.scala.html index d3741c6c3..0f707be2d 100644 --- a/app/views/organization/menu.scala.html +++ b/app/views/organization/menu.scala.html @@ -38,6 +38,11 @@ @Messages("menu.board") +
  • + + @Messages("menu.pullRequest") + +
    • diff --git a/conf/messages b/conf/messages index b2542d88f..d7d8ad4bc 100644 --- a/conf/messages +++ b/conf/messages @@ -930,6 +930,7 @@ title.projectMembers = Member list title.projectSetting = Project settings title.projectTransfer = Project Transfer title.projectWatchers = Watcher list +title.pullrequest = Pull Request title.recently.visited = Recently visited title.rememberMe = Stay logged in title.resetPassword = Reset password diff --git a/conf/routes b/conf/routes index caf7bfcfc..78e4a82f1 100644 --- a/conf/routes +++ b/conf/routes @@ -76,6 +76,7 @@ POST /organizations/new GET /organizations/:organizationName controllers.OrganizationApp.organization(organizationName: String) GET /organizations/:organizationName/issues controllers.IssueApp.organizationIssues(organizationName: String, state:String ?= "", format:String ?= "html", pageNum: Int ?= 1) GET /organizations/:organizationName/boards controllers.BoardApp.organizationBoards(organizationName: String, pageNum: Int ?= 1) +GET /organizations/:organizationName/pullrequests controllers.OrganizationApp.organizationPullRequests(organizationName: String, category: String ?= "open") GET /organizations/:organizationName/settingform controllers.OrganizationApp.settingForm(organizationName: String) GET /organizations/:organizationName/deleteForm controllers.OrganizationApp.deleteForm(organizationName: String) DELETE /organizations/:organizationName controllers.OrganizationApp.deleteOrganization(organizationName: String) From 5d85a49ab9d0607a495acc897f70cf88acad4802 Mon Sep 17 00:00:00 2001 From: Wonjun Hong Date: Tue, 31 Oct 2017 21:10:25 +0900 Subject: [PATCH 02/77] =?UTF-8?q?review=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/OrganizationApp.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/OrganizationApp.java b/app/controllers/OrganizationApp.java index 4dfe64b73..272be8103 100644 --- a/app/controllers/OrganizationApp.java +++ b/app/controllers/OrganizationApp.java @@ -58,10 +58,11 @@ public static Result organizationPullRequests(String organizationName, String ca } SearchCondition condition = Form.form(SearchCondition.class).bindFromRequest().get(); - if (category == "open") + if (category.equals("open")) { condition.setOrganization(organization).setCategory(Category.OPEN); - else + } else { condition.setOrganization(organization).setCategory(Category.CLOSED); + } Page page = PullRequest.findPagingList(condition); return ok(group_pullrequest_list.render("title.pullrequest", organization, page, condition, category)); From a4eb54c5db5e30580b61f2dd6f34727e460ca780 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Wed, 3 Jan 2018 11:36:59 +0900 Subject: [PATCH 03/77] navbar: Change to display pure name only --- app/views/common/usermenu.scala.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/common/usermenu.scala.html b/app/views/common/usermenu.scala.html index a18256cd2..e0dc9d128 100644 --- a/app/views/common/usermenu.scala.html +++ b/app/views/common/usermenu.scala.html @@ -92,7 +92,7 @@ - @currentUser.name + @currentUser.getPureNameOnly From f17c166a784b33d0b5a4f2b043b71f7bf0a35c24 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Wed, 3 Jan 2018 19:56:54 +0900 Subject: [PATCH 04/77] navbar: Add custom link to navbar feature --- app/controllers/Application.java | 2 ++ app/views/common/usermenu.scala.html | 8 ++++++-- conf/application.conf.default | 11 +++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/controllers/Application.java b/app/controllers/Application.java index 33e02f4dc..a74c376bf 100644 --- a/app/controllers/Application.java +++ b/app/controllers/Application.java @@ -38,6 +38,8 @@ public class Application extends Controller { public static String LOGIN_PAGE_LOGINID_PLACEHOLDER = play.Configuration.root().getString("application.login.page.loginId.placeholder", ""); public static String LOGIN_PAGE_PASSWORD_PLACEHOLDER = play.Configuration.root().getString("application.login.page.password.placeholder", ""); public static boolean SHOW_USER_EMAIL = play.Configuration.root().getBoolean("application.show.user.email", true); + public static String NAVBAR_CUSTOM_LINK_NAME = play.Configuration.root().getString("application.navbar.custom.link.name", ""); + public static String NAVBAR_CUSTOM_LINK_URL = play.Configuration.root().getString("application.navbar.custom.link.url", ""); @AnonymousCheck public static Result index() { diff --git a/app/views/common/usermenu.scala.html b/app/views/common/usermenu.scala.html index e0dc9d128..375c5b19b 100644 --- a/app/views/common/usermenu.scala.html +++ b/app/views/common/usermenu.scala.html @@ -71,10 +71,14 @@
      @if( !currentUser.isAnonymous()) { + @if(!StringUtils.isBlank(Application.NAVBAR_CUSTOM_LINK_NAME)) { +
    • + @Application.NAVBAR_CUSTOM_LINK_NAME +
    • + }
    • @Messages("notification")
    • -
    • @Messages("issue.myIssue")@myOpenIssueCount
    • @@ -92,7 +96,7 @@ - @currentUser.getPureNameOnly + @currentUser.getPureNameOnly diff --git a/conf/application.conf.default b/conf/application.conf.default index 496534b2e..a7af7a019 100644 --- a/conf/application.conf.default +++ b/conf/application.conf.default @@ -353,6 +353,17 @@ application.use.social.login.name.sync = false # choice: github, google application.social.login.support = "github, google" +# Custom link to navigation bar +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# If you fill the name of link and url, +# custom menu will be appeared at right upper side navbar +application.navbar.custom { + link { + name="" + url="" + } +} + # LDAP Login Support # ~~~~~~~~~~~~~~~~~ # From d827f72d96cb57b2a5ec80250fb025117fcc5049 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Wed, 3 Jan 2018 21:16:40 +0900 Subject: [PATCH 05/77] project: Add starred project feature at project breadcromb --- app/assets/stylesheets/less/_common.less | 4 ++++ app/assets/stylesheets/less/_page.less | 13 +++++++++++++ app/models/FavoriteProject.java | 7 +++++++ app/views/project/header.scala.html | 7 +++++++ public/javascripts/common/yona.Usermenu.js | 2 +- 5 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/less/_common.less b/app/assets/stylesheets/less/_common.less index 6654cfb87..39b608cf6 100644 --- a/app/assets/stylesheets/less/_common.less +++ b/app/assets/stylesheets/less/_common.less @@ -277,3 +277,7 @@ input.white::-webkit-input-placeholder { color:#fff; opacity:0.8; } input.white:-moz-placeholder { color:#fff; opacity:0.8; } /* Firefox 18- */ input.white::-moz-placeholder { color:#fff; opacity:0.8; } /* Firefox 19+ */ input.white:-ms-input-placeholder { color:#fff; opacity:0.8; } + +.va-text-top { + vertical-align: text-top !important; +} diff --git a/app/assets/stylesheets/less/_page.less b/app/assets/stylesheets/less/_page.less index 11b899ecb..3f9f0b3d9 100644 --- a/app/assets/stylesheets/less/_page.less +++ b/app/assets/stylesheets/less/_page.less @@ -419,6 +419,19 @@ font-size: 14px; margin-left:5px; } + + .user-project-list { + .star { + color: rgba(255, 255, 255, 0.22); + &:hover { + color: #e91e63; + cursor: pointer; + } + } + .starred { + color: #e91e63 !important; + } + } } .project-origin { diff --git a/app/models/FavoriteProject.java b/app/models/FavoriteProject.java index aeefed62e..1fb656522 100644 --- a/app/models/FavoriteProject.java +++ b/app/models/FavoriteProject.java @@ -49,4 +49,11 @@ public static void updateFavoriteProject(@Nonnull Project project){ favoriteProject.update(); } } + + public static FavoriteProject findByProjectId(Long userId, Long projectId){ + return finder.where() + .eq("user.id", userId) + .eq("project.id", projectId) + .findUnique(); + } } diff --git a/app/views/project/header.scala.html b/app/views/project/header.scala.html index 3644abce5..dc60ff854 100644 --- a/app/views/project/header.scala.html +++ b/app/views/project/header.scala.html @@ -45,6 +45,10 @@ } } +@isFavoriteProject = @{ + FavoriteProject.findByProjectId(UserApp.currentUser().id, project.id) != null +} +
      @@ -56,6 +60,9 @@ @project.owner / @project.name + + star + @if(project.isPrivate){ diff --git a/public/javascripts/common/yona.Usermenu.js b/public/javascripts/common/yona.Usermenu.js index 532847a21..14076c7ab 100644 --- a/public/javascripts/common/yona.Usermenu.js +++ b/public/javascripts/common/yona.Usermenu.js @@ -107,7 +107,7 @@ $(function() { } }); - $(".project-list > .star-project").on("click", function toggleProjectFavorite(e) { + $(".project-list > .star-project, .project-breadcrumb > .user-project-list").on("click", function toggleProjectFavorite(e) { e.stopPropagation(); var that = $(this); $.post(UsermenuToggleFavoriteProjectUrl + that.data("projectId")) From 804737d3f898ba184c4cab0cc2c3501a2325e990 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Wed, 3 Jan 2018 21:22:36 +0900 Subject: [PATCH 06/77] css: Fix group project icon alignment --- app/assets/stylesheets/less/_page.less | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/less/_page.less b/app/assets/stylesheets/less/_page.less index 3f9f0b3d9..fa4db4529 100644 --- a/app/assets/stylesheets/less/_page.less +++ b/app/assets/stylesheets/less/_page.less @@ -7010,6 +7010,7 @@ div.diff-body[data-outdated="true"] tr:hover .icon-comment { font-size: 15px; color: rgba(0, 0, 0, 0.7); padding: 0 2px; + vertical-align: top; } .no-margin { From 2b650457798f4cd8c63a342b6d7004c0235b17e3 Mon Sep 17 00:00:00 2001 From: mjpark03 Date: Mon, 8 Jan 2018 09:19:46 +0900 Subject: [PATCH 07/77] conf: Add message for copy email button --- conf/messages | 1 + conf/messages.ko-KR | 1 + 2 files changed, 2 insertions(+) diff --git a/conf/messages b/conf/messages index d7d8ad4bc..667857362 100644 --- a/conf/messages +++ b/conf/messages @@ -51,6 +51,7 @@ button.comment.open = Open comment window button.commentAndNextState.closed = Comment & Close issue button.commentAndNextState.open = Comment & Reopen issue button.confirm = Confirm +button.copy.email = Copy email list button.delete = Delete button.detail = Details button.done = Done diff --git a/conf/messages.ko-KR b/conf/messages.ko-KR index 3ce150742..15de634a1 100644 --- a/conf/messages.ko-KR +++ b/conf/messages.ko-KR @@ -51,6 +51,7 @@ button.comment.open = 댓글 입력 창 열기 button.commentAndNextState.closed = 댓글 입력하고 이슈 닫기 button.commentAndNextState.open = 댓글 입력하고 이슈 다시 열기 button.confirm = 확인 +button.copy.email = 이메일 복사 button.delete = 삭제 button.detail = 자세히 button.done = 완료 From 9dab6135d7071574b4edf54e8a717ad029b031de Mon Sep 17 00:00:00 2001 From: mjpark03 Date: Mon, 8 Jan 2018 09:31:01 +0900 Subject: [PATCH 08/77] issue: Add feature to get the list of voters and to copy emails of voters --- app/assets/stylesheets/less/_page.less | 1 + app/views/issue/partial_voters.scala.html | 5 ++--- app/views/issue/view.scala.html | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/less/_page.less b/app/assets/stylesheets/less/_page.less index fa4db4529..a9da5e1b6 100644 --- a/app/assets/stylesheets/less/_page.less +++ b/app/assets/stylesheets/less/_page.less @@ -4056,6 +4056,7 @@ label.issue-item-row { width:340px; margin-left:-170px; text-align:left; + direction: ltr !important; .close { margin-top:-2px; } diff --git a/app/views/issue/partial_voters.scala.html b/app/views/issue/partial_voters.scala.html index 063086f32..d8461b483 100644 --- a/app/views/issue/partial_voters.scala.html +++ b/app/views/issue/partial_voters.scala.html @@ -27,9 +27,7 @@ @if(issueVoters.size > numOfAvatars + numOfNames) { … }"> - - @Messages("issue.voters.more", issueVoters.size - numOfAvatars) - + ... } } @@ -57,6 +55,7 @@
      @Messages("issue.voters")
    diff --git a/app/views/issue/view.scala.html b/app/views/issue/view.scala.html index df6a19514..faebb007c 100644 --- a/app/views/issue/view.scala.html +++ b/app/views/issue/view.scala.html @@ -180,6 +180,7 @@
    @if(isResourceCreatable(UserApp.currentUser, issue.asResource(), ResourceType.ISSUE_COMMENT)) { + @issue.voters.size @@ -421,6 +422,7 @@

    @Messages("issue.delete")

    + + } From 5b23567eefca7cfa22549ff009876dc6881d6ece Mon Sep 17 00:00:00 2001 From: mjpark03 Date: Mon, 8 Jan 2018 18:00:22 +0900 Subject: [PATCH 09/77] css: Remove unnecessary direction property --- app/assets/stylesheets/less/_page.less | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/less/_page.less b/app/assets/stylesheets/less/_page.less index a9da5e1b6..fa4db4529 100644 --- a/app/assets/stylesheets/less/_page.less +++ b/app/assets/stylesheets/less/_page.less @@ -4056,7 +4056,6 @@ label.issue-item-row { width:340px; margin-left:-170px; text-align:left; - direction: ltr !important; .close { margin-top:-2px; } From af1aa76cd9796e1c7542036af7d029425ec81a8b Mon Sep 17 00:00:00 2001 From: mjpark03 Date: Mon, 8 Jan 2018 18:03:12 +0900 Subject: [PATCH 10/77] issue: Add feature to get the list of voters and to copy emails of voters - Remove duplicated div tag in partial_voters_scala.html --- app/views/issue/partial_voter_list.scala.html | 1 + app/views/issue/partial_voters.scala.html | 26 ------------------- app/views/issue/view.scala.html | 12 +++++++-- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/app/views/issue/partial_voter_list.scala.html b/app/views/issue/partial_voter_list.scala.html index f0da435f9..92f60ad33 100644 --- a/app/views/issue/partial_voter_list.scala.html +++ b/app/views/issue/partial_voter_list.scala.html @@ -43,6 +43,7 @@
    @Messages("issue.voters")
    diff --git a/app/views/issue/partial_voters.scala.html b/app/views/issue/partial_voters.scala.html index d8461b483..4f47e1821 100644 --- a/app/views/issue/partial_voters.scala.html +++ b/app/views/issue/partial_voters.scala.html @@ -33,29 +33,3 @@ } - - diff --git a/app/views/issue/view.scala.html b/app/views/issue/view.scala.html index faebb007c..2da74f131 100644 --- a/app/views/issue/view.scala.html +++ b/app/views/issue/view.scala.html @@ -180,7 +180,9 @@ + + @if(issue.voters.size > 0) { + @partial_voter_list("voters", issue.voters) + } + @if(StringUtils.isNotBlank(IssueApi.TRANSLATION_API)){ } From 2563d9af2c1c1b2c0631eb4b3706632e4041efef Mon Sep 17 00:00:00 2001 From: mjpark03 Date: Mon, 8 Jan 2018 18:52:53 +0900 Subject: [PATCH 11/77] script: Move creation code of clipboard object after loading clipboard lib --- app/views/issue/view.scala.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/views/issue/view.scala.html b/app/views/issue/view.scala.html index 2da74f131..bb22f3137 100644 --- a/app/views/issue/view.scala.html +++ b/app/views/issue/view.scala.html @@ -430,7 +430,6 @@

    @Messages("issue.delete")

    - - } From 5455b130f02dc84ed1f66ed3cc0cbace38b5e08f Mon Sep 17 00:00:00 2001 From: mjpark03 Date: Tue, 9 Jan 2018 10:38:28 +0900 Subject: [PATCH 12/77] script: Use API to check if clipboard is supported & Add alert message when copying email was successful --- app/views/issue/view.scala.html | 8 +++++++- conf/messages | 1 + conf/messages.ko-KR | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/views/issue/view.scala.html b/app/views/issue/view.scala.html index bb22f3137..bb1526097 100644 --- a/app/views/issue/view.scala.html +++ b/app/views/issue/view.scala.html @@ -512,7 +512,13 @@

    @Messages("issue.delete")

    }); }); - var clipboard = new Clipboard('#copyEmailBtn'); + if (Clipboard.isSupported()) { + var clipboard = new Clipboard('#copyEmailBtn'); + + clipboard.on('success', function(e) { + $yobi.alert('@Messages("button.copy.email.success.message")'); + }); + } }); } diff --git a/conf/messages b/conf/messages index 667857362..e4de823dc 100644 --- a/conf/messages +++ b/conf/messages @@ -52,6 +52,7 @@ button.commentAndNextState.closed = Comment & Close issue button.commentAndNextState.open = Comment & Reopen issue button.confirm = Confirm button.copy.email = Copy email list +button.copy.email.success.message = Copying email was successful. button.delete = Delete button.detail = Details button.done = Done diff --git a/conf/messages.ko-KR b/conf/messages.ko-KR index 15de634a1..a79e05e75 100644 --- a/conf/messages.ko-KR +++ b/conf/messages.ko-KR @@ -52,6 +52,7 @@ button.commentAndNextState.closed = 댓글 입력하고 이슈 닫기 button.commentAndNextState.open = 댓글 입력하고 이슈 다시 열기 button.confirm = 확인 button.copy.email = 이메일 복사 +button.copy.email.success.message = 이메일이 복사되었습니다. button.delete = 삭제 button.detail = 자세히 button.done = 완료 From ffc758f8e62974336b6ea58ce0473499cece7c01 Mon Sep 17 00:00:00 2001 From: mjpark03 Date: Wed, 10 Jan 2018 01:25:50 +0900 Subject: [PATCH 13/77] script: Add alert message to notify clipboard support error in browser --- app/views/issue/view.scala.html | 2 ++ conf/messages | 1 + conf/messages.ko-KR | 1 + 3 files changed, 4 insertions(+) diff --git a/app/views/issue/view.scala.html b/app/views/issue/view.scala.html index bb1526097..ddf7117af 100644 --- a/app/views/issue/view.scala.html +++ b/app/views/issue/view.scala.html @@ -518,6 +518,8 @@

    @Messages("issue.delete")

    clipboard.on('success', function(e) { $yobi.alert('@Messages("button.copy.email.success.message")'); }); + } else { + $yobi.notify('@Messages("site.features.error.clipboard")', 1500); } }); diff --git a/conf/messages b/conf/messages index e4de823dc..eb54faf14 100644 --- a/conf/messages +++ b/conf/messages @@ -834,6 +834,7 @@ site.features.issueTracker = Yona provides an issue tracker to help you deal wit site.features.privateRepositories = Keep your code private at your private repositories. site.features.unlimitedProjects = Work based on project/organization which supported by proper roles site.features.workTeam = Yona provides simple and easy team management tool to help you build teams for each project. +site.features.error.clipboard = Browser doesn't support clipboard feature. site.mail.body = Body site.mail.fail = Failed to send a mail. site.mail.from = From diff --git a/conf/messages.ko-KR b/conf/messages.ko-KR index a79e05e75..9ca83eb1c 100644 --- a/conf/messages.ko-KR +++ b/conf/messages.ko-KR @@ -834,6 +834,7 @@ site.features.issueTracker = 팀이 함께 고민하고 처리해야 하는 내 site.features.privateRepositories = 다른 사람에게 공개하고 싶지 않은 비밀 프로젝트 공간을 만들어 자유롭게 생각의 나래를 펼쳐보세요. site.features.unlimitedProjects = 프로젝트/그룹 기반으로 효율적으로 개발을 진행 할 수 있습니다. site.features.workTeam = 프로젝트별로 멤버를 자유롭게 구성할수 있는 쉽고 간편한 멤버관리 기능이 제공 됩니다. +site.features.error.clipboard = 브라우저가 클립보드를 지원하지 않습니다. site.mail.body = 본문 site.mail.fail = 메일 발송에 실패했습니다. site.mail.from = 보내는 메일 주소 From 0317f8abe0084d6de0009b1f493e2f458d1046b8 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Wed, 10 Jan 2018 11:19:57 +0900 Subject: [PATCH 14/77] favorite: Fix shortcut compatibility problem Shortcut key for usermenu is just 'f' key for now (from commit: 23840a63) But it conflicts with Firefox and Safari's native shortcut command CMD + F This commit fix it --- public/javascripts/common/yona.Usermenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/javascripts/common/yona.Usermenu.js b/public/javascripts/common/yona.Usermenu.js index 14076c7ab..b89200620 100644 --- a/public/javascripts/common/yona.Usermenu.js +++ b/public/javascripts/common/yona.Usermenu.js @@ -25,7 +25,7 @@ $(function() { }); function isShortcutKeyPressed(event) { - return (event.which === 102 || event.which === 12601) // keycode => 102: f, 12623: ㄹ + return (!event.metaKey && (event.which === 102 || event.which === 12601)) // keycode => 102: f, 12623: ㄹ && $(':focus').length === 0; // avoid already somewhere focused state } From 04763d85cbd59bf6b0edcf301511bf7e5e40e080 Mon Sep 17 00:00:00 2001 From: mjpark03 Date: Tue, 16 Jan 2018 17:54:02 +0900 Subject: [PATCH 15/77] view: Modify button for copying emails based on application config of showing user email --- app/views/issue/partial_voter_list.scala.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/issue/partial_voter_list.scala.html b/app/views/issue/partial_voter_list.scala.html index 92f60ad33..04a4f6068 100644 --- a/app/views/issue/partial_voter_list.scala.html +++ b/app/views/issue/partial_voter_list.scala.html @@ -43,7 +43,9 @@
    @Messages("issue.voters")
    From 4437e5fd7f147479269d0404292955482cf6b627 Mon Sep 17 00:00:00 2001 From: mjpark03 Date: Wed, 17 Jan 2018 16:59:09 +0900 Subject: [PATCH 16/77] view: Remove ux(number of heart) & Add new feature for copying email to message(voters more) --- app/views/issue/partial_voters.scala.html | 4 +++- app/views/issue/view.scala.html | 5 +---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/views/issue/partial_voters.scala.html b/app/views/issue/partial_voters.scala.html index 4f47e1821..a70f1d921 100644 --- a/app/views/issue/partial_voters.scala.html +++ b/app/views/issue/partial_voters.scala.html @@ -27,7 +27,9 @@ @if(issueVoters.size > numOfAvatars + numOfNames) { … }"> - ... +
    + @Messages("issue.voters.more", issueVoters.size - numOfAvatars) + } } diff --git a/app/views/issue/view.scala.html b/app/views/issue/view.scala.html index ddf7117af..51766925d 100644 --- a/app/views/issue/view.scala.html +++ b/app/views/issue/view.scala.html @@ -180,9 +180,6 @@
    @if(isResourceCreatable(UserApp.currentUser, issue.asResource(), ResourceType.ISSUE_COMMENT)) { - @if(issue.voters.size > 0) { - @issue.voters.size - } @@ -194,7 +191,7 @@ } @if(issue.voters.size > 0) { - @partial_voters(issue, 1) + @partial_voters(issue, 3) }
    From 2fc76d999ff9842d2468133280de08f79a5f58b7 Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Fri, 26 Jan 2018 18:19:57 +0900 Subject: [PATCH 17/77] util: Add function for getting diff(HTML format) between two values --- app/utils/DiffUtil.java | 67 ++++++++++++++++++++++ test/utils/DiffUtilTest.java | 106 +++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 app/utils/DiffUtil.java create mode 100644 test/utils/DiffUtilTest.java diff --git a/app/utils/DiffUtil.java b/app/utils/DiffUtil.java new file mode 100644 index 000000000..92e916618 --- /dev/null +++ b/app/utils/DiffUtil.java @@ -0,0 +1,67 @@ +/** + * Yona, 21st Century Project Hosting SW + *

    + * Copyright Yona & Yobi Authors & NAVER Corp. + * https://yona.io + **/ +package utils; + +import java.util.LinkedList; +import java.util.Optional; + +import org.apache.commons.lang3.StringEscapeUtils; +import utils.diff_match_patch.Diff; + +public class DiffUtil { + + public static final int EQUAL_TEXT_ELLIPSIS_SIZE = 100; + public static final int EQUAL_TEXT_BASE_SIZE = 50; + public static final int DIFF_EDITCOST = 8; + + public static String getDiffText(String oldValue, String newValue) { + + oldValue = Optional.ofNullable(oldValue).orElse(""); + newValue = Optional.ofNullable(newValue).orElse(""); + + diff_match_patch dmp = new diff_match_patch(); + dmp.Diff_EditCost = DIFF_EDITCOST; + String diffString = ""; + + LinkedList diffs = dmp.diff_main(oldValue, newValue); + dmp.diff_cleanupEfficiency(diffs); + + for (Diff diff: diffs) { + switch (diff.operation) { + case DELETE: + diffString += ""; + diffString += StringEscapeUtils.escapeHtml4(diff.text); + diffString += ""; + break; + case EQUAL: + int textLength = diff.text.length(); + if (textLength > EQUAL_TEXT_ELLIPSIS_SIZE) { + diffString += StringEscapeUtils.escapeHtml4(diff.text.substring(0, EQUAL_TEXT_BASE_SIZE)); + + diffString += "...\n"; + diffString += "......\n"; + diffString += "......\n"; + diffString += "..."; + + diffString += StringEscapeUtils.escapeHtml4(diff.text.substring(textLength - EQUAL_TEXT_BASE_SIZE)); + } else { + diffString += StringEscapeUtils.escapeHtml4(diff.text); + } + break; + case INSERT: + diffString += ""; + diffString += StringEscapeUtils.escapeHtml4(diff.text); + diffString += ""; + break; + default: + break; + } + } + + return diffString.replaceAll("\n", " 
    \n"); + } +} diff --git a/test/utils/DiffUtilTest.java b/test/utils/DiffUtilTest.java new file mode 100644 index 000000000..92c47b0af --- /dev/null +++ b/test/utils/DiffUtilTest.java @@ -0,0 +1,106 @@ +/** + * Yona, 21st Century Project Hosting SW + *

    + * Copyright Yona & Yobi Authors & NAVER Corp. + * https://yona.io + **/ +package utils; + +import org.junit.Test; +import static org.fest.assertions.Assertions.assertThat; + +public class DiffUtilTest { + + String DIFF_DELETE_PREFIX = ""; + String DIFF_DELETE_POSTFIX = ""; + + String DIFF_INSERT_PREFIX = ""; + String DIFF_INSERT_POSTFIX = ""; + + String DIFF_EQUAL_PREFIX = "... 
    \n" + + "...... 
    \n" + + "...... 
    \n" + + "...
    "; + + @Test + public void getDiffText_oldValueIsNull_returnString() { + + // GIVEN + String oldValue = null; + String newValue = "new value"; + String expectedResult = DIFF_INSERT_PREFIX + newValue + DIFF_INSERT_POSTFIX; + + // WHEN + String result = DiffUtil.getDiffText(oldValue, newValue); + + // THEN + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void getDiffText_newValueIsNull_returnString() { + + // GIVEN + String oldValue = "oldValue"; + String newValue = null; + String expectedResult = DIFF_DELETE_PREFIX + oldValue + DIFF_DELETE_POSTFIX; + + // WHEN + String result = DiffUtil.getDiffText(oldValue, newValue); + + // THEN + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void getDiffText_equalValuesMoreThanSize100_returnString() { + + // GIVEN + String oldValue = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" + + "123456789012345678901234567890"; + String newValue = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" + + "123456789012345678901234567890"; + int textLength = oldValue.length(); + String expectedResult = oldValue.substring(0, 50) + DIFF_EQUAL_PREFIX + oldValue.substring(textLength - 50); + + // WHEN + String result = DiffUtil.getDiffText(oldValue, newValue); + + // THEN + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void getDiffText_equalValuesLessThanSize100_returnString() { + + // GIVEN + String oldValue = "12345678901234567890"; + String newValue = "12345678901234567890"; + String expectedResult = oldValue; + + // WHEN + String result = DiffUtil.getDiffText(oldValue, newValue); + + // THEN + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void getDiffText_deleteAndInsertValue_returnString() { + + // GIVEN + String oldValue = "Hi, there?"; + String newValue = "Hello, mijeong?"; + String expectedResult = oldValue.substring(0, 1) + + DIFF_DELETE_PREFIX + oldValue.substring(1, oldValue.length() - 1) + DIFF_DELETE_POSTFIX + + DIFF_INSERT_PREFIX + newValue.substring(1, newValue.length() - 1) + DIFF_DELETE_POSTFIX + + oldValue.substring(oldValue.length() - 1); + + // WHEN + String result = DiffUtil.getDiffText(oldValue, newValue); + + // THEN + assertThat(result).isEqualTo(expectedResult); + } + +} From c21984216914656d8d3947e8ae3ae966c199e3d7 Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Sun, 28 Jan 2018 14:56:58 +0900 Subject: [PATCH 18/77] util: Add function for getting diff(Plain format) between two values --- app/utils/DiffUtil.java | 48 +++++++++++++++++++ test/utils/DiffUtilTest.java | 91 ++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/app/utils/DiffUtil.java b/app/utils/DiffUtil.java index 92e916618..d4df1cd6e 100644 --- a/app/utils/DiffUtil.java +++ b/app/utils/DiffUtil.java @@ -64,4 +64,52 @@ public static String getDiffText(String oldValue, String newValue) { return diffString.replaceAll("\n", " 
    \n"); } + + public static String getDiffPlainText(String oldValue, String newValue) { + + oldValue = Optional.ofNullable(oldValue).orElse(""); + newValue = Optional.ofNullable(newValue).orElse(""); + + diff_match_patch dmp = new diff_match_patch(); + dmp.Diff_EditCost = DIFF_EDITCOST; + String diffString = ""; + + LinkedList diffs = dmp.diff_main(oldValue, newValue); + dmp.diff_cleanupEfficiency(diffs); + + for (Diff diff: diffs) { + switch (diff.operation) { + case DELETE: + diffString += "--- "; + diffString += StringEscapeUtils.escapeHtml4(diff.text); + diffString += "\n"; + break; + case EQUAL: + int textLength = diff.text.length(); + if (textLength > EQUAL_TEXT_ELLIPSIS_SIZE) { + diffString += StringEscapeUtils.escapeHtml4(diff.text.substring(0, EQUAL_TEXT_BASE_SIZE)); + + diffString += "......\n"; + diffString += "......\n"; + diffString += "...\n"; + + diffString += StringEscapeUtils.escapeHtml4(diff.text.substring(textLength - EQUAL_TEXT_BASE_SIZE)); + diffString += "\n"; + } else { + diffString += StringEscapeUtils.escapeHtml4(diff.text); + diffString += "\n"; + } + break; + case INSERT: + diffString += "+++ "; + diffString += StringEscapeUtils.escapeHtml4(diff.text); + diffString += "\n"; + break; + default: + break; + } + } + + return diffString; + } } diff --git a/test/utils/DiffUtilTest.java b/test/utils/DiffUtilTest.java index 92c47b0af..c89492ff0 100644 --- a/test/utils/DiffUtilTest.java +++ b/test/utils/DiffUtilTest.java @@ -22,6 +22,16 @@ public class DiffUtilTest { + "...... 
    \n" + "..."; + String DIFF_NEW_LINE = "\n"; + + String DIFF_DELETE_PLAIN_PREFIX = "--- "; + + String DIFF_INSERT_PLAIN_PREFIX = "+++ "; + + String DIFF_EQUAL_PLAIN_PREFIX = "......\n" + + "......\n" + + "...\n"; + @Test public void getDiffText_oldValueIsNull_returnString() { @@ -103,4 +113,85 @@ public void getDiffText_deleteAndInsertValue_returnString() { assertThat(result).isEqualTo(expectedResult); } + @Test + public void getDiffPlainText_oldValueIsNull_returnString() { + + // GIVEN + String oldValue = null; + String newValue = "new value"; + String expectedResult = DIFF_INSERT_PLAIN_PREFIX + newValue + DIFF_NEW_LINE; + + // WHEN + String result = DiffUtil.getDiffPlainText(oldValue, newValue); + + // THEN + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void getDiffPlainText_newValueIsNull_returnString() { + + // GIVEN + String oldValue = "oldValue"; + String newValue = null; + String expectedResult = DIFF_DELETE_PLAIN_PREFIX + oldValue + DIFF_NEW_LINE; + + // WHEN + String result = DiffUtil.getDiffPlainText(oldValue, newValue); + + // THEN + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void getDiffPlainText_equalValuesMoreThanSize100_returnString() { + + // GIVEN + String oldValue = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" + + "123456789012345678901234567890"; + String newValue = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" + + "123456789012345678901234567890"; + int textLength = oldValue.length(); + String expectedResult = oldValue.substring(0, 50) + DIFF_EQUAL_PLAIN_PREFIX + + oldValue.substring(textLength - 50) + DIFF_NEW_LINE; + + // WHEN + String result = DiffUtil.getDiffPlainText(oldValue, newValue); + + // THEN + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void getDiffPlainText_equalValuesLessThanSize100_returnString() { + + // GIVEN + String oldValue = "12345678901234567890"; + String newValue = "12345678901234567890"; + String expectedResult = oldValue + DIFF_NEW_LINE; + + // WHEN + String result = DiffUtil.getDiffPlainText(oldValue, newValue); + + // THEN + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void getDiffPlainText_deleteAndInsertValue_returnString() { + + // GIVEN + String oldValue = "Hi, there?"; + String newValue = "Hello, mijeong?"; + String expectedResult = oldValue.substring(0, 1) + DIFF_NEW_LINE + + DIFF_DELETE_PLAIN_PREFIX + oldValue.substring(1, oldValue.length() - 1) + DIFF_NEW_LINE + + DIFF_INSERT_PLAIN_PREFIX + newValue.substring(1, newValue.length() - 1) + DIFF_NEW_LINE + + oldValue.substring(oldValue.length() - 1) + DIFF_NEW_LINE; + + // WHEN + String result = DiffUtil.getDiffPlainText(oldValue, newValue); + + // THEN + assertThat(result).isEqualTo(expectedResult); + } } From 85c7719f918568ee1704432b952ff8239e535375 Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Mon, 29 Jan 2018 16:42:10 +0900 Subject: [PATCH 19/77] refactoring: Modify String class to StringBuilder class --- app/utils/DiffUtil.java | 66 +++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/app/utils/DiffUtil.java b/app/utils/DiffUtil.java index d4df1cd6e..c9c7e8442 100644 --- a/app/utils/DiffUtil.java +++ b/app/utils/DiffUtil.java @@ -25,7 +25,7 @@ public static String getDiffText(String oldValue, String newValue) { diff_match_patch dmp = new diff_match_patch(); dmp.Diff_EditCost = DIFF_EDITCOST; - String diffString = ""; + StringBuilder sb = new StringBuilder(); LinkedList diffs = dmp.diff_main(oldValue, newValue); dmp.diff_cleanupEfficiency(diffs); @@ -33,36 +33,34 @@ public static String getDiffText(String oldValue, String newValue) { for (Diff diff: diffs) { switch (diff.operation) { case DELETE: - diffString += ""; - diffString += StringEscapeUtils.escapeHtml4(diff.text); - diffString += ""; + sb.append("") + .append(StringEscapeUtils.escapeHtml4(diff.text)) + .append(""); break; case EQUAL: int textLength = diff.text.length(); if (textLength > EQUAL_TEXT_ELLIPSIS_SIZE) { - diffString += StringEscapeUtils.escapeHtml4(diff.text.substring(0, EQUAL_TEXT_BASE_SIZE)); - - diffString += "...\n"; - diffString += "......\n"; - diffString += "......\n"; - diffString += "..."; - - diffString += StringEscapeUtils.escapeHtml4(diff.text.substring(textLength - EQUAL_TEXT_BASE_SIZE)); + sb.append(StringEscapeUtils.escapeHtml4(diff.text.substring(0, EQUAL_TEXT_BASE_SIZE))) + .append("...\n") + .append("......\n") + .append("......\n") + .append("...") + .append(StringEscapeUtils.escapeHtml4(diff.text.substring(textLength - EQUAL_TEXT_BASE_SIZE))); } else { - diffString += StringEscapeUtils.escapeHtml4(diff.text); + sb.append(StringEscapeUtils.escapeHtml4(diff.text)); } break; case INSERT: - diffString += ""; - diffString += StringEscapeUtils.escapeHtml4(diff.text); - diffString += ""; + sb.append("") + .append(StringEscapeUtils.escapeHtml4(diff.text)) + .append(""); break; default: break; } } - return diffString.replaceAll("\n", " 
    \n"); + return sb.toString().replaceAll("\n", " 
    \n"); } public static String getDiffPlainText(String oldValue, String newValue) { @@ -72,7 +70,7 @@ public static String getDiffPlainText(String oldValue, String newValue) { diff_match_patch dmp = new diff_match_patch(); dmp.Diff_EditCost = DIFF_EDITCOST; - String diffString = ""; + StringBuilder sb = new StringBuilder(); LinkedList diffs = dmp.diff_main(oldValue, newValue); dmp.diff_cleanupEfficiency(diffs); @@ -80,36 +78,34 @@ public static String getDiffPlainText(String oldValue, String newValue) { for (Diff diff: diffs) { switch (diff.operation) { case DELETE: - diffString += "--- "; - diffString += StringEscapeUtils.escapeHtml4(diff.text); - diffString += "\n"; + sb.append("--- ") + .append(StringEscapeUtils.escapeHtml4(diff.text)) + .append("\n"); break; case EQUAL: int textLength = diff.text.length(); if (textLength > EQUAL_TEXT_ELLIPSIS_SIZE) { - diffString += StringEscapeUtils.escapeHtml4(diff.text.substring(0, EQUAL_TEXT_BASE_SIZE)); - - diffString += "......\n"; - diffString += "......\n"; - diffString += "...\n"; - - diffString += StringEscapeUtils.escapeHtml4(diff.text.substring(textLength - EQUAL_TEXT_BASE_SIZE)); - diffString += "\n"; + sb.append(StringEscapeUtils.escapeHtml4(diff.text.substring(0, EQUAL_TEXT_BASE_SIZE))) + .append("......\n") + .append("......\n") + .append("...\n") + .append(StringEscapeUtils.escapeHtml4(diff.text.substring(textLength - EQUAL_TEXT_BASE_SIZE))) + .append("\n"); } else { - diffString += StringEscapeUtils.escapeHtml4(diff.text); - diffString += "\n"; + sb.append(StringEscapeUtils.escapeHtml4(diff.text)) + .append("\n"); } break; case INSERT: - diffString += "+++ "; - diffString += StringEscapeUtils.escapeHtml4(diff.text); - diffString += "\n"; + sb.append("+++ ") + .append(StringEscapeUtils.escapeHtml4(diff.text)) + .append("\n"); break; default: break; } } - return diffString; + return sb.toString(); } } From 8b9cacb8eccc1383f1d95224ebac21f8cbcda837 Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Mon, 29 Jan 2018 16:44:32 +0900 Subject: [PATCH 20/77] message: Add NAVER LABS Corp to comment --- app/utils/DiffUtil.java | 2 +- test/utils/DiffUtilTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/utils/DiffUtil.java b/app/utils/DiffUtil.java index c9c7e8442..6bf635ca3 100644 --- a/app/utils/DiffUtil.java +++ b/app/utils/DiffUtil.java @@ -1,7 +1,7 @@ /** * Yona, 21st Century Project Hosting SW *

    - * Copyright Yona & Yobi Authors & NAVER Corp. + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. * https://yona.io **/ package utils; diff --git a/test/utils/DiffUtilTest.java b/test/utils/DiffUtilTest.java index c89492ff0..9733573eb 100644 --- a/test/utils/DiffUtilTest.java +++ b/test/utils/DiffUtilTest.java @@ -1,7 +1,7 @@ /** * Yona, 21st Century Project Hosting SW *

    - * Copyright Yona & Yobi Authors & NAVER Corp. + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. * https://yona.io **/ package utils; From de6defd39852009b8ec9b741a5082a40346ce4be Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Mon, 29 Jan 2018 17:08:19 +0900 Subject: [PATCH 21/77] noti: Add logic for sending email noti with HTML format in case of ISSUE_BODY_CHANGED --- app/models/NotificationEvent.java | 6 +- test/models/NotificationEventTest.java | 89 ++++++++++++++++---------- 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/app/models/NotificationEvent.java b/app/models/NotificationEvent.java index cc00b419a..92dac77a3 100644 --- a/app/models/NotificationEvent.java +++ b/app/models/NotificationEvent.java @@ -1,7 +1,7 @@ /** * Yona, 21st Century Project Hosting SW *

    - * Copyright Yona & Yobi Authors & NAVER Corp. + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. * https://yona.io **/ package models; @@ -29,6 +29,7 @@ import playRepository.*; import scala.concurrent.duration.Duration; import utils.AccessControl; +import utils.DiffUtil; import utils.EventConstants; import utils.RouteUtil; @@ -133,9 +134,10 @@ public String getMessage(Lang lang) { case NEW_COMMENT: case NEW_PULL_REQUEST: case NEW_COMMIT: - case ISSUE_BODY_CHANGED: case COMMENT_UPDATED: return newValue; + case ISSUE_BODY_CHANGED: + return DiffUtil.getDiffText(oldValue, newValue); case NEW_REVIEW_COMMENT: try { ReviewComment reviewComment = ReviewComment.find.byId(Long.valueOf(this.resourceId)); diff --git a/test/models/NotificationEventTest.java b/test/models/NotificationEventTest.java index 19eeb7331..b62637171 100644 --- a/test/models/NotificationEventTest.java +++ b/test/models/NotificationEventTest.java @@ -1,28 +1,16 @@ /** - * Yobi, Project Hosting SW - * - * Copyright 2013 NAVER Corp. - * http://yobi.io - * - * @Author Yi EungJun - * - * 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. - */ + * Yona, 21st Century Project Hosting SW + *

    + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. + * https://yona.io + **/ package models; import models.enumeration.EventType; import models.enumeration.ResourceType; +import org.junit.Ignore; import org.junit.Test; +import play.i18n.Lang; import java.util.HashSet; @@ -32,10 +20,18 @@ public class NotificationEventTest extends ModelTest { + private NotificationEvent getNotificationEvent(ResourceType resourceType) { + NotificationEvent event = new NotificationEvent(); + event.resourceType = resourceType; + event.resourceId = "1"; + return event; + } + + @Ignore("Test is ignored as old test with repository dependency") @Test public void add() { // Given - NotificationEvent event = getNotificationEvent(); + NotificationEvent event = getNotificationEvent(ResourceType.ISSUE_POST); // When NotificationEvent.add(event); @@ -44,20 +40,11 @@ public void add() { assertThat(NotificationMail.find.byId(event.notificationMail.id)).isNotNull(); } - private NotificationEvent getNotificationEvent() { - NotificationEvent event = new NotificationEvent(); - event.resourceType = ResourceType.ISSUE_POST; - event.resourceId = "1"; - HashSet users = new HashSet<>(); - users.add(User.findByLoginId("yobi")); - event.receivers = users; - return event; - } - + @Ignore("Test is ignored as old test with repository dependency") @Test public void addTwoTimes() { // Given - NotificationEvent event = getNotificationEvent(); + NotificationEvent event = getNotificationEvent(ResourceType.ISSUE_POST); NotificationEvent.add(event); int numOfMails = NotificationMail.find.all().size(); @@ -68,10 +55,11 @@ public void addTwoTimes() { assertThat(NotificationEvent.find.all().size()).isEqualTo(numOfMails); } + @Ignore("Test is ignored as old test with repository dependency") @Test public void delete() { // Given - NotificationEvent event = getNotificationEvent(); + NotificationEvent event = getNotificationEvent(ResourceType.ISSUE_POST); NotificationEvent.add(event); // When @@ -81,6 +69,7 @@ public void delete() { assertThat(NotificationMail.find.byId(event.notificationMail.id)).isNull(); } + @Ignore("Test is ignored as old test with repository dependency") @Test public void add_with_filter() { // Given @@ -94,7 +83,7 @@ public void add_with_filter() { User off = getTestUser(3L); UserProjectNotification.unwatchExplictly(off, project, EventType.ISSUE_ASSIGNEE_CHANGED); - NotificationEvent event = getNotificationEvent(); + NotificationEvent event = getNotificationEvent(ResourceType.ISSUE_POST); event.eventType = EventType.ISSUE_ASSIGNEE_CHANGED; event.receivers.add(watching_project_off); event.receivers.add(off); @@ -106,6 +95,7 @@ public void add_with_filter() { assertThat(event.receivers).containsOnly(off); } + @Ignore("Test is ignored as old test with repository dependency") @Test public void getNewMentionedUsers1() { // Given @@ -122,6 +112,7 @@ public void getNewMentionedUsers1() { assertThat(newMentionedUsers.contains(newMentionedUser)).isTrue(); } + @Ignore("Test is ignored as old test with repository dependency") @Test public void getNewMentionedUsers2() { // Given @@ -138,4 +129,36 @@ public void getNewMentionedUsers2() { assertThat(newMentionedUsers.contains(newMentionedUser)).isTrue(); } + @Test + public void getMessage_eventTypeIsIssueBodyChangedWithParameter_returnString() { + + // Given + NotificationEvent notificationEvent = getNotificationEvent(ResourceType.ISSUE_POST); + notificationEvent.eventType = EventType.ISSUE_BODY_CHANGED; + notificationEvent.oldValue = "old value"; + notificationEvent.newValue = "new value"; + + // When + String result = notificationEvent.getMessage(); + + // Then + assertThat(result.length() > 0).isTrue(); + } + + @Test + public void getMessage_eventTypeIsIssueBodyChangedWithNoParameter_returnString() { + + // Given + NotificationEvent notificationEvent = getNotificationEvent(ResourceType.ISSUE_POST); + notificationEvent.eventType = EventType.ISSUE_BODY_CHANGED; + notificationEvent.oldValue = "old value"; + notificationEvent.newValue = "new value"; + + // When + String result = notificationEvent.getMessage(Lang.defaultLang()); + + // Then + assertThat(result.length() > 0).isTrue(); + } + } From b07b52f7372b5c66bf4d868a8e62b760415cf490 Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Mon, 29 Jan 2018 17:35:30 +0900 Subject: [PATCH 22/77] typo: Modify test method name --- test/models/NotificationEventTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/models/NotificationEventTest.java b/test/models/NotificationEventTest.java index b62637171..30d8fe454 100644 --- a/test/models/NotificationEventTest.java +++ b/test/models/NotificationEventTest.java @@ -130,7 +130,7 @@ public void getNewMentionedUsers2() { } @Test - public void getMessage_eventTypeIsIssueBodyChangedWithParameter_returnString() { + public void getMessage_eventTypeIsIssueBodyChangedWithNoParameter_returnString() { // Given NotificationEvent notificationEvent = getNotificationEvent(ResourceType.ISSUE_POST); @@ -146,7 +146,7 @@ public void getMessage_eventTypeIsIssueBodyChangedWithParameter_returnString() { } @Test - public void getMessage_eventTypeIsIssueBodyChangedWithNoParameter_returnString() { + public void getMessage_eventTypeIsIssueBodyChangedWithParameter_returnString() { // Given NotificationEvent notificationEvent = getNotificationEvent(ResourceType.ISSUE_POST); From 7b05d49a09b10acaa6972d231a24bfbfe1a4c4c2 Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Mon, 29 Jan 2018 17:36:54 +0900 Subject: [PATCH 23/77] noti: Add function for sending email noti with Plain format in case of ISSUE_BODY_CHANGED --- app/models/NotificationEvent.java | 16 +++++++++++++ test/models/NotificationEventTest.java | 32 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/app/models/NotificationEvent.java b/app/models/NotificationEvent.java index 92dac77a3..25ed22116 100644 --- a/app/models/NotificationEvent.java +++ b/app/models/NotificationEvent.java @@ -36,6 +36,7 @@ import javax.naming.LimitExceededException; import javax.persistence.*; import javax.servlet.ServletException; +import java.beans.Transient; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; @@ -196,6 +197,21 @@ public String getMessage(Lang lang) { } } + @Transient + public String getPlainMessage() { + return getPlainMessage(Lang.defaultLang()); + } + + @Transient + public String getPlainMessage(Lang lang) { + switch(eventType) { + case ISSUE_BODY_CHANGED: + return DiffUtil.getDiffPlainText(oldValue, newValue); + default: + return null; + } + } + /** * Builds a notification message for a comment on code. * diff --git a/test/models/NotificationEventTest.java b/test/models/NotificationEventTest.java index 30d8fe454..cdfc9c967 100644 --- a/test/models/NotificationEventTest.java +++ b/test/models/NotificationEventTest.java @@ -161,4 +161,36 @@ public void getMessage_eventTypeIsIssueBodyChangedWithParameter_returnString() { assertThat(result.length() > 0).isTrue(); } + @Test + public void getPlainMessage_eventTypeIsIssueBodyChangedWithNoParameter_returnString() { + + // Given + NotificationEvent notificationEvent = getNotificationEvent(ResourceType.ISSUE_POST); + notificationEvent.eventType = EventType.ISSUE_BODY_CHANGED; + notificationEvent.oldValue = "old value"; + notificationEvent.newValue = "new value"; + + // When + String result = notificationEvent.getPlainMessage(); + + // Then + assertThat(result.length() > 0).isTrue(); + } + + @Test + public void getPlainMessage_eventTypeIsIssueBodyChangedWithParameter_returnString() { + + // Given + NotificationEvent notificationEvent = getNotificationEvent(ResourceType.ISSUE_POST); + notificationEvent.eventType = EventType.ISSUE_BODY_CHANGED; + notificationEvent.oldValue = "old value"; + notificationEvent.newValue = "new value"; + + // When + String result = notificationEvent.getPlainMessage(Lang.defaultLang()); + + // Then + assertThat(result.length() > 0).isTrue(); + } + } From c974509a6e8bf0e1fe24695ba77b7793a4b1c5bd Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Wed, 31 Jan 2018 00:04:29 +0900 Subject: [PATCH 24/77] refactoring: Modify html tag for new line --- app/utils/DiffUtil.java | 2 +- test/utils/DiffUtilTest.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/utils/DiffUtil.java b/app/utils/DiffUtil.java index 6bf635ca3..8afbaf8b9 100644 --- a/app/utils/DiffUtil.java +++ b/app/utils/DiffUtil.java @@ -60,7 +60,7 @@ public static String getDiffText(String oldValue, String newValue) { } } - return sb.toString().replaceAll("\n", " 
    \n"); + return sb.toString().replaceAll("\n", "
    \n"); } public static String getDiffPlainText(String oldValue, String newValue) { diff --git a/test/utils/DiffUtilTest.java b/test/utils/DiffUtilTest.java index 9733573eb..92d98b9eb 100644 --- a/test/utils/DiffUtilTest.java +++ b/test/utils/DiffUtilTest.java @@ -17,9 +17,9 @@ public class DiffUtilTest { String DIFF_INSERT_PREFIX = ""; String DIFF_INSERT_POSTFIX = ""; - String DIFF_EQUAL_PREFIX = "... 
    \n" - + "...... 
    \n" - + "...... 
    \n" + String DIFF_EQUAL_PREFIX = "...
    \n" + + "......
    \n" + + "......
    \n" + "...
    "; String DIFF_NEW_LINE = "\n"; From 46f53807af9ef66cf8fe8f400f14dd1ccbacfec0 Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Wed, 31 Jan 2018 00:07:34 +0900 Subject: [PATCH 25/77] noti: Add method for getting noti message with Plain format --- app/notification/INotificationEvent.java | 26 ++++----------- app/notification/MergedNotificationEvent.java | 33 ++++++++----------- 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/app/notification/INotificationEvent.java b/app/notification/INotificationEvent.java index 718ea07b0..17585984c 100644 --- a/app/notification/INotificationEvent.java +++ b/app/notification/INotificationEvent.java @@ -1,23 +1,9 @@ /** - * Yobi, Project Hosting SW - * - * Copyright 2015 NAVER Corp. - * http://yobi.io - * - * @author Yi EungJun - * - * 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. - */ + * Yona, 21st Century Project Hosting SW + *

    + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. + * https://yona.io + **/ package notification; import models.User; @@ -36,6 +22,8 @@ public interface INotificationEvent { String getMessage(Lang lang); + String getPlainMessage(Lang lang); + String getUrlToView(); Date getCreatedDate(); diff --git a/app/notification/MergedNotificationEvent.java b/app/notification/MergedNotificationEvent.java index 5ab7958e0..88e310879 100644 --- a/app/notification/MergedNotificationEvent.java +++ b/app/notification/MergedNotificationEvent.java @@ -1,23 +1,9 @@ /** - * Yobi, Project Hosting SW - * - * Copyright 2015 NAVER Corp. - * http://yobi.io - * - * @author Yi EungJun - * - * 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. - */ + * Yona, 21st Century Project Hosting SW + *

    + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. + * https://yona.io + **/ package notification; import com.google.common.base.Joiner; @@ -64,6 +50,15 @@ public String getMessage(Lang lang) { return Joiner.on("\n\n---\n\n").join(messages); } + @Override + public String getPlainMessage(Lang lang) { + List messages = new ArrayList<>(); + for(INotificationEvent event : messageSources) { + messages.add(event.getPlainMessage(lang)); + } + return Joiner.on("\n\n---\n\n").join(messages); + } + @Override public String getUrlToView() { return main.getUrlToView(); From c9fed999785efad17249f0b19da0ea5a6ef6fcda Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Wed, 31 Jan 2018 00:10:02 +0900 Subject: [PATCH 26/77] noti: Modify logic for sending email noti to use diff format --- app/models/NotificationMail.java | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/app/models/NotificationMail.java b/app/models/NotificationMail.java index a0b21645a..da4ef1332 100644 --- a/app/models/NotificationMail.java +++ b/app/models/NotificationMail.java @@ -1,7 +1,7 @@ /** * Yona, 21st Century Project Hosting SW *

    - * Copyright Yona & Yobi Authors & NAVER Corp. + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. * https://yona.io **/ package models; @@ -532,9 +532,10 @@ private static void sendMail(INotificationEvent event, Set toList Lang lang = Lang.apply(langCode); - String message = event.getMessage(lang); + String htmlMessage = event.getMessage(lang); + String plainMessage = event.getPlainMessage(lang); - if (message == null) { + if (htmlMessage == null || plainMessage == null) { return; } @@ -546,8 +547,9 @@ private static void sendMail(INotificationEvent event, Set toList IssueComment issueComment = IssueComment.find.byId(Long.valueOf(resource.getId())); resource = issueComment.issue.asResource(); } - email.setHtmlMsg(removeHeadAnchor(getHtmlMessage(lang, message, urlToView, resource, acceptsReply))); - email.setTextMsg(getPlainMessage(lang, message, Url.create(urlToView), acceptsReply)); + + email.setHtmlMsg(removeHeadAnchor(getHtmlMessage(lang, htmlMessage, urlToView, resource, acceptsReply))); + email.setTextMsg(getPlainMessage(lang, plainMessage, Url.create(urlToView), acceptsReply)); email.addReferences(); email.setSentDate(event.getCreatedDate()); @@ -609,15 +611,9 @@ public static String getReplyTo(Resource resource) { private static String getHtmlMessage(Lang lang, String message, String urlToView, Resource resource, boolean acceptsReply) { - String renderred = null; - if( resource != null) { - renderred = Markdown.render(message, resource.getProject(), lang.code()); - } else { - renderred = Markdown.render(message); - } String content = views.html.common.notificationMail.render( - lang, renderred, urlToView, resource, acceptsReply).toString(); + lang, message, urlToView, resource, acceptsReply).toString(); Document doc = Jsoup.parse(content); @@ -687,13 +683,7 @@ private static void handleImages(Document doc){ private static String getPlainMessage(Lang lang, String message, String urlToView, boolean acceptsReply) { String msg = message; - String url = urlToView; - String messageKey = acceptsReply ? - "notification.replyOrLinkToView" : "notification.linkToView"; - - if (url != null) { - msg += String.format("\n\n--\n" + HttpUtil.decodeUrlString(Messages.get(lang, messageKey, url))); - } + msg += "\n\n Yona 에서 자세히 보거나 혹은 이 메일에 직접 회신하실 수도 있습니다."; return msg; } From 70318a8df548e97e8405d6a4a426244d8fa30555 Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Fri, 2 Feb 2018 14:23:07 +0900 Subject: [PATCH 27/77] refactoring: Separate logic as private function from public function --- app/utils/DiffUtil.java | 85 ++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/app/utils/DiffUtil.java b/app/utils/DiffUtil.java index 8afbaf8b9..0d7c7fe98 100644 --- a/app/utils/DiffUtil.java +++ b/app/utils/DiffUtil.java @@ -33,27 +33,24 @@ public static String getDiffText(String oldValue, String newValue) { for (Diff diff: diffs) { switch (diff.operation) { case DELETE: - sb.append("") - .append(StringEscapeUtils.escapeHtml4(diff.text)) - .append(""); + String deleteStyle = ""; + sb.append(addDiffStyle(diff, deleteStyle)); break; case EQUAL: int textLength = diff.text.length(); + if (textLength > EQUAL_TEXT_ELLIPSIS_SIZE) { - sb.append(StringEscapeUtils.escapeHtml4(diff.text.substring(0, EQUAL_TEXT_BASE_SIZE))) - .append("...\n") - .append("......\n") - .append("......\n") - .append("...") - .append(StringEscapeUtils.escapeHtml4(diff.text.substring(textLength - EQUAL_TEXT_BASE_SIZE))); + sb.append(addHeadOfDiff(diff)); + sb.append(addEllipsis()); + sb.append(addTailOfDiff(diff)); } else { - sb.append(StringEscapeUtils.escapeHtml4(diff.text)); + sb.append(addAllDiff(diff)); } + break; case INSERT: - sb.append("") - .append(StringEscapeUtils.escapeHtml4(diff.text)) - .append(""); + String insertStyle = ""; + sb.append(addDiffStyle(diff, insertStyle)); break; default: break; @@ -78,28 +75,25 @@ public static String getDiffPlainText(String oldValue, String newValue) { for (Diff diff: diffs) { switch (diff.operation) { case DELETE: - sb.append("--- ") - .append(StringEscapeUtils.escapeHtml4(diff.text)) - .append("\n"); + String deleteText = "--- "; + sb.append(addDiffText(diff, deleteText)); break; case EQUAL: int textLength = diff.text.length(); + if (textLength > EQUAL_TEXT_ELLIPSIS_SIZE) { - sb.append(StringEscapeUtils.escapeHtml4(diff.text.substring(0, EQUAL_TEXT_BASE_SIZE))) - .append("......\n") - .append("......\n") - .append("...\n") - .append(StringEscapeUtils.escapeHtml4(diff.text.substring(textLength - EQUAL_TEXT_BASE_SIZE))) - .append("\n"); + sb.append(addHeadOfDiff(diff)) + .append(addEllipsisText()) + .append(addTailOfDiff(diff)); } else { - sb.append(StringEscapeUtils.escapeHtml4(diff.text)) - .append("\n"); + sb.append(addAllDiff(diff)); } + + sb.append("\n"); break; case INSERT: - sb.append("+++ ") - .append(StringEscapeUtils.escapeHtml4(diff.text)) - .append("\n"); + String insertText = "+++ "; + sb.append(addDiffText(diff, insertText)); break; default: break; @@ -108,4 +102,41 @@ public static String getDiffPlainText(String oldValue, String newValue) { return sb.toString(); } + + private static String addHeadOfDiff(Diff diff) { + return StringEscapeUtils.escapeHtml4(diff.text.substring(0, EQUAL_TEXT_BASE_SIZE)); + } + + private static String addTailOfDiff(Diff diff) { + return StringEscapeUtils.escapeHtml4(diff.text.substring(diff.text.length() - EQUAL_TEXT_BASE_SIZE)); + } + + private static String addAllDiff(Diff diff) { + return StringEscapeUtils.escapeHtml4(diff.text); + } + + private static String addEllipsis() { + return "...\n" + + "......\n" + + "......\n" + + "..."; + } + + private static String addDiffStyle(Diff diff, String style) { + return style + + StringEscapeUtils.escapeHtml4(diff.text) + + ""; + } + + private static String addDiffText(Diff diff, String text) { + return text + + StringEscapeUtils.escapeHtml4(diff.text) + + "\n"; + } + + private static String addEllipsisText() { + return "......\n" + + "......\n" + + "...\n"; + } } From fc69f0410341bc7e06497397500f208142e1edbc Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Fri, 2 Feb 2018 14:43:23 +0900 Subject: [PATCH 28/77] fix: Add logic for rendering markdown again --- app/models/NotificationMail.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/models/NotificationMail.java b/app/models/NotificationMail.java index da4ef1332..e7351ad30 100644 --- a/app/models/NotificationMail.java +++ b/app/models/NotificationMail.java @@ -611,9 +611,16 @@ public static String getReplyTo(Resource resource) { private static String getHtmlMessage(Lang lang, String message, String urlToView, Resource resource, boolean acceptsReply) { + String renderred = null; + + if(resource != null) { + renderred = Markdown.render(message, resource.getProject(), lang.code()); + } else { + renderred = Markdown.render(message); + } String content = views.html.common.notificationMail.render( - lang, message, urlToView, resource, acceptsReply).toString(); + lang, renderred, urlToView, resource, acceptsReply).toString(); Document doc = Jsoup.parse(content); From c5b809ae2fdcb83f12e78cd798b4ca1e9dbc1f63 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Sat, 27 Jan 2018 18:04:39 +0900 Subject: [PATCH 29/77] LDAP: Add more fallback working case It adds LDAP server network timeout case --- app/controllers/UserApp.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/controllers/UserApp.java b/app/controllers/UserApp.java index b90607f37..edb082dd3 100644 --- a/app/controllers/UserApp.java +++ b/app/controllers/UserApp.java @@ -1176,6 +1176,10 @@ public static User authenticateWithLdap(String loginIdOrEmail, String password) } catch (CommunicationException e) { play.Logger.error("Cannot connect to ldap server \n" + e.getMessage()); e.printStackTrace(); + if(FALLBACK_TO_LOCAL_LOGIN){ + play.Logger.warn("fallback to local login: " + loginIdOrEmail); + return authenticateWithPlainPassword(loginIdOrEmail, password); + } return User.anonymous; } catch (AuthenticationException e) { flash(Constants.WARNING, Messages.get("user.login.invalid")); From 609e2111414e840159923f24eecfe6244b28bef6 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Mon, 29 Jan 2018 00:02:41 +0900 Subject: [PATCH 30/77] issue: Issue sharing feature - Add model --- app/models/Issue.java | 16 +++++++++++ app/models/IssueSharer.java | 49 ++++++++++++++++++++++++++++++++++ conf/evolutions/default/19.sql | 19 +++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 app/models/IssueSharer.java create mode 100644 conf/evolutions/default/19.sql diff --git a/app/models/Issue.java b/app/models/Issue.java index 82cd8ae79..ed0a365d1 100644 --- a/app/models/Issue.java +++ b/app/models/Issue.java @@ -75,6 +75,9 @@ public class Issue extends AbstractPosting implements LabelOwner { @OneToMany(cascade = CascadeType.ALL, mappedBy="issue") public List events; + @OneToMany(cascade = CascadeType.ALL, mappedBy = "issue") + public Set sharers = new LinkedHashSet<>(); + @ManyToMany(cascade = CascadeType.ALL) @JoinTable( name = "issue_voter", @@ -675,4 +678,17 @@ public static int countOpenIssuesByUser(User user) { .eq("state", State.OPEN) .findRowCount(); } + + public IssueSharer findSharerByUserId(Long id){ + for (IssueSharer sharer : sharers) { + if (sharer.user.id.equals(id)) { + return sharer; + } + } + return null; + } + + public List getSortedSharer() { + return new ArrayList<>(sharers); + } } diff --git a/app/models/IssueSharer.java b/app/models/IssueSharer.java new file mode 100644 index 000000000..7aa4cd272 --- /dev/null +++ b/app/models/IssueSharer.java @@ -0,0 +1,49 @@ +/** + * Yona, 21st Century Project Hosting SW + *

    + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. + * https://yona.io + **/ + +package models; + +import play.db.ebean.Model; + +import javax.persistence.*; +import java.util.Date; + +@Entity +public class IssueSharer extends Model { + private static final long serialVersionUID = 6199025373911652405L; + + @Id + public Long id; + + public Date created; + + public String loginId; + + @OneToOne + public User user; + + @OneToOne + public Issue issue; + + public static final Finder find = new Finder<>(Long.class, + IssueSharer.class); + + public static IssueSharer createSharer(String loginId, Issue issue) { + IssueSharer issueSharer = new IssueSharer(); + issueSharer.loginId = loginId; + issueSharer.created = new Date(); + issueSharer.issue = issue; + issueSharer.user = User.findByLoginId(loginId); + + if (issueSharer.user == null) { + String errorMsg = "Wrong loginId for issue sharing: " + loginId; + play.Logger.error(errorMsg); + throw new IllegalArgumentException(errorMsg); + } + return issueSharer; + } +} diff --git a/conf/evolutions/default/19.sql b/conf/evolutions/default/19.sql new file mode 100644 index 000000000..894bcb983 --- /dev/null +++ b/conf/evolutions/default/19.sql @@ -0,0 +1,19 @@ +# --- !Ups +CREATE TABLE issue_sharer ( + id BIGINT AUTO_INCREMENT NOT NULL, + created DATE, + login_id VARCHAR(255), + user_id BIGINT, + issue_id BIGINT, + CONSTRAINT pk_issue_sharer PRIMARY KEY (id), + CONSTRAINT fk_issue_sharer_user FOREIGN KEY (user_id) REFERENCES n4user (id) on DELETE CASCADE, + CONSTRAINT fk_issue_sharer_issue FOREIGN KEY (issue_id) REFERENCES issue (id) on DELETE CASCADE +) +row_format=compressed, key_block_size=8; + +CREATE index ix_issue_sharer_login_id ON issue_sharer (login_id); +CREATE index ix_issue_sharer_user_id ON issue_sharer (user_id); +CREATE index ix_issue_sharer_issue_id ON issue_sharer (issue_id); + +# --- !Downs +DROP TABLE issue_sharer; From bcc7bfd078e184c01952f2dd44e3e46a2fb611c2 Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Fri, 2 Feb 2018 17:57:40 +0900 Subject: [PATCH 31/77] refactoring: Remove reassigning value to argument --- app/utils/DiffUtil.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/utils/DiffUtil.java b/app/utils/DiffUtil.java index 0d7c7fe98..e613787bc 100644 --- a/app/utils/DiffUtil.java +++ b/app/utils/DiffUtil.java @@ -20,14 +20,14 @@ public class DiffUtil { public static String getDiffText(String oldValue, String newValue) { - oldValue = Optional.ofNullable(oldValue).orElse(""); - newValue = Optional.ofNullable(newValue).orElse(""); + String oldVal = Optional.ofNullable(oldValue).orElse(""); + String newVal = Optional.ofNullable(newValue).orElse(""); diff_match_patch dmp = new diff_match_patch(); dmp.Diff_EditCost = DIFF_EDITCOST; StringBuilder sb = new StringBuilder(); - LinkedList diffs = dmp.diff_main(oldValue, newValue); + LinkedList diffs = dmp.diff_main(oldVal, newVal); dmp.diff_cleanupEfficiency(diffs); for (Diff diff: diffs) { @@ -62,14 +62,14 @@ public static String getDiffText(String oldValue, String newValue) { public static String getDiffPlainText(String oldValue, String newValue) { - oldValue = Optional.ofNullable(oldValue).orElse(""); - newValue = Optional.ofNullable(newValue).orElse(""); + String oldVal = Optional.ofNullable(oldValue).orElse(""); + String newVal = Optional.ofNullable(newValue).orElse(""); diff_match_patch dmp = new diff_match_patch(); dmp.Diff_EditCost = DIFF_EDITCOST; StringBuilder sb = new StringBuilder(); - LinkedList diffs = dmp.diff_main(oldValue, newValue); + LinkedList diffs = dmp.diff_main(oldVal, newVal); dmp.diff_cleanupEfficiency(diffs); for (Diff diff: diffs) { From 420452788d31de3fa92be454121265da5eb0a49a Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Mon, 5 Feb 2018 14:59:26 +0900 Subject: [PATCH 32/77] fix: Remove logic for rendering markdown (duplicated HTML tags) --- app/models/NotificationMail.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/models/NotificationMail.java b/app/models/NotificationMail.java index e7351ad30..da4ef1332 100644 --- a/app/models/NotificationMail.java +++ b/app/models/NotificationMail.java @@ -611,16 +611,9 @@ public static String getReplyTo(Resource resource) { private static String getHtmlMessage(Lang lang, String message, String urlToView, Resource resource, boolean acceptsReply) { - String renderred = null; - - if(resource != null) { - renderred = Markdown.render(message, resource.getProject(), lang.code()); - } else { - renderred = Markdown.render(message); - } String content = views.html.common.notificationMail.render( - lang, renderred, urlToView, resource, acceptsReply).toString(); + lang, message, urlToView, resource, acceptsReply).toString(); Document doc = Jsoup.parse(content); From d8143b55c76f63910da0660187cabc946604d50d Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Mon, 29 Jan 2018 00:04:56 +0900 Subject: [PATCH 33/77] issue: Issue sharing feature - Build api --- app/controllers/api/IssueApi.java | 126 +++++++++++++++++++++++++++++- app/utils/AccessControl.java | 17 +++- conf/routes | 3 + 3 files changed, 144 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/IssueApi.java b/app/controllers/api/IssueApi.java index 4d8c4c259..1c87819cf 100644 --- a/app/controllers/api/IssueApi.java +++ b/app/controllers/api/IssueApi.java @@ -385,7 +385,7 @@ private static void addMyself(Issue issue, List users) { } } - private static void addUserToUsers(User user, List users) { + static void addUserToUsers(User user, List users) { ObjectNode userNode = Json.newObject(); userNode.put("loginId", user.loginId); userNode.put("name", user.getDisplayName()); @@ -535,4 +535,128 @@ private static F.Promise getTranslation(String text, Project project) { return ok(node); }); } + + @AnonymousCheck + public static Result findSharerByloginIds(String ownerName, String projectName, Long number, + String commaSeperatedIds) { + if (!request().accepts("application/json")) { + return status(Http.Status.NOT_ACCEPTABLE); + } + Project project = Project.findByOwnerAndProjectName(ownerName, projectName); + Issue issue = Issue.findByNumber(project, number); + + List list = getExpressionListByExtractingLoginIds(issue, commaSeperatedIds).findList(); + sortListByAddedDate(list); + + List users = new ArrayList<>(); + for (IssueSharer sharer :list) { + addUserToUsers(sharer.user, users); + } + return ok(toJson(users)); + } + + private static void sortListByAddedDate(List list) { + list.sort(new Comparator() { + @Override + public int compare(IssueSharer o1, IssueSharer o2) { + return o1.created.compareTo(o2.created); + } + }); + } + + private static ExpressionList getExpressionListByExtractingLoginIds(Issue issue, String query) { + String[] queryItems = query.split(","); + ExpressionList el = IssueSharer.find + .where() + .in("loginId", Arrays.asList(queryItems)) + .eq("issue.id", issue.id); + return el; + } + + @IsAllowed(Operation.READ) + public static Result findSharableUsers(String ownerName, String projectName, Long number, String query) { + if (!request().accepts("application/json")) { + return status(Http.Status.NOT_ACCEPTABLE); + } + + List users = new ArrayList<>(); + + ExpressionList el = getUserExpressionList(query, request().getQueryString("type")); + + int total = el.findRowCount(); + if (total > MAX_FETCH_USERS) { + el.setMaxRows(MAX_FETCH_USERS); + response().setHeader("Content-Range", "items " + MAX_FETCH_USERS + "/" + total); + } + + for (User user :el.findList()) { + addUserToUsers(user, users); + } + + return ok(toJson(users)); + } + + public static Result updateSharer(String owner, String projectName, Long number){ + JsonNode json = request().body().asJson(); + if (json == null) { + return badRequest(Json.newObject().put("message", "Expecting Json data")); + } + + if(noSharer(json.findValue("sharer"))){ + return badRequest(Json.newObject().put("message", "No sharer")); + } + + Project project = Project.findByOwnerAndProjectName(owner, projectName); + Issue issue = Issue.findByNumber(project, number); + if (!AccessControl.isAllowed(UserApp.currentUser(), issue.asResource(), + Operation.UPDATE)) { + return forbidden(Json.newObject().put("message", "Permission denied")); + } + + ObjectNode result = Json.newObject(); + String action = json.findValue("action").asText(); + + for(JsonNode sharerLoginId: json.findValue("sharer")){ + switch (action) { + case "delete": + removeSharer(issue, sharerLoginId.asText()); + result.put("action", "deleted"); + break; + case "add": + addSharer(issue, sharerLoginId.asText()); + result.put("action", "added"); + break; + default: + result.put("action", "Do nothing"); + } + result.put("sharer", User.findByLoginId(sharerLoginId.asText()).getDisplayName()); + } + + return ok(result); + } + + private static boolean noSharer(JsonNode sharers) { + return sharers == null || sharers.size() == 0; + } + + private static void addSharer(Issue issue, String loginId) { + IssueSharer issueSharer = IssueSharer.find.where() + .eq("loginId", loginId) + .eq("issue.id", issue.id).findUnique(); + if(issueSharer == null) { + issueSharer = IssueSharer.createSharer(loginId, issue); + issueSharer.save(); + } + issue.sharers.add(issueSharer); + } + + private static void removeSharer(Issue issue, String loginId) { + IssueSharer issueSharer = + IssueSharer.find.where() + .eq("loginId", loginId) + .eq("issue.id", issue.id) + .findUnique(); + issueSharer.delete(); + issue.sharers.remove(issueSharer); + } } diff --git a/app/utils/AccessControl.java b/app/utils/AccessControl.java index 0d0ffb051..d6da68297 100644 --- a/app/utils/AccessControl.java +++ b/app/utils/AccessControl.java @@ -27,6 +27,8 @@ import models.resource.Resource; import org.apache.commons.lang.BooleanUtils; +import java.util.Optional; + import static models.OrganizationUser.isAdmin; import static models.OrganizationUser.isMember; @@ -114,7 +116,8 @@ public static boolean isResourceCreatable(User user, Resource container, Resourc return false; } - if (isAllowedIfAuthor(user, container) || isAllowedIfAssignee(user, container)) { + if (isAllowedIfAuthor(user, container) || isAllowedIfAssignee(user, container) + || isAllowedIfSharer(user, container)) { return true; } @@ -285,6 +288,7 @@ private static boolean isProjectResourceAllowed(User user, Project project, Reso case READ: return project.isPublic() && !user.isGuest || user.isMemberOf(project) + || isAllowedIfSharer(user, resource) || isAllowedIfGroupMember(project, user); case UPDATE: return user.isMemberOf(project) @@ -374,6 +378,17 @@ private static boolean isAllowedIfAuthor(User user, Resource resource) { } } + private static boolean isAllowedIfSharer(User user, Resource resource) { + switch (resource.getType()) { + case ISSUE_POST: + case ISSUE_COMMENT: + Issue issue = Issue.finder.byId(Long.valueOf(resource.getId())); + return issue != null && Optional.ofNullable(issue.findSharerByUserId(user.id)).isPresent(); + default: + return false; + } + } + /** * Checks if an user has a permission to do something to the given * resource as an assignee. diff --git a/conf/routes b/conf/routes index 78e4a82f1..165cd9c1e 100644 --- a/conf/routes +++ b/conf/routes @@ -56,6 +56,9 @@ POST /-_-api/v1/favoriteOrganizations/:organizationId GET /-_-api/v1/owners/:owner/projects/:projectName/issues/:number/assignableUsers controllers.api.IssueApi.findAssignableUsers(owner:String, projectName:String, number:Long, query: String ?= "") GET /-_-api/v1/owners/:owner/projects/:projectName/assignableUsers controllers.api.IssueApi.findAssignableUsersOfProject(owner:String, projectName:String, query: String ?= "") POST /-_-api/v1/owners/:owner/projects/:projectName/issues/:number/assignees controllers.api.IssueApi.updateAssginees(owner:String, projectName:String, number:Long) +GET /-_-api/v1/owners/:owner/projects/:projectName/issues/:number/findSharer controllers.api.IssueApi.findSharerByloginIds(owner:String, projectName:String, number:Long, query: String ?= "") +GET /-_-api/v1/owners/:owner/projects/:projectName/issues/:number/sharableUsers controllers.api.IssueApi.findSharableUsers(owner:String, projectName:String, number:Long, query: String ?= "") +POST /-_-api/v1/owners/:owner/projects/:projectName/issues/:number/share controllers.api.IssueApi.updateSharer(owner:String, projectName:String, number:Long) GET /-_-api/v1/users controllers.UserApp.users(query: String ?= "") POST /-_-api/v1/users controllers.api.UserApi.newUser() POST /-_-api/v1/owners/:owner/projects controllers.api.ProjectApi.newProject(owner:String) From 49812f7abaee35b151f57e8affd3ea5bf11aeb6c Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Mon, 29 Jan 2018 00:05:29 +0900 Subject: [PATCH 34/77] issue: Issue sharing feature - view and message --- app/assets/stylesheets/less/_common.less | 19 +++ app/assets/stylesheets/less/_override.less | 17 +++ app/assets/stylesheets/less/_page.less | 23 ++++ app/views/common/sharerCount.scala.html | 11 ++ app/views/issue/partial_list.scala.html | 5 +- app/views/issue/view.scala.html | 81 ++++++++++--- conf/messages | 4 + conf/messages.ko-KR | 4 + .../javascripts/service/yona.issue.Sharer.js | 113 ++++++++++++++++++ 9 files changed, 260 insertions(+), 17 deletions(-) create mode 100644 app/views/common/sharerCount.scala.html create mode 100644 public/javascripts/service/yona.issue.Sharer.js diff --git a/app/assets/stylesheets/less/_common.less b/app/assets/stylesheets/less/_common.less index 39b608cf6..cc55779ad 100644 --- a/app/assets/stylesheets/less/_common.less +++ b/app/assets/stylesheets/less/_common.less @@ -219,6 +219,7 @@ input[type=number]::-webkit-outer-spin-button { .mt4 { margin-top:4px; } .mr3 { margin-right:3px; } .pb4 { padding-bottom: 4px} +.pl0 { padding-left: 0} .margin-top-20 { margin-top:20px; } .margin-left-20 { margin-left: 20px; } @@ -281,3 +282,21 @@ input.white:-ms-input-placeholder { color:#fff; opacity:0.8; } .va-text-top { vertical-align: text-top !important; } + +.width100p { + width: 100% +} + +.text-ellipsis { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.z-index-1 { + z-index: 1 !important; +} + +.hideFromDisplayOnly { + display: none; +} diff --git a/app/assets/stylesheets/less/_override.less b/app/assets/stylesheets/less/_override.less index 595bcec9d..640629063 100644 --- a/app/assets/stylesheets/less/_override.less +++ b/app/assets/stylesheets/less/_override.less @@ -182,6 +182,23 @@ .box-shadow(none); } +.sharer-list { + .select2-container{ + border: none; + box-shadow: none; + border-radius: 0 !important; + border-bottom: 1px solid rgba(0, 0, 0, 0.15); + } + .select2-container-multi { + .select2-choices { + .select2-search-choice { + background-color: #ececec; + border: 1px solid #dfdfdf; + } + } + } +} + .select2-dropdown-open { .select2-choice { border-bottom-color: transparent; diff --git a/app/assets/stylesheets/less/_page.less b/app/assets/stylesheets/less/_page.less index fa4db4529..fce03360c 100644 --- a/app/assets/stylesheets/less/_page.less +++ b/app/assets/stylesheets/less/_page.less @@ -3745,11 +3745,18 @@ label.issue-item-row { .comments-count-color { color: @darkmagenta; } + .sharer-color { + color: @green; + } a:nth-child(2) { margin-left: -5px; } + a:nth-child(3) { + margin-left: -5px; + } + .count-groups { margin:0 auto; display:inline-block; @@ -7070,3 +7077,19 @@ div.diff-body[data-outdated="true"] tr:hover .icon-comment { border-radius: 4px; } } + +.sharer-list { + margin-top: 40px; + padding: 10px; + + .issue-share-title { + font-size: 16px; + } + .sharer-item{ + display: inline-block; + background-color: #ececec; + border: 1px solid #dfdfdf; + border-radius: 3px; + padding: 1px 8px; + } +} diff --git a/app/views/common/sharerCount.scala.html b/app/views/common/sharerCount.scala.html new file mode 100644 index 000000000..382d4e3d7 --- /dev/null +++ b/app/views/common/sharerCount.scala.html @@ -0,0 +1,11 @@ +@** +* Yona, 21st Century Project Hosting SW +* +* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. +* https://yona.io +**@ +@(countNumber:Int, showColorAlways:Boolean = false) + + + + @countNumber diff --git a/app/views/issue/partial_list.scala.html b/app/views/issue/partial_list.scala.html index 751a95945..1e4df991f 100644 --- a/app/views/issue/partial_list.scala.html +++ b/app/views/issue/partial_list.scala.html @@ -62,7 +62,7 @@ } - @if(issue.comments.size>0 || issue.voters.size>0) { + @if(issue.comments.size > 0 || issue.voters.size > 0 || issue.sharers.size > 0) { @if(issue.comments.size>0){ @views.html.common.commentCount(routes.IssueApp.issue(project.owner, project.name, issue.getNumber).toString + "#comments", issue.comments.size, true) @@ -70,6 +70,9 @@ @if(issue.voters.size>0){ @views.html.common.voteCount(routes.IssueApp.issue(project.owner, project.name, issue.getNumber).toString + "#vote", issue.voters.size, true) } + @if(issue.sharers.size > 0){ + @views.html.common.sharerCount(issue.sharers.size, true) + } } diff --git a/app/views/issue/view.scala.html b/app/views/issue/view.scala.html index 51766925d..427778404 100644 --- a/app/views/issue/view.scala.html +++ b/app/views/issue/view.scala.html @@ -5,6 +5,7 @@ * https://yona.io **@ @(title:String, issue:Issue, issueForm: play.data.Form[Issue], commentForm: play.data.Form[Comment],project:Project) +@import scala.collection.mutable.ArrayBuffer @import org.apache.commons.lang.StringUtils @import models.enumeration.ResourceType @import models.enumeration.Operation @@ -58,6 +59,22 @@ } } +@hasAssignee = @{ + issue.assigneeName != null +} + +@hasSharer = @{ + issue.sharers.size > 0 +} + +@sharers = @{ + var sharerIds = ArrayBuffer[String]() + for( sharedUser <- issue.sharers ) { + sharerIds += sharedUser.loginId + } + sharerIds.mkString(",") +} + @VOTER_AVATAR_SHOW_LIMIT = @{ 5 } @showChildIssues(parentIssueId: Long) = { @@ -175,6 +192,9 @@ } } + @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE) && !hasSharer) { + + } @@ -219,6 +239,24 @@ } +

    +
    + @Messages("issue.sharer") @if(issue.sharers.size > 0) { @issue.sharers.size } +
    +
    + @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) { + + } else { + @for(sharer <- issue.getSortedSharer){ + + } + } +
    +
    @if(issue.parent == null) { @@ -254,23 +292,21 @@
    @Messages("issue.assignee")
    - @defining(issue.assigneeName != null) { isAssigned => - @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) { - @partial_assignee(project, issue) + @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) { + @partial_assignee(project, issue) + } else { + @if(hasAssignee){ + + + + + @issue.assignee.user.getDisplayName + @{"@"}@issue.assignee.user.loginId + } else { - @if(isAssigned){ - - - - - @issue.assignee.user.getDisplayName - @{"@"}@issue.assignee.user.loginId - - } else { -
    - @Messages("issue.noAssignee") -
    - } +
    + @Messages("issue.noAssignee") +
    } }
    @@ -427,6 +463,7 @@

    @Messages("issue.delete")

    + } diff --git a/app/views/issue/my_partial_list.scala.html b/app/views/issue/my_partial_list.scala.html index 5ae290801..4b6965ca6 100644 --- a/app/views/issue/my_partial_list.scala.html +++ b/app/views/issue/my_partial_list.scala.html @@ -10,6 +10,35 @@ @import utils.TemplateHelper._ @import utils.AccessControl._ +@isAuthoredMeTab = @{ + UserApp.currentUser().id == searchCondition.authorId +} + +@displayAuthorName(isAuthoredMeTab: Boolean, user: User) = { + @if(!isAuthoredMeTab) { + @if(user.name) { + + @user.getPureNameOnly + + } else { + @Messages("issue.noAuthor") + } + } +} + +@displayCommentsAndVoterCount(issue:Issue, project:Project) = { + @if(issue.comments.size > 0 || issue.voters.size > 0) { + + @if(issue.comments.size > 0) { + @views.html.common.commentCount(routes.IssueApp.issue(project.owner, project.name, issue.getNumber).toString + "#comments", issue.comments.size) + } + @if(issue.voters.size > 0) { + @views.html.common.voteCount(routes.IssueApp.issue(project.owner, project.name, issue.getNumber).toString + "#vote", issue.voters.size) + } + + } +} + @urlToList(project:Project, state:String) = {@routes.IssueApp.issues(project.owner, project.name, "open", "html", 1)} @issueLabels(issue:Issue) = {@for(label <- issue.labels.toList.sortBy(r => (r.category.name, r.name))) {@label.category.name,@label.id,@label.name|}} @@ -17,43 +46,47 @@ @isAssignedToMeTab = @{ UserApp.currentUser().id == searchCondition.assigneeId } -@isAuthoredMeTab = @{ - UserApp.currentUser().id == searchCondition.authorId -} +
    + diff --git a/app/views/issue/partial_comments.scala.html b/app/views/issue/partial_comments.scala.html index 03ac33689..ae3f6fab2 100644 --- a/app/views/issue/partial_comments.scala.html +++ b/app/views/issue/partial_comments.scala.html @@ -6,252 +6,26 @@ **@ @(project:Project, issue:Issue) -@import org.apache.commons.lang3.StringUtils -@import utils.TemplateHelper._ -@import utils.AccessControl._ -@import utils.JodaDateUtil -@import play.libs.Json.toJson -@import utils.Markdown -@import controllers.api.IssueApi - -@avatarByLoginId(loginId: String, loginName: String) = { - - - -} - -@linkToUser(loginId: String, loginName: String, showAvatar: Boolean = true) = { - @loginId match { - case (loginId: String) => { - @if(showAvatar){ @avatarByLoginId(loginId, loginName) } - - @loginName - - } - case _ => { Anonymous } - } -} - -@milestoneSpan(project:Project, milestone:Milestone) = { - - - @milestone.title - - -} - -@noMilestoneSpan() = { - - @Messages("common.none") - -} - - -@linkOfMilestone(milestoneId: String, project:Project) = @{ - val milestone = Milestone.findById(Long.valueOf(milestoneId)) - - if(milestone == null || milestone.isNullMilestone){ - noMilestoneSpan - } else { - milestoneSpan(project, milestone) - } -} - -@issueLabelBox(categoryAndName: String, project:Project ) = @{ - val splitedCategoryAndName = categoryAndName.split(" - ") - if(splitedCategoryAndName.length != 2) { - categoryAndName - } else { - var categoryName = splitedCategoryAndName(0).trim - var labelName = splitedCategoryAndName(1).trim - val issueLabel = IssueLabel.findByName(labelName, categoryName, project) - if( issueLabel != null) { - val labelColor = issueLabel.color - s"
    $labelName
    " - } else { - labelName - } - } -} - -@assginedMesssage(newValue: String, user:User) = @{ - val LoginId = user.loginId - newValue match { - case LoginId => "issue.event.assignedToMe" - case _: String => "issue.event.assigned" - case _ => "issue.event.unassigned" - } -} -@isAuthorComment(commentId: String) = @{ - if(commentId == UserApp.currentUser().loginId) {"author"} -} - -@linkToPullRequest(pull: PullRequest) ={ - @Messages("pullRequest")-@pull.number @pull.title -} - -@linkToProject(owner: String, name: String) ={ - @owner/@name -} - -@linkToCommit(commitId: String) ={ - @Messages("code.commits") @{"@"}@commitId -} - -@VOTER_AVATAR_SHOW_LIMIT = @{ 5 } -
    @Messages("common.comment") @issue.comments.size

    @if(issue.comments.size + issue.events.size > 0) { -
      -@for(item <- issue.getTimeline){ - @item match { - case (comment: Comment) => { -
    • -
      - - @comment.authorName - -
      -
      -
      - - - - @comment.authorLoginId - - - @User.findByLoginId(comment.authorLoginId).getDisplayName - - - @utils.TemplateHelper.agoOrDateString(comment.createdDate) - - - @if(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.READ) && comment.isInstanceOf[IssueComment]) { - @defining(comment.asInstanceOf[IssueComment]) { issueComment => - @if(issueComment.voters.size > VOTER_AVATAR_SHOW_LIMIT) { - - - @if(issueComment.voters.size == 1) { - @Messages("common.comment.vote.agreement", issueComment.voters.size) - } else { - @Messages("common.comment.vote.agreements", issueComment.voters.size) - } - - - - @partial_voter_list("voters-" + issueComment.id, issueComment.voters) - } else { - @for(voter <- issueComment.voters){ - - - - } - } - - @if(issueComment.voters.contains(UserApp.currentUser())) { - - } else { - @if(UserApp.currentUser().isAnonymous()) { - - } else { - - } - } - } - } - - @if(StringUtils.isNotBlank(IssueApi.TRANSLATION_API)){ - - } - - @if(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.READ)) { - - } - - @if(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.DELETE)) { - - } - -
      - - @common.commentUpdateForm(comment, routes.IssueApp.newComment(project.owner, project.name, issue.getNumber).toString(), comment.contents) - -
      -
      @Html(Markdown.render(comment.contents, project))
      -
      -
      -
      -
    • - - } - case (event: IssueEvent) => { - @if(event.eventType != EventType.ISSUE_BODY_CHANGED) { -
    • - @defining(User.findByLoginId(event.senderLoginId)) { user => - @event.eventType match { - case EventType.ISSUE_STATE_CHANGED => { - @Messages("issue.state." + event.newValue) @Html(Messages("issue.event." + event.newValue, linkToUser(user.loginId, user.getDisplayName))) - } - case EventType.ISSUE_ASSIGNEE_CHANGED => { - @Messages("issue.state.assigned") - @Html(Messages(assginedMesssage(event.newValue, user), linkToUser(user.loginId, user.getDisplayName), linkToUser(event.newValue,User.findByLoginId(event.newValue).getDisplayName, true))) - } - case EventType.ISSUE_MILESTONE_CHANGED => { - @Messages("issue.update.milestone.id") - @Html(Messages("issue.event.milestone.changed", linkToUser(user.loginId, user.getDisplayName), linkOfMilestone(event.newValue, project))) - } - case EventType.ISSUE_REFERRED_FROM_COMMIT => { - @Messages("issue.event.referred.title") - @Html(Messages("issue.event.referred",linkToUser(user.loginId, user.getDisplayName),linkToCommit(event.newValue))) - } - case EventType.ISSUE_MOVED => { - @Messages("issue.event.moved.title") - @Html(Messages("issue.event.moved", linkToUser(user.loginId, user.getDisplayName), linkToProject(event.oldValue.split("/")(0), event.oldValue.split("/")(1)))) - } - case EventType.ISSUE_REFERRED_FROM_PULL_REQUEST => { - @Messages("issue.event.referred.title") - @defining(PullRequest.findById(Long.valueOf(event.newValue))) { pull => - @Html(Messages("issue.event.referred",linkToUser(user.loginId, user.getDisplayName),linkToPullRequest(pull))) - } - } - case EventType.ISSUE_SHARER_CHANGED => { - @if(StringUtils.isBlank(event.oldValue) && StringUtils.isNotBlank(event.newValue)){ - @Messages("issue.sharer") - @Html(Messages("issue.event.sharer.added", linkToUser(user.loginId, user.getPureNameOnly), linkToUser(event.newValue, User.findByLoginId(event.newValue).getPureNameOnly))) - } else { - @Messages("issue.event.sharer.deleted.title") - @Html(Messages("issue.event.sharer.deleted", linkToUser(user.loginId, user.getPureNameOnly), linkToUser(event.oldValue, User.findByLoginId(event.oldValue).getPureNameOnly))) - } - } - case EventType.ISSUE_LABEL_CHANGED => { - @if(StringUtils.isBlank(event.oldValue) && StringUtils.isNotBlank(event.newValue)){ - @Messages("issue.event.label.added.title") - @Html(Messages("issue.event.label.added", linkToUser(user.loginId, user.getPureNameOnly), issueLabelBox(event.newValue, project))) - } else { - @Messages("issue.event.label.deleted.title") - @Html(Messages("issue.event.label.deleted", linkToUser(user.loginId, user.getPureNameOnly), issueLabelBox(event.oldValue, project))) - } - } - case _ => { - @event.newValue by @linkToUser(user.loginId, user.getDisplayName) - } +
        + @defining(issue.getTimeline) { timeline => + @for((item, index) <- timeline.view.zipWithIndex) { + @item match { + case (comment: Comment) => { + @partial_comment(comment, project, issue) + } + case (event: IssueEvent) => { + @if(index > 0 && timeline(index-1).isInstanceOf[IssueEvent]) { + @partial_event_timeline(event, project, issue, timeline(index-1).asInstanceOf[IssueEvent]) + } else { + @partial_event_timeline(event, project, issue) } } - @utils.TemplateHelper.agoOrDateString(event.getDate()) - + } } } - } -} -
      +
    } diff --git a/app/views/issue/partial_event_timeline.scala.html b/app/views/issue/partial_event_timeline.scala.html new file mode 100644 index 000000000..d6a64b6c6 --- /dev/null +++ b/app/views/issue/partial_event_timeline.scala.html @@ -0,0 +1,186 @@ +@** +* Yona, 21st Century Project Hosting SW +* +* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. +* https://yona.io +**@ +@(event: IssueEvent, project:Project, issue:Issue, previousEvent: IssueEvent = null) + +@import org.apache.commons.lang3.StringUtils +@import utils.TemplateHelper._ +@import models.enumeration.EventType._ + +@avatarByLoginId(loginId: String, loginName: String, sameTypeAsPrevious: Boolean = false) = { + + +} + +@linkToUser(loginId: String, loginName: String, showAvatar: Boolean = true) = { + @loginId match { + case (loginId: String) => { + @if(showAvatar) { + @avatarByLoginId(loginId, loginName) + } + + @loginName + } + case _ => { Anonymous } + } +} + +@milestoneSpan(project: Project, milestone: Milestone) = { + + + @milestone.title + + +} + +@noMilestoneSpan() = { + + @Messages("common.none") + +} + +@linkOfMilestone(milestoneId: String, project: Project) = @{ + val milestone = Milestone.findById(Long.valueOf(milestoneId)) + + if(milestone == null || milestone.isNullMilestone) { + noMilestoneSpan + } else { + milestoneSpan(project, milestone) + } +} + +@issueLabelBox(categoryAndName: String, project: Project) = @{ + val splitedCategoryAndName = categoryAndName.split(" - ") + if(splitedCategoryAndName.length != 2) { + categoryAndName + } else { + var categoryName = splitedCategoryAndName(0).trim + var labelName = splitedCategoryAndName(1).trim + val issueLabel = IssueLabel.findByName(labelName, categoryName, project) + if(issueLabel != null) { + val labelColor = issueLabel.color + s"
    $labelName
    " + } else { + labelName + } + } +} + +@assginedMesssage(newValue: String, user: User) = @{ + val LoginId = user.loginId + newValue match { + case LoginId => "issue.event.assignedToMe" + case _: String => "issue.event.assigned" + case _ => "issue.event.unassigned" + } +} + +@linkToPullRequest(pull: PullRequest) = { + @Messages("pullRequest") + -@pull.number @pull.title +} + +@linkToProject(owner: String, name: String) = { + @owner/@name +} + +@linkToCommit(commitId: String) = { + @Messages("code.commits") @{ + "@" + }@commitId +} + +@isAddingEvent(event: IssueEvent) = @{ + event != null && StringUtils.isBlank(event.oldValue) && StringUtils.isNotBlank(event.newValue) +} + +@isDeletingEvent(event: IssueEvent) = @{ + event != null && StringUtils.isBlank(event.newValue) && StringUtils.isNotBlank(event.oldValue) +} + +@isSameEventTypeAsPrevious(event: IssueEvent, previousEvent: IssueEvent) = @{ + previousEvent != null && event.eventType == previousEvent.eventType +} + +@isSameEventTypeAndSameAction(event: IssueEvent, previousEvent: IssueEvent) = @{ + isSameEventTypeAsPrevious(event, previousEvent) && ( + isAddingEvent(event) && isAddingEvent(previousEvent) + || isDeletingEvent(event) && isDeletingEvent(previousEvent) + ) +} + +@if(event.eventType != ISSUE_BODY_CHANGED) { +
  • + @defining(User.findByLoginId(event.senderLoginId)) { user => + @event.eventType match { + case ISSUE_STATE_CHANGED => { + @Messages("issue.state." + event.newValue) + @Html(Messages("issue.event." + event.newValue, linkToUser(user.loginId, user.getDisplayName))) + } + case ISSUE_ASSIGNEE_CHANGED => { + @Messages("issue.state.assigned") + @Html(Messages(assginedMesssage(event.newValue, user), linkToUser(user.loginId, user.getDisplayName), linkToUser(event.newValue, User.findByLoginId(event.newValue).getDisplayName, true))) + } + case ISSUE_MILESTONE_CHANGED => { + @Messages("issue.update.milestone.id") + @Html(Messages("issue.event.milestone.changed", linkToUser(user.loginId, user.getDisplayName), linkOfMilestone(event.newValue, project))) + } + case ISSUE_REFERRED_FROM_COMMIT => { + @Messages("issue.event.referred.title") + @Html(Messages("issue.event.referred", linkToUser(user.loginId, user.getDisplayName), linkToCommit(event.newValue))) + } + case ISSUE_MOVED => { + @Messages("issue.event.moved.title") + @Html(Messages("issue.event.moved", linkToUser(user.loginId, user.getDisplayName), linkToProject(event.oldValue.split("/")(0), event.oldValue.split("/")(1)))) + } + case ISSUE_REFERRED_FROM_PULL_REQUEST => { + @Messages("issue.event.referred.title") + @defining(PullRequest.findById(Long.valueOf(event.newValue))) { pull => + @Html(Messages("issue.event.referred", linkToUser(user.loginId, user.getDisplayName), linkToPullRequest(pull))) + } + } + case ISSUE_SHARER_CHANGED => { + @if(isAddingEvent(event)) { + @if(isSameEventTypeAndSameAction(event, previousEvent)){ + + } else { + @Messages("issue.sharer") + } + @Html(Messages("issue.event.sharer.added", linkToUser(user.loginId, user.getPureNameOnly), linkToUser(event.newValue, User.findByLoginId(event.newValue).getPureNameOnly))) + } else { + @if(isSameEventTypeAndSameAction(event, previousEvent)){ + + } else { + @Messages("issue.event.sharer.deleted.title") + } + @Html(Messages("issue.event.sharer.deleted", linkToUser(user.loginId, user.getPureNameOnly), linkToUser(event.oldValue, User.findByLoginId(event.oldValue).getPureNameOnly))) + } + } + case ISSUE_LABEL_CHANGED => { + @if(isAddingEvent(event)) { + @if(isSameEventTypeAndSameAction(event, previousEvent)) { + + } else { + @Messages("issue.event.label.added.title") + } + @Html(Messages("issue.event.label.added", linkToUser(user.loginId, user.getPureNameOnly), issueLabelBox(event.newValue, project))) + } else { + @if(isSameEventTypeAndSameAction(event, previousEvent)) { + + } else { + @Messages("issue.event.label.deleted.title") + } + @Html(Messages("issue.event.label.deleted", linkToUser(user.loginId, user.getPureNameOnly), issueLabelBox(event.oldValue, project))) + } + } + case _ => { + @event.newValue by @linkToUser(user.loginId, user.getDisplayName) + } + } + } + @utils.TemplateHelper.agoOrDateString(event.getDate()) +
  • +} From e11f85a8e60be8ee8530300745b5f6eda08d90fd Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Tue, 13 Feb 2018 17:20:20 +0900 Subject: [PATCH 62/77] AUTHORS: Add new author --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 14631c26f..afb3a124b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -48,3 +48,4 @@ Bean rimi kenu DongHo Byun +Mijeong Park From 2082808ae1991d2d8476ada5697e80308e8f8adb Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Sun, 18 Feb 2018 11:43:34 +0900 Subject: [PATCH 63/77] noti-mail: Add logic for rendering markdown except for event of issue body change --- app/models/NotificationMail.java | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/app/models/NotificationMail.java b/app/models/NotificationMail.java index da4ef1332..2dc4b35d3 100644 --- a/app/models/NotificationMail.java +++ b/app/models/NotificationMail.java @@ -532,10 +532,10 @@ private static void sendMail(INotificationEvent event, Set toList Lang lang = Lang.apply(langCode); - String htmlMessage = event.getMessage(lang); + String message = event.getMessage(lang); String plainMessage = event.getPlainMessage(lang); - if (htmlMessage == null || plainMessage == null) { + if (message == null || plainMessage == null) { return; } @@ -548,7 +548,13 @@ private static void sendMail(INotificationEvent event, Set toList resource = issueComment.issue.asResource(); } - email.setHtmlMsg(removeHeadAnchor(getHtmlMessage(lang, htmlMessage, urlToView, resource, acceptsReply))); + String renderedHtmlMessage = null; + if (event.getType() == EventType.ISSUE_BODY_CHANGED) { + renderedHtmlMessage = getRenderedMail(lang, message, urlToView, resource, acceptsReply); + } else { + renderedHtmlMessage = getHtmlMessage(lang, message, urlToView, resource, acceptsReply); + } + email.setHtmlMsg(removeHeadAnchor(renderedHtmlMessage)); email.setTextMsg(getPlainMessage(lang, plainMessage, Url.create(urlToView), acceptsReply)); email.addReferences(); @@ -612,6 +618,20 @@ public static String getReplyTo(Resource resource) { private static String getHtmlMessage(Lang lang, String message, String urlToView, Resource resource, boolean acceptsReply) { + String renderred = null; + + if(resource != null) { + renderred = Markdown.render(message, resource.getProject(), lang.code()); + } else { + renderred = Markdown.render(message); + } + + return getRenderedMail(lang, renderred, urlToView, resource, acceptsReply); + } + + private static String getRenderedMail(Lang lang, String message, String urlToView, + Resource resource, boolean acceptsReply) { + String content = views.html.common.notificationMail.render( lang, message, urlToView, resource, acceptsReply).toString(); From 7527fae410f6dc4e0fd096c797cad013d600b676 Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Wed, 14 Feb 2018 11:47:53 +0900 Subject: [PATCH 64/77] issue: Fix the bug of missing new line highlight in email noti --- app/utils/DiffUtil.java | 2 +- test/utils/DiffUtilTest.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/utils/DiffUtil.java b/app/utils/DiffUtil.java index e613787bc..f71057c43 100644 --- a/app/utils/DiffUtil.java +++ b/app/utils/DiffUtil.java @@ -57,7 +57,7 @@ public static String getDiffText(String oldValue, String newValue) { } } - return sb.toString().replaceAll("\n", "
    \n"); + return sb.toString().replaceAll("\n", " 
    \n"); } public static String getDiffPlainText(String oldValue, String newValue) { diff --git a/test/utils/DiffUtilTest.java b/test/utils/DiffUtilTest.java index 92d98b9eb..9733573eb 100644 --- a/test/utils/DiffUtilTest.java +++ b/test/utils/DiffUtilTest.java @@ -17,9 +17,9 @@ public class DiffUtilTest { String DIFF_INSERT_PREFIX = ""; String DIFF_INSERT_POSTFIX = ""; - String DIFF_EQUAL_PREFIX = "...
    \n" - + "......
    \n" - + "......
    \n" + String DIFF_EQUAL_PREFIX = "... 
    \n" + + "...... 
    \n" + + "...... 
    \n" + "...
    "; String DIFF_NEW_LINE = "\n"; From f4651618991f348735362387eb33655157b3cf82 Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Mon, 19 Feb 2018 11:48:42 +0900 Subject: [PATCH 65/77] noti-mail: Refactor unnecessary temp variable --- app/models/NotificationMail.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/models/NotificationMail.java b/app/models/NotificationMail.java index 2dc4b35d3..cf9acd35a 100644 --- a/app/models/NotificationMail.java +++ b/app/models/NotificationMail.java @@ -548,13 +548,11 @@ private static void sendMail(INotificationEvent event, Set toList resource = issueComment.issue.asResource(); } - String renderedHtmlMessage = null; if (event.getType() == EventType.ISSUE_BODY_CHANGED) { - renderedHtmlMessage = getRenderedMail(lang, message, urlToView, resource, acceptsReply); + email.setHtmlMsg(removeHeadAnchor(getRenderedMail(lang, message, urlToView, resource, acceptsReply))); } else { - renderedHtmlMessage = getHtmlMessage(lang, message, urlToView, resource, acceptsReply); + email.setHtmlMsg(removeHeadAnchor(getHtmlMessage(lang, message, urlToView, resource, acceptsReply))); } - email.setHtmlMsg(removeHeadAnchor(renderedHtmlMessage)); email.setTextMsg(getPlainMessage(lang, plainMessage, Url.create(urlToView), acceptsReply)); email.addReferences(); From d5fcdd84f0f58504e57cc90fb64ab7709537354e Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Wed, 14 Feb 2018 11:18:18 +0900 Subject: [PATCH 66/77] issue: Reduce issue state change notification receivers --- app/models/NotificationEvent.java | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/app/models/NotificationEvent.java b/app/models/NotificationEvent.java index 71ec6aa15..d6294f1cc 100644 --- a/app/models/NotificationEvent.java +++ b/app/models/NotificationEvent.java @@ -767,7 +767,7 @@ public static NotificationEvent afterStateChanged(State oldState, Issue issue) { NotificationEvent notiEvent = createFromCurrentUser(issue); notiEvent.title = formatReplyTitle(issue); - notiEvent.receivers = getReceivers(issue); + notiEvent.receivers = getMandatoryReceivers(issue); notiEvent.eventType = ISSUE_STATE_CHANGED; notiEvent.oldValue = oldState != null ? oldState.state() : null; notiEvent.newValue = issue.state.state(); @@ -833,23 +833,13 @@ public static NotificationEvent afterAssigneeChanged(User oldAssignee, Issue iss } private static Set getReceiversWhenAssigneeChanged(User oldAssignee, Issue issue) { - Set receivers = findWatchers(issue.asResource()); - receivers.add(issue.getAuthor()); - - if (issue.assignee != null) { - receivers.add(issue.assignee.user); - } + Set receivers = getMandatoryReceivers(issue); - if (oldAssignee != null && !oldAssignee.isAnonymous()) { + if (oldAssignee != null && !oldAssignee.isAnonymous() + && !oldAssignee.loginId.equals(UserApp.currentUser().loginId)) { receivers.add(oldAssignee); } - for (IssueSharer issueSharer : issue.sharers) { - receivers.add(User.findByLoginId(issueSharer.loginId)); - } - - receivers.remove(UserApp.currentUser()); - return receivers; } @@ -960,13 +950,17 @@ private static Set getMandatoryReceivers(Issue issue) { receivers.add(User.findByLoginId(issueSharer.loginId)); } + if (issue.assignee != null) { + receivers.add(issue.assignee.user); + } + receivers.remove(UserApp.currentUser()); return receivers; } private static Set getReceiversForIssueBodyChanged(String oldBody, Issue issue) { - Set receivers = issue.getWatchers(); + Set receivers = getMandatoryReceivers(issue); receivers.addAll(getNewMentionedUsers(oldBody, issue.body)); receivers.remove(UserApp.currentUser()); return receivers; @@ -1200,13 +1194,6 @@ private static Set getCommentReceivers(Comment comment, User except) { includeAssigneeIfExist(comment, receivers); receivers.remove(except); - // Filter the watchers who has no permission to read this resource. - CollectionUtils.filter(receivers, new Predicate() { - @Override - public boolean evaluate(Object watcher) { - return AccessControl.isAllowed((User) watcher, parent.asResource(), Operation.READ); - } - }); return receivers; } From 758349c67c81262e24e7a0331287522de5a93bf9 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Thu, 22 Feb 2018 12:22:39 +0900 Subject: [PATCH 67/77] global: Log elapsed time of booting sequence --- app/Global.java | 10 ++++++++++ app/utils/Timestamp.java | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 app/utils/Timestamp.java diff --git a/app/Global.java b/app/Global.java index 27b2c3d0a..672813b57 100644 --- a/app/Global.java +++ b/app/Global.java @@ -158,17 +158,27 @@ public void onStart(Application app) { isSecretInvalid = equalsDefaultSecret(); insertInitialData(); + Timestamp timestamp = new Timestamp("=== Yona server starting initialization ==="); Config.onStart(); + timestamp.logElapsedTime("--- Config reading: ok!"); Property.onStart(); + timestamp.logElapsedTime("--- Property reading: ok!"); PullRequest.onStart(); + timestamp.logElapsedTime("--- Pull request checking: ok!"); NotificationMail.onStart(); + timestamp.logElapsedTime("--- Notification mail scheduler: ok!"); NotificationEvent.onStart(); + timestamp.logElapsedTime("--- Notification event cleanup scheduler: ok!"); Attachment.onStart(); + timestamp.logElapsedTime("--- Temporary files cleanup scheduler: ok!"); AccessControl.onStart(); + timestamp.logElapsedTime("--- Basic access controller config reading: ok!"); if (!isSecretInvalid) { YobiUpdate.onStart(); + timestamp.logElapsedTime("--- Update checker run: ok! "); mailboxService.start(); + timestamp.logElapsedTime("--- MailboxService checker run: ok!"); } PlayAuthenticate.setResolver(new PlayAuthenticate.Resolver() { diff --git a/app/utils/Timestamp.java b/app/utils/Timestamp.java new file mode 100644 index 000000000..664d9757c --- /dev/null +++ b/app/utils/Timestamp.java @@ -0,0 +1,24 @@ +/** + * Yona, 21st Century Project Hosting SW + *

    + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. + * https://yona.io + **/ + +package utils; + +public class Timestamp { + + private long lastCheckedTime; + + public Timestamp(String title) { + this.lastCheckedTime = System.currentTimeMillis(); + play.Logger.info(title); + } + + public void logElapsedTime(String message) { + long currentTimeMillis = System.currentTimeMillis(); + play.Logger.info(message + " - " + (currentTimeMillis - lastCheckedTime) + " ms"); + lastCheckedTime = currentTimeMillis; + } +} From c348db8b9f760735e42135ffb6602138272e0f1b Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Sun, 25 Feb 2018 22:29:33 +0900 Subject: [PATCH 68/77] notification: Fix notification bug when edit posting --- app/controllers/BoardApp.java | 2 +- app/models/NotificationEvent.java | 31 +++++++++++++++++++++++++-- app/models/NotificationMail.java | 4 +++- app/models/enumeration/EventType.java | 9 +++----- conf/messages | 1 + conf/messages.ko-KR | 1 + 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/app/controllers/BoardApp.java b/app/controllers/BoardApp.java index 49c13a5eb..ce9cfb1eb 100644 --- a/app/controllers/BoardApp.java +++ b/app/controllers/BoardApp.java @@ -354,7 +354,7 @@ public static Result editPost(String userName, String projectName, Long number) public void run() { post.comments = original.comments; if(isSelectedToSendNotificationMail() || !original.isAuthoredBy(UserApp.currentUser())){ - NotificationEvent.afterNewPost(post); + NotificationEvent.afterUpdatePosting(original.body, post); } } }; diff --git a/app/models/NotificationEvent.java b/app/models/NotificationEvent.java index d6294f1cc..04d400640 100644 --- a/app/models/NotificationEvent.java +++ b/app/models/NotificationEvent.java @@ -144,6 +144,7 @@ public String getMessage(Lang lang) { case COMMENT_UPDATED: return newValue; case ISSUE_BODY_CHANGED: + case POSTING_BODY_CHANGED: return DiffUtil.getDiffText(oldValue, newValue); case NEW_REVIEW_COMMENT: try { @@ -213,8 +214,10 @@ public String getMessage(Lang lang) { return Messages.get(lang, "notification.issue.label.deleted"); } default: - play.Logger.error("Unknown event message: " + this); - return null; + play.Logger.warn("Unknown event message: " + this); + play.Logger.warn("Event Type: " + eventType); + play.Logger.warn("See: NotificationEvent.getMessage"); + return eventType.getDescr(); } } @@ -227,6 +230,7 @@ public String getPlainMessage() { public String getPlainMessage(Lang lang) { switch(eventType) { case ISSUE_BODY_CHANGED: + case POSTING_BODY_CHANGED: return DiffUtil.getDiffPlainText(oldValue, newValue); default: return getMessage(lang); @@ -959,6 +963,15 @@ private static Set getMandatoryReceivers(Issue issue) { return receivers; } + private static Set getMandatoryReceivers(Posting posting) { + Set receivers = findWatchers(posting.asResource()); + receivers.add(posting.getAuthor()); + + receivers.remove(UserApp.currentUser()); + + return receivers; + } + private static Set getReceiversForIssueBodyChanged(String oldBody, Issue issue) { Set receivers = getMandatoryReceivers(issue); receivers.addAll(getNewMentionedUsers(oldBody, issue.body)); @@ -970,6 +983,10 @@ public static void afterNewPost(Posting post) { NotificationEvent.add(forNewPosting(post, UserApp.currentUser())); } + public static void afterUpdatePosting(String oldValue, Posting post) { + NotificationEvent.add(forUpdatePosting(oldValue, post, UserApp.currentUser())); + } + public static NotificationEvent forNewPosting(Posting post, User author) { NotificationEvent notiEvent = createFrom(author, post); notiEvent.title = formatNewTitle(post); @@ -980,6 +997,16 @@ public static NotificationEvent forNewPosting(Posting post, User author) { return notiEvent; } + public static NotificationEvent forUpdatePosting(String oldValue, Posting post, User author) { + NotificationEvent notiEvent = createFrom(author, post); + notiEvent.title = formatNewTitle(post); + notiEvent.receivers = getMandatoryReceivers(post); + notiEvent.eventType = POSTING_BODY_CHANGED; + notiEvent.oldValue = oldValue; + notiEvent.newValue = post.body; + return notiEvent; + } + public static void afterNewCommitComment(Project project, ReviewComment comment, String commitId) throws IOException, SVNException, ServletException { diff --git a/app/models/NotificationMail.java b/app/models/NotificationMail.java index cf9acd35a..f8eabcc6f 100644 --- a/app/models/NotificationMail.java +++ b/app/models/NotificationMail.java @@ -548,7 +548,9 @@ private static void sendMail(INotificationEvent event, Set toList resource = issueComment.issue.asResource(); } - if (event.getType() == EventType.ISSUE_BODY_CHANGED) { + // ToDo: needed to refactor + if (event.getType() == EventType.ISSUE_BODY_CHANGED || + event.getType() == EventType.POSTING_BODY_CHANGED) { email.setHtmlMsg(removeHeadAnchor(getRenderedMail(lang, message, urlToView, resource, acceptsReply))); } else { email.setHtmlMsg(removeHeadAnchor(getHtmlMessage(lang, message, urlToView, resource, acceptsReply))); diff --git a/app/models/enumeration/EventType.java b/app/models/enumeration/EventType.java index ec82255da..e6286d278 100644 --- a/app/models/enumeration/EventType.java +++ b/app/models/enumeration/EventType.java @@ -37,7 +37,8 @@ public enum EventType { ISSUE_MOVED("notification.type.issue.is.moved", 21), ISSUE_SHARER_CHANGED("notification.type.issue.sharer.changed", 22), ISSUE_LABEL_CHANGED("notification.type.issue.label.changed", 23), - ISSUE_MILESTONE_CHANGED("notification.type.milestone.changed", 24); + ISSUE_MILESTONE_CHANGED("notification.type.milestone.changed", 24), + POSTING_BODY_CHANGED("notification.type.posting.body.changed", 25); private String descr; @@ -86,10 +87,6 @@ public boolean isCreating() { @Override public String toString() { - return "EventType{" + - "descr='" + descr + '\'' + - ", order=" + order + - ", messageKey='" + messageKey + '\'' + - '}'; + return name(); } } diff --git a/conf/messages b/conf/messages index c6cc6dbdc..33b8a77d8 100644 --- a/conf/messages +++ b/conf/messages @@ -472,6 +472,7 @@ notification.type.new.issue = New issue added notification.type.new.posting = New post added notification.type.new.pullrequest = New pull request added notification.type.new.simple.comment = New comment on pull request added +notification.type.posting.body.changed = Posting changed notification.type.pullrequest.commit.changed = PullRequest Commit Change notification.type.pullrequest.conflicts = Pull request conflicts notification.type.pullrequest.merged = Pull request merged diff --git a/conf/messages.ko-KR b/conf/messages.ko-KR index ea0c91b22..b1a82705e 100644 --- a/conf/messages.ko-KR +++ b/conf/messages.ko-KR @@ -473,6 +473,7 @@ notification.type.new.issue = 새 이슈 등록 notification.type.new.posting = 새 게시물 등록 notification.type.new.pullrequest = 새 코드보내기 등록 notification.type.new.simple.comment = 코드보내기에 새 댓글 등록 +notification.type.posting.body.changed = 게시글 본문 변경 notification.type.pullrequest.commit.changed = 코드보내기 커밋 변경 notification.type.pullrequest.conflicts = 코드보내기 충돌 notification.type.pullrequest.merged = 코드보내기 반영됨(merged) From 104d07e10906a058434e4b2d7e2658f0b1800ee3 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Mon, 26 Feb 2018 00:16:49 +0900 Subject: [PATCH 69/77] notification: Fix notification setting page bug Recently, default project watching scope is changed, which not to receive comments before explicitly subscribed it. But it isn't applied to user notificiation setting page. This commit fix it. --- app/controllers/WatchProjectApp.java | 16 ++++-- app/models/NotificationEvent.java | 2 + app/models/UserProjectNotification.java | 76 ++++++++++++++++--------- 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/app/controllers/WatchProjectApp.java b/app/controllers/WatchProjectApp.java index 34ed2ea5c..cbf199aeb 100644 --- a/app/controllers/WatchProjectApp.java +++ b/app/controllers/WatchProjectApp.java @@ -1,7 +1,7 @@ /** * Yona, 21st Century Project Hosting SW *

    - * Copyright Yona & Yobi Authors & NAVER Corp. + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. * https://yona.io **/ package controllers; @@ -21,6 +21,8 @@ import utils.AccessControl; import utils.ErrorViews; +import static models.UserProjectNotification.*; + @AnonymousCheck(requiresLogin = true, displaysFlashMessage = true) public class WatchProjectApp extends Controller { @@ -55,11 +57,15 @@ public static Result toggle(Long projectId, String notificationType) { return badRequest(Messages.get("error.notfound.watch")); } - UserProjectNotification upn = UserProjectNotification.findOne(user, project, notiType); - if(upn == null) { // make the EventType OFF, because default is ON. - UserProjectNotification.unwatchExplictly(user, project, notiType); + UserProjectNotification userProjectNotification = findOne(user, project, notiType); + if(userProjectNotification == null) { // not specified yet + if (isNotifiedByDefault(notiType)) { + watchExplictly(user, project, notiType); + } else { + unwatchExplictly(user, project, notiType); + } } else { - upn.toggle(); + userProjectNotification.toggle(); } return ok(); diff --git a/app/models/NotificationEvent.java b/app/models/NotificationEvent.java index 04d400640..607bf473b 100644 --- a/app/models/NotificationEvent.java +++ b/app/models/NotificationEvent.java @@ -47,6 +47,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static models.UserProjectNotification.findEventWatchersByEventType; import static models.Watch.findWatchers; import static models.enumeration.EventType.*; @@ -730,6 +731,7 @@ public static NotificationEvent forComment(Comment comment, User author, EventTy notiEvent.title = formatReplyTitle(post); notiEvent.eventType = eventType; Set receivers = getCommentReceivers(comment, author); + receivers.addAll(findEventWatchersByEventType(comment.projectId, eventType)); receivers.addAll(getMentionedUsers(comment.contents)); receivers.remove(author); notiEvent.receivers = receivers; diff --git a/app/models/UserProjectNotification.java b/app/models/UserProjectNotification.java index 1199f5766..93c25df28 100644 --- a/app/models/UserProjectNotification.java +++ b/app/models/UserProjectNotification.java @@ -1,32 +1,16 @@ /** - * Yobi, Project Hosting SW - * - * Copyright 2013 NAVER Corp. - * http://yobi.io - * - * @author Keesun Baik - * - * 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. - */ + * Yona, 21st Century Project Hosting SW + *

    + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. + * https://yona.io + **/ package models; import models.enumeration.EventType; import play.db.ebean.Model; import javax.persistence.*; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * User this class when someone want to know whether a user is receiving notification alarm from the project or not @@ -84,18 +68,26 @@ public static Map> getProjectNotifications(User * @return */ public static boolean isEnabledNotiType(Map> notiMap, Project project, EventType notiType) { - if(!notiMap.containsKey(project)) { - return true; + if(isJustDefaultWatching(notiMap, project)) { + return isNotifiedByDefault(notiType); } Map projectNoti = notiMap.get(project); - if(!projectNoti.containsKey(notiType)) { - return true; - } else { + if(isCustomizedByUser(notiType, projectNoti)) { return projectNoti.get(notiType); + } else { + return isNotifiedByDefault(notiType); } } + private static boolean isCustomizedByUser(EventType notiType, Map projectNoti) { + return projectNoti.containsKey(notiType); + } + + private static boolean isJustDefaultWatching(Map> notiMap, Project project) { + return !notiMap.containsKey(project); + } + public static UserProjectNotification findOne(User user, Project project, EventType notificationType) { return find.where() .eq("user", user) @@ -118,6 +110,15 @@ public static void unwatchExplictly(User user, Project project, EventType notiTy newOne.save(); } + public static void watchExplictly(User user, Project project, EventType notiType) { + UserProjectNotification newOne = new UserProjectNotification(); + newOne.user = user; + newOne.project = project; + newOne.notificationType = notiType; + newOne.allowed = true; + newOne.save(); + } + /** * * Basically, if there is no information about {@code project}' {@code notiType} @@ -132,4 +133,25 @@ public static boolean isEnabledNotiType(User user, Project project, EventType ev UserProjectNotification notification = findOne(user, project, eventType); return notification == null || notification.allowed; } + + public static boolean isNotifiedByDefault(EventType eventType) { + switch (eventType) { + case NEW_COMMENT: // events not notified by project watch default + return false; + default: + return true; + } + } + + public static Set findEventWatchersByEventType(Long projectId, EventType eventType) { + List userProjectNotifications = find.where() + .eq("project.id", projectId) + .eq("notificationType", eventType) + .findList(); + Set users = new LinkedHashSet<>(); + for (UserProjectNotification notification : userProjectNotifications) { + users.add(notification.user); + } + return users; + } } From 134d17aa15da0dc238eee2f61b7f0b1709023ee8 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Mon, 26 Feb 2018 00:44:27 +0900 Subject: [PATCH 70/77] misc: Fix Index Coverage Issue by search engine Some url shouldn't be called directly except when search engine crawlers do. Sometimes it make an error page. Google named it as 'Index Coverage Issue' Anyway it's good to be fixed. See: https://support.google.com/webmasters/answer/7440203?hl=en --- app/controllers/LabelApp.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/controllers/LabelApp.java b/app/controllers/LabelApp.java index 5d668644c..15a79e83c 100644 --- a/app/controllers/LabelApp.java +++ b/app/controllers/LabelApp.java @@ -53,6 +53,10 @@ public static Result labels(String query, String category, Integer limit) { return status(Http.Status.NOT_ACCEPTABLE); } + if (limit == null) { + return badRequest("No limit"); + } + ExpressionList

    + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. + * https://yona.io + **/ package utils; @@ -166,6 +152,9 @@ public static String getUrl(ReviewComment comment) { } public static String getUrl(CommentThread thread) { + if (thread == null) { + return ""; + } return diffRenderer.urlToContainer(thread) + "#thread-" + thread.id; } From bf210022e28875908847fb1a4235f064cedf4564 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Mon, 26 Feb 2018 01:12:44 +0900 Subject: [PATCH 72/77] mailbox: call by async when to start Mailbox service It may reduce starting up time of Yona. See: Yona Github issue #317 --- app/mailbox/MailboxService.java | 59 ++++++++++++++++----------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/app/mailbox/MailboxService.java b/app/mailbox/MailboxService.java index b2229a075..ebf7eca93 100644 --- a/app/mailbox/MailboxService.java +++ b/app/mailbox/MailboxService.java @@ -1,23 +1,10 @@ /** - * Yobi, Project Hosting SW - * - * Copyright 2014 NAVER Corp. - * http://yobi.io - * - * @Author Yi EungJun - * - * 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. - */ + * Yona, 21st Century Project Hosting SW + *

    + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. + * https://yona.io + **/ + package mailbox; import akka.actor.Cancellable; @@ -29,6 +16,7 @@ import play.Configuration; import play.Logger; import play.libs.Akka; +import play.libs.F; import scala.concurrent.duration.Duration; import utils.Diagnostic; import utils.SimpleDiagnostic; @@ -184,17 +172,7 @@ public void start() { return; } - try { - EmailHandler.handleNewMessages(folder); - } catch (MessagingException e) { - play.Logger.error("Failed to handle new messages"); - } - - try { - startEmailListener(); - } catch (Exception e) { - startEmailPolling(); - } + handleNewMessagesAndStartListener(); Diagnostic.register(new SimpleDiagnostic() { @Override @@ -210,6 +188,27 @@ public String checkOne() { }); } + private void handleNewMessagesAndStartListener() { + F.Promise promise = F.Promise.promise( + new F.Function0() { + public Void apply() { + try { + EmailHandler.handleNewMessages(folder); + } catch (MessagingException e) { + Logger.error("Failed to handle new messages"); + } + + try { + startEmailListener(); + } catch (Exception e) { + startEmailPolling(); + } + return null; + } + } + ); + } + /** * Reopen the IMAP folder which is used by MailboxService. * From af7aeebd233ce4edd576884f9360c460ae4c0041 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Mon, 26 Feb 2018 01:42:43 +0900 Subject: [PATCH 73/77] index: Show more notification body contents at index page --- app/assets/stylesheets/less/_page.less | 7 +------ app/views/index/partial_notifications.scala.html | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/less/_page.less b/app/assets/stylesheets/less/_page.less index f45211283..d653d9517 100644 --- a/app/assets/stylesheets/less/_page.less +++ b/app/assets/stylesheets/less/_page.less @@ -1172,12 +1172,7 @@ &.nowrap { min-height:20px; - height:20px; - .message { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } + max-height:200px; } } } diff --git a/app/views/index/partial_notifications.scala.html b/app/views/index/partial_notifications.scala.html index fbb3459e2..e1a1bc947 100644 --- a/app/views/index/partial_notifications.scala.html +++ b/app/views/index/partial_notifications.scala.html @@ -67,7 +67,7 @@ }

    -
    @Html(HtmlUtil.defaultSanitize(noti.getMessage.replaceAll("\n", "
    \n")))
    +
    @Html(HtmlUtil.defaultSanitize(noti.getMessage.replaceAll("[^>]\n", "
    \n")))
    @if(user != null){ From fb9352496e92de40e72feef409ee0dcce9e7c87d Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Thu, 1 Mar 2018 16:57:52 +0900 Subject: [PATCH 74/77] noti: Fix missing condition of findEventWatchersByEventType When to find watchers of specific event, important option was omitted. This mistake is occurring receiving unintended notification mails. Originally this bug was derived from commit 104d07e --- app/models/UserProjectNotification.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/UserProjectNotification.java b/app/models/UserProjectNotification.java index 93c25df28..ed7f43b6f 100644 --- a/app/models/UserProjectNotification.java +++ b/app/models/UserProjectNotification.java @@ -147,6 +147,7 @@ public static Set findEventWatchersByEventType(Long projectId, EventType e List userProjectNotifications = find.where() .eq("project.id", projectId) .eq("notificationType", eventType) + .eq("allowed", true) .findList(); Set users = new LinkedHashSet<>(); for (UserProjectNotification notification : userProjectNotifications) { From 5b73382c4ea7f402f3ca28d438ce1e6e224018ad Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Tue, 27 Feb 2018 11:14:20 +0900 Subject: [PATCH 75/77] noti: Send notification when issue/posting is deleted See: Yona Github issue #306 --- app/controllers/BoardApp.java | 1 + app/controllers/IssueApp.java | 1 + app/models/NotificationEvent.java | 18 ++++++++++++++++++ app/models/enumeration/EventType.java | 3 ++- conf/messages | 2 ++ conf/messages.ko-KR | 2 ++ 6 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/controllers/BoardApp.java b/app/controllers/BoardApp.java index ce9cfb1eb..78ea97b95 100644 --- a/app/controllers/BoardApp.java +++ b/app/controllers/BoardApp.java @@ -380,6 +380,7 @@ public static Result deletePost(String owner, String projectName, Long number) { Posting posting = Posting.findByNumber(project, number); Call redirectTo = routes.BoardApp.posts(project.owner, project.name, 1); + NotificationEvent.afterResourceDeleted(posting, UserApp.currentUser()); return delete(posting, posting.asResource(), redirectTo); } diff --git a/app/controllers/IssueApp.java b/app/controllers/IssueApp.java index 9a6a6134c..afe61d6ff 100644 --- a/app/controllers/IssueApp.java +++ b/app/controllers/IssueApp.java @@ -850,6 +850,7 @@ public static Result deleteIssue(String ownerName, String projectName, Long numb Call redirectTo = routes.IssueApp.issues(project.owner, project.name, State.OPEN.state(), "html", 1); + NotificationEvent.afterResourceDeleted(issue, UserApp.currentUser()); return delete(issue, issue.asResource(), redirectTo); } diff --git a/app/models/NotificationEvent.java b/app/models/NotificationEvent.java index 607bf473b..8e1616e16 100644 --- a/app/models/NotificationEvent.java +++ b/app/models/NotificationEvent.java @@ -214,6 +214,9 @@ public String getMessage(Lang lang) { } else if (StringUtils.isNotBlank(oldValue)) { return Messages.get(lang, "notification.issue.label.deleted"); } + case RESOURCE_DELETED: + User user = User.findByLoginId(newValue); + return Messages.get(lang, "notification.resource.deleted", user.getDisplayName(user)); default: play.Logger.warn("Unknown event message: " + this); play.Logger.warn("Event Type: " + eventType); @@ -864,6 +867,21 @@ public static NotificationEvent forNewIssue(Issue issue, User author) { return notiEvent; } + public static NotificationEvent afterResourceDeleted(AbstractPosting item, User reuqestedUser) { + NotificationEvent notiEvent = createFrom(reuqestedUser, item.project); + notiEvent.title = formatNewTitle(item); + notiEvent.receivers = getReceivers(item, reuqestedUser); + notiEvent.eventType = RESOURCE_DELETED; + notiEvent.oldValue = item.body; + notiEvent.newValue = reuqestedUser.loginId; + + NotificationEvent.add(notiEvent); + if (item instanceof Issue) { + webhookRequest(RESOURCE_DELETED, (Issue)item, false); + } + return notiEvent; + } + public static NotificationEvent afterIssueBodyChanged(String oldBody, Issue issue) { webhookRequest(ISSUE_BODY_CHANGED, issue, false); diff --git a/app/models/enumeration/EventType.java b/app/models/enumeration/EventType.java index e6286d278..6cd407710 100644 --- a/app/models/enumeration/EventType.java +++ b/app/models/enumeration/EventType.java @@ -38,7 +38,8 @@ public enum EventType { ISSUE_SHARER_CHANGED("notification.type.issue.sharer.changed", 22), ISSUE_LABEL_CHANGED("notification.type.issue.label.changed", 23), ISSUE_MILESTONE_CHANGED("notification.type.milestone.changed", 24), - POSTING_BODY_CHANGED("notification.type.posting.body.changed", 25); + POSTING_BODY_CHANGED("notification.type.posting.body.changed", 25), + RESOURCE_DELETED("notification.type.resource.deleted", 26); private String descr; diff --git a/conf/messages b/conf/messages index 33b8a77d8..f38f7fb5e 100644 --- a/conf/messages +++ b/conf/messages @@ -453,6 +453,7 @@ notification.replyOrLinkToViewHtml = Reply to this email directly or {0}에서 notification.reviewthread.closed = 리뷰 스레드 닫힘 notification.reviewthread.inTheFile = {0} 에서: notification.reviewthread.reopened = 리뷰 스레드 다시 열림 +notification.resource.deleted = {0} 님에 의해 삭제되었습니다 notification.send.mail = 수정에 대한 알림 메일 발송 notification.send.mail.warning = 만약 해당글의 원 작성자가 아니라면 이 옵션은 무시되고 알림 메일이 발송됩니다. notification.type.comment.updated = 댓글 수정 @@ -483,6 +484,7 @@ notification.type.pullrequest.review.action.changed = 코드보내기 리뷰 액 notification.type.pullrequest.reviewed = 코드보내기 리뷰가 완료되었습니다. notification.type.pullrequest.state.changed = 코드보내기 상태 변경 notification.type.pullrequest.unreviewed = 코드보내기 리뷰가 취소되었습니다. +notification.type.resource.deleted = 삭제되었습니다 notification.type.review.state.changed = 리뷰 스레드 상태 변경 notification.unwatch = 그만 지켜보기 notification.watch = 지켜보기 From ff95d5e5ec15dc9091e8a8d3fd1bd238afa50350 Mon Sep 17 00:00:00 2001 From: Suwon Chae Date: Fri, 2 Mar 2018 16:12:20 +0900 Subject: [PATCH 76/77] noti: Polish notification features - Refactor methods - Change messages - Reduce notification scope of new commit codes --- app/actors/CommitsNotificationActor.java | 32 ++------ app/controllers/IssueApp.java | 10 +-- app/controllers/WatchProjectApp.java | 6 +- app/models/NotificationEvent.java | 94 ++++++++++++++++-------- app/models/UserProjectNotification.java | 22 ++++-- conf/messages | 2 +- conf/messages.ko-KR | 2 +- 7 files changed, 98 insertions(+), 70 deletions(-) diff --git a/app/actors/CommitsNotificationActor.java b/app/actors/CommitsNotificationActor.java index b7ede31b1..f1a88ccc7 100644 --- a/app/actors/CommitsNotificationActor.java +++ b/app/actors/CommitsNotificationActor.java @@ -1,23 +1,10 @@ /** - * Yobi, Project Hosting SW - * - * Copyright 2013 NAVER Corp. - * http://yobi.io - * - * @author Keesun Baik - * - * 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. - */ + * Yona, 21st Century Project Hosting SW + *

    + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. + * https://yona.io + **/ + package actors; import models.*; @@ -29,8 +16,6 @@ /** * Creates new commit notifications. - * - * @author Keesun Baik */ public class CommitsNotificationActor extends PostReceiveActor { @@ -50,10 +35,7 @@ void doReceive(PostReceiveMessage message) { title = Messages.get("notification.pushed.commits", project.name, commits.size()); } - Set watchers = Watch.findWatchers(project.asResource()); - watchers.remove(sender); - - NotificationEvent.afterNewCommits(commits, refNames, project, sender, title, watchers); + NotificationEvent.afterNewCommits(commits, refNames, project, sender, title); } } diff --git a/app/controllers/IssueApp.java b/app/controllers/IssueApp.java index afe61d6ff..c6fae772c 100644 --- a/app/controllers/IssueApp.java +++ b/app/controllers/IssueApp.java @@ -900,16 +900,16 @@ public static Result newComment(String ownerName, String projectName, Long numbe private static Comment saveComment(Project project, Issue issue, IssueComment comment) { Comment savedComment; IssueComment existingComment = IssueComment.find.where().eq("id", comment.id).findUnique(); - if (existingComment != null) { + if (existingComment == null) { + comment.projectId = project.id; + savedComment = saveComment(comment, getContainerUpdater(issue, comment)); + NotificationEvent.afterNewComment(savedComment); + } else { existingComment.contents = comment.contents; savedComment = saveComment(existingComment, getContainerUpdater(issue, comment)); if(isSelectedToSendNotificationMail() || !existingComment.isAuthoredBy(UserApp.currentUser())){ NotificationEvent.afterCommentUpdated(savedComment); } - } else { - comment.projectId = project.id; - savedComment = saveComment(comment, getContainerUpdater(issue, comment)); - NotificationEvent.afterNewComment(savedComment); } return savedComment; } diff --git a/app/controllers/WatchProjectApp.java b/app/controllers/WatchProjectApp.java index cbf199aeb..fada9cd56 100644 --- a/app/controllers/WatchProjectApp.java +++ b/app/controllers/WatchProjectApp.java @@ -60,12 +60,12 @@ public static Result toggle(Long projectId, String notificationType) { UserProjectNotification userProjectNotification = findOne(user, project, notiType); if(userProjectNotification == null) { // not specified yet if (isNotifiedByDefault(notiType)) { - watchExplictly(user, project, notiType); - } else { unwatchExplictly(user, project, notiType); + } else { + watchExplictly(user, project, notiType); } } else { - userProjectNotification.toggle(); + userProjectNotification.toggle(notiType); } return ok(); diff --git a/app/models/NotificationEvent.java b/app/models/NotificationEvent.java index 8e1616e16..545dadd87 100644 --- a/app/models/NotificationEvent.java +++ b/app/models/NotificationEvent.java @@ -39,15 +39,14 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static models.UserProjectNotification.findEventUnwatchersByEventType; import static models.UserProjectNotification.findEventWatchersByEventType; +import static models.Watch.findUnwatchers; import static models.Watch.findWatchers; import static models.enumeration.EventType.*; @@ -733,11 +732,7 @@ public static NotificationEvent forComment(Comment comment, User author, EventTy NotificationEvent notiEvent = createFrom(author, comment); notiEvent.title = formatReplyTitle(post); notiEvent.eventType = eventType; - Set receivers = getCommentReceivers(comment, author); - receivers.addAll(findEventWatchersByEventType(comment.projectId, eventType)); - receivers.addAll(getMentionedUsers(comment.contents)); - receivers.remove(author); - notiEvent.receivers = receivers; + notiEvent.receivers = getMandatoryReceivers(comment, eventType); notiEvent.oldValue = null; notiEvent.newValue = comment.contents; notiEvent.resourceType = comment.asResource().getType(); @@ -776,7 +771,7 @@ public static NotificationEvent afterStateChanged(State oldState, Issue issue) { NotificationEvent notiEvent = createFromCurrentUser(issue); notiEvent.title = formatReplyTitle(issue); - notiEvent.receivers = getMandatoryReceivers(issue); + notiEvent.receivers = getMandatoryReceivers(issue, EventType.ISSUE_STATE_CHANGED); notiEvent.eventType = ISSUE_STATE_CHANGED; notiEvent.oldValue = oldState != null ? oldState.state() : null; notiEvent.newValue = issue.state.state(); @@ -842,7 +837,7 @@ public static NotificationEvent afterAssigneeChanged(User oldAssignee, Issue iss } private static Set getReceiversWhenAssigneeChanged(User oldAssignee, Issue issue) { - Set receivers = getMandatoryReceivers(issue); + Set receivers = getMandatoryReceivers(issue, ISSUE_ASSIGNEE_CHANGED); if (oldAssignee != null && !oldAssignee.isAnonymous() && !oldAssignee.loginId.equals(UserApp.currentUser().loginId)) { @@ -953,7 +948,7 @@ public static NotificationEvent afterMilestoneChanged(Long oldMilestoneId, Issue NotificationEvent notiEvent = createFromCurrentUser(issue); - Set receivers = getMandatoryReceivers(issue); + Set receivers = getMandatoryReceivers(issue, ISSUE_MILESTONE_CHANGED); notiEvent.title = formatReplyTitle(issue); notiEvent.receivers = receivers; @@ -966,7 +961,7 @@ public static NotificationEvent afterMilestoneChanged(Long oldMilestoneId, Issue return notiEvent; } - private static Set getMandatoryReceivers(Issue issue) { + private static Set getMandatoryReceivers(Issue issue, EventType eventType) { Set receivers = findWatchers(issue.asResource()); receivers.add(issue.getAuthor()); @@ -978,22 +973,74 @@ private static Set getMandatoryReceivers(Issue issue) { receivers.add(issue.assignee.user); } + receivers.addAll(findWatchers(issue.asResource())); + receivers.addAll(findEventWatchersByEventType(issue.project.id, eventType)); + + receivers.removeAll(findUnwatchers(issue.asResource())); + receivers.removeAll(findEventUnwatchersByEventType(issue.project.id, eventType)); receivers.remove(UserApp.currentUser()); return receivers; } - private static Set getMandatoryReceivers(Posting posting) { + private static Set getMandatoryReceivers(Posting posting, EventType eventType) { Set receivers = findWatchers(posting.asResource()); receivers.add(posting.getAuthor()); + receivers.addAll(findWatchers(posting.asResource())); + receivers.addAll(findEventWatchersByEventType(posting.project.id, eventType)); + + receivers.removeAll(findUnwatchers(posting.asResource())); + receivers.removeAll(findEventUnwatchersByEventType(posting.project.id, eventType)); + receivers.remove(UserApp.currentUser()); + + return receivers; + } + + private static Set getMandatoryReceivers(Comment comment, EventType eventType) { + AbstractPosting parent = comment.getParent(); + Set receivers = findWatchers(parent.asResource()); + receivers.add(parent.getAuthor()); + receivers.addAll(findEventWatchersByEventType(comment.projectId, eventType)); + receivers.addAll(getMentionedUsers(comment.contents)); + includeAssigneeIfExist(comment, receivers); + + receivers.removeAll(findUnwatchers(parent.asResource())); + receivers.removeAll(findEventUnwatchersByEventType(comment.projectId, eventType)); + receivers.remove(UserApp.currentUser()); + + return receivers; + } + private static Set getProjectCommitReceivers(Project project, EventType eventType) { + Set receivers = findMembersOnlyFromWatchers(project); + receivers.removeAll(findUnwatchers(project.asResource())); + receivers.removeAll(findEventUnwatchersByEventType(project.id, eventType)); receivers.remove(UserApp.currentUser()); return receivers; } + private static Set findMembersOnlyFromWatchers(Project project) { + Set receivers = new HashSet<>(); + Set projectMembers = extractMembers(project); + for (User watcher : findWatchers(project.asResource())) { + if (projectMembers.contains(watcher)) { + receivers.add(watcher); + } + } + return receivers; + } + + private static Set extractMembers(Project project) { + Set projectMembers = new HashSet<>(); + for (ProjectUser projectUser : project.members()) { + projectMembers.add(projectUser.user); + } + return projectMembers; + } + private static Set getReceiversForIssueBodyChanged(String oldBody, Issue issue) { - Set receivers = getMandatoryReceivers(issue); + Set receivers = getMandatoryReceivers(issue, ISSUE_BODY_CHANGED); receivers.addAll(getNewMentionedUsers(oldBody, issue.body)); receivers.remove(UserApp.currentUser()); return receivers; @@ -1020,7 +1067,7 @@ public static NotificationEvent forNewPosting(Posting post, User author) { public static NotificationEvent forUpdatePosting(String oldValue, Posting post, User author) { NotificationEvent notiEvent = createFrom(author, post); notiEvent.title = formatNewTitle(post); - notiEvent.receivers = getMandatoryReceivers(post); + notiEvent.receivers = getMandatoryReceivers(post, EventType.POSTING_BODY_CHANGED); notiEvent.eventType = POSTING_BODY_CHANGED; notiEvent.oldValue = oldValue; notiEvent.newValue = post.body; @@ -1133,10 +1180,10 @@ public static void afterOrganizationMemberRequest(Organization organization, Use NotificationEvent.add(notiEvent); } - public static void afterNewCommits(List commits, List refNames, Project project, User sender, String title, Set watchers) { + public static void afterNewCommits(List commits, List refNames, Project project, User sender, String title) { NotificationEvent notiEvent = createFrom(sender, project); notiEvent.title = title; - notiEvent.receivers = watchers; + notiEvent.receivers = getProjectCommitReceivers(project, NEW_COMMIT); notiEvent.eventType = NEW_COMMIT; notiEvent.oldValue = null; notiEvent.newValue = newCommitsMessage(commits, refNames, project); @@ -1233,17 +1280,6 @@ private static Set getReceivers(AbstractPosting abstractPosting, User exce return receivers; } - private static Set getCommentReceivers(Comment comment, User except) { - AbstractPosting parent = comment.getParent(); - - Set receivers = new HashSet<>(findWatchers(parent.asResource())); - receivers.add(comment.getParent().getAuthor()); - includeAssigneeIfExist(comment, receivers); - receivers.remove(except); - - return receivers; - } - private static void includeAssigneeIfExist(Comment comment, Set receivers) { if (comment instanceof IssueComment) { Assignee assignee = ((Issue) comment.getParent()).assignee; diff --git a/app/models/UserProjectNotification.java b/app/models/UserProjectNotification.java index ed7f43b6f..708a07689 100644 --- a/app/models/UserProjectNotification.java +++ b/app/models/UserProjectNotification.java @@ -13,9 +13,7 @@ import java.util.*; /** - * User this class when someone want to know whether a user is receiving notification alarm from the project or not - * - * @author Keesun Baik + * Project notification subscribing settings with events which are customized by user */ @Entity @Table(uniqueConstraints = @UniqueConstraint(columnNames = {"project_id", "user_id", "notification_type"})) @@ -96,9 +94,13 @@ public static UserProjectNotification findOne(User user, Project project, EventT .findUnique(); } - public void toggle() { + public void toggle(EventType notificationType) { this.allowed = !this.allowed; - update(); + if (allowed == isNotifiedByDefault(notificationType)) { + delete(); + } else { + update(); + } } public static void unwatchExplictly(User user, Project project, EventType notiType) { @@ -144,10 +146,18 @@ public static boolean isNotifiedByDefault(EventType eventType) { } public static Set findEventWatchersByEventType(Long projectId, EventType eventType) { + return findByEventTypeAndOption(projectId, eventType, true); + } + + public static Set findEventUnwatchersByEventType(Long projectId, EventType eventType) { + return findByEventTypeAndOption(projectId, eventType, false); + } + + private static Set findByEventTypeAndOption(Long projectId, EventType eventType, boolean isAllowd) { List userProjectNotifications = find.where() .eq("project.id", projectId) .eq("notificationType", eventType) - .eq("allowed", true) + .eq("allowed", isAllowd) .findList(); Set users = new LinkedHashSet<>(); for (UserProjectNotification notification : userProjectNotifications) { diff --git a/conf/messages b/conf/messages index f38f7fb5e..e12fbeabb 100644 --- a/conf/messages +++ b/conf/messages @@ -483,7 +483,7 @@ notification.type.pullrequest.review.action.changed = PullRequest Review Action notification.type.pullrequest.reviewed = Pull request review completed. notification.type.pullrequest.state.changed = Pull request Status changed. notification.type.pullrequest.unreviewed = Pull request review is canceled. -notification.type.resource.deleted = Deleted +notification.type.resource.deleted = Issue/Posting deletion notification.type.review.state.changed = Review Thread State Change notification.unwatch = Unwatch notification.watch = Watch diff --git a/conf/messages.ko-KR b/conf/messages.ko-KR index c985553f4..01ac2224b 100644 --- a/conf/messages.ko-KR +++ b/conf/messages.ko-KR @@ -484,7 +484,7 @@ notification.type.pullrequest.review.action.changed = 코드보내기 리뷰 액 notification.type.pullrequest.reviewed = 코드보내기 리뷰가 완료되었습니다. notification.type.pullrequest.state.changed = 코드보내기 상태 변경 notification.type.pullrequest.unreviewed = 코드보내기 리뷰가 취소되었습니다. -notification.type.resource.deleted = 삭제되었습니다 +notification.type.resource.deleted = 이슈/게시글 삭제 notification.type.review.state.changed = 리뷰 스레드 상태 변경 notification.unwatch = 그만 지켜보기 notification.watch = 지켜보기 From 7f5f31b26c259c5764a7722c21ccd95bf052e4aa Mon Sep 17 00:00:00 2001 From: Mijeong Park Date: Tue, 6 Mar 2018 14:40:48 +0900 Subject: [PATCH 77/77] version: v1.9.0 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index ec901f027..1b2490cc5 100644 --- a/build.sbt +++ b/build.sbt @@ -3,7 +3,7 @@ import java.nio.file.Paths name := """yona""" -version := "1.8.1" +version := "1.9.0" libraryDependencies ++= Seq( // Add your project dependencies here,