Merge branch 'next' before release v1.9.0
Reviewed-by: Suwon Chae Reviewed-by: Mijeong Park
@033bbd48516253afec671807b4e55505af27c88d
--- AUTHORS
+++ AUTHORS
... | ... | @@ -48,3 +48,4 @@ |
48 | 48 |
rimi <rimi@userinsight.co.kr> |
49 | 49 |
kenu <kenu.heo@gmail.com> |
50 | 50 |
DongHo Byun <cpascal@nextfloor.com> |
51 |
+Mijeong Park <p.mj@naverlabs.com> |
--- app/Global.java
+++ app/Global.java
... | ... | @@ -158,17 +158,27 @@ |
158 | 158 |
isSecretInvalid = equalsDefaultSecret(); |
159 | 159 |
insertInitialData(); |
160 | 160 |
|
161 |
+ Timestamp timestamp = new Timestamp("=== Yona server starting initialization ==="); |
|
161 | 162 |
Config.onStart(); |
163 |
+ timestamp.logElapsedTime("--- Config reading: ok!"); |
|
162 | 164 |
Property.onStart(); |
165 |
+ timestamp.logElapsedTime("--- Property reading: ok!"); |
|
163 | 166 |
PullRequest.onStart(); |
167 |
+ timestamp.logElapsedTime("--- Pull request checking: ok!"); |
|
164 | 168 |
NotificationMail.onStart(); |
169 |
+ timestamp.logElapsedTime("--- Notification mail scheduler: ok!"); |
|
165 | 170 |
NotificationEvent.onStart(); |
171 |
+ timestamp.logElapsedTime("--- Notification event cleanup scheduler: ok!"); |
|
166 | 172 |
Attachment.onStart(); |
173 |
+ timestamp.logElapsedTime("--- Temporary files cleanup scheduler: ok!"); |
|
167 | 174 |
AccessControl.onStart(); |
175 |
+ timestamp.logElapsedTime("--- Basic access controller config reading: ok!"); |
|
168 | 176 |
|
169 | 177 |
if (!isSecretInvalid) { |
170 | 178 |
YobiUpdate.onStart(); |
179 |
+ timestamp.logElapsedTime("--- Update checker run: ok! "); |
|
171 | 180 |
mailboxService.start(); |
181 |
+ timestamp.logElapsedTime("--- MailboxService checker run: ok!"); |
|
172 | 182 |
} |
173 | 183 |
|
174 | 184 |
PlayAuthenticate.setResolver(new PlayAuthenticate.Resolver() { |
--- app/actors/CommitsNotificationActor.java
+++ app/actors/CommitsNotificationActor.java
... | ... | @@ -1,23 +1,10 @@ |
1 | 1 |
/** |
2 |
- * Yobi, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2013 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @author Keesun Baik |
|
8 |
- * |
|
9 |
- * Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
- * you may not use this file except in compliance with the License. |
|
11 |
- * You may obtain a copy of the License at |
|
12 |
- * |
|
13 |
- * http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
- * |
|
15 |
- * Unless required by applicable law or agreed to in writing, software |
|
16 |
- * distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
- * See the License for the specific language governing permissions and |
|
19 |
- * limitations under the License. |
|
20 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
7 |
+ |
|
21 | 8 |
package actors; |
22 | 9 |
|
23 | 10 |
import models.*; |
... | ... | @@ -29,8 +16,6 @@ |
29 | 16 |
|
30 | 17 |
/** |
31 | 18 |
* Creates new commit notifications. |
32 |
- * |
|
33 |
- * @author Keesun Baik |
|
34 | 19 |
*/ |
35 | 20 |
public class CommitsNotificationActor extends PostReceiveActor { |
36 | 21 |
|
... | ... | @@ -50,10 +35,7 @@ |
50 | 35 |
title = Messages.get("notification.pushed.commits", project.name, commits.size()); |
51 | 36 |
} |
52 | 37 |
|
53 |
- Set<User> watchers = Watch.findWatchers(project.asResource()); |
|
54 |
- watchers.remove(sender); |
|
55 |
- |
|
56 |
- NotificationEvent.afterNewCommits(commits, refNames, project, sender, title, watchers); |
|
38 |
+ NotificationEvent.afterNewCommits(commits, refNames, project, sender, title); |
|
57 | 39 |
} |
58 | 40 |
|
59 | 41 |
} |
--- app/assets/stylesheets/less/_common.less
+++ app/assets/stylesheets/less/_common.less
... | ... | @@ -219,6 +219,7 @@ |
219 | 219 |
.mt4 { margin-top:4px; } |
220 | 220 |
.mr3 { margin-right:3px; } |
221 | 221 |
.pb4 { padding-bottom: 4px} |
222 |
+.pl0 { padding-left: 0} |
|
222 | 223 |
|
223 | 224 |
.margin-top-20 { margin-top:20px; } |
224 | 225 |
.margin-left-20 { margin-left: 20px; } |
... | ... | @@ -277,3 +278,31 @@ |
277 | 278 |
input.white:-moz-placeholder { color:#fff; opacity:0.8; } /* Firefox 18- */ |
278 | 279 |
input.white::-moz-placeholder { color:#fff; opacity:0.8; } /* Firefox 19+ */ |
279 | 280 |
input.white:-ms-input-placeholder { color:#fff; opacity:0.8; } |
281 |
+ |
|
282 |
+.va-text-top { |
|
283 |
+ vertical-align: text-top !important; |
|
284 |
+} |
|
285 |
+ |
|
286 |
+.width100p { |
|
287 |
+ width: 100% |
|
288 |
+} |
|
289 |
+ |
|
290 |
+.text-ellipsis { |
|
291 |
+ white-space: nowrap; |
|
292 |
+ overflow: hidden; |
|
293 |
+ text-overflow: ellipsis; |
|
294 |
+} |
|
295 |
+ |
|
296 |
+.z-index-1 { |
|
297 |
+ z-index: 1 !important; |
|
298 |
+} |
|
299 |
+ |
|
300 |
+.hideFromDisplayOnly { |
|
301 |
+ display: none; |
|
302 |
+} |
|
303 |
+ |
|
304 |
+.fixed-height-my-issues-list { |
|
305 |
+ line-height: 36px; |
|
306 |
+} |
|
307 |
+ |
|
308 |
+.dimgray { color:dimgray; }(No newline at end of file) |
--- app/assets/stylesheets/less/_override.less
+++ app/assets/stylesheets/less/_override.less
... | ... | @@ -182,6 +182,23 @@ |
182 | 182 |
.box-shadow(none); |
183 | 183 |
} |
184 | 184 |
|
185 |
+.sharer-list { |
|
186 |
+ .select2-container{ |
|
187 |
+ border: none; |
|
188 |
+ box-shadow: none; |
|
189 |
+ border-radius: 0 !important; |
|
190 |
+ border-bottom: 1px solid rgba(0, 0, 0, 0.15); |
|
191 |
+ } |
|
192 |
+ .select2-container-multi { |
|
193 |
+ .select2-choices { |
|
194 |
+ .select2-search-choice { |
|
195 |
+ background-color: #ececec; |
|
196 |
+ border: 1px solid #dfdfdf; |
|
197 |
+ } |
|
198 |
+ } |
|
199 |
+ } |
|
200 |
+} |
|
201 |
+ |
|
185 | 202 |
.select2-dropdown-open { |
186 | 203 |
.select2-choice { |
187 | 204 |
border-bottom-color: transparent; |
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
... | ... | @@ -419,6 +419,19 @@ |
419 | 419 |
font-size: 14px; |
420 | 420 |
margin-left:5px; |
421 | 421 |
} |
422 |
+ |
|
423 |
+ .user-project-list { |
|
424 |
+ .star { |
|
425 |
+ color: rgba(255, 255, 255, 0.22); |
|
426 |
+ &:hover { |
|
427 |
+ color: #e91e63; |
|
428 |
+ cursor: pointer; |
|
429 |
+ } |
|
430 |
+ } |
|
431 |
+ .starred { |
|
432 |
+ color: #e91e63 !important; |
|
433 |
+ } |
|
434 |
+ } |
|
422 | 435 |
} |
423 | 436 |
|
424 | 437 |
.project-origin { |
... | ... | @@ -1159,12 +1172,7 @@ |
1159 | 1172 |
|
1160 | 1173 |
&.nowrap { |
1161 | 1174 |
min-height:20px; |
1162 |
- height:20px; |
|
1163 |
- .message { |
|
1164 |
- text-overflow: ellipsis; |
|
1165 |
- white-space: nowrap; |
|
1166 |
- overflow: hidden; |
|
1167 |
- } |
|
1175 |
+ max-height:200px; |
|
1168 | 1176 |
} |
1169 | 1177 |
} |
1170 | 1178 |
} |
... | ... | @@ -3029,7 +3037,7 @@ |
3029 | 3037 |
} |
3030 | 3038 |
|
3031 | 3039 |
.event { |
3032 |
- padding: 10px 0 10px 55px; |
|
3040 |
+ padding: 2px 0 2px 55px; |
|
3033 | 3041 |
font-size: 1em; |
3034 | 3042 |
line-height: 30px; |
3035 | 3043 |
|
... | ... | @@ -3053,6 +3061,25 @@ |
3053 | 3061 |
&.rejected {background: #F39C12;} |
3054 | 3062 |
&.conflict {background: #C0392B;} |
3055 | 3063 |
&.resolved {background: #468847;} |
3064 |
+ &.sharer-added { |
|
3065 |
+ background-color: #B2EBF2; |
|
3066 |
+ color: #0097A7; |
|
3067 |
+ } |
|
3068 |
+ &.sharer-deleted { |
|
3069 |
+ background-color: #FFCCBC; |
|
3070 |
+ color: #F4511E; |
|
3071 |
+ } |
|
3072 |
+ &.label-added { |
|
3073 |
+ background-color: #B2EBF2; |
|
3074 |
+ color: #0097A7; |
|
3075 |
+ } |
|
3076 |
+ &.label-deleted { |
|
3077 |
+ background-color: #FFCCBC; |
|
3078 |
+ color: #F4511E; |
|
3079 |
+ } |
|
3080 |
+ &.milestone-changed { |
|
3081 |
+ background-color: #0088cc; |
|
3082 |
+ } |
|
3056 | 3083 |
} |
3057 | 3084 |
|
3058 | 3085 |
em { font-size:12px; color:#7F8C8D; margin-right:5px;} |
... | ... | @@ -3652,6 +3679,7 @@ |
3652 | 3679 |
line-height: 20px; |
3653 | 3680 |
font-size:12px; |
3654 | 3681 |
color:#999; |
3682 |
+ overflow: hidden; |
|
3655 | 3683 |
|
3656 | 3684 |
.infos-item { |
3657 | 3685 |
margin-right:6px; |
... | ... | @@ -3732,8 +3760,15 @@ |
3732 | 3760 |
.comments-count-color { |
3733 | 3761 |
color: @darkmagenta; |
3734 | 3762 |
} |
3763 |
+ .sharer-color { |
|
3764 |
+ color: @green; |
|
3765 |
+ } |
|
3735 | 3766 |
|
3736 | 3767 |
a:nth-child(2) { |
3768 |
+ margin-left: -5px; |
|
3769 |
+ } |
|
3770 |
+ |
|
3771 |
+ a:nth-child(3) { |
|
3737 | 3772 |
margin-left: -5px; |
3738 | 3773 |
} |
3739 | 3774 |
|
... | ... | @@ -3783,7 +3818,6 @@ |
3783 | 3818 |
.mileston-tag { |
3784 | 3819 |
max-width: 135px; |
3785 | 3820 |
text-overflow: ellipsis; |
3786 |
- white-space: nowrap; |
|
3787 | 3821 |
overflow: hidden; |
3788 | 3822 |
color: #2196f3; |
3789 | 3823 |
font-size:11px; |
... | ... | @@ -6984,7 +7018,7 @@ |
6984 | 7018 |
-moz-shadow: inset 0 0 5px 5px #222; |
6985 | 7019 |
//box-shadow: inset 0 0 5px 5px #222; |
6986 | 7020 |
box-shadow: 2px 2px 8px #000; |
6987 |
- background: #FFF url('/assets/images/loading-gif-2.gif') no-repeat center; |
|
7021 |
+ background: #FFF url('@{base-image-path}/loading-gif-2.gif') no-repeat center; |
|
6988 | 7022 |
} |
6989 | 7023 |
|
6990 | 7024 |
.myissues-search-input { |
... | ... | @@ -6997,6 +7031,7 @@ |
6997 | 7031 |
font-size: 15px; |
6998 | 7032 |
color: rgba(0, 0, 0, 0.7); |
6999 | 7033 |
padding: 0 2px; |
7034 |
+ vertical-align: top; |
|
7000 | 7035 |
} |
7001 | 7036 |
|
7002 | 7037 |
.no-margin { |
... | ... | @@ -7005,11 +7040,56 @@ |
7005 | 7040 |
|
7006 | 7041 |
.my-issues { |
7007 | 7042 |
.post-item { |
7008 |
- padding: 6px 10px; |
|
7043 |
+ padding: 2px 10px; |
|
7044 |
+ color: #999999; |
|
7009 | 7045 |
|
7010 | 7046 |
.title-wrap { |
7011 | 7047 |
margin-top: 2px; |
7048 |
+ white-space: normal; |
|
7049 |
+ overflow: auto; |
|
7050 |
+ display:table; |
|
7051 |
+ |
|
7052 |
+ .title-cell { |
|
7053 |
+ display:table-cell; |
|
7054 |
+ vertical-align:middle; |
|
7055 |
+ } |
|
7056 |
+ |
|
7057 |
+ .item-count-groups { |
|
7058 |
+ font-size: 10px; |
|
7059 |
+ } |
|
7060 |
+ .title { |
|
7061 |
+ font-size: 14px; |
|
7062 |
+ font-weight: 400; |
|
7063 |
+ } |
|
7012 | 7064 |
} |
7065 |
+ |
|
7066 |
+ .author { |
|
7067 |
+ display:table; |
|
7068 |
+ |
|
7069 |
+ .author-cell { |
|
7070 |
+ display:table-cell; |
|
7071 |
+ vertical-align:middle; |
|
7072 |
+ text-overflow: ellipsis; |
|
7073 |
+ overflow: hidden; |
|
7074 |
+ white-space: nowrap; |
|
7075 |
+ } |
|
7076 |
+ } |
|
7077 |
+ .meta { |
|
7078 |
+ display:table; |
|
7079 |
+ |
|
7080 |
+ .meta-cell { |
|
7081 |
+ display:table-cell; |
|
7082 |
+ vertical-align:middle; |
|
7083 |
+ } |
|
7084 |
+ } |
|
7085 |
+ |
|
7086 |
+ .post-id { |
|
7087 |
+ color:#999; |
|
7088 |
+ margin-right:5px; |
|
7089 |
+ font-size: 12px; |
|
7090 |
+ font-weight: normal; |
|
7091 |
+ } |
|
7092 |
+ |
|
7013 | 7093 |
.infos { |
7014 | 7094 |
margin-top: 4px; |
7015 | 7095 |
} |
... | ... | @@ -7056,3 +7136,34 @@ |
7056 | 7136 |
border-radius: 4px; |
7057 | 7137 |
} |
7058 | 7138 |
} |
7139 |
+ |
|
7140 |
+.sharer-list { |
|
7141 |
+ margin-top: 40px; |
|
7142 |
+ padding: 10px; |
|
7143 |
+ |
|
7144 |
+ .issue-share-title { |
|
7145 |
+ font-size: 16px; |
|
7146 |
+ } |
|
7147 |
+ .sharer-item{ |
|
7148 |
+ display: inline-block; |
|
7149 |
+ background-color: #ececec; |
|
7150 |
+ border: 1px solid #dfdfdf; |
|
7151 |
+ border-radius: 3px; |
|
7152 |
+ padding: 1px 8px; |
|
7153 |
+ } |
|
7154 |
+} |
|
7155 |
+ |
|
7156 |
+.project-name-in-my-issues { |
|
7157 |
+ display: flex !important; |
|
7158 |
+ flex-direction: row; |
|
7159 |
+ flex-wrap: nowrap; |
|
7160 |
+ flex-grow: 1; |
|
7161 |
+ justify-content: space-between; |
|
7162 |
+ align-items: center; |
|
7163 |
+ |
|
7164 |
+ .project-name { |
|
7165 |
+ text-overflow: ellipsis; |
|
7166 |
+ overflow: hidden; |
|
7167 |
+ white-space: nowrap; |
|
7168 |
+ } |
|
7169 |
+} |
--- app/assets/stylesheets/less/_yobiUI.less
+++ app/assets/stylesheets/less/_yobiUI.less
... | ... | @@ -581,8 +581,8 @@ |
581 | 581 |
.yobiToasts { |
582 | 582 |
position: fixed; |
583 | 583 |
overflow: hidden; |
584 |
- left: 20px !important; |
|
585 |
- top: 25px !important; |
|
584 |
+ right: 20px !important; |
|
585 |
+ bottom: 25px !important; |
|
586 | 586 |
margin: 10px; |
587 | 587 |
z-index:9999; |
588 | 588 |
|
--- app/controllers/Application.java
+++ app/controllers/Application.java
... | ... | @@ -38,6 +38,8 @@ |
38 | 38 |
public static String LOGIN_PAGE_LOGINID_PLACEHOLDER = play.Configuration.root().getString("application.login.page.loginId.placeholder", ""); |
39 | 39 |
public static String LOGIN_PAGE_PASSWORD_PLACEHOLDER = play.Configuration.root().getString("application.login.page.password.placeholder", ""); |
40 | 40 |
public static boolean SHOW_USER_EMAIL = play.Configuration.root().getBoolean("application.show.user.email", true); |
41 |
+ public static String NAVBAR_CUSTOM_LINK_NAME = play.Configuration.root().getString("application.navbar.custom.link.name", ""); |
|
42 |
+ public static String NAVBAR_CUSTOM_LINK_URL = play.Configuration.root().getString("application.navbar.custom.link.url", ""); |
|
41 | 43 |
|
42 | 44 |
@AnonymousCheck |
43 | 45 |
public static Result index() { |
--- app/controllers/AttachmentApp.java
+++ app/controllers/AttachmentApp.java
... | ... | @@ -1,8 +1,9 @@ |
1 | 1 |
/** |
2 |
- * Yona, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2016 the original author or authors. |
|
5 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
6 | 7 |
package controllers; |
7 | 8 |
|
8 | 9 |
import com.fasterxml.jackson.databind.JsonNode; |
... | ... | @@ -67,7 +68,7 @@ |
67 | 68 |
|
68 | 69 |
User uploader = findUploader(request().body().asMultipartFormData().asFormUrlEncoded()); |
69 | 70 |
if (uploader.isAnonymous()) { |
70 |
- uploader = User.findByUserToken(request().getHeader("Yona-Token")); |
|
71 |
+ uploader = User.findByUserToken(User.extractUserTokenFromRequestHeader(request())); |
|
71 | 72 |
} |
72 | 73 |
|
73 | 74 |
// Anonymous cannot upload a file. |
--- app/controllers/BoardApp.java
+++ app/controllers/BoardApp.java
... | ... | @@ -1,8 +1,9 @@ |
1 | 1 |
/** |
2 |
- * Yona, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2016 the original author or authors. |
|
5 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
6 | 7 |
package controllers; |
7 | 8 |
|
8 | 9 |
import actions.NullProjectCheckAction; |
... | ... | @@ -353,7 +354,7 @@ |
353 | 354 |
public void run() { |
354 | 355 |
post.comments = original.comments; |
355 | 356 |
if(isSelectedToSendNotificationMail() || !original.isAuthoredBy(UserApp.currentUser())){ |
356 |
- NotificationEvent.afterNewPost(post); |
|
357 |
+ NotificationEvent.afterUpdatePosting(original.body, post); |
|
357 | 358 |
} |
358 | 359 |
} |
359 | 360 |
}; |
... | ... | @@ -379,6 +380,7 @@ |
379 | 380 |
Posting posting = Posting.findByNumber(project, number); |
380 | 381 |
Call redirectTo = routes.BoardApp.posts(project.owner, project.name, 1); |
381 | 382 |
|
383 |
+ NotificationEvent.afterResourceDeleted(posting, UserApp.currentUser()); |
|
382 | 384 |
return delete(posting, posting.asResource(), redirectTo); |
383 | 385 |
} |
384 | 386 |
|
... | ... | @@ -436,6 +438,7 @@ |
436 | 438 |
return new Runnable() { |
437 | 439 |
@Override |
438 | 440 |
public void run() { |
441 |
+ posting.updatedDate = JodaDateUtil.now(); |
|
439 | 442 |
comment.posting = posting; |
440 | 443 |
} |
441 | 444 |
}; |
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
... | ... | @@ -1,7 +1,7 @@ |
1 | 1 |
/** |
2 | 2 |
* Yona, 21st Century Project Hosting SW |
3 | 3 |
* <p> |
4 |
- * Copyright Yona & Yobi Authors & NAVER Corp. |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 | 5 |
* https://yona.io |
6 | 6 |
**/ |
7 | 7 |
package controllers; |
... | ... | @@ -44,6 +44,7 @@ |
44 | 44 |
|
45 | 45 |
@AnonymousCheck(requiresLogin = false, displaysFlashMessage = true) |
46 | 46 |
public static Result organizationIssues(@Nonnull String organizationName, @Nonnull String state, @Nonnull String format, int pageNum) throws WriteException, IOException { |
47 |
+ |
|
47 | 48 |
// SearchCondition from param |
48 | 49 |
Form<models.support.SearchCondition> issueParamForm = new Form<>(models.support.SearchCondition.class); |
49 | 50 |
models.support.SearchCondition searchCondition = issueParamForm.bindFromRequest().get(); |
... | ... | @@ -107,7 +108,7 @@ |
107 | 108 |
|
108 | 109 |
private static boolean hasNotConditions(models.support.SearchCondition searchCondition) { |
109 | 110 |
return searchCondition.assigneeId == null && searchCondition.authorId == null && searchCondition.mentionId == null |
110 |
- && searchCondition.commenterId == null; |
|
111 |
+ && searchCondition.commenterId == null && searchCondition.sharerId == null; |
|
111 | 112 |
} |
112 | 113 |
|
113 | 114 |
@Transactional |
... | ... | @@ -384,49 +385,11 @@ |
384 | 385 |
continue; |
385 | 386 |
} |
386 | 387 |
|
387 |
- boolean assigneeChanged = false; |
|
388 |
- User oldAssignee = null; |
|
389 |
- if (issueMassUpdate.assignee != null) { |
|
390 |
- if(hasAssignee(issue)) { |
|
391 |
- oldAssignee = issue.assignee.user; |
|
392 |
- } |
|
393 |
- Assignee newAssignee; |
|
394 |
- if (issueMassUpdate.assignee.isAnonymous()) { |
|
395 |
- newAssignee = null; |
|
396 |
- } else { |
|
397 |
- newAssignee = Assignee.add(issueMassUpdate.assignee.id, project.id); |
|
398 |
- } |
|
399 |
- assigneeChanged = !issue.assignedUserEquals(newAssignee); |
|
400 |
- issue.assignee = newAssignee; |
|
401 |
- } |
|
402 |
- |
|
403 |
- boolean stateChanged = false; |
|
404 |
- State oldState = null; |
|
405 |
- if ((issueMassUpdate.state != null) && (issue.state != issueMassUpdate.state)) { |
|
406 |
- stateChanged = true; |
|
407 |
- oldState = issue.state; |
|
408 |
- issue.state = issueMassUpdate.state; |
|
409 |
- } |
|
410 |
- |
|
411 |
- if (issueMassUpdate.milestone != null) { |
|
412 |
- if(issueMassUpdate.milestone.isNullMilestone()) { |
|
413 |
- issue.milestone = null; |
|
414 |
- } else { |
|
415 |
- issue.milestone = issueMassUpdate.milestone; |
|
416 |
- } |
|
417 |
- } |
|
418 |
- |
|
419 |
- if (issueMassUpdate.attachingLabelIds != null) { |
|
420 |
- for (Long labelId : issueMassUpdate.attachingLabelIds) { |
|
421 |
- issue.labels.add(IssueLabel.finder.byId(labelId)); |
|
422 |
- } |
|
423 |
- } |
|
424 |
- |
|
425 |
- if (issueMassUpdate.detachingLabelIds != null) { |
|
426 |
- for (Long labelId : issueMassUpdate.detachingLabelIds) { |
|
427 |
- issue.labels.remove(IssueLabel.finder.byId(labelId)); |
|
428 |
- } |
|
429 |
- } |
|
388 |
+ updateAssigneeIfChanged(issueMassUpdate.assignee, project, issue); |
|
389 |
+ updateStateIfChanged(issueMassUpdate.state, issue); |
|
390 |
+ updateMilestoneIfChanged(issueMassUpdate.milestone, issue); |
|
391 |
+ updateLabelIfChanged(issueMassUpdate.attachingLabelIds, |
|
392 |
+ issueMassUpdate.detachingLabelIds, issue); |
|
430 | 393 |
|
431 | 394 |
if (issueMassUpdate.isDueDateChanged) { |
432 | 395 |
issue.dueDate = JodaDateUtil.lastSecondOfDay(issueMassUpdate.dueDate); |
... | ... | @@ -435,15 +398,6 @@ |
435 | 398 |
issue.updatedDate = JodaDateUtil.now(); |
436 | 399 |
issue.update(); |
437 | 400 |
updatedItems++; |
438 |
- |
|
439 |
- if(assigneeChanged) { |
|
440 |
- NotificationEvent notiEvent = NotificationEvent.afterAssigneeChanged(oldAssignee, issue); |
|
441 |
- IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId); |
|
442 |
- } |
|
443 |
- if(stateChanged) { |
|
444 |
- NotificationEvent notiEvent = NotificationEvent.afterStateChanged(oldState, issue); |
|
445 |
- IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId); |
|
446 |
- } |
|
447 | 401 |
} |
448 | 402 |
|
449 | 403 |
if (updatedItems == 0 && rejectedByPermission > 0) { |
... | ... | @@ -465,6 +419,106 @@ |
465 | 419 |
} |
466 | 420 |
} else { |
467 | 421 |
return redirect(request().getHeader("Referer")); |
422 |
+ } |
|
423 |
+ } |
|
424 |
+ |
|
425 |
+ private static void updateLabelIfChanged(List<Long> attachingLabelIds, List<Long> detachingLabelIds, |
|
426 |
+ Issue issue) { |
|
427 |
+ boolean isLabelChanged = false; |
|
428 |
+ StringBuilder addedLabels = new StringBuilder(); |
|
429 |
+ StringBuilder deletedLabels = new StringBuilder(); |
|
430 |
+ |
|
431 |
+ if (attachingLabelIds != null) { |
|
432 |
+ for (Long labelId : attachingLabelIds) { |
|
433 |
+ IssueLabel label = IssueLabel.finder.byId(labelId); |
|
434 |
+ issue.labels.add(label); |
|
435 |
+ isLabelChanged = true; |
|
436 |
+ addedLabels.append(label.category.name).append(" - ").append(label.name); |
|
437 |
+ } |
|
438 |
+ } |
|
439 |
+ |
|
440 |
+ if (detachingLabelIds != null) { |
|
441 |
+ for (Long labelId : detachingLabelIds) { |
|
442 |
+ IssueLabel label = IssueLabel.finder.byId(labelId); |
|
443 |
+ issue.labels.remove(label); |
|
444 |
+ isLabelChanged = true; |
|
445 |
+ deletedLabels.append(label.category.name).append(" - ").append(label.name); |
|
446 |
+ } |
|
447 |
+ } |
|
448 |
+ |
|
449 |
+ if(isLabelChanged) { |
|
450 |
+ NotificationEvent notiEvent = NotificationEvent.afterIssueLabelChanged( |
|
451 |
+ addedLabels.toString(), |
|
452 |
+ deletedLabels.toString(), |
|
453 |
+ issue); |
|
454 |
+ IssueEvent.addFromNotificationEventWithoutSkipEvent(notiEvent, issue, UserApp.currentUser().loginId); |
|
455 |
+ } |
|
456 |
+ } |
|
457 |
+ |
|
458 |
+ private static void updateMilestoneIfChanged(Milestone newMilestone, Issue issue) { |
|
459 |
+ |
|
460 |
+ Long oldMilestoneId = issue.milestoneId(); |
|
461 |
+ |
|
462 |
+ if (!isMilestoneChanged(newMilestone, issue.milestone)) { |
|
463 |
+ return; |
|
464 |
+ } |
|
465 |
+ |
|
466 |
+ if(newMilestone.isNullMilestone()) { |
|
467 |
+ issue.milestone = null; |
|
468 |
+ } else { |
|
469 |
+ issue.milestone = newMilestone; |
|
470 |
+ } |
|
471 |
+ NotificationEvent notiEvent = NotificationEvent.afterMilestoneChanged(oldMilestoneId, issue); |
|
472 |
+ IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId); |
|
473 |
+ } |
|
474 |
+ |
|
475 |
+ private static boolean isMilestoneChanged(Milestone newMilestone, Milestone oldMilestone) { |
|
476 |
+ if (newMilestone == null) { |
|
477 |
+ return false; |
|
478 |
+ } |
|
479 |
+ |
|
480 |
+ if (oldMilestone != null && oldMilestone.id.equals(newMilestone.id)) { |
|
481 |
+ return false; |
|
482 |
+ } |
|
483 |
+ |
|
484 |
+ return true; |
|
485 |
+ } |
|
486 |
+ |
|
487 |
+ private static void updateStateIfChanged(State newState, Issue issue) { |
|
488 |
+ boolean stateChanged = false; |
|
489 |
+ State oldState = null; |
|
490 |
+ if ((newState != null) && (issue.state != newState)) { |
|
491 |
+ stateChanged = true; |
|
492 |
+ oldState = issue.state; |
|
493 |
+ issue.state = newState; |
|
494 |
+ } |
|
495 |
+ if(stateChanged) { |
|
496 |
+ NotificationEvent notiEvent = NotificationEvent.afterStateChanged(oldState, issue); |
|
497 |
+ IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId); |
|
498 |
+ } |
|
499 |
+ } |
|
500 |
+ |
|
501 |
+ private static void updateAssigneeIfChanged(User assignee, Project project, Issue issue) { |
|
502 |
+ boolean assigneeChanged = false; |
|
503 |
+ User oldAssignee = null; |
|
504 |
+ |
|
505 |
+ if (assignee != null) { |
|
506 |
+ if(hasAssignee(issue)) { |
|
507 |
+ oldAssignee = issue.assignee.user; |
|
508 |
+ } |
|
509 |
+ Assignee newAssignee; |
|
510 |
+ if (assignee.isAnonymous()) { |
|
511 |
+ newAssignee = null; |
|
512 |
+ } else { |
|
513 |
+ newAssignee = Assignee.add(assignee.id, project.id); |
|
514 |
+ } |
|
515 |
+ assigneeChanged = !issue.assignedUserEquals(newAssignee); |
|
516 |
+ issue.assignee = newAssignee; |
|
517 |
+ } |
|
518 |
+ |
|
519 |
+ if(assigneeChanged) { |
|
520 |
+ NotificationEvent notiEvent = NotificationEvent.afterAssigneeChanged(oldAssignee, issue); |
|
521 |
+ IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId); |
|
468 | 522 |
} |
469 | 523 |
} |
470 | 524 |
|
... | ... | @@ -796,6 +850,7 @@ |
796 | 850 |
Call redirectTo = |
797 | 851 |
routes.IssueApp.issues(project.owner, project.name, State.OPEN.state(), "html", 1); |
798 | 852 |
|
853 |
+ NotificationEvent.afterResourceDeleted(issue, UserApp.currentUser()); |
|
799 | 854 |
return delete(issue, issue.asResource(), redirectTo); |
800 | 855 |
} |
801 | 856 |
|
... | ... | @@ -845,16 +900,16 @@ |
845 | 900 |
private static Comment saveComment(Project project, Issue issue, IssueComment comment) { |
846 | 901 |
Comment savedComment; |
847 | 902 |
IssueComment existingComment = IssueComment.find.where().eq("id", comment.id).findUnique(); |
848 |
- if (existingComment != null) { |
|
903 |
+ if (existingComment == null) { |
|
904 |
+ comment.projectId = project.id; |
|
905 |
+ savedComment = saveComment(comment, getContainerUpdater(issue, comment)); |
|
906 |
+ NotificationEvent.afterNewComment(savedComment); |
|
907 |
+ } else { |
|
849 | 908 |
existingComment.contents = comment.contents; |
850 | 909 |
savedComment = saveComment(existingComment, getContainerUpdater(issue, comment)); |
851 | 910 |
if(isSelectedToSendNotificationMail() || !existingComment.isAuthoredBy(UserApp.currentUser())){ |
852 | 911 |
NotificationEvent.afterCommentUpdated(savedComment); |
853 | 912 |
} |
854 |
- } else { |
|
855 |
- comment.projectId = project.id; |
|
856 |
- savedComment = saveComment(comment, getContainerUpdater(issue, comment)); |
|
857 |
- NotificationEvent.afterNewComment(savedComment); |
|
858 | 913 |
} |
859 | 914 |
return savedComment; |
860 | 915 |
} |
--- app/controllers/LabelApp.java
+++ app/controllers/LabelApp.java
... | ... | @@ -53,6 +53,10 @@ |
53 | 53 |
return status(Http.Status.NOT_ACCEPTABLE); |
54 | 54 |
} |
55 | 55 |
|
56 |
+ if (limit == null) { |
|
57 |
+ return badRequest("No limit"); |
|
58 |
+ } |
|
59 |
+ |
|
56 | 60 |
ExpressionList<Label> el = |
57 | 61 |
Label.find.where().and(icontains("category", category), icontains("name", query)); |
58 | 62 |
|
... | ... | @@ -77,6 +81,10 @@ |
77 | 81 |
return status(Http.Status.NOT_ACCEPTABLE); |
78 | 82 |
} |
79 | 83 |
|
84 |
+ if (limit == null) { |
|
85 |
+ return badRequest("No limit"); |
|
86 |
+ } |
|
87 |
+ |
|
80 | 88 |
SqlQuery sqlQuery; |
81 | 89 |
SqlQuery sqlCountQuery; |
82 | 90 |
|
--- app/controllers/OrganizationApp.java
+++ app/controllers/OrganizationApp.java
... | ... | @@ -1,14 +1,17 @@ |
1 | 1 |
/** |
2 |
- * Yona, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2017 the original author or authors. |
|
5 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
6 | 7 |
package controllers; |
7 | 8 |
|
8 | 9 |
import com.avaje.ebean.ExpressionList; |
9 | 10 |
import com.avaje.ebean.Page; |
10 | 11 |
import controllers.annotation.AnonymousCheck; |
11 | 12 |
import controllers.annotation.GuestProhibit; |
13 |
+import controllers.PullRequestApp.SearchCondition; |
|
14 |
+import controllers.PullRequestApp.Category; |
|
12 | 15 |
import models.*; |
13 | 16 |
import models.enumeration.Operation; |
14 | 17 |
import models.enumeration.RequestState; |
... | ... | @@ -30,6 +33,7 @@ |
30 | 33 |
import views.html.organization.members; |
31 | 34 |
import views.html.organization.setting; |
32 | 35 |
import views.html.organization.view; |
36 |
+import views.html.organization.group_pullrequest_list; |
|
33 | 37 |
|
34 | 38 |
import javax.servlet.ServletException; |
35 | 39 |
import javax.validation.ConstraintViolation; |
... | ... | @@ -45,6 +49,31 @@ |
45 | 49 |
*/ |
46 | 50 |
@AnonymousCheck |
47 | 51 |
public class OrganizationApp extends Controller { |
52 |
+ |
|
53 |
+ @AnonymousCheck(requiresLogin = false, displaysFlashMessage = true) |
|
54 |
+ public static Result organizationPullRequests(String organizationName, String category) { |
|
55 |
+ |
|
56 |
+ Organization organization = Organization.findByName(organizationName); |
|
57 |
+ if (organization == null) { |
|
58 |
+ return notFound(ErrorViews.NotFound.render("error.notfound.organization")); |
|
59 |
+ } |
|
60 |
+ |
|
61 |
+ SearchCondition condition = Form.form(SearchCondition.class).bindFromRequest().get(); |
|
62 |
+ if (category.equals("open")) { |
|
63 |
+ condition.setOrganization(organization).setCategory(Category.OPEN); |
|
64 |
+ } else { |
|
65 |
+ condition.setOrganization(organization).setCategory(Category.CLOSED); |
|
66 |
+ } |
|
67 |
+ Page<PullRequest> page = PullRequest.findPagingList(condition); |
|
68 |
+ |
|
69 |
+ return ok(group_pullrequest_list.render("title.pullrequest", organization, page, condition, category)); |
|
70 |
+ } |
|
71 |
+ |
|
72 |
+ @AnonymousCheck(requiresLogin = false, displaysFlashMessage = true) |
|
73 |
+ public static Result organizationClosedPullRequests(String organizationName) { |
|
74 |
+ return organizationPullRequests(organizationName, "closed"); |
|
75 |
+ } |
|
76 |
+ |
|
48 | 77 |
/** |
49 | 78 |
* show New Group page |
50 | 79 |
* @return {@link Result} |
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
... | ... | @@ -1,7 +1,7 @@ |
1 | 1 |
/** |
2 | 2 |
* Yona, 21st Century Project Hosting SW |
3 | 3 |
* <p> |
4 |
- * Copyright Yona & Yobi Authors & NAVER Corp. |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 | 5 |
* https://yona.io |
6 | 6 |
**/ |
7 | 7 |
package controllers; |
... | ... | @@ -634,7 +634,6 @@ |
634 | 634 |
return redirect(url); |
635 | 635 |
} |
636 | 636 |
|
637 |
- @Transactional |
|
638 | 637 |
@AnonymousCheck(requiresLogin = true, displaysFlashMessage = true) |
639 | 638 |
public static synchronized Result acceptTransfer(Long id, String confirmKey) throws IOException, ServletException { |
640 | 639 |
ProjectTransfer pt = ProjectTransfer.findValidOne(id); |
... | ... | @@ -653,15 +652,25 @@ |
653 | 652 |
|
654 | 653 |
// Change the project's name and move the repository. |
655 | 654 |
String newProjectName = Project.newProjectName(pt.destination, project.name); |
656 |
- PlayRepository repository = RepositoryService.getRepository(project); |
|
657 |
- repository.move(project.owner, project.name, pt.destination, newProjectName); |
|
658 | 655 |
|
659 |
- User newOwnerUser = User.findByLoginId(pt.destination); |
|
660 |
- Organization newOwnerOrg = Organization.findByName(pt.destination); |
|
656 |
+ // Following three local variables are used for bottom of this method |
|
657 |
+ String originalProjectOwner = project.owner; |
|
658 |
+ String originalProjectName = project.name; |
|
659 |
+ String destinationOwner = pt.destination; |
|
660 |
+ Long senderId = pt.sender.id; |
|
661 |
+ |
|
662 |
+ disableProjectTransferLink(pt, project, newProjectName); |
|
663 |
+ PlayRepository repository = RepositoryService.getRepository(project); |
|
664 |
+ |
|
665 |
+ // intentionally placed to the last of method |
|
666 |
+ repository.move(originalProjectOwner, originalProjectName, destinationOwner, newProjectName); |
|
667 |
+ |
|
668 |
+ User newOwnerUser = User.findByLoginId(destinationOwner); |
|
669 |
+ Organization newOwnerOrg = Organization.findByName(destinationOwner); |
|
661 | 670 |
|
662 | 671 |
// Change the project's information. |
663 | 672 |
project.recordRenameOrTransferHistoryIfLastChangePassed24HoursFrom(project); |
664 |
- project.owner = pt.destination; |
|
673 |
+ project.owner = destinationOwner; |
|
665 | 674 |
project.name = newProjectName; |
666 | 675 |
if (newOwnerOrg != null) { |
667 | 676 |
project.organization = newOwnerOrg; |
... | ... | @@ -671,25 +680,27 @@ |
671 | 680 |
project.update(); |
672 | 681 |
|
673 | 682 |
// Change roles. |
674 |
- if (ProjectUser.isManager(pt.sender.id, project.id)) { |
|
675 |
- ProjectUser.assignRole(pt.sender.id, project.id, RoleType.MEMBER); |
|
683 |
+ if (ProjectUser.isManager(senderId, project.id)) { |
|
684 |
+ ProjectUser.assignRole(senderId, project.id, RoleType.MEMBER); |
|
676 | 685 |
} |
677 | 686 |
if (!newOwnerUser.isAnonymous()) { |
678 | 687 |
ProjectUser.assignRole(newOwnerUser.id, project.id, RoleType.MANAGER); |
679 | 688 |
} |
680 | 689 |
|
681 |
- // Change the tranfer's status to be accepted. |
|
682 |
- pt.newProjectName = newProjectName; |
|
683 |
- pt.accepted = true; |
|
684 |
- pt.update(); |
|
685 |
- |
|
686 |
- // If the opposite request is exists, delete it. |
|
687 |
- ProjectTransfer.deleteExisting(project, pt.sender, pt.destination); |
|
688 | 690 |
CacheStore.refreshProjectMap(); |
689 | 691 |
|
690 | 692 |
return redirect(routes.ProjectApp.project(project.owner, project.name)); |
691 | 693 |
} |
692 | 694 |
|
695 |
+ private static void disableProjectTransferLink(ProjectTransfer pt, Project project, String newProjectName) { |
|
696 |
+ // Change the tranfer's status to be accepted. |
|
697 |
+ pt.newProjectName = newProjectName; |
|
698 |
+ pt.accepted = true; |
|
699 |
+ |
|
700 |
+ // If the opposite request is exists, delete it. |
|
701 |
+ ProjectTransfer.deleteExisting(project, pt.sender, pt.destination); |
|
702 |
+ } |
|
703 |
+ |
|
693 | 704 |
@IsAllowed(Operation.UPDATE) |
694 | 705 |
public static Result changeVCSForm(String ownerId, String projectName) { |
695 | 706 |
Project project = Project.findByOwnerAndProjectName(ownerId, projectName); |
--- app/controllers/PullRequestApp.java
+++ app/controllers/PullRequestApp.java
... | ... | @@ -343,7 +343,7 @@ |
343 | 343 |
|
344 | 344 |
// Only members can access code? |
345 | 345 |
if(project.isCodeAccessibleMemberOnly && !project.hasMember(UserApp.currentUser())) { |
346 |
- return forbidden(ErrorViews.Forbidden.render("error.forbidden", project)); |
|
346 |
+ return forbidden(ErrorViews.Forbidden.render("error.forbidden", project)); |
|
347 | 347 |
} |
348 | 348 |
|
349 | 349 |
SearchCondition condition = Form.form(SearchCondition.class).bindFromRequest().get(); |
... | ... | @@ -697,6 +697,12 @@ |
697 | 697 |
public Long contributorId; |
698 | 698 |
public int pageNum = Constants.DEFAULT_PAGE; |
699 | 699 |
public Category category; |
700 |
+ public Organization organization; |
|
701 |
+ |
|
702 |
+ public SearchCondition setOrganization(Organization organization) { |
|
703 |
+ this.organization = organization; |
|
704 |
+ return this; |
|
705 |
+ } |
|
700 | 706 |
|
701 | 707 |
public SearchCondition setProject(Project project) { |
702 | 708 |
this.project = project; |
... | ... | @@ -731,6 +737,7 @@ |
731 | 737 |
clone.contributorId = this.contributorId; |
732 | 738 |
clone.pageNum = this.pageNum; |
733 | 739 |
clone.category = this.category; |
740 |
+ clone.organization = this.organization; |
|
734 | 741 |
return clone; |
735 | 742 |
} |
736 | 743 |
|
--- app/controllers/UserApp.java
+++ app/controllers/UserApp.java
... | ... | @@ -1177,6 +1177,10 @@ |
1177 | 1177 |
} catch (CommunicationException e) { |
1178 | 1178 |
play.Logger.error("Cannot connect to ldap server \n" + e.getMessage()); |
1179 | 1179 |
e.printStackTrace(); |
1180 |
+ if(FALLBACK_TO_LOCAL_LOGIN){ |
|
1181 |
+ play.Logger.warn("fallback to local login: " + loginIdOrEmail); |
|
1182 |
+ return authenticateWithPlainPassword(loginIdOrEmail, password); |
|
1183 |
+ } |
|
1180 | 1184 |
return User.anonymous; |
1181 | 1185 |
} catch (AuthenticationException e) { |
1182 | 1186 |
flash(Constants.WARNING, Messages.get("user.login.invalid")); |
--- app/controllers/WatchProjectApp.java
+++ app/controllers/WatchProjectApp.java
... | ... | @@ -1,7 +1,7 @@ |
1 | 1 |
/** |
2 | 2 |
* Yona, 21st Century Project Hosting SW |
3 | 3 |
* <p> |
4 |
- * Copyright Yona & Yobi Authors & NAVER Corp. |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 | 5 |
* https://yona.io |
6 | 6 |
**/ |
7 | 7 |
package controllers; |
... | ... | @@ -20,6 +20,8 @@ |
20 | 20 |
import play.mvc.Result; |
21 | 21 |
import utils.AccessControl; |
22 | 22 |
import utils.ErrorViews; |
23 |
+ |
|
24 |
+import static models.UserProjectNotification.*; |
|
23 | 25 |
|
24 | 26 |
@AnonymousCheck(requiresLogin = true, displaysFlashMessage = true) |
25 | 27 |
public class WatchProjectApp extends Controller { |
... | ... | @@ -55,11 +57,15 @@ |
55 | 57 |
return badRequest(Messages.get("error.notfound.watch")); |
56 | 58 |
} |
57 | 59 |
|
58 |
- UserProjectNotification upn = UserProjectNotification.findOne(user, project, notiType); |
|
59 |
- if(upn == null) { // make the EventType OFF, because default is ON. |
|
60 |
- UserProjectNotification.unwatchExplictly(user, project, notiType); |
|
60 |
+ UserProjectNotification userProjectNotification = findOne(user, project, notiType); |
|
61 |
+ if(userProjectNotification == null) { // not specified yet |
|
62 |
+ if (isNotifiedByDefault(notiType)) { |
|
63 |
+ unwatchExplictly(user, project, notiType); |
|
64 |
+ } else { |
|
65 |
+ watchExplictly(user, project, notiType); |
|
66 |
+ } |
|
61 | 67 |
} else { |
62 |
- upn.toggle(); |
|
68 |
+ userProjectNotification.toggle(notiType); |
|
63 | 69 |
} |
64 | 70 |
|
65 | 71 |
return ok(); |
--- app/controllers/api/IssueApi.java
+++ app/controllers/api/IssueApi.java
... | ... | @@ -39,6 +39,7 @@ |
39 | 39 |
import java.util.*; |
40 | 40 |
|
41 | 41 |
import static controllers.UserApp.MAX_FETCH_USERS; |
42 |
+import static controllers.UserApp.currentUser; |
|
42 | 43 |
import static controllers.api.UserApi.createUserNode; |
43 | 44 |
import static play.libs.Json.toJson; |
44 | 45 |
|
... | ... | @@ -385,7 +386,7 @@ |
385 | 386 |
} |
386 | 387 |
} |
387 | 388 |
|
388 |
- private static void addUserToUsers(User user, List<ObjectNode> users) { |
|
389 |
+ static void addUserToUsers(User user, List<ObjectNode> users) { |
|
389 | 390 |
ObjectNode userNode = Json.newObject(); |
390 | 391 |
userNode.put("loginId", user.loginId); |
391 | 392 |
userNode.put("name", user.getDisplayName()); |
... | ... | @@ -535,4 +536,151 @@ |
535 | 536 |
return ok(node); |
536 | 537 |
}); |
537 | 538 |
} |
539 |
+ |
|
540 |
+ @AnonymousCheck |
|
541 |
+ public static Result findSharerByloginIds(String ownerName, String projectName, Long number, |
|
542 |
+ String commaSeperatedIds) { |
|
543 |
+ if (!request().accepts("application/json")) { |
|
544 |
+ return status(Http.Status.NOT_ACCEPTABLE); |
|
545 |
+ } |
|
546 |
+ Project project = Project.findByOwnerAndProjectName(ownerName, projectName); |
|
547 |
+ Issue issue = Issue.findByNumber(project, number); |
|
548 |
+ |
|
549 |
+ List<IssueSharer> list = getExpressionListByExtractingLoginIds(issue, commaSeperatedIds).findList(); |
|
550 |
+ sortListByAddedDate(list); |
|
551 |
+ |
|
552 |
+ List<ObjectNode> users = new ArrayList<>(); |
|
553 |
+ for (IssueSharer sharer :list) { |
|
554 |
+ addUserToUsers(sharer.user, users); |
|
555 |
+ } |
|
556 |
+ return ok(toJson(users)); |
|
557 |
+ } |
|
558 |
+ |
|
559 |
+ private static void sortListByAddedDate(List<IssueSharer> list) { |
|
560 |
+ list.sort(new Comparator<IssueSharer>() { |
|
561 |
+ @Override |
|
562 |
+ public int compare(IssueSharer o1, IssueSharer o2) { |
|
563 |
+ return o1.created.compareTo(o2.created); |
|
564 |
+ } |
|
565 |
+ }); |
|
566 |
+ } |
|
567 |
+ |
|
568 |
+ private static ExpressionList<IssueSharer> getExpressionListByExtractingLoginIds(Issue issue, String query) { |
|
569 |
+ String[] queryItems = query.split(","); |
|
570 |
+ ExpressionList<IssueSharer> el = IssueSharer.find |
|
571 |
+ .where() |
|
572 |
+ .in("loginId", Arrays.asList(queryItems)) |
|
573 |
+ .eq("issue.id", issue.id); |
|
574 |
+ return el; |
|
575 |
+ } |
|
576 |
+ |
|
577 |
+ @IsAllowed(Operation.READ) |
|
578 |
+ public static Result findSharableUsers(String ownerName, String projectName, Long number, String query) { |
|
579 |
+ if (!request().accepts("application/json")) { |
|
580 |
+ return status(Http.Status.NOT_ACCEPTABLE); |
|
581 |
+ } |
|
582 |
+ |
|
583 |
+ List<ObjectNode> users = new ArrayList<>(); |
|
584 |
+ |
|
585 |
+ ExpressionList<User> el = getUserExpressionList(query, request().getQueryString("type")); |
|
586 |
+ |
|
587 |
+ int total = el.findRowCount(); |
|
588 |
+ if (total > MAX_FETCH_USERS) { |
|
589 |
+ el.setMaxRows(MAX_FETCH_USERS); |
|
590 |
+ response().setHeader("Content-Range", "items " + MAX_FETCH_USERS + "/" + total); |
|
591 |
+ } |
|
592 |
+ |
|
593 |
+ for (User user :el.findList()) { |
|
594 |
+ addUserToUsers(user, users); |
|
595 |
+ } |
|
596 |
+ |
|
597 |
+ return ok(toJson(users)); |
|
598 |
+ } |
|
599 |
+ |
|
600 |
+ public static Result updateSharer(String owner, String projectName, Long number){ |
|
601 |
+ JsonNode json = request().body().asJson(); |
|
602 |
+ if (json == null) { |
|
603 |
+ return badRequest(Json.newObject().put("message", "Expecting Json data")); |
|
604 |
+ } |
|
605 |
+ |
|
606 |
+ Project project = Project.findByOwnerAndProjectName(owner, projectName); |
|
607 |
+ Issue issue = Issue.findByNumber(project, number); |
|
608 |
+ if (!AccessControl.isAllowed(UserApp.currentUser(), issue.asResource(), |
|
609 |
+ Operation.UPDATE)) { |
|
610 |
+ return forbidden(Json.newObject().put("message", "Permission denied")); |
|
611 |
+ } |
|
612 |
+ |
|
613 |
+ JsonNode sharer = json.findValue("sharer"); |
|
614 |
+ if(noSharer(sharer)){ |
|
615 |
+ return badRequest(Json.newObject().put("message", "No sharer")); |
|
616 |
+ } |
|
617 |
+ |
|
618 |
+ final String action = json.findValue("action").asText(); |
|
619 |
+ |
|
620 |
+ ObjectNode result = changeSharer(sharer, issue, action); |
|
621 |
+ sendNotification(sharer, issue, action); |
|
622 |
+ |
|
623 |
+ return ok(result); |
|
624 |
+ } |
|
625 |
+ |
|
626 |
+ private static ObjectNode changeSharer(JsonNode sharer, Issue issue, String action) { |
|
627 |
+ ObjectNode result = Json.newObject(); |
|
628 |
+ for (JsonNode sharerLoginId : sharer) { |
|
629 |
+ if ("add".equalsIgnoreCase(action)) { |
|
630 |
+ addSharer(issue, sharerLoginId.asText()); |
|
631 |
+ result.put("action", "added"); |
|
632 |
+ } else if ("delete".equalsIgnoreCase(action)) { |
|
633 |
+ result.put("action", "deleted"); |
|
634 |
+ removeSharer(issue, sharerLoginId.asText()); |
|
635 |
+ } else { |
|
636 |
+ play.Logger.error("Unknown issue sharing action: " + issue + ":" + action + " by " + currentUser()); |
|
637 |
+ result.put("action", "Do nothing. Unsupported action: " + action); |
|
638 |
+ } |
|
639 |
+ result.put("sharer", User.findByLoginId(sharerLoginId.asText()).getDisplayName()); |
|
640 |
+ } |
|
641 |
+ return result; |
|
642 |
+ } |
|
643 |
+ |
|
644 |
+ private static void sendNotification(JsonNode sharer, Issue issue, String action) { |
|
645 |
+ Runnable preUpdateHook = new Runnable() { |
|
646 |
+ @Override |
|
647 |
+ public void run() { |
|
648 |
+ for(JsonNode sharerLoginId: sharer){ |
|
649 |
+ addSharerChangedNotification(issue, sharerLoginId.asText(), action); |
|
650 |
+ } |
|
651 |
+ } |
|
652 |
+ }; |
|
653 |
+ preUpdateHook.run(); |
|
654 |
+ } |
|
655 |
+ |
|
656 |
+ |
|
657 |
+ private static void addSharerChangedNotification(Issue issue, String sharerLoginId, String action) { |
|
658 |
+ NotificationEvent notiEvent = NotificationEvent.afterIssueSharerChanged(issue, sharerLoginId, action); |
|
659 |
+ IssueEvent.addFromNotificationEventWithoutSkipEvent(notiEvent, issue, UserApp.currentUser().loginId); |
|
660 |
+ } |
|
661 |
+ |
|
662 |
+ private static boolean noSharer(JsonNode sharers) { |
|
663 |
+ return sharers == null || sharers.size() == 0; |
|
664 |
+ } |
|
665 |
+ |
|
666 |
+ private static void addSharer(Issue issue, String loginId) { |
|
667 |
+ IssueSharer issueSharer = IssueSharer.find.where() |
|
668 |
+ .eq("loginId", loginId) |
|
669 |
+ .eq("issue.id", issue.id).findUnique(); |
|
670 |
+ if(issueSharer == null) { |
|
671 |
+ issueSharer = IssueSharer.createSharer(loginId, issue); |
|
672 |
+ issueSharer.save(); |
|
673 |
+ } |
|
674 |
+ issue.sharers.add(issueSharer); |
|
675 |
+ } |
|
676 |
+ |
|
677 |
+ private static void removeSharer(Issue issue, String loginId) { |
|
678 |
+ IssueSharer issueSharer = |
|
679 |
+ IssueSharer.find.where() |
|
680 |
+ .eq("loginId", loginId) |
|
681 |
+ .eq("issue.id", issue.id) |
|
682 |
+ .findUnique(); |
|
683 |
+ issueSharer.delete(); |
|
684 |
+ issue.sharers.remove(issueSharer); |
|
685 |
+ } |
|
538 | 686 |
} |
--- app/mailbox/MailboxService.java
+++ app/mailbox/MailboxService.java
... | ... | @@ -1,23 +1,10 @@ |
1 | 1 |
/** |
2 |
- * Yobi, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2014 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @Author Yi EungJun |
|
8 |
- * |
|
9 |
- * Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
- * you may not use this file except in compliance with the License. |
|
11 |
- * You may obtain a copy of the License at |
|
12 |
- * |
|
13 |
- * http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
- * |
|
15 |
- * Unless required by applicable law or agreed to in writing, software |
|
16 |
- * distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
- * See the License for the specific language governing permissions and |
|
19 |
- * limitations under the License. |
|
20 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
7 |
+ |
|
21 | 8 |
package mailbox; |
22 | 9 |
|
23 | 10 |
import akka.actor.Cancellable; |
... | ... | @@ -29,6 +16,7 @@ |
29 | 16 |
import play.Configuration; |
30 | 17 |
import play.Logger; |
31 | 18 |
import play.libs.Akka; |
19 |
+import play.libs.F; |
|
32 | 20 |
import scala.concurrent.duration.Duration; |
33 | 21 |
import utils.Diagnostic; |
34 | 22 |
import utils.SimpleDiagnostic; |
... | ... | @@ -184,17 +172,7 @@ |
184 | 172 |
return; |
185 | 173 |
} |
186 | 174 |
|
187 |
- try { |
|
188 |
- EmailHandler.handleNewMessages(folder); |
|
189 |
- } catch (MessagingException e) { |
|
190 |
- play.Logger.error("Failed to handle new messages"); |
|
191 |
- } |
|
192 |
- |
|
193 |
- try { |
|
194 |
- startEmailListener(); |
|
195 |
- } catch (Exception e) { |
|
196 |
- startEmailPolling(); |
|
197 |
- } |
|
175 |
+ handleNewMessagesAndStartListener(); |
|
198 | 176 |
|
199 | 177 |
Diagnostic.register(new SimpleDiagnostic() { |
200 | 178 |
@Override |
... | ... | @@ -210,6 +188,27 @@ |
210 | 188 |
}); |
211 | 189 |
} |
212 | 190 |
|
191 |
+ private void handleNewMessagesAndStartListener() { |
|
192 |
+ F.Promise<Void> promise = F.Promise.promise( |
|
193 |
+ new F.Function0<Void>() { |
|
194 |
+ public Void apply() { |
|
195 |
+ try { |
|
196 |
+ EmailHandler.handleNewMessages(folder); |
|
197 |
+ } catch (MessagingException e) { |
|
198 |
+ Logger.error("Failed to handle new messages"); |
|
199 |
+ } |
|
200 |
+ |
|
201 |
+ try { |
|
202 |
+ startEmailListener(); |
|
203 |
+ } catch (Exception e) { |
|
204 |
+ startEmailPolling(); |
|
205 |
+ } |
|
206 |
+ return null; |
|
207 |
+ } |
|
208 |
+ } |
|
209 |
+ ); |
|
210 |
+ } |
|
211 |
+ |
|
213 | 212 |
/** |
214 | 213 |
* Reopen the IMAP folder which is used by MailboxService. |
215 | 214 |
* |
--- app/models/AbstractPosting.java
+++ app/models/AbstractPosting.java
... | ... | @@ -254,12 +254,6 @@ |
254 | 254 |
actualWatchers.addAll(baseWatchers); |
255 | 255 |
|
256 | 256 |
actualWatchers.add(getAuthor()); |
257 |
- for (Comment c : getComments()) { |
|
258 |
- User user = User.find.byId(c.authorId); |
|
259 |
- if (user != null) { |
|
260 |
- actualWatchers.add(user); |
|
261 |
- } |
|
262 |
- } |
|
263 | 257 |
|
264 | 258 |
return Watch.findActualWatchers(actualWatchers, asResource(), allowedWatchersOnly); |
265 | 259 |
} |
--- app/models/FavoriteProject.java
+++ app/models/FavoriteProject.java
... | ... | @@ -49,4 +49,11 @@ |
49 | 49 |
favoriteProject.update(); |
50 | 50 |
} |
51 | 51 |
} |
52 |
+ |
|
53 |
+ public static FavoriteProject findByProjectId(Long userId, Long projectId){ |
|
54 |
+ return finder.where() |
|
55 |
+ .eq("user.id", userId) |
|
56 |
+ .eq("project.id", projectId) |
|
57 |
+ .findUnique(); |
|
58 |
+ } |
|
52 | 59 |
} |
--- app/models/Issue.java
+++ app/models/Issue.java
... | ... | @@ -75,6 +75,9 @@ |
75 | 75 |
@OneToMany(cascade = CascadeType.ALL, mappedBy="issue") |
76 | 76 |
public List<IssueEvent> events; |
77 | 77 |
|
78 |
+ @OneToMany(cascade = CascadeType.ALL, mappedBy = "issue") |
|
79 |
+ public Set<IssueSharer> sharers = new LinkedHashSet<>(); |
|
80 |
+ |
|
78 | 81 |
@ManyToMany(cascade = CascadeType.ALL) |
79 | 82 |
@JoinTable( |
80 | 83 |
name = "issue_voter", |
... | ... | @@ -130,6 +133,13 @@ |
130 | 133 |
|
131 | 134 |
public String assigneeName() { |
132 | 135 |
return ((assignee != null && assignee.user != null) ? assignee.user.name : null); |
136 |
+ } |
|
137 |
+ |
|
138 |
+ public Long milestoneId() { |
|
139 |
+ if (milestone == null) { |
|
140 |
+ return Milestone.NULL_MILESTONE_ID; |
|
141 |
+ } |
|
142 |
+ return milestone.id; |
|
133 | 143 |
} |
134 | 144 |
|
135 | 145 |
public boolean hasAssignee() { |
... | ... | @@ -245,6 +255,7 @@ |
245 | 255 |
Messages.get("issue.label"), |
246 | 256 |
Messages.get("issue.createdDate"), |
247 | 257 |
Messages.get("issue.dueDate"), |
258 |
+ Messages.get("milestone"), |
|
248 | 259 |
"URL", |
249 | 260 |
Messages.get("common.comment"), |
250 | 261 |
Messages.get("common.comment.author"), |
... | ... | @@ -261,6 +272,7 @@ |
261 | 272 |
|
262 | 273 |
lineNumber++; |
263 | 274 |
int columnPos = 0; |
275 |
+ String milestoneName = issue.milestone != null ? issue.milestone.title : ""; |
|
264 | 276 |
sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, issue.getNumber().toString(), bodyCellFormat)); |
265 | 277 |
sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, issue.state.toString(), bodyCellFormat)); |
266 | 278 |
sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, issue.title, bodyCellFormat)); |
... | ... | @@ -269,6 +281,7 @@ |
269 | 281 |
sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, getIssueLabels(issue), bodyCellFormat)); |
270 | 282 |
sheet.addCell(new jxl.write.DateTime(columnPos++, lineNumber, issue.createdDate, dateCellFormat)); |
271 | 283 |
sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, JodaDateUtil.geYMDDate(issue.dueDate), bodyCellFormat)); |
284 |
+ sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, milestoneName, bodyCellFormat)); |
|
272 | 285 |
sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, controllers.routes.IssueApp.issue(issue.project.owner, issue.project.name, issue.number).toString(), bodyCellFormat)); |
273 | 286 |
if (comments.size() > 0) { |
274 | 287 |
for (int j = 0; j < comments.size(); j++) { |
... | ... | @@ -675,4 +688,24 @@ |
675 | 688 |
.eq("state", State.OPEN) |
676 | 689 |
.findRowCount(); |
677 | 690 |
} |
691 |
+ |
|
692 |
+ public IssueSharer findSharerByUserId(Long id){ |
|
693 |
+ for (IssueSharer sharer : sharers) { |
|
694 |
+ if (sharer.user.id.equals(id)) { |
|
695 |
+ return sharer; |
|
696 |
+ } |
|
697 |
+ } |
|
698 |
+ return null; |
|
699 |
+ } |
|
700 |
+ |
|
701 |
+ public List<IssueSharer> getSortedSharer() { |
|
702 |
+ return new ArrayList<>(sharers); |
|
703 |
+ } |
|
704 |
+ |
|
705 |
+ public static int getCountOfMentionedOpenIssues(Long userId) { |
|
706 |
+ return finder.where() |
|
707 |
+ .in("id", Mention.getMentioningIssueIds(userId)) |
|
708 |
+ .eq("state", State.OPEN) |
|
709 |
+ .findRowCount(); |
|
710 |
+ } |
|
678 | 711 |
} |
--- app/models/IssueEvent.java
+++ app/models/IssueEvent.java
... | ... | @@ -1,23 +1,10 @@ |
1 | 1 |
/** |
2 |
- * Yobi, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2013 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @author Yi EungJun |
|
8 |
- * |
|
9 |
- * Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
- * you may not use this file except in compliance with the License. |
|
11 |
- * You may obtain a copy of the License at |
|
12 |
- * |
|
13 |
- * http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
- * |
|
15 |
- * Unless required by applicable law or agreed to in writing, software |
|
16 |
- * distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
- * See the License for the specific language governing permissions and |
|
19 |
- * limitations under the License. |
|
20 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
7 |
+ |
|
21 | 8 |
package models; |
22 | 9 |
|
23 | 10 |
import models.enumeration.EventType; |
... | ... | @@ -91,8 +78,7 @@ |
91 | 78 |
.orderBy("id desc").setMaxRows(1).findUnique(); |
92 | 79 |
|
93 | 80 |
if (lastEvent != null) { |
94 |
- if (lastEvent.eventType == event.eventType && |
|
95 |
- StringUtils.equals(event.senderLoginId, lastEvent.senderLoginId)) { |
|
81 |
+ if (isSameUserEventAsPrevious(event, lastEvent)) { |
|
96 | 82 |
// A -> B, B -> C ==> A -> C |
97 | 83 |
event.oldValue = lastEvent.oldValue; |
98 | 84 |
lastEvent.delete(); |
... | ... | @@ -107,6 +93,45 @@ |
107 | 93 |
} |
108 | 94 |
|
109 | 95 |
event.save(); |
96 |
+ } |
|
97 |
+ |
|
98 |
+ |
|
99 |
+ /** |
|
100 |
+ * It is nearly same as {@link #add(IssueEvent)} except that it doesn't skip waypoint events. |
|
101 |
+ * |
|
102 |
+ * For example, if events of an issue are occurred continuously |
|
103 |
+ * A -> B -> C, then {@link #add(IssueEvent)} method skip B. |
|
104 |
+ * |
|
105 |
+ * This method doesn't skip B and leave it. |
|
106 |
+ * |
|
107 |
+ * @param event |
|
108 |
+ */ |
|
109 |
+ public static void addWithoutSkipEvent(IssueEvent event) { |
|
110 |
+ Date draftDate = DateTime.now().minusMillis(DRAFT_TIME_IN_MILLIS).toDate(); |
|
111 |
+ |
|
112 |
+ IssueEvent lastEvent = IssueEvent.find.where() |
|
113 |
+ .eq("issue.id", event.issue.id) |
|
114 |
+ .gt("created", draftDate) |
|
115 |
+ .orderBy("id desc").setMaxRows(1).findUnique(); |
|
116 |
+ |
|
117 |
+ if (lastEvent != null) { |
|
118 |
+ if (isSameUserEventAsPrevious(event, lastEvent) && |
|
119 |
+ isRevertingTheValue(event, lastEvent)) { |
|
120 |
+ lastEvent.delete(); |
|
121 |
+ return; |
|
122 |
+ } |
|
123 |
+ } |
|
124 |
+ event.save(); |
|
125 |
+ } |
|
126 |
+ |
|
127 |
+ private static boolean isRevertingTheValue(IssueEvent event, IssueEvent lastEvent) { |
|
128 |
+ return StringUtils.equals(event.oldValue, lastEvent.newValue) && |
|
129 |
+ StringUtils.equals(event.newValue, lastEvent.oldValue); |
|
130 |
+ } |
|
131 |
+ |
|
132 |
+ private static boolean isSameUserEventAsPrevious(IssueEvent event, IssueEvent lastEvent) { |
|
133 |
+ return lastEvent.eventType == event.eventType && |
|
134 |
+ StringUtils.equals(event.senderLoginId, lastEvent.senderLoginId); |
|
110 | 135 |
} |
111 | 136 |
|
112 | 137 |
/** |
... | ... | @@ -130,6 +155,18 @@ |
130 | 155 |
add(event); |
131 | 156 |
} |
132 | 157 |
|
158 |
+ public static void addFromNotificationEventWithoutSkipEvent(NotificationEvent notiEvent, Issue updatedIssue, |
|
159 |
+ String senderLoginId) { |
|
160 |
+ IssueEvent event = new IssueEvent(); |
|
161 |
+ event.created = notiEvent.created; |
|
162 |
+ event.senderLoginId = senderLoginId; |
|
163 |
+ event.issue = updatedIssue; |
|
164 |
+ event.eventType = notiEvent.eventType; |
|
165 |
+ event.oldValue = notiEvent.oldValue; |
|
166 |
+ event.newValue = notiEvent.newValue; |
|
167 |
+ addWithoutSkipEvent(event); |
|
168 |
+ } |
|
169 |
+ |
|
133 | 170 |
@Override |
134 | 171 |
public Date getDate() { |
135 | 172 |
return created; |
--- app/models/Mention.java
+++ app/models/Mention.java
... | ... | @@ -1,23 +1,10 @@ |
1 | 1 |
/** |
2 |
- * Yobi, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2014 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @author Yi EungJun |
|
8 |
- * |
|
9 |
- * Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
- * you may not use this file except in compliance with the License. |
|
11 |
- * You may obtain a copy of the License at |
|
12 |
- * |
|
13 |
- * http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
- * |
|
15 |
- * Unless required by applicable law or agreed to in writing, software |
|
16 |
- * distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
- * See the License for the specific language governing permissions and |
|
19 |
- * limitations under the License. |
|
20 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
7 |
+ |
|
21 | 8 |
package models; |
22 | 9 |
|
23 | 10 |
import models.enumeration.ResourceType; |
... | ... | @@ -27,7 +14,13 @@ |
27 | 14 |
import javax.persistence.Entity; |
28 | 15 |
import javax.persistence.Id; |
29 | 16 |
import javax.persistence.ManyToOne; |
17 |
+import java.util.ArrayList; |
|
18 |
+import java.util.HashSet; |
|
19 |
+import java.util.List; |
|
30 | 20 |
import java.util.Set; |
21 |
+ |
|
22 |
+import static models.enumeration.ResourceType.ISSUE_COMMENT; |
|
23 |
+import static models.enumeration.ResourceType.ISSUE_POST; |
|
31 | 24 |
|
32 | 25 |
@Entity |
33 | 26 |
public class Mention extends Model { |
... | ... | @@ -73,4 +66,37 @@ |
73 | 66 |
mention.save(); |
74 | 67 |
} |
75 | 68 |
} |
69 |
+ |
|
70 |
+ public static List<Long> getMentioningIssueIds(Long mentionUserId) { |
|
71 |
+ Set<Long> ids = new HashSet<>(); |
|
72 |
+ Set<Long> commentIds = new HashSet<>(); |
|
73 |
+ |
|
74 |
+ for (Mention mention : Mention.find.where() |
|
75 |
+ .eq("user.id", mentionUserId) |
|
76 |
+ .in("resourceType", ISSUE_POST, ISSUE_COMMENT) |
|
77 |
+ .findList()) { |
|
78 |
+ |
|
79 |
+ switch (mention.resourceType) { |
|
80 |
+ case ISSUE_POST: |
|
81 |
+ ids.add(Long.valueOf(mention.resourceId)); |
|
82 |
+ break; |
|
83 |
+ case ISSUE_COMMENT: |
|
84 |
+ commentIds.add(Long.valueOf(mention.resourceId)); |
|
85 |
+ break; |
|
86 |
+ default: |
|
87 |
+ play.Logger.warn("'" + mention.resourceType + "' is not supported."); |
|
88 |
+ break; |
|
89 |
+ } |
|
90 |
+ } |
|
91 |
+ |
|
92 |
+ if (!commentIds.isEmpty()) { |
|
93 |
+ for (IssueComment comment : IssueComment.find.where() |
|
94 |
+ .idIn(new ArrayList<>(commentIds)) |
|
95 |
+ .findList()) { |
|
96 |
+ ids.add(comment.issue.id); |
|
97 |
+ } |
|
98 |
+ } |
|
99 |
+ |
|
100 |
+ return new ArrayList<>(ids); |
|
101 |
+ } |
|
76 | 102 |
} |
--- app/models/NotificationEvent.java
+++ app/models/NotificationEvent.java
... | ... | @@ -1,7 +1,7 @@ |
1 | 1 |
/** |
2 | 2 |
* Yona, 21st Century Project Hosting SW |
3 | 3 |
* <p> |
4 |
- * Copyright Yona & Yobi Authors & NAVER Corp. |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 | 5 |
* https://yona.io |
6 | 6 |
**/ |
7 | 7 |
package models; |
... | ... | @@ -14,7 +14,6 @@ |
14 | 14 |
import models.resource.GlobalResource; |
15 | 15 |
import models.resource.Resource; |
16 | 16 |
import models.resource.ResourceConvertible; |
17 |
-import models.Webhook; |
|
18 | 17 |
import org.apache.commons.collections.CollectionUtils; |
19 | 18 |
import org.apache.commons.collections.Predicate; |
20 | 19 |
import org.apache.commons.lang3.StringUtils; |
... | ... | @@ -29,23 +28,26 @@ |
29 | 28 |
import playRepository.*; |
30 | 29 |
import scala.concurrent.duration.Duration; |
31 | 30 |
import utils.AccessControl; |
31 |
+import utils.DiffUtil; |
|
32 | 32 |
import utils.EventConstants; |
33 | 33 |
import utils.RouteUtil; |
34 | 34 |
|
35 | 35 |
import javax.naming.LimitExceededException; |
36 | 36 |
import javax.persistence.*; |
37 | 37 |
import javax.servlet.ServletException; |
38 |
+import java.beans.Transient; |
|
38 | 39 |
import java.io.IOException; |
39 | 40 |
import java.io.UnsupportedEncodingException; |
40 | 41 |
import java.net.URLEncoder; |
41 |
-import java.util.Date; |
|
42 |
-import java.util.HashSet; |
|
43 |
-import java.util.List; |
|
44 |
-import java.util.Set; |
|
42 |
+import java.util.*; |
|
45 | 43 |
import java.util.concurrent.TimeUnit; |
46 | 44 |
import java.util.regex.Matcher; |
47 | 45 |
import java.util.regex.Pattern; |
48 | 46 |
|
47 |
+import static models.UserProjectNotification.findEventUnwatchersByEventType; |
|
48 |
+import static models.UserProjectNotification.findEventWatchersByEventType; |
|
49 |
+import static models.Watch.findUnwatchers; |
|
50 |
+import static models.Watch.findWatchers; |
|
49 | 51 |
import static models.enumeration.EventType.*; |
50 | 52 |
|
51 | 53 |
@Entity |
... | ... | @@ -108,6 +110,10 @@ |
108 | 110 |
return oldValue; |
109 | 111 |
} |
110 | 112 |
|
113 |
+ public String getNewValue() { |
|
114 |
+ return newValue; |
|
115 |
+ } |
|
116 |
+ |
|
111 | 117 |
@Transient |
112 | 118 |
public String getMessage() { |
113 | 119 |
return getMessage(Lang.defaultLang()); |
... | ... | @@ -128,14 +134,18 @@ |
128 | 134 |
} else { |
129 | 135 |
return Messages.get(lang, "notification.issue.assigned", newValue); |
130 | 136 |
} |
137 |
+ case ISSUE_MILESTONE_CHANGED: |
|
138 |
+ return Messages.get(lang, "notification.milestone.changed", newValue); |
|
131 | 139 |
case NEW_ISSUE: |
132 | 140 |
case NEW_POSTING: |
133 | 141 |
case NEW_COMMENT: |
134 | 142 |
case NEW_PULL_REQUEST: |
135 | 143 |
case NEW_COMMIT: |
136 |
- case ISSUE_BODY_CHANGED: |
|
137 | 144 |
case COMMENT_UPDATED: |
138 | 145 |
return newValue; |
146 |
+ case ISSUE_BODY_CHANGED: |
|
147 |
+ case POSTING_BODY_CHANGED: |
|
148 |
+ return DiffUtil.getDiffText(oldValue, newValue); |
|
139 | 149 |
case NEW_REVIEW_COMMENT: |
140 | 150 |
try { |
141 | 151 |
ReviewComment reviewComment = ReviewComment.find.byId(Long.valueOf(this.resourceId)); |
... | ... | @@ -189,8 +199,44 @@ |
189 | 199 |
} |
190 | 200 |
case ISSUE_MOVED: |
191 | 201 |
return Messages.get(lang, "notification.type.issue.moved", oldValue, newValue); |
202 |
+ case ISSUE_SHARER_CHANGED: |
|
203 |
+ if (StringUtils.isNotBlank(newValue)) { |
|
204 |
+ User user = User.findByLoginId(newValue); |
|
205 |
+ return Messages.get(lang, "notification.issue.sharer.added", user.getDisplayName(user)); |
|
206 |
+ } else if (StringUtils.isNotBlank(oldValue)) { |
|
207 |
+ return Messages.get(lang, "notification.issue.sharer.deleted"); |
|
208 |
+ } |
|
209 |
+ case ISSUE_LABEL_CHANGED: |
|
210 |
+ if (StringUtils.isNotBlank(newValue)) { |
|
211 |
+ User user = User.findByLoginId(newValue); |
|
212 |
+ return Messages.get(lang, "notification.issue.label.added", user.getDisplayName(user)); |
|
213 |
+ } else if (StringUtils.isNotBlank(oldValue)) { |
|
214 |
+ return Messages.get(lang, "notification.issue.label.deleted"); |
|
215 |
+ } |
|
216 |
+ case RESOURCE_DELETED: |
|
217 |
+ User user = User.findByLoginId(newValue); |
|
218 |
+ return Messages.get(lang, "notification.resource.deleted", user.getDisplayName(user)); |
|
192 | 219 |
default: |
193 |
- return null; |
|
220 |
+ play.Logger.warn("Unknown event message: " + this); |
|
221 |
+ play.Logger.warn("Event Type: " + eventType); |
|
222 |
+ play.Logger.warn("See: NotificationEvent.getMessage"); |
|
223 |
+ return eventType.getDescr(); |
|
224 |
+ } |
|
225 |
+ } |
|
226 |
+ |
|
227 |
+ @Transient |
|
228 |
+ public String getPlainMessage() { |
|
229 |
+ return getPlainMessage(Lang.defaultLang()); |
|
230 |
+ } |
|
231 |
+ |
|
232 |
+ @Transient |
|
233 |
+ public String getPlainMessage(Lang lang) { |
|
234 |
+ switch(eventType) { |
|
235 |
+ case ISSUE_BODY_CHANGED: |
|
236 |
+ case POSTING_BODY_CHANGED: |
|
237 |
+ return DiffUtil.getDiffPlainText(oldValue, newValue); |
|
238 |
+ default: |
|
239 |
+ return getMessage(lang); |
|
194 | 240 |
} |
195 | 241 |
} |
196 | 242 |
|
... | ... | @@ -360,8 +406,7 @@ |
360 | 406 |
.orderBy("id desc").setMaxRows(1).findUnique(); |
361 | 407 |
|
362 | 408 |
if (lastEvent != null) { |
363 |
- if (lastEvent.eventType == event.eventType && |
|
364 |
- event.senderId.equals(lastEvent.senderId)) { |
|
409 |
+ if (isSameUserEventAsPrevious(event, lastEvent)) { |
|
365 | 410 |
// If the last event is A -> B and the current event is B -> C, |
366 | 411 |
// they are merged into the new event A -> C. |
367 | 412 |
event.oldValue = lastEvent.getOldValue(); |
... | ... | @@ -381,6 +426,55 @@ |
381 | 426 |
} |
382 | 427 |
event.save(); |
383 | 428 |
event.saveManyToManyAssociations("receivers"); |
429 |
+ } |
|
430 |
+ |
|
431 |
+ public static void addWithoutSkipEvent(NotificationEvent event) { |
|
432 |
+ if (event.notificationMail == null) { |
|
433 |
+ event.notificationMail = new NotificationMail(); |
|
434 |
+ event.notificationMail.notificationEvent = event; |
|
435 |
+ } |
|
436 |
+ |
|
437 |
+ Date draftDate = DateTime.now().minusMillis(EventConstants.DRAFT_TIME_IN_MILLIS).toDate(); |
|
438 |
+ |
|
439 |
+ NotificationEvent lastEvent = NotificationEvent.find.where() |
|
440 |
+ .eq("resourceId", event.resourceId) |
|
441 |
+ .eq("resourceType", event.resourceType) |
|
442 |
+ .gt("created", draftDate) |
|
443 |
+ .orderBy("id desc").setMaxRows(1).findUnique(); |
|
444 |
+ |
|
445 |
+ if (lastEvent != null) { |
|
446 |
+ if (isSameUserEventAsPrevious(event, lastEvent) && |
|
447 |
+ isRevertingTheValue(event, lastEvent)) { |
|
448 |
+ lastEvent.delete(); |
|
449 |
+ return; |
|
450 |
+ } |
|
451 |
+ } |
|
452 |
+ |
|
453 |
+ if(isAddingSharerEvent(event)){ |
|
454 |
+ filterReceivers(event); |
|
455 |
+ } |
|
456 |
+ |
|
457 |
+ if (event.receivers.isEmpty()) { |
|
458 |
+ return; |
|
459 |
+ } |
|
460 |
+ event.save(); |
|
461 |
+ event.saveManyToManyAssociations("receivers"); |
|
462 |
+ } |
|
463 |
+ |
|
464 |
+ private static boolean isSameUserEventAsPrevious(NotificationEvent event, NotificationEvent lastEvent) { |
|
465 |
+ return lastEvent.eventType == event.eventType && |
|
466 |
+ event.senderId.equals(lastEvent.senderId); |
|
467 |
+ } |
|
468 |
+ |
|
469 |
+ private static boolean isRevertingTheValue(NotificationEvent event, NotificationEvent lastEvent) { |
|
470 |
+ return StringUtils.equals(event.oldValue, lastEvent.newValue) && |
|
471 |
+ StringUtils.equals(event.newValue, lastEvent.oldValue); |
|
472 |
+ } |
|
473 |
+ |
|
474 |
+ private static boolean isAddingSharerEvent(NotificationEvent event) { |
|
475 |
+ return event.eventType.equals(EventType.ISSUE_SHARER_CHANGED) |
|
476 |
+ && StringUtils.isBlank(event.oldValue) |
|
477 |
+ && StringUtils.isNotBlank(event.newValue); |
|
384 | 478 |
} |
385 | 479 |
|
386 | 480 |
private static void filterReceivers(final NotificationEvent event) { |
... | ... | @@ -638,10 +732,7 @@ |
638 | 732 |
NotificationEvent notiEvent = createFrom(author, comment); |
639 | 733 |
notiEvent.title = formatReplyTitle(post); |
640 | 734 |
notiEvent.eventType = eventType; |
641 |
- Set<User> receivers = getReceivers(post, author); |
|
642 |
- receivers.addAll(getMentionedUsers(comment.contents)); |
|
643 |
- receivers.remove(author); |
|
644 |
- notiEvent.receivers = receivers; |
|
735 |
+ notiEvent.receivers = getMandatoryReceivers(comment, eventType); |
|
645 | 736 |
notiEvent.oldValue = null; |
646 | 737 |
notiEvent.newValue = comment.contents; |
647 | 738 |
notiEvent.resourceType = comment.asResource().getType(); |
... | ... | @@ -680,7 +771,7 @@ |
680 | 771 |
|
681 | 772 |
NotificationEvent notiEvent = createFromCurrentUser(issue); |
682 | 773 |
notiEvent.title = formatReplyTitle(issue); |
683 |
- notiEvent.receivers = getReceivers(issue); |
|
774 |
+ notiEvent.receivers = getMandatoryReceivers(issue, EventType.ISSUE_STATE_CHANGED); |
|
684 | 775 |
notiEvent.eventType = ISSUE_STATE_CHANGED; |
685 | 776 |
notiEvent.oldValue = oldState != null ? oldState.state() : null; |
686 | 777 |
notiEvent.newValue = issue.state.state(); |
... | ... | @@ -728,12 +819,9 @@ |
728 | 819 |
|
729 | 820 |
NotificationEvent notiEvent = createFromCurrentUser(issue); |
730 | 821 |
|
731 |
- Set<User> receivers = getReceivers(issue); |
|
822 |
+ Set<User> receivers = getReceiversWhenAssigneeChanged(oldAssignee, issue); |
|
732 | 823 |
if(oldAssignee != null) { |
733 | 824 |
notiEvent.oldValue = oldAssignee.loginId; |
734 |
- if(!oldAssignee.loginId.equals(UserApp.currentUser().loginId)) { |
|
735 |
- receivers.add(oldAssignee); |
|
736 |
- } |
|
737 | 825 |
} |
738 | 826 |
|
739 | 827 |
if (issue.assignee != null) { |
... | ... | @@ -748,6 +836,17 @@ |
748 | 836 |
return notiEvent; |
749 | 837 |
} |
750 | 838 |
|
839 |
+ private static Set<User> getReceiversWhenAssigneeChanged(User oldAssignee, Issue issue) { |
|
840 |
+ Set<User> receivers = getMandatoryReceivers(issue, ISSUE_ASSIGNEE_CHANGED); |
|
841 |
+ |
|
842 |
+ if (oldAssignee != null && !oldAssignee.isAnonymous() |
|
843 |
+ && !oldAssignee.loginId.equals(UserApp.currentUser().loginId)) { |
|
844 |
+ receivers.add(oldAssignee); |
|
845 |
+ } |
|
846 |
+ |
|
847 |
+ return receivers; |
|
848 |
+ } |
|
849 |
+ |
|
751 | 850 |
public static void afterNewIssue(Issue issue) { |
752 | 851 |
NotificationEvent.add(forNewIssue(issue, UserApp.currentUser())); |
753 | 852 |
webhookRequest(NEW_ISSUE, issue, false); |
... | ... | @@ -760,6 +859,21 @@ |
760 | 859 |
notiEvent.eventType = NEW_ISSUE; |
761 | 860 |
notiEvent.oldValue = null; |
762 | 861 |
notiEvent.newValue = issue.body; |
862 |
+ return notiEvent; |
|
863 |
+ } |
|
864 |
+ |
|
865 |
+ public static NotificationEvent afterResourceDeleted(AbstractPosting item, User reuqestedUser) { |
|
866 |
+ NotificationEvent notiEvent = createFrom(reuqestedUser, item.project); |
|
867 |
+ notiEvent.title = formatNewTitle(item); |
|
868 |
+ notiEvent.receivers = getReceivers(item, reuqestedUser); |
|
869 |
+ notiEvent.eventType = RESOURCE_DELETED; |
|
870 |
+ notiEvent.oldValue = item.body; |
|
871 |
+ notiEvent.newValue = reuqestedUser.loginId; |
|
872 |
+ |
|
873 |
+ NotificationEvent.add(notiEvent); |
|
874 |
+ if (item instanceof Issue) { |
|
875 |
+ webhookRequest(RESOURCE_DELETED, (Issue)item, false); |
|
876 |
+ } |
|
763 | 877 |
return notiEvent; |
764 | 878 |
} |
765 | 879 |
|
... | ... | @@ -793,8 +907,140 @@ |
793 | 907 |
return notiEvent; |
794 | 908 |
} |
795 | 909 |
|
910 |
+ public static NotificationEvent afterIssueSharerChanged(Issue issue, String sharerLoginId, String action) { |
|
911 |
+ NotificationEvent notiEvent = createFromCurrentUser(issue); |
|
912 |
+ notiEvent.title = formatReplyTitle(issue); |
|
913 |
+ notiEvent.receivers = findSharer(sharerLoginId); |
|
914 |
+ notiEvent.eventType = ISSUE_SHARER_CHANGED; |
|
915 |
+ if (IssueSharer.ADD.equalsIgnoreCase(action)) { |
|
916 |
+ notiEvent.oldValue = ""; |
|
917 |
+ notiEvent.newValue = sharerLoginId; |
|
918 |
+ } else if (IssueSharer.DELETE.equalsIgnoreCase(action)) { |
|
919 |
+ notiEvent.oldValue = sharerLoginId; |
|
920 |
+ notiEvent.newValue = ""; |
|
921 |
+ } |
|
922 |
+ |
|
923 |
+ NotificationEvent.addWithoutSkipEvent(notiEvent); |
|
924 |
+ |
|
925 |
+ return notiEvent; |
|
926 |
+ } |
|
927 |
+ |
|
928 |
+ private static Set<User> findSharer(String sharerLoginId) { |
|
929 |
+ Set<User> receivers = new HashSet<>(); |
|
930 |
+ receivers.add(User.findByLoginId(sharerLoginId)); |
|
931 |
+ return receivers; |
|
932 |
+ } |
|
933 |
+ |
|
934 |
+ public static NotificationEvent afterIssueLabelChanged(String addedLabels, String deletedLabels, Issue issue) { |
|
935 |
+ NotificationEvent notiEvent = createFromCurrentUser(issue); |
|
936 |
+ notiEvent.title = formatReplyTitle(issue); |
|
937 |
+ notiEvent.receivers = null; // no receivers |
|
938 |
+ notiEvent.eventType = ISSUE_LABEL_CHANGED; |
|
939 |
+ notiEvent.oldValue = deletedLabels; |
|
940 |
+ notiEvent.newValue = addedLabels; |
|
941 |
+ |
|
942 |
+ NotificationEvent.addWithoutSkipEvent(notiEvent); |
|
943 |
+ return notiEvent; |
|
944 |
+ } |
|
945 |
+ |
|
946 |
+ public static NotificationEvent afterMilestoneChanged(Long oldMilestoneId, Issue issue) { |
|
947 |
+ webhookRequest(ISSUE_MILESTONE_CHANGED, issue, false); |
|
948 |
+ |
|
949 |
+ NotificationEvent notiEvent = createFromCurrentUser(issue); |
|
950 |
+ |
|
951 |
+ Set<User> receivers = getMandatoryReceivers(issue, ISSUE_MILESTONE_CHANGED); |
|
952 |
+ |
|
953 |
+ notiEvent.title = formatReplyTitle(issue); |
|
954 |
+ notiEvent.receivers = receivers; |
|
955 |
+ notiEvent.eventType = ISSUE_MILESTONE_CHANGED; |
|
956 |
+ notiEvent.oldValue = oldMilestoneId.toString(); |
|
957 |
+ notiEvent.newValue = issue.milestoneId().toString(); |
|
958 |
+ |
|
959 |
+ NotificationEvent.add(notiEvent); |
|
960 |
+ |
|
961 |
+ return notiEvent; |
|
962 |
+ } |
|
963 |
+ |
|
964 |
+ private static Set<User> getMandatoryReceivers(Issue issue, EventType eventType) { |
|
965 |
+ Set<User> receivers = findWatchers(issue.asResource()); |
|
966 |
+ receivers.add(issue.getAuthor()); |
|
967 |
+ |
|
968 |
+ for (IssueSharer issueSharer : issue.sharers) { |
|
969 |
+ receivers.add(User.findByLoginId(issueSharer.loginId)); |
|
970 |
+ } |
|
971 |
+ |
|
972 |
+ if (issue.assignee != null) { |
|
973 |
+ receivers.add(issue.assignee.user); |
|
974 |
+ } |
|
975 |
+ |
|
976 |
+ receivers.addAll(findWatchers(issue.asResource())); |
|
977 |
+ receivers.addAll(findEventWatchersByEventType(issue.project.id, eventType)); |
|
978 |
+ |
|
979 |
+ receivers.removeAll(findUnwatchers(issue.asResource())); |
|
980 |
+ receivers.removeAll(findEventUnwatchersByEventType(issue.project.id, eventType)); |
|
981 |
+ receivers.remove(UserApp.currentUser()); |
|
982 |
+ |
|
983 |
+ return receivers; |
|
984 |
+ } |
|
985 |
+ |
|
986 |
+ private static Set<User> getMandatoryReceivers(Posting posting, EventType eventType) { |
|
987 |
+ Set<User> receivers = findWatchers(posting.asResource()); |
|
988 |
+ receivers.add(posting.getAuthor()); |
|
989 |
+ receivers.addAll(findWatchers(posting.asResource())); |
|
990 |
+ receivers.addAll(findEventWatchersByEventType(posting.project.id, eventType)); |
|
991 |
+ |
|
992 |
+ receivers.removeAll(findUnwatchers(posting.asResource())); |
|
993 |
+ receivers.removeAll(findEventUnwatchersByEventType(posting.project.id, eventType)); |
|
994 |
+ receivers.remove(UserApp.currentUser()); |
|
995 |
+ |
|
996 |
+ return receivers; |
|
997 |
+ } |
|
998 |
+ |
|
999 |
+ private static Set<User> getMandatoryReceivers(Comment comment, EventType eventType) { |
|
1000 |
+ AbstractPosting parent = comment.getParent(); |
|
1001 |
+ Set<User> receivers = findWatchers(parent.asResource()); |
|
1002 |
+ receivers.add(parent.getAuthor()); |
|
1003 |
+ receivers.addAll(findEventWatchersByEventType(comment.projectId, eventType)); |
|
1004 |
+ receivers.addAll(getMentionedUsers(comment.contents)); |
|
1005 |
+ includeAssigneeIfExist(comment, receivers); |
|
1006 |
+ |
|
1007 |
+ receivers.removeAll(findUnwatchers(parent.asResource())); |
|
1008 |
+ receivers.removeAll(findEventUnwatchersByEventType(comment.projectId, eventType)); |
|
1009 |
+ receivers.remove(UserApp.currentUser()); |
|
1010 |
+ |
|
1011 |
+ return receivers; |
|
1012 |
+ } |
|
1013 |
+ |
|
1014 |
+ private static Set<User> getProjectCommitReceivers(Project project, EventType eventType) { |
|
1015 |
+ Set<User> receivers = findMembersOnlyFromWatchers(project); |
|
1016 |
+ receivers.removeAll(findUnwatchers(project.asResource())); |
|
1017 |
+ receivers.removeAll(findEventUnwatchersByEventType(project.id, eventType)); |
|
1018 |
+ receivers.remove(UserApp.currentUser()); |
|
1019 |
+ |
|
1020 |
+ return receivers; |
|
1021 |
+ } |
|
1022 |
+ |
|
1023 |
+ private static Set<User> findMembersOnlyFromWatchers(Project project) { |
|
1024 |
+ Set<User> receivers = new HashSet<>(); |
|
1025 |
+ Set<User> projectMembers = extractMembers(project); |
|
1026 |
+ for (User watcher : findWatchers(project.asResource())) { |
|
1027 |
+ if (projectMembers.contains(watcher)) { |
|
1028 |
+ receivers.add(watcher); |
|
1029 |
+ } |
|
1030 |
+ } |
|
1031 |
+ return receivers; |
|
1032 |
+ } |
|
1033 |
+ |
|
1034 |
+ private static Set<User> extractMembers(Project project) { |
|
1035 |
+ Set<User> projectMembers = new HashSet<>(); |
|
1036 |
+ for (ProjectUser projectUser : project.members()) { |
|
1037 |
+ projectMembers.add(projectUser.user); |
|
1038 |
+ } |
|
1039 |
+ return projectMembers; |
|
1040 |
+ } |
|
1041 |
+ |
|
796 | 1042 |
private static Set<User> getReceiversForIssueBodyChanged(String oldBody, Issue issue) { |
797 |
- Set<User> receivers = issue.getWatchers(); |
|
1043 |
+ Set<User> receivers = getMandatoryReceivers(issue, ISSUE_BODY_CHANGED); |
|
798 | 1044 |
receivers.addAll(getNewMentionedUsers(oldBody, issue.body)); |
799 | 1045 |
receivers.remove(UserApp.currentUser()); |
800 | 1046 |
return receivers; |
... | ... | @@ -804,12 +1050,26 @@ |
804 | 1050 |
NotificationEvent.add(forNewPosting(post, UserApp.currentUser())); |
805 | 1051 |
} |
806 | 1052 |
|
1053 |
+ public static void afterUpdatePosting(String oldValue, Posting post) { |
|
1054 |
+ NotificationEvent.add(forUpdatePosting(oldValue, post, UserApp.currentUser())); |
|
1055 |
+ } |
|
1056 |
+ |
|
807 | 1057 |
public static NotificationEvent forNewPosting(Posting post, User author) { |
808 | 1058 |
NotificationEvent notiEvent = createFrom(author, post); |
809 | 1059 |
notiEvent.title = formatNewTitle(post); |
810 | 1060 |
notiEvent.receivers = getReceivers(post); |
811 | 1061 |
notiEvent.eventType = NEW_POSTING; |
812 | 1062 |
notiEvent.oldValue = null; |
1063 |
+ notiEvent.newValue = post.body; |
|
1064 |
+ return notiEvent; |
|
1065 |
+ } |
|
1066 |
+ |
|
1067 |
+ public static NotificationEvent forUpdatePosting(String oldValue, Posting post, User author) { |
|
1068 |
+ NotificationEvent notiEvent = createFrom(author, post); |
|
1069 |
+ notiEvent.title = formatNewTitle(post); |
|
1070 |
+ notiEvent.receivers = getMandatoryReceivers(post, EventType.POSTING_BODY_CHANGED); |
|
1071 |
+ notiEvent.eventType = POSTING_BODY_CHANGED; |
|
1072 |
+ notiEvent.oldValue = oldValue; |
|
813 | 1073 |
notiEvent.newValue = post.body; |
814 | 1074 |
return notiEvent; |
815 | 1075 |
} |
... | ... | @@ -920,10 +1180,10 @@ |
920 | 1180 |
NotificationEvent.add(notiEvent); |
921 | 1181 |
} |
922 | 1182 |
|
923 |
- public static void afterNewCommits(List<RevCommit> commits, List<String> refNames, Project project, User sender, String title, Set<User> watchers) { |
|
1183 |
+ public static void afterNewCommits(List<RevCommit> commits, List<String> refNames, Project project, User sender, String title) { |
|
924 | 1184 |
NotificationEvent notiEvent = createFrom(sender, project); |
925 | 1185 |
notiEvent.title = title; |
926 |
- notiEvent.receivers = watchers; |
|
1186 |
+ notiEvent.receivers = getProjectCommitReceivers(project, NEW_COMMIT); |
|
927 | 1187 |
notiEvent.eventType = NEW_COMMIT; |
928 | 1188 |
notiEvent.oldValue = null; |
929 | 1189 |
notiEvent.newValue = newCommitsMessage(commits, refNames, project); |
... | ... | @@ -1018,6 +1278,15 @@ |
1018 | 1278 |
receivers.addAll(getMentionedUsers(abstractPosting.body)); |
1019 | 1279 |
receivers.remove(except); |
1020 | 1280 |
return receivers; |
1281 |
+ } |
|
1282 |
+ |
|
1283 |
+ private static void includeAssigneeIfExist(Comment comment, Set<User> receivers) { |
|
1284 |
+ if (comment instanceof IssueComment) { |
|
1285 |
+ Assignee assignee = ((Issue) comment.getParent()).assignee; |
|
1286 |
+ if (assignee != null) { |
|
1287 |
+ receivers.add(assignee.user); |
|
1288 |
+ } |
|
1289 |
+ } |
|
1021 | 1290 |
} |
1022 | 1291 |
|
1023 | 1292 |
private static String getPrefixedNumber(AbstractPosting posting) { |
... | ... | @@ -1266,4 +1535,21 @@ |
1266 | 1535 |
webhookRequest(COMMENT_UPDATED, comment, false); |
1267 | 1536 |
NotificationEvent.add(forUpdatedComment(comment, UserApp.currentUser())); |
1268 | 1537 |
} |
1538 |
+ |
|
1539 |
+ @Override |
|
1540 |
+ public String toString() { |
|
1541 |
+ return "NotificationEvent{" + |
|
1542 |
+ "id=" + id + |
|
1543 |
+ ", title='" + title + '\'' + |
|
1544 |
+ ", senderId=" + senderId + |
|
1545 |
+ ", receivers=" + receivers + |
|
1546 |
+ ", created=" + created + |
|
1547 |
+ ", resourceType=" + resourceType + |
|
1548 |
+ ", resourceId='" + resourceId + '\'' + |
|
1549 |
+ ", eventType=" + eventType + |
|
1550 |
+ ", oldValue='" + oldValue + '\'' + |
|
1551 |
+ ", newValue='" + newValue + '\'' + |
|
1552 |
+ ", notificationMail=" + notificationMail + |
|
1553 |
+ '}'; |
|
1554 |
+ } |
|
1269 | 1555 |
} |
--- app/models/NotificationMail.java
+++ app/models/NotificationMail.java
... | ... | @@ -1,7 +1,7 @@ |
1 | 1 |
/** |
2 | 2 |
* Yona, 21st Century Project Hosting SW |
3 | 3 |
* <p> |
4 |
- * Copyright Yona & Yobi Authors & NAVER Corp. |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 | 5 |
* https://yona.io |
6 | 6 |
**/ |
7 | 7 |
package models; |
... | ... | @@ -533,8 +533,9 @@ |
533 | 533 |
Lang lang = Lang.apply(langCode); |
534 | 534 |
|
535 | 535 |
String message = event.getMessage(lang); |
536 |
+ String plainMessage = event.getPlainMessage(lang); |
|
536 | 537 |
|