mirror of
				https://github.com/gitbucket/gitbucket.git
				synced 2025-10-31 10:36:05 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			359 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Scala
		
	
	
	
	
	
			
		
		
	
	
			359 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Scala
		
	
	
	
	
	
| package app
 | |
| 
 | |
| import jp.sf.amateras.scalatra.forms._
 | |
| 
 | |
| import service._
 | |
| import IssuesService._
 | |
| import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier}
 | |
| import util.Implicits._
 | |
| import util.ControlUtil._
 | |
| import org.scalatra.Ok
 | |
| 
 | |
| class IssuesController extends IssuesControllerBase
 | |
|   with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
 | |
|   with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
 | |
| 
 | |
| trait IssuesControllerBase extends ControllerBase {
 | |
|   self: IssuesService with RepositoryService with LabelsService with MilestonesService with ActivityService
 | |
|     with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
 | |
| 
 | |
|   case class IssueCreateForm(title: String, content: Option[String],
 | |
|     assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
 | |
|   case class IssueEditForm(title: String, content: Option[String])
 | |
|   case class CommentForm(issueId: Int, content: String)
 | |
|   case class IssueStateForm(issueId: Int, content: Option[String])
 | |
| 
 | |
|   val issueCreateForm = mapping(
 | |
|       "title"            -> trim(label("Title", text(required))),
 | |
|       "content"          -> trim(optional(text())),
 | |
|       "assignedUserName" -> trim(optional(text())),
 | |
|       "milestoneId"      -> trim(optional(number())),
 | |
|       "labelNames"       -> trim(optional(text()))
 | |
|     )(IssueCreateForm.apply)
 | |
| 
 | |
|   val issueEditForm = mapping(
 | |
|       "title"   -> trim(label("Title", text(required))),
 | |
|       "content" -> trim(optional(text()))
 | |
|     )(IssueEditForm.apply)
 | |
| 
 | |
|   val commentForm = mapping(
 | |
|       "issueId" -> label("Issue Id", number()),
 | |
|       "content" -> trim(label("Comment", text(required)))
 | |
|     )(CommentForm.apply)
 | |
| 
 | |
|   val issueStateForm = mapping(
 | |
|       "issueId" -> label("Issue Id", number()),
 | |
|       "content" -> trim(optional(text()))
 | |
|     )(IssueStateForm.apply)
 | |
| 
 | |
|   get("/:owner/:repository/issues")(referrersOnly {
 | |
|     searchIssues("all", _)
 | |
|   })
 | |
| 
 | |
|   get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
 | |
|     searchIssues("assigned", _)
 | |
|   })
 | |
| 
 | |
|   get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
 | |
|     searchIssues("created_by", _)
 | |
|   })
 | |
| 
 | |
|   get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
 | |
|     defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
 | |
|       getIssue(owner, name, issueId) map {
 | |
|         issues.html.issue(
 | |
|           _,
 | |
|           getComments(owner, name, issueId.toInt),
 | |
|           getIssueLabels(owner, name, issueId.toInt),
 | |
|           (getCollaborators(owner, name) :+ owner).sorted,
 | |
|           getMilestonesWithIssueCount(owner, name),
 | |
|           getLabels(owner, name),
 | |
|           hasWritePermission(owner, name, context.loginAccount),
 | |
|           repository)
 | |
|       } getOrElse NotFound
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
 | |
