THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

Browse Source

Merge branch 'next' for the intermission

next
Suwon Chae 1 year ago
parent
commit
03f4a1b909
66 changed files with 5905 additions and 1364 deletions
  1. +1
    -1
      .travis.yml
  2. +3
    -0
      AUTHORS
  3. +37
    -36
      README.md
  4. +6
    -1
      app/assets/stylesheets/less/_common.less
  5. +79
    -65
      app/assets/stylesheets/less/_markdown.less
  6. +61
    -2
      app/assets/stylesheets/less/_page.less
  7. +15
    -0
      app/assets/stylesheets/less/_yobiUI.less
  8. +5
    -0
      app/controllers/BoardApp.java
  9. +24
    -1
      app/controllers/IssueApp.java
  10. +14
    -11
      app/controllers/ProjectApp.java
  11. +76
    -2
      app/controllers/api/BoardApi.java
  12. +291
    -20
      app/controllers/api/IssueApi.java
  13. +54
    -17
      app/controllers/api/ProjectApi.java
  14. +78
    -5
      app/controllers/api/UserApi.java
  15. +3
    -0
      app/models/Comment.java
  16. +9
    -0
      app/models/Issue.java
  17. +40
    -7
      app/models/NotificationEvent.java
  18. +4
    -3
      app/models/NotificationMail.java
  19. +9
    -0
      app/models/Posting.java
  20. +3
    -0
      app/models/Project.java
  21. +73
    -34
      app/models/Webhook.java
  22. +3
    -1
      app/models/enumeration/EventType.java
  23. +24
    -0
      app/models/enumeration/IssueFilterType.java
  24. +17
    -0
      app/models/enumeration/WebhookType.java
  25. +119
    -0
      app/models/support/IssueSearchCondition.java
  26. +27
    -1
      app/utils/JodaDateUtil.java
  27. +6
    -1
      app/utils/Markdown.java
  28. +5
    -2
      app/views/board/partial_comments.scala.html
  29. +17
    -3
      app/views/board/view.scala.html
  30. +3
    -3
      app/views/common/commentUpdateForm.scala.html
  31. +3
    -0
      app/views/common/editor.scala.html
  32. +23
    -0
      app/views/common/scripts.scala.html
  33. +12
    -0
      app/views/common/tasklistBar.scala.html
  34. +1
    -0
      app/views/git/partial_search.scala.html
  35. +67
    -48
      app/views/help/markdown.scala.html
  36. +5
    -2
      app/views/issue/partial_comment.scala.html
  37. +30
    -10
      app/views/issue/view.scala.html
  38. +35
    -25
      app/views/project/partial_webhooks_list.scala.html
  39. +4
    -0
      app/views/project/webhooks.scala.html
  40. +10
    -4
      build.sbt
  41. +2
    -2
      conf/application.conf.default
  42. +6
    -0
      conf/evolutions/default/24.sql
  43. +7
    -0
      conf/evolutions/default/25.sql
  44. +1
    -0
      conf/messages
  45. +3
    -2
      conf/messages.ko-KR
  46. +1009
    -0
      conf/messages.ru-RU
  47. +1009
    -0
      conf/messages.uz-UZ
  48. +19
    -8
      conf/routes
  49. +3
    -1
      public/javascripts/common/yobi.CommentForm.js
  50. +12
    -24
      public/javascripts/common/yobi.Markdown.js
  51. +88
    -0
      public/javascripts/common/yona.KeyControl.js
  52. +9
    -0
      public/javascripts/common/yona.Sha1.js
  53. +136
    -0
      public/javascripts/common/yona.Tasklist.js
  54. +1
    -1
      public/javascripts/lib/elevator/jquery.elevator.css
  55. +1298
    -1012
      public/javascripts/lib/marked.js
  56. +2
    -0
      public/javascripts/lib/tasklist/gfm-task-list.css
  57. +182
    -0
      public/javascripts/lib/tasklist/gfm-task-list.js
  58. +4
    -1
      public/javascripts/service/yobi.issue.List.js
  59. +1
    -1
      public/javascripts/service/yobi.user.Setting.js
  60. +3
    -3
      public/javascripts/service/yona.temporarySaveHandler.js
  61. +4
    -4
      public/javascripts/yona-lib.js
  62. +171
    -0
      test/controllers/api/IssueApiGetIssueTest.java
  63. +178
    -0
      test/controllers/api/IssueApiNewIssueCommentTest.java
  64. +168
    -0
      test/controllers/api/IssueApiUpdateIssueCommentTest.java
  65. +153
    -0
      test/controllers/api/IssueApiUpdateIssueTest.java
  66. +140
    -0
      test/controllers/api/UserApiGetIssuesByUserTest.java

+ 1
- 1
.travis.yml View File

@ -7,6 +7,6 @@ env:
before_script:
- wget http://downloads.typesafe.com/typesafe-activator/${ACTIVATOR_VERSION}/${ACTIVATOR_ZIP_FILE}
- unzip -q ${ACTIVATOR_ZIP_FILE}
- set SBT_OPTS= -Xms256m Xmx52m -XX:MaxPermSize=512m
- set SBT_OPTS= -Xms256m -Xmx512m
script:
- activator-${ACTIVATOR_VERSION}-minimal/activator compile

+ 3
- 0
AUTHORS View File

@ -49,3 +49,6 @@ rimi
kenu <kenu.heo@gmail.com>
DongHo Byun <cpascal@nextfloor.com>
Mijeong Park <p.mj@naverlabs.com>
Joonho Choi <timberx@naver.com>
GwanYeong Kim <gy741.kim@gmail.com>
Father Vlasie <fv@spots.school>

+ 37
- 36
README.md View File

