Newer
Older
gitbucket_jkp / src / main / scala / app / IssuesController.scala
  1. package app
  2.  
  3. import jp.sf.amateras.scalatra.forms._
  4.  
  5. import service._
  6. import IssuesService._
  7. import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator}
  8. import org.scalatra.Ok
  9.  
  10. class IssuesController extends IssuesControllerBase
  11. with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
  12. with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
  13.  
  14. trait IssuesControllerBase extends ControllerBase {
  15. self: IssuesService with RepositoryService with LabelsService with MilestonesService with ActivityService
  16. with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
  17.  
  18. case class IssueCreateForm(title: String, content: Option[String],
  19. assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
  20. case class IssueEditForm(title: String, content: Option[String])
  21. case class CommentForm(issueId: Int, content: String)
  22. case class IssueStateForm(issueId: Int, content: Option[String])
  23.  
  24. val issueCreateForm = mapping(
  25. "title" -> trim(label("Title", text(required))),
  26. "content" -> trim(optional(text())),
  27. "assignedUserName" -> trim(optional(text())),
  28. "milestoneId" -> trim(optional(number())),
  29. "labelNames" -> trim(optional(text()))
  30. )(IssueCreateForm.apply)
  31.  
  32. val issueEditForm = mapping(
  33. "title" -> trim(label("Title", text(required))),
  34. "content" -> trim(optional(text()))
  35. )(IssueEditForm.apply)
  36.  
  37. val commentForm = mapping(
  38. "issueId" -> label("Issue Id", number()),
  39. "content" -> trim(label("Comment", text(required)))
  40. )(CommentForm.apply)
  41.  
  42. val issueStateForm = mapping(
  43. "issueId" -> label("Issue Id", number()),
  44. "content" -> trim(optional(text()))
  45. )(IssueStateForm.apply)
  46.  
  47. get("/:owner/:repository/issues")(referrersOnly {
  48. searchIssues("all", _)
  49. })
  50.  
  51. get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
  52. searchIssues("assigned", _)
  53. })
  54.  
  55. get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
  56. searchIssues("created_by", _)
  57. })
  58.  
  59. get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
  60. val owner = repository.owner
  61. val name = repository.name
  62. val issueId = params("id")
  63.  
  64. getIssue(owner, name, issueId) map {
  65. issues.html.issue(
  66. _,
  67. getComments(owner, name, issueId.toInt),
  68. getIssueLabels(owner, name, issueId.toInt),
  69. (getCollaborators(owner, name) :+ owner).sorted,
  70. getMilestonesWithIssueCount(owner, name),
  71. getLabels(owner, name),
  72. hasWritePermission(owner, name, context.loginAccount),
  73. repository)
  74. } getOrElse NotFound
  75. })
  76.  
  77. get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
  78. val owner = repository.owner
  79. val name = repository.name
  80.  
  81. issues.html.create(
  82. (getCollaborators(owner, name) :+ owner).sorted,
  83. getMilestones(owner, name),
  84. getLabels(owner, name),
  85. hasWritePermission(owner, name, context.loginAccount),
  86. repository)
  87. })
  88.  
  89. post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
  90. val owner = repository.owner
  91. val name = repository.name
  92. val writable = hasWritePermission(owner, name, context.loginAccount)
  93. val userName = context.loginAccount.get.userName
  94.  
  95. // insert issue
  96. val issueId = createIssue(owner, name, userName, form.title, form.content,
  97. if(writable) form.assignedUserName else None,
  98. if(writable) form.milestoneId else None)
  99.  
  100. // insert labels
  101. if(writable){
  102. form.labelNames.map { value =>
  103. val labels = getLabels(owner, name)
  104. value.split(",").foreach { labelName =>
  105. labels.find(_.labelName == labelName).map { label =>
  106. registerIssueLabel(owner, name, issueId, label.labelId)
  107. }
  108. }
  109. }
  110. }
  111.  
  112. // record activity
  113. recordCreateIssueActivity(owner, name, userName, issueId, form.title)
  114.  
  115. redirect(s"/${owner}/${name}/issues/${issueId}")
  116. })
  117.  
  118. ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
  119. val owner = repository.owner
  120. val name = repository.name
  121.  
  122. getIssue(owner, name, params("id")).map { issue =>
  123. if(isEditable(owner, name, issue.openedUserName)){
  124. updateIssue(owner, name, issue.issueId, form.title, form.content)
  125. redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
  126. } else Unauthorized
  127. } getOrElse NotFound
  128. })
  129.  
  130. post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
  131. handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
  132. if(issue.isPullRequest){
  133. redirect(s"/${repository.owner}/${repository.name}/pull/${form.issueId}#comment-${id}")
  134. } else {
  135. redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
  136. }
  137. } getOrElse NotFound
  138. })
  139.  
  140. post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
  141. handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
  142. if(issue.isPullRequest){
  143. redirect(s"/${repository.owner}/${repository.name}/pull/${form.issueId}#comment-${id}")
  144. } else {
  145. redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
  146. }
  147. } getOrElse NotFound
  148. })
  149.  
  150. ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
  151. val owner = repository.owner
  152. val name = repository.name
  153.  
  154. getComment(owner, name, params("id")).map { comment =>
  155. if(isEditable(owner, name, comment.commentedUserName)){
  156. updateComment(comment.commentId, form.content)
  157. redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
  158. } else Unauthorized
  159. } getOrElse NotFound
  160. })
  161.  
  162. ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
  163. getIssue(repository.owner, repository.name, params("id")) map { x =>
  164. if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
  165. params.get("dataType") collect {
  166. case t if t == "html" => issues.html.editissue(
  167. x.title, x.content, x.issueId, x.userName, x.repositoryName)
  168. } getOrElse {
  169. contentType = formats("json")
  170. org.json4s.jackson.Serialization.write(
  171. Map("title" -> x.title,
  172. "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
  173. repository, false, true)
  174. ))
  175. }
  176. } else Unauthorized
  177. } getOrElse NotFound
  178. })
  179.  
  180. ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
  181. getComment(repository.owner, repository.name, params("id")) map { x =>
  182. if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
  183. params.get("dataType") collect {
  184. case t if t == "html" => issues.html.editcomment(
  185. x.content, x.commentId, x.userName, x.repositoryName)
  186. } getOrElse {
  187. contentType = formats("json")
  188. org.json4s.jackson.Serialization.write(
  189. Map("content" -> view.Markdown.toHtml(x.content,
  190. repository, false, true)
  191. ))
  192. }
  193. } else Unauthorized
  194. } getOrElse NotFound
  195. })
  196.  
  197. ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
  198. val issueId = params("id").toInt
  199.  
  200. registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
  201. issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
  202. })
  203.  
  204. ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
  205. val issueId = params("id").toInt
  206.  
  207. deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
  208. issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
  209. })
  210.  
  211. ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
  212. updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
  213. Ok("updated")
  214. })
  215.  
  216. ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
  217. updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
  218. milestoneId("milestoneId").map { milestoneId =>
  219. getMilestonesWithIssueCount(repository.owner, repository.name)
  220. .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
  221. issues.milestones.html.progress(openCount + closeCount, closeCount, false)
  222. } getOrElse NotFound
  223. } getOrElse Ok()
  224. })
  225.  
  226. post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
  227. val action = params.get("value")
  228.  
  229. executeBatch(repository) {
  230. handleComment(_, None, repository)( _ => action)
  231. }
  232. })
  233.  
  234. post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
  235. val labelId = params("value").toInt
  236.  
  237. executeBatch(repository) { issueId =>
  238. getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
  239. registerIssueLabel(repository.owner, repository.name, issueId, labelId)
  240. }
  241. }
  242. })
  243.  
  244. post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
  245. val value = assignedUserName("value")
  246.  
  247. executeBatch(repository) {
  248. updateAssignedUserName(repository.owner, repository.name, _, value)
  249. }
  250. })
  251.  
  252. post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
  253. val value = milestoneId("value")
  254.  
  255. executeBatch(repository) {
  256. updateMilestoneId(repository.owner, repository.name, _, value)
  257. }
  258. })
  259.  
  260. val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
  261. val milestoneId = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt }
  262.  
  263. private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
  264. hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
  265.  
  266. private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
  267. params("checked").split(',') map(_.toInt) foreach execute
  268. redirect(s"/${repository.owner}/${repository.name}/issues")
  269. }
  270.  
  271. /**
  272. * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
  273. */
  274. private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
  275. (getAction: model.Issue => Option[String] =
  276. p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
  277. val owner = repository.owner
  278. val name = repository.name
  279. val userName = context.loginAccount.get.userName
  280.  
  281. getIssue(owner, name, issueId.toString) map { issue =>
  282. val (action, recordActivity) =
  283. getAction(issue)
  284. .collect {
  285. case "close" => true -> (Some("close") -> Some(recordCloseIssueActivity _))
  286. case "reopen" => false -> (Some("reopen") -> Some(recordReopenIssueActivity _))
  287. }
  288. .map { case (closed, t) =>
  289. updateClosed(owner, name, issueId, closed)
  290. t
  291. }
  292. .getOrElse(None -> None)
  293.  
  294. val commentId = content
  295. .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
  296. .getOrElse ( action.get.capitalize -> action.get )
  297. match {
  298. case (content, action) => createComment(owner, name, userName, issueId, content, action)
  299. }
  300.  
  301. // record activity
  302. content foreach ( recordCommentIssueActivity(owner, name, userName, issueId, _) )
  303. recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
  304.  
  305. (issue, commentId)
  306. }
  307. }
  308.  
  309. private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
  310. val owner = repository.owner
  311. val repoName = repository.name
  312. val filterUser = Map(filter -> params.getOrElse("userName", ""))
  313. val page = IssueSearchCondition.page(request)
  314. val sessionKey = s"${owner}/${repoName}/issues"
  315.  
  316. // retrieve search condition
  317. val condition = if(request.getQueryString == null){
  318. session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
  319. } else IssueSearchCondition(request)
  320.  
  321. session.put(sessionKey, condition)
  322.  
  323. issues.html.list(
  324. searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
  325. page,
  326. (getCollaborators(owner, repoName) :+ owner).sorted,
  327. getMilestones(owner, repoName),
  328. getLabels(owner, repoName),
  329. countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
  330. countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
  331. countIssue(condition, Map.empty, false, owner -> repoName),
  332. context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
  333. context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
  334. countIssueGroupByLabels(owner, repoName, condition, filterUser),
  335. condition,
  336. filter,
  337. repository,
  338. hasWritePermission(owner, repoName, context.loginAccount))
  339. }
  340.  
  341. }