|     defining(repository.owner, repository.name){ case (owner, name) =>
 | |
|       issues.html.create(
 | |
|           (getCollaborators(owner, name) :+ owner).sorted,
 | |
|           getMilestones(owner, name),
 | |
|           getLabels(owner, name),
 | |
|           hasWritePermission(owner, name, context.loginAccount),
 | |
|           repository)
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
 | |
|     defining(repository.owner, repository.name){ case (owner, name) =>
 | |
|       val writable = hasWritePermission(owner, name, context.loginAccount)
 | |
|       val userName = context.loginAccount.get.userName
 | |
| 
 | |
|       // insert issue
 | |
|       val issueId = createIssue(owner, name, userName, form.title, form.content,
 | |
|         if(writable) form.assignedUserName else None,
 | |
|         if(writable) form.milestoneId else None)
 | |
| 
 | |
|       // insert labels
 | |
|       if(writable){
 | |
|         form.labelNames.map { value =>
 | |
|           val labels = getLabels(owner, name)
 | |
|           value.split(",").foreach { labelName =>
 | |
|             labels.find(_.labelName == labelName).map { label =>
 | |
|               registerIssueLabel(owner, name, issueId, label.labelId)
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // record activity
 | |
|       recordCreateIssueActivity(owner, name, userName, issueId, form.title)
 | |
| 
 | |
|       // notifications
 | |
|       Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
 | |
|         Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
 | |
|       }
 | |
| 
 | |
|       redirect(s"/${owner}/${name}/issues/${issueId}")
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
 | |
|     defining(repository.owner, repository.name){ case (owner, name) =>
 | |
|       getIssue(owner, name, params("id")).map { issue =>
 | |
|         if(isEditable(owner, name, issue.openedUserName)){
 | |
|           updateIssue(owner, name, issue.issueId, form.title, form.content)
 | |
|           redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
 | |
|         } else Unauthorized
 | |
|       } getOrElse NotFound
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
 | |
|     handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
 | |
|       redirect(s"/${repository.owner}/${repository.name}/${
 | |
|         if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
 | |
|     } getOrElse NotFound
 | |
|   })
 | |
| 
 | |
|   post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
 | |
|     handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
 | |
|       redirect(s"/${repository.owner}/${repository.name}/${
 | |
|         if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
 | |
|     } getOrElse NotFound
 | |
|   })
 | |
| 
 | |
|   ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
 | |
|     defining(repository.owner, repository.name){ case (owner, name) =>
 | |
|       getComment(owner, name, params("id")).map { comment =>
 | |
|         if(isEditable(owner, name, comment.commentedUserName)){
 | |
|           updateComment(comment.commentId, form.content)
 | |
|           redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
 | |
|         } else Unauthorized
 | |
|       } getOrElse NotFound
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
 | |
|     getIssue(repository.owner, repository.name, params("id")) map { x =>
 | |
|       if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
 | |
|         params.get("dataType") collect {
 | |
|           case t if t == "html" => issues.html.editissue(
 | |
|               x.title, x.content, x.issueId, x.userName, x.repositoryName)
 | |
|         } getOrElse {
 | |
|           contentType = formats("json")
 | |
|           org.json4s.jackson.Serialization.write(
 | |
|               Map("title"   -> x.title,
 | |
|                   "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
 | |
|                       repository, false, true)
 | |
|               ))
 | |
|         }
 | |
|       } else Unauthorized
 | |
|     } getOrElse NotFound
 | |
|   })
 | |
| 
 | |
|   ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
 | |
|     getComment(repository.owner, repository.name, params("id")) map { x =>
 | |
|       if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
 | |
|         params.get("dataType") collect {
 | |
|           case t if t == "html" => issues.html.editcomment(
 | |
|               x.content, x.commentId, x.userName, x.repositoryName)
 | |
|         } getOrElse {
 | |
|           contentType = formats("json")
 | |
|           org.json4s.jackson.Serialization.write(
 | |
|               Map("content" -> view.Markdown.toHtml(x.content,
 | |
|                   repository, false, true)
 | |
|               ))
 | |
|         }
 | |
|       } else Unauthorized
 | |
|     } getOrElse NotFound
 | |
|   })
 | |
| 
 | |
|   ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
 | |
|     defining(params("id").toInt){ issueId =>
 | |
|       registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
 | |
|       issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
 | |
|     defining(params("id").toInt){ issueId =>
 | |
|       deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
 | |
|       issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
 | |
|     updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
 | |
|     Ok("updated")
 | |
|   })
 | |
| 
 | |
|   ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
 | |
|     updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
 | |
|     milestoneId("milestoneId").map { milestoneId =>
 | |
|       getMilestonesWithIssueCount(repository.owner, repository.name)
 | |
|           .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
 | |
|         issues.milestones.html.progress(openCount + closeCount, closeCount, false)
 | |
|       } getOrElse NotFound
 | |
|     } getOrElse Ok()
 | |
|   })
 | |
| 
 | |
|   post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
 | |