@ -14,7 +14,7 @@ Official Site: [http://yona.io](http://yona.io)
Yona?
--
- Git 저장소 기능이 내장된 설치형 이슈트래커
- Naver를 비롯하여 게임회사, 통신회사 고객센터, 투자사, 학교, 기업등에서 수년 간 실제로 사용되어 왔고 개선되어 온(Real world battled) 애플리케이션입니다
- Naver, Naver Labs 를 비롯하여 게임회사, 통신회사 고객센터, 공공기관, 투자사, 학교, 기업등에서 수년 간 실제로 사용되어 왔고 개선되어 온(Real world battled) 애플리케이션입니다
주요기능
---
@ -65,11 +65,11 @@ Yona 배포판
- MariaDB 버전
- 기본 권장 버전
- `yona-v1.8.0-bin.zip` 같은 형식으로 파일로 배포
- `yona-v1.10.0-bin.zip` 같은 형식으로 파일로 배포
- DB 설치에 약간의 시간이 필요하지만 안정적으로 운영이 가능
- H2 DB 내장형
- DB 설정없이 내려받아서 바로 실행해서 쓸 수 있는 버전
- `yona-h2-v1.8.0-bin.zip` 같은 형식으로 파일로 배포
- `yona-h2-v1.10.0-bin.zip` 같은 형식으로 파일로 배포
- USB 등에 담아서 이동해가면서 사용하거나 작업후 통째로 zip으로 묶어서 들고 다니는 것이 가능함
- 대규모 사이트에서 사용하기에는 적합하지 않음. 참고: [Yona가 MariaDB를 기본 DB로 사용하게 된 이유](https://repo.yona.io/yona-projects/yona/post/4)
@ -96,7 +96,7 @@ Yona 실행 및 업그레이드/백업 및 복구/문제 해결
- [발생 가능한 문제상황들과 해결방법](docs/ko/trouble-shootings.md)
소스코드를 직접 내려 받아서 빌드 실행하
소스코드를 직접 내려 받아서 빌드하거나 자신만의 배포판을 만들
---
자신의 입맛에 맛게 코드를 직접 수정해서 작업하거나 코드를 기여하고 싶을 경우에는 코드 저장소로부터 코드를 직접 내려받아서 빌드/실행하는 것도 가능합니다.
[소스코드를 직접 내려 받아서 실행하기](https://repo.yona.io/yona-projects/yona/post/5)를 참고해 주세요
@ -135,7 +135,6 @@ Contribution
- `next`브랜치는 내부 개발용입니다. 어떠한 기능들이 추가되고 있는지 현장을 보고 싶으시면 `next`브랜치를 참고해주세요.
- 코드리뷰 후 merge 되면 Yona Author로 파일에 기록되며 작은 기념품을 보내드립니다.
<br/>
<a name="english"></a>[[한국어]](#korean)
@ -145,38 +144,40 @@ Yona
=======
Yona is a web-based project hosting software.
What is Yona?
What you can do with Yona:
--
Yona is designed to increase the speed and efficiency of your team's work and development.
Yona is designed to increase the speed and efficiency of team work and team development.
- Issue tracker
- Transferable Issue
- Issue change history
- Issues can be transferred to other projects
- Issues' change histories can be viewed
- Bulletin board
- Embedded Git/SVN respository features
- Pull-request & Block based code review
- Online Commit
- Pull requests & Block-based code review
- Online Commits
- LDAP support
- Social Login
- Migration from/to another service/instance
- Github/Github Enterprise, Redmine, Yona
- Social login
- Migration to/from other services or Yona instances
- Github/Github Enterprise, Redmine, Yona
Yona Distribution
Requirements
---
Currently, Yona offers two distributions per version.
- Java 8+
- System Memory 2Gb+ (Recommendation: 4Gb+)
#### MariaDB Version
- Recommended version.
- Distributed as a file in the similar format as yona-v1.3.0-bin.zip
- It takes a little effort to install DB, but it can be operated stably.
Distribution
---
Currently, There are two distribution types.
#### Embedded H2 DB Version
- Portable version that can be downloaded and run immediately. No need to setting a DB.
- Distributed as a file in the similar format as yona-h2-v1.3.0-bin.zip
- It can be used portable. For example, along with USB etc. And it can be carried around with zip as a whole of work.
 - Not suitable for large-scale team. (over 500 people)
#### MariaDB version
- Recommended version
- It takes a little effort to install DB, but it guarantees stable operation
#### Embedded H2 DB version
- Portable version that can be downloaded and run immediately.
- Setting a DB is not required.
- Also, can run the software directly from a USB device
- Suitable for small teams (under 500 users).
How to install
---
@ -189,7 +190,7 @@ Basically, Yona installation is in two steps:
If you want to use [Docker](https://www.docker.com/), See https://github.com/pokev25/docker-yona by [pokev25](https://github.com/pokev25)
Yona Start/Upgrade/Backup/Trouble Shootings
Start/Upgrade/Backup/Trouble Shootings
---
- [Start and Restart](docs/yona-run-and-restart.md)
- [Start Options](docs/yona-run-options.md) for stable operation
@ -206,17 +207,17 @@ Server Settings
Migration
---
- [Yona Export](https://github.com/yona-projects/yona-export)
- Support repository local backup
- Yona to another Yoan instance
- Any source corresponding to export format, can be imported into Yona
- Support Yona to Github/Github Enterprise migration
- Local backup
- Move projects to another Yona instance
- If you can match the format, anything can be imported into Yona
- Github/Github Enterprise migration
- [See here](https://github.com/yona-projects/yona/blob/master/conf/application.conf.default#L297)
Google Analytics
---
- Basically, distributed Yona include Google Analytics.
- This data is used to better understand how users use Yona and make constantly improving Yona development.
- To disable this for any reason, set the following option to false in conf/application.conf file.
- Distributed Yona includes Google Analytics
- This data is used for making us to improve Yona
- If you want to disable this for any reason, set the following option to false in conf/application.conf file.
```
application.send.yona.usage = true
```
@ -224,8 +225,8 @@ application.send.yona.usage = true
Contribution
---
- The branch for contributions is `master`.
- Fork the repository, then work on the `master` branch and send a pull request to the` master` branch.
   - The `next` branch is for internal development. If you want to see what features are being added, please refer to the `next` branch.
- At first, fork the repository, then work on the `master` branch. And send a pull request to the`master` branch.
- The `next` branch is for internal development. If you want to see what features are being added, please refer to the `next` branch.
License

+ 6
- 1
app/assets/stylesheets/less/_common.less View File

@ -305,4 +305,9 @@ input.white:-ms-input-placeholder { color:#fff; opacity:0.8; }
line-height: 36px;
}
.dimgray { color:dimgray; }
.dimgray { color:dimgray; }
.vertical-align {
display: flex;
align-items: center;
}

+ 79
- 65
app/assets/stylesheets/less/_markdown.less View File

@ -15,11 +15,20 @@ code {
-o-font-feature-settings: "kern" 1;
font-feature-settings: "kern" 1;
font-kerning: normal;
padding: 15px 20px !important;
word-wrap: break-word;
> *:first-child {
margin-top: 0 !important;
}
> *:last-child {
margin-bottom: 0 !important;
}
ul, ol {
padding: 5px 0 5px 2.5em;
padding: 0 0 5px 2.5em;
font-weight: normal;
margin-left: 0;
}
@ -43,27 +52,22 @@ code {
margin-bottom: 12px;
}
> ul {
margin-top: 14px;
margin-bottom: 7px;
}
a {
color:#4183c4;
text-decoration:none;
color: #4183c4;
text-decoration: none;
}
a:hover {
color:#4183c4;
text-decoration:underline;
color: #4183c4;
text-decoration: underline;
}
a:active {
color:#4183c4;
text-decoration:none;
color: #4183c4;
text-decoration: none;
}
.anchor(){
.anchor() {
.head-anchor {
margin-left: 3px;
opacity: 0;
@ -75,10 +79,15 @@ code {
}
}
h1,
h2,
h3 {
line-height: 40px;
margin-bottom: 16px;
}
h1 {
font-size:2.0em;
margin-top: 1.5em;
margin-bottom: 15px;
font-size: 2.0em;
padding-bottom: 0.3em;
border-bottom: 1px solid #eee;
.anchor();
@ -87,16 +96,17 @@ code {
}
h2 {
margin: 1em 0 5px;
font-size:1.5em;
line-height: 1.25;
font-size: 1.5em;
width: 95%;
padding: 0 0 0.3em 0;
border-bottom: 1px solid #eaecef;
.anchor();
}
h3 {
margin:1em 0 5px;
font-size:1.4em;
margin: 1em 0 5px;
font-size: 1.4em;
padding: 0;
.anchor();
}
@ -115,115 +125,119 @@ code {
}
hr {
height:1px;
margin:10px 0;
border:0;color:#ccc;
background-color:#ccc;
height: 1px;
margin: 10px 0;
border: 0;
color: #ccc;
background-color: #ccc;
}
p {
margin : 10px 0;
margin: 0 0 16px 0;
line-height: 1.6em;
}
blockquote {
p {
font-size: 0.9em;
font-weight: normal;
}
p {
font-size: 0.9em;
font-weight: normal;
}
}
code {
padding: 5px 5px 2px 5px;
border: 1px solid #ddd;
border-radius: 3px;
font-family:@fixed-font-family;
font-family: @fixed-font-family;
font-size: 13px;
.title {
font-size:inherit;
font-size: inherit;
}
}
blockquote{
blockquote {
border-left: 4px solid #DDD;
padding: 0 15px;
color: #777;
}
li > img {
max-width:80%;
max-width: 80%;
}
img{
max-width:100%;
margin:10px 0;
li > input[type='checkbox'] {
vertical-align: text-top;
}
img {
max-width: 100%;
margin: 10px 0;
padding: 5px;
border:1px solid rgba(0,0,0,0.1);
border: 1px solid rgba(0, 0, 0, 0.1);
.box-sizing(border-box);
max-height: 300px;
}
ul {
line-height:20px;
list-style:disc;
> ul {
line-height: 20px;
list-style: disc;
margin-bottom: 16px;
}
li{
line-height: 1.6em;
li {
line-height: 1.6em;
}
ul ul, ol ul {
list-style:circle;
list-style: circle;
}
ul ul ul, ol ul ul, ol ol ul, ul ol ul {
list-style:square;
list-style: square;
}
ol{
line-height: 1.6em;
list-style:decimal;
ol {
line-height: 1.6em;
list-style: decimal;
}
pre {
font-size: 1em;
background-color:#EFEFEF;
background-color: #EFEFEF;
padding: 10px;
margin:10px 0;
word-break : normal;
margin: 10px 0;
word-break: normal;
border: none;
code {
margin : 0;
margin: 0;
padding: 0;
border : none;
border: none;
}
}
table {
border-collapse:collapse;
margin: 15px 15px;
border-collapse: collapse;
margin: 15px 15px;
th {
padding:5px;
border:1px solid #dcddde;
background-color:#f7f7f7;
padding: 5px;
border: 1px solid #dcddde;
background-color: #f7f7f7;
min-width: 45px;
}
td {
padding:5px;
border:1px solid #dcddde;
padding: 5px;
border: 1px solid #dcddde;
word-break: break-all;
}
}
&.markdown-before {
visibility : hidden;
}
&.markdown-before {
visibility: hidden;
}
.popover {
max-width: 400px;

+ 61
- 2
app/assets/stylesheets/less/_page.less View File

@ -2888,6 +2888,16 @@ label.inline-list {
padding: 10px 0px;
position: relative;
&:target {
.media-body {
border: 2px solid #03a9f4;
&::before {
border-color: #03a9f4;
border-width: 0 0 2px 2px;
}
}
}
.comment-avatar {
float: left;
padding-left: 5px;
@ -5526,7 +5536,7 @@ label.issue-item-row {
border-bottom: none;
li {
display: inline-block;
padding:5px 10px;
padding:5px 7px;
line-height: 20px;
.label {
@ -5537,7 +5547,7 @@ label.issue-item-row {
&.help-nav {
cursor: pointer;
position: relative;
color:#788ba7;
color:#9e9e9e;
&:hover {
color:#333;
@ -7311,3 +7321,52 @@ div.diff-body[data-outdated="true"] tr:hover .icon-comment {
color: #e91e63 !important;
}
}
@keyframes showNav {
from {opacity: 0;}
to {opacity: 1;}
}
.task-list-button {
margin-top: 2px;
button {
margin-top: 1px;
}
}
.tasklist {
padding: 10px 20px 0 20px;
box-shadow: none;
filter: none;
display: none;
&.task-show {
display: block;
animation: showNav 250ms ease-in-out both;
}
.task-title {
font-weight: 500;
.done-counter {
margin-left: 5px;
}
}
.task-progress{
background-color: #D4D4D4;
.bar {
transition: .2s;
height: 2px;
width: 100%;
}
.red {
background-color: red;
}
.green {
background-color: #8bc34a;
}
}
}

+ 15
- 0
app/assets/stylesheets/less/_yobiUI.less View File

@ -843,6 +843,21 @@ span.issue-label {
}
}
&.ybtn-danger-no-outline {
font-weight: 600;
box-shadow: none;
color: #666;
padding: 1px 10px !important;
border: 1px solid transparent;
background-color: #eee;
&:hover, &:focus {
color: @yobi-btn-danger;
background-color: #fbe9e7;
border: 1px solid #EF9A9A;
}
}
&.ybtn-inverse {
background-color : @yobi-btn-inverse !important;
border:1px solid @yobi-btn-inverse-hover;

+ 5
- 0
app/controllers/BoardApp.java View File

@ -417,6 +417,11 @@ public class BoardApp extends AbstractPostingApp {
return redirect(RouteUtil.getUrl(savedComment));
}
// Just made for compatibility. No meanings.
public static Result updateComment(String ownerName, String projectName, Long number, Long commentId) throws IOException {
return newComment(ownerName, projectName, number);
}
private static Comment saveComment(Project project, Posting posting, PostingComment comment) {
Comment savedComment;
PostingComment existingComment = PostingComment.find.where().eq("id", comment.id).findUnique();

+ 24
- 1
app/controllers/IssueApp.java View File

@ -37,6 +37,8 @@ import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.*;
import static utils.JodaDateUtil.getOptionalShortDate;
@AnonymousCheck
public class IssueApp extends AbstractPostingApp {
private static final String EXCEL_EXT = "xls";
@ -221,7 +223,9 @@ public class IssueApp extends AbstractPostingApp {
result.put("title", issue.title);
result.put("state", issue.state.toString());
result.put("createdDate", issue.createdDate.toString());
result.put("link", routes.IssueApp.issue(project.owner, project.name, issueId).toString());
if (project != null) {
result.put("link", routes.IssueApp.issue(project.owner, project.name, issueId).toString());
}
listData.put(issue.id.toString(), result);
}
@ -895,6 +899,16 @@ public class IssueApp extends AbstractPostingApp {
if(StringUtils.isNotEmpty(comment.parentCommentId)){
comment.setParentComment(IssueComment.find.byId(Long.valueOf(comment.parentCommentId)));
}
if(issue.comments.size() == 0) {
User user = User.find.byId(issue.authorId);
comment.previousContents = getPrevious("Original issue", issue.body, issue.updatedDate, user.loginId);
} else {
Comment previousComment = issue.comments.get(issue.comments.size() - 1);
User user = User.find.byId(previousComment.authorId);
comment.previousContents = getPrevious("Previous comment", previousComment.contents, previousComment.createdDate, user.loginId);
}
Comment savedComment = saveComment(project, issue, comment);
if( containsStateTransitionRequest() ){
@ -910,6 +924,15 @@ public class IssueApp extends AbstractPostingApp {
return redirect(RouteUtil.getUrl(savedComment));
}
private static String getPrevious(String templateTitle, String contents, Date updatedDate, String authorLoginId) {
return "\n\n<br />\n\n--- " + templateTitle + " from @" + authorLoginId + " " + getOptionalShortDate(updatedDate) + " ---\n\n<br />\n\n" + contents;
}
// Just made for compatibility. No meanings.
public static Result updateComment(String ownerName, String projectName, Long number, Long commentId) throws IOException {
return newComment(ownerName, projectName, number);
}
private static Comment saveComment(Project project, Issue issue, IssueComment comment) {
Comment savedComment;
IssueComment existingComment = IssueComment.find.where().eq("id", comment.id).findUnique();

+ 14
- 11
app/controllers/ProjectApp.java View File

@ -18,7 +18,6 @@ import models.*;
import models.enumeration.*;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.mail.HtmlEmail;
import com.fasterxml.jackson.databind.node.ObjectNode;
@ -49,7 +48,6 @@ import views.html.project.setting;
import views.html.project.transfer;
import views.html.project.change_vcs;
import javax.annotation.Nonnull;
import javax.servlet.ServletException;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
@ -1291,22 +1289,27 @@ public class ProjectApp extends Controller {
return notFound(ErrorViews.NotFound.render("error.notfound"));
}
Form<Webhook> addWebhookForm = form(Webhook.class).bindFromRequest();
if (addWebhookForm == null) {
Form<Webhook> addNewWebhookForm = form(Webhook.class).bindFromRequest();
if (addNewWebhookForm == null) {
Logger.warn("Failed creating webhook: got null form from newWebhook request");
return badRequest();
} else if (addWebhookForm.hasErrors()) {
return badRequest(ErrorViews.BadRequest.render());
return badRequest("Failed creating webhook: got null form from newWebhook request");
} else if (addNewWebhookForm.hasErrors()) {
return badRequest(ErrorViews.BadRequest.render(addNewWebhookForm.errorsAsJson().toString()));
}
Webhook webhook = addWebhookForm.get();
Webhook.create(project.id, webhook.payloadUrl.trim(), webhook.secret,
BooleanUtils.toBooleanDefaultIfNull(webhook.gitPushOnly, false));
createWebhook(project, addNewWebhookForm);
return redirect(routes.ProjectApp.webhooks(project.owner, project.name));
}
private static void createWebhook(Project project, Form<Webhook> forms) {
Webhook webhook = forms.get();
webhook.project = project;
if(webhook.gitPushOnly == null) webhook.gitPushOnly = false;
webhook.createdAt = new Date();
webhook.save();
}
@Transactional
@IsAllowed(Operation.UPDATE)
public static Result deleteWebhook(String ownerId, String projectName, Long id) {

+ 76
- 2
app/controllers/api/BoardApi.java View File

@ -14,7 +14,9 @@ import controllers.annotation.IsCreatable;
import models.*;
import models.enumeration.Operation;
import models.enumeration.ResourceType;
import org.apache.commons.codec.digest.DigestUtils;
import org.joda.time.DateTime;
import play.api.mvc.Codec;
import play.db.ebean.Transactional;
import play.libs.Json;
import play.mvc.Result;
@ -26,8 +28,7 @@ import utils.RouteUtil;
import java.io.IOException;
import java.util.*;
import static controllers.api.IssueApi.findAuthor;
import static controllers.api.IssueApi.parseDateString;
import static controllers.api.IssueApi.*;
import static play.libs.Json.toJson;
public class BoardApi extends AbstractPostingApp {
@ -120,6 +121,38 @@ public class BoardApi extends AbstractPostingApp {
}
@Transactional
public static Result updatePostingContent(String owner, String projectName, Long number) {
User user = UserApp.currentUser();
if (user.isAnonymous()) {
return unauthorized(Json.newObject().put("message", "unauthorized request"));
}
JsonNode json = request().body().asJson();
if(json == null) {
return badRequest(Json.newObject().put("message", "Expecting Json data"));
}
Project project = Project.findByOwnerAndProjectName(owner, projectName);
final Posting posting = Posting.findByNumber(project, number);
if (!AccessControl.isAllowed(user, posting.asResource(), Operation.UPDATE)) {
return forbidden(Json.newObject().put("message", "Forbidden request"));
}
String content = json.findValue("content").asText();
String rememberedChecksum = json.findValue("sha1").asText();
if (isModifiedByOthers(posting.body, rememberedChecksum)) {
return conflicted(posting.body);
}
posting.body = content;
posting.update();
return ok(ProjectApi.getResult(posting));
}
@Transactional
@IsCreatable(ResourceType.NONISSUE_COMMENT)
public static Result newPostingComment(String ownerName, String projectName, Long number)
@ -156,4 +189,45 @@ public class BoardApi extends AbstractPostingApp {
return created(result);
}
@Transactional
public static Result updatePostingComment(String ownerName, String projectName, Long number, Long commentId) {
ObjectNode result = Json.newObject();
User user = UserApp.currentUser();
if (user.isAnonymous()) {
return unauthorized(result.put("message", "unauthorized request"));
}
JsonNode json = request().body().asJson();
if(json == null) {
return badRequest(result.put("message", "Expecting Json data"));
}
String comment = json.findValue("content").asText();
String rememberedChecksum = json.findValue("sha1").asText();
Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
final Posting posting = Posting.findByNumber(project, number);
PostingComment postingComment = posting.findCommentByCommentId(commentId);
if (!AccessControl.isAllowed(user, postingComment.asResource(), Operation.UPDATE)) {
return forbidden(Json.newObject().put("message", "Forbidden request"));
}
if (isModifiedByOthers(postingComment.contents, rememberedChecksum)) {
return conflicted(postingComment.contents);
}
postingComment.contents = comment;
postingComment.save();
ObjectNode commentNode = getCommentJsonNode(postingComment);
ObjectNode authorNode = getAuthorJsonNode(user);
commentNode.set("author", toJson(authorNode));
result.set("result", commentNode);
return ok(result);
}
}

+ 291
- 20
app/controllers/api/IssueApi.java View File

@ -9,6 +9,7 @@ package controllers.api;
import com.avaje.ebean.ExpressionList;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import controllers.AbstractPostingApp;
import controllers.UserApp;
@ -18,7 +19,9 @@ import controllers.annotation.IsCreatable;
import controllers.routes;
import models.*;
import models.enumeration.*;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import play.api.mvc.Codec;
import play.db.ebean.Transactional;
import play.i18n.Messages;
import play.libs.F;
@ -37,7 +40,7 @@ import java.util.*;
import static controllers.UserApp.MAX_FETCH_USERS;
import static controllers.UserApp.currentUser;
import static controllers.api.UserApi.createUserNode;
import static controllers.api.UserApi.*;
import static play.libs.Json.toJson;
public class IssueApi extends AbstractPostingApp {
@ -69,12 +72,52 @@ public class IssueApi extends AbstractPostingApp {
return ok(result);
}
@IsAllowed(value = Operation.READ, resourceType = ResourceType.BOARD_POST)
@Transactional
public static Result getIssue(String owner, String projectName, Long number) {
ObjectNode result = Json.newObject();
if (!isAuthored(request())) {
return unauthorized(result.put("message", "unauthorized request"));
}
Project project = Project.findByOwnerAndProjectName(owner, projectName);
if (project == null) {
return badRequest(result.put("message", "no project by request"));
}
Issue issue = Issue.findByNumber(project, number);
JsonNode json = ProjectApi.getResult(issue);
return ok(json);
if (issue == null) {
return badRequest(result.put("message", "no issue by request"));
}
ObjectNode json = ProjectApi.getResult(issue);
return ok(Json.newObject().set("result", toJson(addIssueEvents(issue, json))));
}
private static ObjectNode addIssueEvents(Issue issue, ObjectNode json) {
if (issue.events.size() > 0) {
json.put("events", getIssueEvents(issue));
}
return json;
}
private static ArrayNode getIssueEvents(Issue issue) {
ArrayNode array = Json.newObject().arrayNode();
if (issue.events.size() > 0) {
for (IssueEvent event: issue.events) {
ObjectNode result = Json.newObject();
result.put("id", event.id);
result.put("createdDate", JodaDateUtil.getDateString(event.created, JodaDateUtil.ISO_FORMAT));
result.put("eventType", event.eventType.toString());
result.put("eventDescription", event.eventType.getDescr());
result.put("oldValue", event.oldValue);
result.put("newValue", event.newValue);
array.add(result);
}
}
return array;
}
@Transactional
@ -91,7 +134,7 @@ public class IssueApi extends AbstractPostingApp {
return badRequest(result.put("message", "No issues key exists or value wasn't array!"));
}
boolean sendNotification = json.findValue("sendNotification") != null;
boolean sendNotification = json.findValue("sendNotification") != null && json.findValue("sendNotification").asBoolean();
Project project = Project.findByOwnerAndProjectName(owner, projectName);
@ -103,6 +146,128 @@ public class IssueApi extends AbstractPostingApp {
return created(toJson(createdIssues));
}
@Transactional
public static Result updateIssue(String owner, String projectName, Long number) {
ObjectNode result = Json.newObject();
if (!isAuthored(request())) {
return unauthorized(result.put("message", "unauthorized request"));
}
JsonNode json = request().body().asJson();
if(json == null) {
return badRequest(result.put("message", "Expecting Json data"));
}
User user = getAuthorizedUser(getAuthorizationToken(request()));
Project project = Project.findByOwnerAndProjectName(owner, projectName);
final Issue issue = Issue.findByNumber(project, number);
return updateIssueNode(json, project, issue, user);
}
@Transactional
public static Result updateIssueState(String owner, String projectName, Long number) {
ObjectNode result = Json.newObject();
if (!isAuthored(request())) {
return unauthorized(result.put("message", "unauthorized request"));
}
JsonNode json = request().body().asJson();
if(json == null) {
return badRequest(result.put("message", "Expecting Json data"));
}
User user = getAuthorizedUser(getAuthorizationToken(request()));
Project project = Project.findByOwnerAndProjectName(owner, projectName);
final Issue issue = Issue.findByNumber(project, number);
State newIssueState = findIssueState(json);
if (!newIssueState.equals(issue.state)) {
addNewIssueEvent(issue, user, EventType.ISSUE_STATE_CHANGED, issue.state.state(), newIssueState.state());
}
issue.state = newIssueState;
issue.save();
result = ProjectApi.getResult(issue);
return ok(Json.newObject().set("result", toJson(addIssueEvents(issue, result))));
}
@Transactional
public static Result updateIssueContent(String owner, String projectName, Long number) {
User user = UserApp.currentUser();
if (user.isAnonymous()) {
return unauthorized(Json.newObject().put("message", "unauthorized request"));
}
JsonNode json = request().body().asJson();
if(json == null) {
return badRequest(Json.newObject().put("message", "Expecting Json data"));
}
Project project = Project.findByOwnerAndProjectName(owner, projectName);
final Issue issue = Issue.findByNumber(project, number);
if (!AccessControl.isAllowed(user, issue.asResource(), Operation.UPDATE)) {
return forbidden(Json.newObject().put("message", "Forbidden request"));
}
String content = json.findValue("content").asText();
String rememberedChecksum = json.findValue("sha1").asText();
if (isModifiedByOthers(issue.body, rememberedChecksum)) {
return conflicted(issue.body);
}
issue.body = content;
issue.update();
return ok(ProjectApi.getResult(issue));
}
private static Result updateIssueNode(JsonNode json, Project project, Issue issue, User user) {
issue.title = json.findValue("title").asText();
issue.body = json.findValue("body").asText();
issue.milestone = findMilestone(json.findValue("milestoneTitle"), project);
issue.updatedDate = JodaDateUtil.now();
// TODO: Separate function for adding possible events
String state = json.findValue("state").asText();
if (!state.equals(issue.state.toString())) {
addNewIssueEvent(issue, user, EventType.ISSUE_STATE_CHANGED, issue.state.state(), State.valueOf(state).state());
}
issue.state = findIssueState(json);
JsonNode assigneeNode = json.findValue("assignees").get(0);
String oldAssignee = issue.assignee != null ? issue.assignee.user.loginId : "";
String newAssignee = assigneeNode != null ? assigneeNode.findValue("loginId").asText() : "";
if (!oldAssignee.equals(newAssignee)) {
oldAssignee = oldAssignee.length() == 0 ? null : oldAssignee;
newAssignee = newAssignee.length() == 0 ? null : newAssignee;
addNewIssueEvent(issue, user, EventType.ISSUE_ASSIGNEE_CHANGED, oldAssignee, newAssignee);
}
issue.assignee = findAssginee(json.findValue("assignees"), project);
issue.save();
ObjectNode issueNode = ProjectApi.getResult(issue);
return ok(Json.newObject().set("result", toJson(addIssueEvents(issue, issueNode))));
}
private static void addNewIssueEvent(Issue issue, User user, EventType eventType, String oldValue, String newValue) {
IssueEvent issueEvent = new IssueEvent();
issueEvent.issue = issue;
issueEvent.senderLoginId = user.loginId;
issueEvent.oldValue = oldValue;
issueEvent.newValue = newValue;
issueEvent.created = new Date();
issueEvent.eventType = eventType;
issueEvent.save();
}
private static JsonNode createIssuesNode(JsonNode json, Project project, boolean sendNotification) {
JsonNode files = json.findValue("temporaryUploadFiles");
@ -177,17 +342,17 @@ public class IssueApi extends AbstractPostingApp {
private static State findIssueState(JsonNode json){
JsonNode issueNode = json.findValue("state");
State state = State.OPEN;
if(issueNode != null) {
if ("CLOSED".equalsIgnoreCase(issueNode.asText())) {
state = State.CLOSED;
}
if( issueNode == null) {
return State.OPEN;
}
if ("OPEN".equalsIgnoreCase(issueNode.asText())) {
return State.OPEN;
} else {
return State.CLOSED;
}
return state;
}
@Transactional
@IsCreatable(ResourceType.ISSUE_COMMENT)
public static Result newIssueComment(String ownerName, String projectName, Long number)
throws IOException {
JsonNode json = request().body().asJson();
@ -198,6 +363,80 @@ public class IssueApi extends AbstractPostingApp {
Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
final Issue issue = Issue.findByNumber(project, number);
if (request().getHeader("Authorization") != null) {
ObjectNode result = Json.newObject();
if (!isAuthored(request())) {
return unauthorized(result.put("message", "unauthorized request"));
}
User user = getAuthorizedUser(getAuthorizationToken(request()));
String comment = json.findValue("comment").asText();
return createCommentUsingToken(issue, user, comment);
} else {
return createCommentByUser(project, issue, json);
}
}
public static boolean isModifiedByOthers(String current, String rememberedChecksum){
// At present, using .val() on textarea elements strips carriage return characters
// https://stackoverflow.com/a/8601601/1450196
// At first, I added hook of above link at the front page.
// But I found that it introduce another problem, cursor location detection error.
// So, decided to calculate sha1 without \r char.
String currentChecksum = DigestUtils.sha1Hex(current.replaceAll("\r","").trim());
return !currentChecksum.equals(rememberedChecksum);
}
public static Status conflicted(String content) {
ObjectNode result = Json.newObject();
result.put("message", "Already modified by someone.");
result.put("storedContent", content);
return new Status(play.core.j.JavaResults.Conflict(), result, Codec.javaSupported("utf-8"));
}
@Transactional
public static Result updateIssueComment(String ownerName, String projectName, Long number, Long commentId) {
User user = UserApp.currentUser();
if (user.isAnonymous()) {
return unauthorized(Json.newObject().put("message", "unauthorized request"));
}
JsonNode json = request().body().asJson();
if(json == null) {
return badRequest(Json.newObject().put("message", "Expecting Json data"));
}
String comment = json.findValue("content").asText();
String rememberedChecksum = json.findValue("sha1").asText();
Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
final Issue issue = Issue.findByNumber(project, number);
IssueComment issueComment = issue.findCommentByCommentId(commentId);
if (isModifiedByOthers(issueComment.contents, rememberedChecksum)) {
return conflicted(issueComment.contents);
}
if (!AccessControl.isAllowed(user, issueComment.asResource(), Operation.UPDATE)) {
return forbidden(Json.newObject().put("message", "Forbidden request"));
}
issueComment.contents = comment;
issueComment.save();
ObjectNode commentNode = getCommentJsonNode(issueComment);
ObjectNode authorNode = getAuthorJsonNode(user);
commentNode.set("author", toJson(authorNode));
ObjectNode result = Json.newObject();
result.set("result", commentNode);
return ok(result);
}
private static Result createCommentByUser(Project project, Issue issue, JsonNode json) {
if (!AccessControl.isResourceCreatable(
UserApp.currentUser(), issue.asResource(), ResourceType.ISSUE_COMMENT)) {
return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
@ -206,22 +445,54 @@ public class IssueApi extends AbstractPostingApp {
User user = findAuthor(json.findValue("author"));
String body = json.findValue("body").asText();
final IssueComment comment = new IssueComment(issue, user, body);
comment.createdDate = parseDateString(json.findValue("createdAt"));
comment.setAuthor(user);
comment.issue = issue;
comment.save();
IssueComment issueComment = createComment(issue, user, body, json.findValue("createdAt"));
attachUploadFilesToPost(json.findValue("temporaryUploadFiles"), comment.asResource());
attachUploadFilesToPost(json.findValue("temporaryUploadFiles"), issueComment.asResource());
ObjectNode result = Json.newObject();
result.put("status", 201);
result.put("location", RouteUtil.getUrl(comment));
result.put("location", RouteUtil.getUrl(issueComment));
return created(result);
}
private static Result createCommentUsingToken(Issue issue, User user, String comment) {
createComment(issue, user, comment, null);
ObjectNode result = ProjectApi.getResult(issue);
return created(Json.newObject().set("result", toJson(addIssueEvents(issue, result))));
}
private static IssueComment createComment(Issue issue, User user, String comment, JsonNode dateNode) {
final IssueComment issueComment = new IssueComment(issue, user, comment);
issueComment.createdDate = dateNode == null ? JodaDateUtil.now() : parseDateString(dateNode);
issueComment.setAuthor(user);
issueComment.issue = issue;
issueComment.save();
return issueComment;
}
public static ObjectNode getCommentJsonNode(Comment comment) {
ObjectNode commentNode = Json.newObject();
commentNode.put("id", comment.id);
commentNode.put("contents", comment.contents);
commentNode.put("createdDate", JodaDateUtil.getDateString(comment.createdDate, JodaDateUtil.ISO_FORMAT));
return commentNode;
}
public static ObjectNode getAuthorJsonNode(User user) {
ObjectNode authorNode = Json.newObject();
authorNode.put("id", user.id);
authorNode.put("loginId", user.loginId);
authorNode.put("name", user.name);
return authorNode;
}
public static User findAuthor(JsonNode authorNode){
if (authorNode != null) {
String email = authorNode.findValue("email").asText();

+ 54
- 17
app/controllers/api/ProjectApi.java View File

@ -27,6 +27,8 @@ import play.mvc.Http;
import play.mvc.Result;
import playRepository.RepositoryService;
import utils.AccessControl;
import utils.Config;
import utils.JodaDateUtil;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@ -279,9 +281,11 @@ public class ProjectApi extends Controller {
json.put("title", posting.title);
json.put("type", posting.asResource().getType().toString());
json.put("author", composeAuthorJson(posting.getAuthor()));
json.put("createdAt", getDateString(posting.createdDate));
json.put("updatedAt", getDateString(posting.updatedDate));
json.put("createdAt", JodaDateUtil.getDateString(posting.createdDate, JodaDateUtil.ISO_FORMAT));
json.put("updatedAt", JodaDateUtil.getDateString(posting.updatedDate, JodaDateUtil.ISO_FORMAT));
json.put("body", posting.body);
json.put("owner", posting.project.owner);
json.put("projectName", posting.project.name);
if (posting.asResource().getType() == ResourceType.ISSUE_POST) {
Issue issue = ((Issue) posting);
@ -300,6 +304,9 @@ public class ProjectApi extends Controller {
Optional.ofNullable(issue.dueDate).ifPresent(dueDate ->
json.put("dueDate", getDateString(dueDate)));
String refUrl = Config.getScheme() + "://" + Config.getHostport()
+ controllers.routes.IssueApp.issue(posting.project.owner, posting.project.name, posting.getNumber()).url();
json.put("refUrl", refUrl);
}
List<Attachment> attachments = Attachment.findByContainer(posting.asResource());
if (attachments.size() > 0) {
@ -383,23 +390,53 @@ public class ProjectApi extends Controller {
}
public static List<ObjectNode> composePlainCommentsJson(AbstractPosting posting) {
List<ObjectNode> comments = new ArrayList<>();
for (Comment comment : posting.getComments()) {
ObjectNode commentNode = Json.newObject();
commentNode.put("id", comment.id);
commentNode.put("type", comment.asResource().getType().toString());
User commentAuthor = User.find.byId(comment.authorId);
commentNode.put("author", composeAuthorJson(commentAuthor));
commentNode.put("createdAt", getDateString(comment.createdDate));
commentNode.put("body", comment.contents);
List<Attachment> attachments = Attachment.findByContainer(comment.asResource());
if (attachments.size() > 0) {
commentNode.put("attachments", toJson(attachments));
Map<Long, ObjectNode> commentMap = new HashMap<>();
Map<Long, List<ObjectNode>> childCommentMap = new HashMap<>();
for (Comment comment: posting.getComments()) {
Comment parentComment = comment.getParentComment();
if (parentComment != null) {
Long parentId = comment.getParentComment().id;
ObjectNode childCommentNode = getCommentNode(comment);
List<ObjectNode> childCommentList = childCommentMap.get(parentId);
if (childCommentList == null) {
childCommentList = new ArrayList<>();
}
childCommentList.add(childCommentNode);
childCommentMap.put(parentId, childCommentList);
} else {
ObjectNode commentNode = getCommentNode(comment);
commentMap.put(comment.id, commentNode);
}
comments.add(commentNode);
}
return comments;
for (Long key: childCommentMap.keySet()) {
ObjectNode comment = commentMap.get(key);
comment.set("childComments", toJson(childCommentMap.get(key)));
}
return new ArrayList<ObjectNode>(commentMap.values());
}
private static ObjectNode getCommentNode(Comment comment) {
ObjectNode commentNode = Json.newObject();
commentNode.put("id", comment.id);
commentNode.put("type", comment.asResource().getType().toString());
User commentAuthor = User.find.byId(comment.authorId);
commentNode.put("author", composeAuthorJson(commentAuthor));
commentNode.put("createdAt", JodaDateUtil.getDateString(comment.createdDate, JodaDateUtil.ISO_FORMAT));
commentNode.put("body", comment.contents);
List<Attachment> attachments = Attachment.findByContainer(comment.asResource());
if (attachments.size() > 0) {
commentNode.put("attachments", toJson(attachments));
}
return commentNode;
}
public static ObjectNode getMilestoneNode(Milestone m) {

+ 78
- 5
app/controllers/api/UserApi.java View File

@ -6,29 +6,32 @@
**/
package controllers.api;
import com.avaje.ebean.ExpressionList;
import com.avaje.ebean.Page;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import controllers.UserApp;
import models.FavoriteIssue;
import models.FavoriteOrganization;
import models.FavoriteProject;
import models.User;
import models.*;
import models.enumeration.IssueFilterType;
import models.enumeration.UserState;
import models.support.IssueSearchCondition;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.util.ByteSource;
import play.db.ebean.Transactional;
import play.i18n.Messages;
import play.libs.F;
import play.libs.Json;
import play.mvc.Controller;
import play.mvc.Http;
import play.mvc.Result;
import utils.JodaDateUtil;
import utils.SHA256Util;
import java.util.ArrayList;
import java.util.List;
import static controllers.UserApp.addUserInfoToSession;
import static controllers.UserApp.createNewUser;
import static models.NotificationMail.isAllowedEmailDomains;
import static play.libs.Json.toJson;
@ -38,6 +41,7 @@ public class UserApi extends Controller {
private static final int HASH_ITERATIONS = 1024;
private static final String AUTHORIZATION_HEADER_PREFIX = "token";
private static final int AUTHORIZATION_HEADER_MIN_LENGTH = 2;
private static final String HOSTNAME = play.Configuration.root().getString("application.hostname", "http://localhost");
@Transactional
public static Result toggleFoveriteProject(String projectId) {
@ -106,6 +110,68 @@ public class UserApi extends Controller {
return ok(json);
}
@Transactional
public static Result getIssuesByUser(String filter, int page, int pageNum) {
ObjectNode result = Json.newObject();
if (!isAuthored(request())) {
return unauthorized(result.put("message", "unauthorized request"));
}
String token = request().getHeader("Authorization").split(AUTHORIZATION_HEADER_PREFIX)[1].replaceAll("\\s", "");
User user = User.findByUserToken(token);
IssueSearchCondition issueSearchCondition = new IssueSearchCondition();
issueSearchCondition.pageNum = page - 1;
ExpressionList<Issue> el = issueSearchCondition.getExpressionListByFilter(IssueFilterType.getValue(filter), user);
Page<Issue> issues = el.findPagingList(pageNum).getPage(issueSearchCondition.pageNum);
return issuesAsJson(issues);
}
private static Result issuesAsJson(Page<Issue> issues) {
ObjectNode listData = Json.newObject();
ArrayNode array = Json.newObject().arrayNode();
List<Issue> issueList = issues.getList();
for (Issue issue : issueList){
ObjectNode result = Json.newObject();
result.put("id", issue.id);
result.put("number", issue.getNumber());
result.put("state", issue.state.toString());
result.put("title", issue.title);
result.put("createdDate", JodaDateUtil.getDateString(issue.createdDate, JodaDateUtil.ISO_FORMAT));
result.put("updatedDate", JodaDateUtil.getDateString(issue.updatedDate, JodaDateUtil.ISO_FORMAT));
ObjectNode authorNode = Json.newObject();
authorNode.put("id", issue.authorId);
authorNode.put("loginId", issue.authorLoginId);
authorNode.put("name", issue.authorName);
result.put("author", authorNode);
ObjectNode assigneeNode = Json.newObject();
if (issue.assignee != null) {
assigneeNode.put("id", issue.assignee.id);
assigneeNode.put("loginId", issue.assignee.user.loginId);
assigneeNode.put("name", issue.assignee.user.name);
}
result.put("assignee", assigneeNode);
ObjectNode projectNode = Json.newObject();
projectNode.put("id", issue.project.id);
projectNode.put("name", issue.project.name);
result.put("project", projectNode);
result.put("owner", issue.project.owner);
result.put("refUrl", HOSTNAME + "/" + issue.project.owner + "/" + issue.project.name + "/issue/" + issue.getNumber());
array.add(result);
}
listData.put("result", array);
return ok(listData);
}
@Transactional
public static Result toggleFoveriteOrganization(String organizationId) {
if (organizationId == null) {
@ -179,6 +245,7 @@ public class UserApi extends Controller {
if (!checkUserPassword(user, password))
return unauthorized(result.put("message", "No user by id and password"));
addUserInfoToSession(user);
result.put("access_token", getNewUserToken(user));
return ok(toJson(result));
}
@ -199,6 +266,12 @@ public class UserApi extends Controller {
return true;
}
public static String getAuthorizationToken(Http.Request request) {
String header = request.getHeader("Authorization");
String[] tokenValues = header.split(AUTHORIZATION_HEADER_PREFIX);
return tokenValues[1].replaceAll("\\s", "");
}
public static User getAuthorizedUser(String token) {
return User.findByUserToken(token);
}

+ 3
- 0
app/models/Comment.java View File

@ -44,6 +44,9 @@ abstract public class Comment extends Model implements TimelineItem, ResourceCon
@Transient
public String parentCommentId;
@Transient
public String previousContents;
public Comment() {
createdDate = new Date();
}

+ 9
- 0
app/models/Issue.java View File

@ -698,6 +698,15 @@ public class Issue extends AbstractPosting implements LabelOwner {
return null;
}
public IssueComment findCommentByCommentId(Long id) {
for (IssueComment comment: comments) {
if (comment.id.equals(id)) {
return comment;
}
}
return null;
}
public List<IssueSharer> getSortedSharer() {
return new ArrayList<>(sharers);
}

+ 40
- 7
app/models/NotificationEvent.java View File

@ -135,14 +135,19 @@ public class NotificationEvent extends Model implements INotificationEvent {
return Messages.get(lang, "notification.issue.assigned", newValue);
}
case ISSUE_MILESTONE_CHANGED:
return Messages.get(lang, "notification.milestone.changed", newValue);
if (Milestone.findById(Long.parseLong(newValue)) == null) {
return Messages.get(lang, "notification.milestone.changed", Messages.get(Lang.defaultLang(), "issue.noMilestone"));
} else {
return Messages.get(lang, "notification.milestone.changed", Milestone.findById(Long.parseLong(newValue)).title);
}
case NEW_ISSUE:
case NEW_POSTING:
case NEW_COMMENT:
case NEW_PULL_REQUEST:
case NEW_COMMIT:
case COMMENT_UPDATED:
return newValue;
case NEW_COMMENT:
return newValue + oldValue;
case ISSUE_BODY_CHANGED:
case POSTING_BODY_CHANGED:
return DiffUtil.getDiffText(oldValue, newValue);
@ -236,7 +241,7 @@ public class NotificationEvent extends Model implements INotificationEvent {
case POSTING_BODY_CHANGED:
return DiffUtil.getDiffPlainText(oldValue, newValue);
default:
return getMessage(lang);
return getMessage(lang).replaceAll("\n\n<br />\n", "\n\n");
}
}
@ -527,6 +532,7 @@ public class NotificationEvent extends Model implements INotificationEvent {
}
public String getUrlToView() {
Organization organization;
switch(eventType) {
case MEMBER_ENROLL_REQUEST:
if (getProject() == null) {
@ -535,13 +541,25 @@ public class NotificationEvent extends Model implements INotificationEvent {
return routes.ProjectApp.members(
getProject().owner, getProject().name).url();
}
case MEMBER_ENROLL_ACCEPT:
if (getProject() == null) {
return null;
} else {
return routes.ProjectApp.project(
getProject().owner, getProject().name).url();
}
case ORGANIZATION_MEMBER_ENROLL_REQUEST:
Organization organization = getOrganization();
organization = getOrganization();
if (organization == null) {
return null;
}
return routes.OrganizationApp.members(organization.name).url();
case ORGANIZATION_MEMBER_ENROLL_ACCEPT:
organization = getOrganization();
if (organization == null) {
return null;
}
return routes.OrganizationApp.organization(organization.name).url();
case NEW_COMMIT:
if (getProject() == null) {
return null;
@ -609,6 +627,16 @@ public class NotificationEvent extends Model implements INotificationEvent {