
Merge branch `issue-278` of changsung/yobi
from pull request 593
@d9adbd98e1b1a7aafa5881cc0c63b6fa5d3c1780
--- app/controllers/ImportApp.java
+++ app/controllers/ImportApp.java
... | ... | @@ -44,7 +44,7 @@ |
44 | 44 |
*/ |
45 | 45 |
@Transactional |
46 | 46 |
public static Result newProject() throws GitAPIException, IOException { |
47 |
- if( !AccessControl.isCreatable(UserApp.currentUser()) ){ |
|
47 |
+ if( !AccessControl.isGlobalResourceCreatable(UserApp.currentUser()) ){ |
|
48 | 48 |
return forbidden("'" + UserApp.currentUser().name + "' has no permission"); |
49 | 49 |
} |
50 | 50 |
|
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
... | ... | @@ -586,6 +586,36 @@ |
586 | 586 |
return redirect(redirectTo); |
587 | 587 |
} |
588 | 588 |
|
589 |
+ private static void addAssigneeChangedNotification(Issue modifiedIssue, Issue originalIssue, Call redirectTo) { |
|
590 |
+ if(!originalIssue.assignedUserEquals(modifiedIssue.assignee)) { |
|
591 |
+ Issue updatedIssue = Issue.finder.byId(originalIssue.id); |
|
592 |
+ User oldAssignee = null; |
|
593 |
+ if(originalIssue.assignee != null) { |
|
594 |
+ oldAssignee = originalIssue.assignee.user; |
|
595 |
+ } |
|
596 |
+ NotificationEvent notiEvent = NotificationEvent.afterAssigneeChanged(oldAssignee, updatedIssue, redirectTo.absoluteURL(request())); |
|
597 |
+ IssueEvent.addFromNotificationEvent(notiEvent, modifiedIssue, UserApp.currentUser().loginId); |
|
598 |
+ } |
|
599 |
+ } |
|
600 |
+ |
|
601 |
+ private static void addStateChangedNotification(Issue modifiedIssue, Issue originalIssue, Call redirectTo) { |
|
602 |
+ if(modifiedIssue.state != originalIssue.state) { |
|
603 |
+ Issue updatedIssue = Issue.finder.byId(originalIssue.id); |
|
604 |
+ NotificationEvent notiEvent = NotificationEvent.afterStateChanged(originalIssue.state, updatedIssue, |
|
605 |
+ redirectTo.absoluteURL(request())); |
|
606 |
+ IssueEvent.addFromNotificationEvent(notiEvent, modifiedIssue, UserApp.currentUser().loginId); |
|
607 |
+ } |
|
608 |
+ } |
|
609 |
+ |
|
610 |
+ private static void addBodyChangedNotification(Issue modifiedIssue, Issue originalIssue, Call redirectTo) { |
|
611 |
+ if (!modifiedIssue.body.equals(originalIssue.body)) { |
|
612 |
+ Issue updatedIssue = Issue.finder.byId(originalIssue.id); |
|
613 |
+ NotificationEvent notiEvent = NotificationEvent.afterIssueBodyChanged(originalIssue.body, updatedIssue, |
|
614 |
+ redirectTo.absoluteURL(request())); |
|
615 |
+ IssueEvent.addFromNotificationEvent(notiEvent, modifiedIssue, UserApp.currentUser().loginId); |
|
616 |
+ } |
|
617 |
+ } |
|
618 |
+ |
|
589 | 619 |
/** |
590 | 620 |
* 이슈 수정 |
591 | 621 |
* |
... | ... | @@ -624,29 +654,20 @@ |
624 | 654 |
Runnable updateIssueBeforeSave = new Runnable() { |
625 | 655 |
@Override |
626 | 656 |
public void run() { |
657 |
+ // Below addAll() method is needed to avoid the exception, 'Timeout trying to lock table ISSUE'. |
|
658 |
+ // This is just workaround and the cause of the exception is not figured out yet. |
|
659 |
+ // Do not replace it to 'issue.comments = originalIssue.comments;' |
|
660 |
+ issue.voters.addAll(originalIssue.voters); |
|
627 | 661 |
issue.comments = originalIssue.comments; |
628 | 662 |
addLabels(issue, request()); |
629 | 663 |
} |
630 | 664 |
}; |
631 | 665 |
|
666 |
+ addAssigneeChangedNotification(issue, originalIssue, redirectTo); |
|
667 |
+ addStateChangedNotification(issue, originalIssue, redirectTo); |
|
668 |
+ addBodyChangedNotification(issue, originalIssue, redirectTo); |
|
669 |
+ |
|
632 | 670 |
Result result = editPosting(originalIssue, issue, issueForm, redirectTo, updateIssueBeforeSave); |
633 |
- |
|
634 |
- if(!originalIssue.assignedUserEquals(issue.assignee)) { |
|
635 |
- Issue updatedIssue = Issue.finder.byId(originalIssue.id); |
|
636 |
- User oldAssignee = null; |
|
637 |
- if(originalIssue.assignee != null) { |
|
638 |
- oldAssignee = originalIssue.assignee.user; |
|
639 |
- } |
|
640 |
- NotificationEvent notiEvent = NotificationEvent.afterAssigneeChanged(oldAssignee, updatedIssue, redirectTo.absoluteURL(request())); |
|
641 |
- IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId); |
|
642 |
- } |
|
643 |
- |
|
644 |
- if(issue.state != originalIssue.state) { |
|
645 |
- Issue updatedIssue = Issue.finder.byId(originalIssue.id); |
|
646 |
- NotificationEvent notiEvent = NotificationEvent.afterStateChanged(originalIssue.state, updatedIssue, |
|
647 |
- redirectTo.absoluteURL(request())); |
|
648 |
- IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId); |
|
649 |
- } |
|
650 | 671 |
|
651 | 672 |
return result; |
652 | 673 |
} |
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
... | ... | @@ -191,7 +191,7 @@ |
191 | 191 |
*/ |
192 | 192 |
@Transactional |
193 | 193 |
public static Result newProject() throws Exception { |
194 |
- if( !AccessControl.isCreatable(UserApp.currentUser()) ){ |
|
194 |
+ if( !AccessControl.isGlobalResourceCreatable(UserApp.currentUser()) ){ |
|
195 | 195 |
return forbidden(ErrorViews.Forbidden.render("'" + UserApp.currentUser().name + "' has no permission")); |
196 | 196 |
} |
197 | 197 |
Form<Project> filledNewProjectForm = form(Project.class).bindFromRequest(); |
+++ app/controllers/VoteApp.java
... | ... | @@ -0,0 +1,86 @@ |
1 | +/* | |
2 | + * Yobi, Project Hosting SW | |
3 | + * | |
4 | + * Copyright 2013 NAVER Corp. | |
5 | + * http://yobi.io | |
6 | + * | |
7 | + * @Author Changsung Kim | |
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 | + */ | |
21 | +package controllers; | |
22 | + | |
23 | +import models.Issue; | |
24 | +import play.mvc.Call; | |
25 | +import play.mvc.With; | |
26 | +import models.Project; | |
27 | +import play.mvc.Result; | |
28 | +import play.mvc.Controller; | |
29 | +import play.db.ebean.Transactional; | |
30 | +import actions.AnonymousCheckAction; | |
31 | +import models.enumeration.ResourceType; | |
32 | +import controllers.annotation.IsCreatable; | |
33 | + | |
34 | +/** | |
35 | + * 이슈에서 투표하기 위한 Controller | |
36 | + */ | |
37 | +@With(AnonymousCheckAction.class) | |
38 | +public class VoteApp extends Controller { | |
39 | + | |
40 | + /** | |
41 | + * 투표하기 | |
42 | + * | |
43 | + * 투표요청이 들어온 이슈에서 로그인 사용자의 투표가 등록된다. | |
44 | + * | |
45 | + * @param ownerName | |
46 | + * @param projectName | |
47 | + * @param issueNumber | |
48 | + * @return | |
49 | + */ | |
50 | + @Transactional | |
51 | + @IsCreatable(ResourceType.ISSUE_COMMENT) | |
52 | + public static Result vote(String ownerName, String projectName, Long issueNumber) { | |
53 | + | |
54 | + Project project = Project.findByOwnerAndProjectName(ownerName, projectName); | |
55 | + Issue issue = Issue.findByNumber(project, issueNumber); | |
56 | + | |
57 | + issue.addVoter(UserApp.currentUser()); | |
58 | + | |
59 | + Call call = routes.IssueApp.issue(ownerName, projectName, issueNumber); | |
60 | + | |
61 | + return redirect(call); | |
62 | + } | |
63 | + | |
64 | + /** | |
65 | + * 투표 취소하기 | |
66 | + * | |
67 | + * 투표요청이 들어온 이슈에서 로그인 사용자의 투표가 취소된다. | |
68 | + * | |
69 | + * @param ownerName | |
70 | + * @param projectName | |
71 | + * @param issueNumber | |
72 | + * @return | |
73 | + */ | |
74 | + @Transactional | |
75 | + @IsCreatable(ResourceType.ISSUE_COMMENT) | |
76 | + public static Result unvote(String ownerName, String projectName, Long issueNumber) { | |
77 | + Project project = Project.findByOwnerAndProjectName(ownerName, projectName); | |
78 | + Issue issue = Issue.findByNumber(project, issueNumber); | |
79 | + | |
80 | + issue.removeVoter(UserApp.currentUser()); | |
81 | + | |
82 | + Call call = routes.IssueApp.issue(ownerName, projectName, issueNumber); | |
83 | + | |
84 | + return redirect(call); | |
85 | + } | |
86 | +} |
--- app/models/Issue.java
+++ app/models/Issue.java
... | ... | @@ -73,6 +73,14 @@ |
73 | 73 |
@OneToMany(cascade = CascadeType.ALL, mappedBy="issue") |
74 | 74 |
public List<IssueEvent> events; |
75 | 75 |
|
76 |
+ @ManyToMany(cascade = CascadeType.ALL) |
|
77 |
+ @JoinTable( |
|
78 |
+ name = "issue_voter", |
|
79 |
+ joinColumns = @JoinColumn(name = "issue_id"), |
|
80 |
+ inverseJoinColumns = @JoinColumn(name = "user_id") |
|
81 |
+ ) |
|
82 |
+ public List<User> voters = new ArrayList<>(); |
|
83 |
+ |
|
76 | 84 |
/** |
77 | 85 |
* @return |
78 | 86 |
* @see models.AbstractPosting#computeNumOfComments() |
... | ... | @@ -328,9 +336,9 @@ |
328 | 336 |
} |
329 | 337 |
|
330 | 338 |
/** |
331 |
- * 이 이슈를 지켜보고 있는 모든 사용자들을 얻는다. |
|
339 |
+ * 이 이슈를 지켜보고 있는 모든 사용자와 이슈에 투표를 한 모든 사용자를 얻는다. |
|
332 | 340 |
* |
333 |
- * @return 이 이슈를 지켜보고 있는 모든 사용자들의 집합 |
|
341 |
+ * @return 이 이슈를 지켜보고 있는 모든 사용자와 이슈에 투표를 한 모든 사용자의 집합. |
|
334 | 342 |
*/ |
335 | 343 |
@Transient |
336 | 344 |
public Set<User> getWatchers() { |
... | ... | @@ -338,6 +346,8 @@ |
338 | 346 |
if (assignee != null) { |
339 | 347 |
baseWatchers.add(assignee.user); |
340 | 348 |
} |
349 |
+ baseWatchers.addAll(this.voters); |
|
350 |
+ |
|
341 | 351 |
return super.getWatchers(baseWatchers); |
342 | 352 |
} |
343 | 353 |
|
... | ... | @@ -490,4 +500,35 @@ |
490 | 500 |
|
491 | 501 |
return true; |
492 | 502 |
} |
503 |
+ |
|
504 |
+ /** |
|
505 |
+ * {@code user}를 투표자로 추가한다. |
|
506 |
+ * |
|
507 |
+ * @param user |
|
508 |
+ */ |
|
509 |
+ public void addVoter(User user) { |
|
510 |
+ this.voters.add(user); |
|
511 |
+ this.update(); |
|
512 |
+ } |
|
513 |
+ |
|
514 |
+ /** |
|
515 |
+ * {@code user}의 투표를 취소한다. |
|
516 |
+ * |
|
517 |
+ * @param user |
|
518 |
+ */ |
|
519 |
+ public void removeVoter(User user) { |
|
520 |
+ this.voters.remove(user); |
|
521 |
+ this.update(); |
|
522 |
+ } |
|
523 |
+ |
|
524 |
+ /** |
|
525 |
+ * {@code user}의 투표 여부를 반환한다. |
|
526 |
+ * |
|
527 |
+ * @param user |
|
528 |
+ * @return 투표를 했으면 true, 아니면 false. |
|
529 |
+ */ |
|
530 |
+ public boolean isVotedBy(User user) { |
|
531 |
+ return this.voters.contains(user); |
|
532 |
+ } |
|
533 |
+ |
|
493 | 534 |
} |
--- app/models/NotificationEvent.java
+++ app/models/NotificationEvent.java
... | ... | @@ -108,6 +108,7 @@ |
108 | 108 |
case NEW_PULL_REQUEST: |
109 | 109 |
case NEW_PULL_REQUEST_COMMENT: |
110 | 110 |
case NEW_COMMIT: |
111 |
+ case ISSUE_BODY_CHANGED: |
|
111 | 112 |
return newValue; |
112 | 113 |
case PULL_REQUEST_STATE_CHANGED: |
113 | 114 |
if (State.OPEN.state().equals(newValue)) { |
... | ... | @@ -459,6 +460,20 @@ |
459 | 460 |
NotificationEvent.add(notiEvent); |
460 | 461 |
} |
461 | 462 |
|
463 |
+ public static NotificationEvent afterIssueBodyChanged(String oldBody, Issue issue, String urlToView) { |
|
464 |
+ NotificationEvent notiEvent = createFromCurrentUser(issue); |
|
465 |
+ notiEvent.title = formatReplyTitle(issue); |
|
466 |
+ notiEvent.urlToView = urlToView; |
|
467 |
+ notiEvent.receivers = getReceivers(issue); |
|
468 |
+ notiEvent.eventType = EventType.ISSUE_BODY_CHANGED; |
|
469 |
+ notiEvent.oldValue = oldBody; |
|
470 |
+ notiEvent.newValue = issue.body; |
|
471 |
+ |
|
472 |
+ NotificationEvent.add(notiEvent); |
|
473 |
+ |
|
474 |
+ return notiEvent; |
|
475 |
+ } |
|
476 |
+ |
|
462 | 477 |
public static void afterNewPost(Posting post) { |
463 | 478 |
NotificationEvent notiEvent = createFromCurrentUser(post); |
464 | 479 |
notiEvent.title = formatNewTitle(post); |
--- app/models/enumeration/EventType.java
+++ app/models/enumeration/EventType.java
... | ... | @@ -21,6 +21,7 @@ |
21 | 21 |
NEW_COMMIT("notification.type.new.commit", 13), |
22 | 22 |
PULL_REQUEST_REVIEWED("notification.type.pullrequest.reviewed", 14), |
23 | 23 |
PULL_REQUEST_UNREVIEWED("notification.type.pullrequest.unreviewed", 15), |
24 |
+ ISSUE_BODY_CHANGED("notification.type.issue.body.changed", 17), |
|
24 | 25 |
ISSUE_REFERRED_FROM_PULL_REQUEST("notification.type.issue.referred.from.pullrequest", 16); |
25 | 26 |
|
26 | 27 |
private String descr; |
--- app/utils/AccessControl.java
+++ app/utils/AccessControl.java
... | ... | @@ -21,7 +21,7 @@ |
21 | 21 |
* @param user |
22 | 22 |
* @return user가 해당 resourceType을 생성할 수 있는지 여부 |
23 | 23 |
*/ |
24 |
- public static boolean isCreatable(User user) { |
|
24 |
+ public static boolean isGlobalResourceCreatable(User user) { |
|
25 | 25 |
return !user.isAnonymous(); |
26 | 26 |
} |
27 | 27 |
|
... | ... | @@ -147,7 +147,7 @@ |
147 | 147 |
* @return |
148 | 148 |
*/ |
149 | 149 |
private static boolean isProjectResourceAllowed(User user, Project project, Resource resource, Operation operation) { |
150 |
- if (ProjectUser.isManager(user.id, project.id)) { |
|
150 |
+ if (user.isSiteManager() || ProjectUser.isManager(user.id, project.id)) { |
|
151 | 151 |
return true; |
152 | 152 |
} |
153 | 153 |
|
--- app/views/issue/partial_comments.scala.html
+++ app/views/issue/partial_comments.scala.html
... | ... | @@ -119,6 +119,8 @@ |
119 | 119 |
@Html(Messages("issue.event.referred",linkToUser(user.loginId, user.name),linkToPullRequest(pull))) |
120 | 120 |
} |
121 | 121 |
} |
122 |
+ case EventType.ISSUE_BODY_CHANGED => { |
|
123 |
+ } |
|
122 | 124 |
case _ => { |
123 | 125 |
@event.newValue by @linkToUser(user.loginId, user.name) |
124 | 126 |
} |
--- app/views/issue/view.scala.html
+++ app/views/issue/view.scala.html
... | ... | @@ -195,6 +195,24 @@ |
195 | 195 |
</dl> |
196 | 196 |
@**<!-- // -->**@ |
197 | 197 |
|
198 |
+ @**<!-- voters -->**@ |
|
199 |
+ <dl> |
|
200 |
+ <dt>@Messages("issue.vote")</dt> |
|
201 |
+ <dd> |
|
202 |
+ @if(isProjectResourceCreatable(User.findByLoginId(session.get("loginId")), project, ResourceType.ISSUE_COMMENT)) { |
|
203 |
+ @if(issue.isVotedBy(UserApp.currentUser())) { |
|
204 |
+ <a href="@routes.VoteApp.unvote(project.owner, project.name, issue.getNumber)" data-request-method="post" class="ybtn ybtn-success" data-toggle="tooltip" data-placement="right" data-original-title="@Messages("issue.unvote.description")">+@issue.voters.size</a> |
|
205 |
+ } else { |
|
206 |
+ <a href="@routes.VoteApp.vote(project.owner, project.name, issue.getNumber)" data-request-method="post" class="ybtn" data-toggle="tooltip" data-placement="right" data-original-title="@Messages("issue.vote.description")">+@issue.voters.size</a> |
|
207 |
+ } |
|
208 |
+ <br><br> |
|
209 |
+ } |
|
210 |
+ @for(voter <- issue.voters) { |
|
211 |
+ @Html(getUserAvatar(voter, "medium")) |
|
212 |
+ } |
|
213 |
+ </dd> |
|
214 |
+ </dl> |
|
215 |
+ @**<!-- // -->**@ |
|
198 | 216 |
</form> |
199 | 217 |
</div> |
200 | 218 |
</div> |
+++ conf/evolutions/default/63.sql
... | ... | @@ -0,0 +1,18 @@ |
1 | +# --- !Ups | |
2 | +create table issue_voter ( | |
3 | + issue_id BIGINT NOT NULL, | |
4 | + user_id INT NOT NULL | |
5 | +); | |
6 | + | |
7 | +alter table issue_voter add constraint fk_issue_voter_1 foreign key (issue_id) references issue (id) on delete restrict on update restrict; | |
8 | +alter table issue_voter add constraint fk_issue_voter_2 foreign key (user_id) references n4user (id) on delete restrict on update restrict; | |
9 | + | |
10 | +ALTER TABLE issue_event DROP CONSTRAINT IF EXISTS ck_issue_event_event_type; | |
11 | +ALTER TABLE issue_event ADD CONSTRAINT ck_issue_event_event_type check (event_type in ('NEW_ISSUE','NEW_POSTING','ISSUE_ASSIGNEE_CHANGED','ISSUE_STATE_CHANGED','NEW_COMMENT','NEW_COMMIT_COMMENT','NEW_PULL_REQUEST','NEW_PULL_REQUEST_COMMENT','PULL_REQUEST_STATE_CHANGED','ISSUE_REFERRED_FROM_COMMIT','ISSUE_REFERRED_FROM_PULL_REQUEST', 'ISSUE_BODY_CHANGED')); | |
12 | +# --- !Downs | |
13 | +alter table issue_voter drop constraint if exists fk_issue_voter_1; | |
14 | +alter table issue_voter drop constraint if exists fk_issue_voter_2; | |
15 | +drop table if exists issue_voter; | |
16 | + | |
17 | +ALTER TABLE issue_event DROP CONSTRAINT IF EXISTS ck_issue_event_event_type; | |
18 | +ALTER TABLE issue_event ADD CONSTRAINT ck_issue_event_event_type check (event_type in ('NEW_ISSUE','NEW_POSTING','ISSUE_ASSIGNEE_CHANGED','ISSUE_STATE_CHANGED','NEW_COMMENT','NEW_COMMIT_COMMENT','NEW_PULL_REQUEST','NEW_PULL_REQUEST_COMMENT','PULL_REQUEST_STATE_CHANGED','ISSUE_REFERRED_FROM_COMMIT','ISSUE_REFERRED_FROM_PULL_REQUEST')); |
--- conf/messages
+++ conf/messages
... | ... | @@ -239,6 +239,9 @@ |
239 | 239 |
issue.update.detachLabel = Detach label |
240 | 240 |
issue.update.milestone = Update milestone |
241 | 241 |
issue.update.state = Update state |
242 |
+issue.vote = Sympathy |
|
243 |
+issue.vote.description = Click if you sympathize with this. |
|
244 |
+issue.unvote.description = Click if you don't sympathize with this. |
|
242 | 245 |
issue.watch.start = You will receive notifications of this issue |
243 | 246 |
label = Label |
244 | 247 |
label.add = Add Label |
... | ... | @@ -304,6 +307,7 @@ |
304 | 307 |
notification.type.issue.referred.from.commit = Issue Referred from Commit |
305 | 308 |
notification.type.issue.referred.from.pullrequest = Issue Referred from PullRequest |
306 | 309 |
notification.type.issue.state.changed = Changed Issue's State |
310 |
+notification.type.issue.body.changed = Changed Issue's Body |
|
307 | 311 |
notification.type.member.enroll = Requests for joining projects |
308 | 312 |
notification.type.new.comment = New Comment on a Posting or a Issue |
309 | 313 |
notification.type.new.commit = New Commits on a Project |
--- conf/messages.ko
+++ conf/messages.ko
... | ... | @@ -239,6 +239,9 @@ |
239 | 239 |
issue.update.detachLabel = 라벨 제거 |
240 | 240 |
issue.update.milestone = 마일스톤 변경 |
241 | 241 |
issue.update.state = 상태 변경 |
242 |
+issue.vote = 공감 |
|
243 |
+issue.vote.description = 이 이슈에 공감하기 위해 버튼을 누릅니다. |
|
244 |
+issue.unvote.description = 공감을 취소하려면 버튼을 누릅니다. |
|
242 | 245 |
issue.watch.start = 이제 이 이슈에 관한 알림을 받습니다 |
243 | 246 |
label = 라벨 |
244 | 247 |
label.add = 라벨 추가 |
... | ... | @@ -304,6 +307,7 @@ |
304 | 307 |
notification.type.issue.referred.from.commit = 커밋에서의 이슈 언급 |
305 | 308 |
notification.type.issue.referred.from.pullrequest = 코드 주고받기에서의 이슈 언급 |
306 | 309 |
notification.type.issue.state.changed = 이슈 상태 변경 |
310 |
+notification.type.issue.body.changed = 이슈 본문 변경 |
|
307 | 311 |
notification.type.member.enroll = 멤버 등록 요청 |
308 | 312 |
notification.type.new.comment = 게시물과 이슈 새 댓글 등록 |
309 | 313 |
notification.type.new.commit = 새 커밋 |
--- conf/routes
+++ conf/routes
... | ... | @@ -243,5 +243,9 @@ |
243 | 243 |
# Compare |
244 | 244 |
GET /:user/:project/compare/:revA..:revB controllers.CompareApp.compare(user, project, revA, revB) |
245 | 245 |
|
246 |
+# Vote |
|
247 |
+POST /:user/:project/issue/:number/vote controllers.VoteApp.vote(user, project, number: Long) |
|
248 |
+POST /:user/:project/issue/:number/unvote controllers.VoteApp.unvote(user, project, number: Long) |
|
249 |
+ |
|
246 | 250 |
# remove trailing slash - must be the bottom of this file |
247 | 251 |
GET /*paths controllers.Application.removeTrailer(paths) |
--- test/models/IssueTest.java
+++ test/models/IssueTest.java
... | ... | @@ -50,6 +50,39 @@ |
50 | 50 |
} |
51 | 51 |
|
52 | 52 |
@Test |
53 |
+ public void vote() { |
|
54 |
+ // when |
|
55 |
+ issue.addVoter(admin); |
|
56 |
+ issue.addVoter(manager); |
|
57 |
+ |
|
58 |
+ // then |
|
59 |
+ assertThat(issue.voters.size()).isEqualTo(2); |
|
60 |
+ } |
|
61 |
+ |
|
62 |
+ @Test |
|
63 |
+ public void unvote() { |
|
64 |
+ // given |
|
65 |
+ issue.addVoter(admin); |
|
66 |
+ issue.addVoter(manager); |
|
67 |
+ |
|
68 |
+ // when |
|
69 |
+ issue.removeVoter(admin); |
|
70 |
+ |
|
71 |
+ // then |
|
72 |
+ assertThat(issue.voters.size()).isEqualTo(1); |
|
73 |
+ } |
|
74 |
+ |
|
75 |
+ @Test |
|
76 |
+ public void watchersAfterVoting() { |
|
77 |
+ // when |
|
78 |
+ issue.addVoter(member); |
|
79 |
+ issue.addVoter(manager); |
|
80 |
+ |
|
81 |
+ // then |
|
82 |
+ assertThat(issue.getWatchers().size()).isEqualTo(3); |
|
83 |
+ } |
|
84 |
+ |
|
85 |
+ @Test |
|
53 | 86 |
public void unwatchByDefault() { |
54 | 87 |
// given |
55 | 88 |
assertThat(issue.getWatchers().contains(admin)).isFalse(); |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?