|     defining(params.get("value")){ action =>
 | |
|       executeBatch(repository) {
 | |
|         handleComment(_, None, repository)( _ => action)
 | |
|       }
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
 | |
|     defining(params("value").toInt){ labelId =>
 | |
|       executeBatch(repository) { issueId =>
 | |
|         getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
 | |
|           registerIssueLabel(repository.owner, repository.name, issueId, labelId)
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
 | |
|     defining(assignedUserName("value")){ value =>
 | |
|       executeBatch(repository) {
 | |
|         updateAssignedUserName(repository.owner, repository.name, _, value)
 | |
|       }
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
 | |
|     defining(milestoneId("value")){ value =>
 | |
|       executeBatch(repository) {
 | |
|         updateMilestoneId(repository.owner, repository.name, _, value)
 | |
|       }
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
 | |
|   val milestoneId      = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt }
 | |
| 
 | |
|   private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
 | |
|     hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
 | |
| 
 | |
|   private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
 | |
|     params("checked").split(',') map(_.toInt) foreach execute
 | |
|     redirect(s"/${repository.owner}/${repository.name}/issues")
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
 | |
|    */
 | |
|   private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
 | |
|       (getAction: model.Issue => Option[String] =
 | |
|            p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
 | |
| 
 | |
|     defining(repository.owner, repository.name){ case (owner, name) =>
 | |
|       val userName = context.loginAccount.get.userName
 | |
| 
 | |
|       getIssue(owner, name, issueId.toString) map { issue =>
 | |
|         val (action, recordActivity) =
 | |
|           getAction(issue)
 | |
|             .collect {
 | |
|             case "close"  => true  -> (Some("close")  ->
 | |
|               Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
 | |
|             case "reopen" => false -> (Some("reopen") ->
 | |
|               Some(recordReopenIssueActivity _))
 | |
|           }
 | |
|             .map { case (closed, t) =>
 | |
|             updateClosed(owner, name, issueId, closed)
 | |
|             t
 | |
|           }
 | |
|             .getOrElse(None -> None)
 | |
| 
 | |
|         val commentId = content
 | |
|           .map       ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
 | |
|           .getOrElse ( action.get.capitalize -> action.get )
 | |
|         match {
 | |
|           case (content, action) => createComment(owner, name, userName, issueId, content, action)
 | |
|         }
 | |
| 
 | |
|         // record activity
 | |
|         content foreach {
 | |
|           (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
 | |
|           (owner, name, userName, issueId, _)
 | |
|         }
 | |
|         recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
 | |
| 
 | |
|         // notifications
 | |
|         Notifier() match {
 | |
|           case f =>
 | |
|             content foreach {
 | |
|               f.toNotify(repository, issueId, _){
 | |
|                 Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
 | |
|                   if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
 | |
|               }
 | |
|             }
 | |
|             action foreach {
 | |
|               f.toNotify(repository, issueId, _){
 | |
|                 Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
 | |
|               }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         issue -> commentId
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
 | |
|     defining(repository.owner, repository.name){ case (owner, repoName) =>
 | |
|       val filterUser = Map(filter -> params.getOrElse("userName", ""))
 | |
|       val page       = IssueSearchCondition.page(request)
 | |
|       val sessionKey = s"${owner}/${repoName}/issues"
 | |
| 
 | |
|       // retrieve search condition
 | |
|       val condition = session.putAndGet(sessionKey,
 | |
|         if(request.hasQueryString) IssueSearchCondition(request)
 | |
|         else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
 | |
|       )
 | |
| 
 | |
|       issues.html.list(
 | |
|           searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
 | |
|           page,
 | |
|           (getCollaborators(owner, repoName) :+ owner).sorted,
 | |
|           getMilestones(owner, repoName),
 | |
|           getLabels(owner, repoName),
 | |
|           countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
 | |
|           countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
 | |
|           countIssue(condition, Map.empty, false, owner -> repoName),
 | |
|           context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
 | |
|           context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
 | |
|           countIssueGroupByLabels(owner, repoName, condition, filterUser),
 | |
|           condition,
 | |
|           filter,
 | |
|           repository,
 | |
|           hasWritePermission(owner, repoName, context.loginAccount))
 | |
|     }
 | |
|   }
 | |
| 
 | |
| }
 |