Start to implement milestone pages.

This commit is contained in:
takezoe
2013-06-23 02:49:01 +09:00
parent 6702fdf24f
commit 49369f253a
4 changed files with 289 additions and 3 deletions

View File

@@ -3,21 +3,31 @@ package app
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import util.UsersOnlyAuthenticator import util.{WritableRepositoryAuthenticator, ReadableRepositoryAuthenticator, UsersOnlyAuthenticator}
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with UsersOnlyAuthenticator with IssuesService with RepositoryService with AccountService
with UsersOnlyAuthenticator with ReadableRepositoryAuthenticator with WritableRepositoryAuthenticator
trait IssuesControllerBase extends ControllerBase { trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with UsersOnlyAuthenticator => self: IssuesService with RepositoryService
with UsersOnlyAuthenticator with ReadableRepositoryAuthenticator with WritableRepositoryAuthenticator =>
case class IssueForm(title: String, content: Option[String]) case class IssueForm(title: String, content: Option[String])
case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.sql.Date])
val form = mapping( val form = mapping(
"title" -> trim(label("Title", text(required))), "title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(IssueForm.apply) )(IssueForm.apply)
val milestoneForm = mapping(
"title" -> trim(label("Title", text(required))),
"description" -> trim(label("Description", optional(text()))),
"dueDate" -> trim(label("Due Date", optional(date())))
)(MilestoneForm.apply)
get("/:owner/:repository/issues"){ get("/:owner/:repository/issues"){
val owner = params("owner") val owner = params("owner")
val repository = params("repository") val repository = params("repository")
@@ -53,4 +63,86 @@ trait IssuesControllerBase extends ControllerBase {
saveIssue(owner, repository, context.loginAccount.get.userName, form.title, form.content))) saveIssue(owner, repository, context.loginAccount.get.userName, form.title, form.content)))
}) })
get("/:owner/:repository/issues/milestones")(readableRepository {
val owner = params("owner")
val repository = params("repository")
val state = params.getOrElse("state", "open")
getRepository(owner, repository, baseUrl) match {
case None => NotFound()
case Some(r) => issues.html.milestones(state, getMilestones(owner, repository), r, isWritable(owner, repository, context.loginAccount))
}
})
get("/:owner/:repository/issues/milestones/new")(writableRepository {
val owner = params("owner")
val repository = params("repository")
getRepository(owner, repository, baseUrl) match {
case None => NotFound()
case Some(r) => issues.html.milestoneedit(None, r)
}
})
post("/:owner/:repository/issues/milestones/new", milestoneForm)(writableRepository { form =>
val owner = params("owner")
val repository = params("repository")
createMilestone(owner, repository, form.title, form.description, form.dueDate)
redirect("/%s/%s/issues/milestones".format(owner, repository))
})
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(writableRepository {
val owner = params("owner")
val repository = params("repository")
val milestoneId = params("milestoneId").toInt
getRepository(owner, repository, baseUrl) match {
case None => NotFound()
case Some(r) => issues.html.milestoneedit(getMilestone(owner, repository, milestoneId), r)
}
})
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(writableRepository { form =>
val owner = params("owner")
val repository = params("repository")
val milestoneId = params("milestoneId").toInt
getMilestone(owner, repository, milestoneId) match {
case None => NotFound()
case Some(m) => {
updateMilestone(m.copy(title = form.title, description = form.description, dueDate = form.dueDate))
redirect("/%s/%s/issues/milestones".format(owner, repository))
}
}
})
get("/:owner/:repository/issues/milestones/:milestoneId/close")(writableRepository {
val owner = params("owner")
val repository = params("repository")
val milestoneId = params("milestoneId").toInt
getMilestone(owner, repository, milestoneId) match {
case None => NotFound()
case Some(m) => {
updateMilestone(m.copy(closed = true))
redirect("/%s/%s/issues/milestones".format(owner, repository))
}
}
})
get("/:owner/:repository/issues/milestones/:milestoneId/open")(writableRepository {
val owner = params("owner")
val repository = params("repository")
val milestoneId = params("milestoneId").toInt
getMilestone(owner, repository, milestoneId) match {
case None => NotFound()
case Some(m) => {
updateMilestone(m.copy(closed = false))
redirect("/%s/%s/issues/milestones".format(owner, repository))
}
}
})
} }

View File

@@ -50,4 +50,31 @@ trait IssuesService {
}.map(_.issueId).update(id) > 0 }.map(_.issueId).update(id) > 0
} get } get
def createMilestone(owner: String, repository: String,
title: String, description: Option[String], dueDate: Option[java.sql.Date]): Unit = {
Milestones.ins insert (owner, repository, title, description, dueDate, false)
}
def updateMilestone(milestone: Milestone): Unit =
Query(Milestones)
.filter { m => (m.userName is milestone.userName.bind) && (m.repositoryName is milestone.repositoryName.bind) && (m.milestoneId is milestone.milestoneId.bind)}
.map { m => m.title ~ m.description.? ~ m.dueDate.? ~ m.closed }
.update (
milestone.title,
milestone.description,
milestone.dueDate,
milestone.closed)
def getMilestone(owner: String, repository: String, milestoneId: Int): Option[Milestone] =
Query(Milestones)
.filter(m => (m.userName is owner.bind) && (m.repositoryName is repository.bind) && (m.milestoneId is milestoneId.bind))
.sortBy(_.milestoneId desc)
.firstOption
def getMilestones(owner: String, repository: String): List[Milestone] =
Query(Milestones)
.filter(m => (m.userName is owner.bind) && (m.repositoryName is repository.bind))
.sortBy(_.milestoneId desc)
.list
} }

View File

@@ -0,0 +1,47 @@
@(milestone: Option[model.Milestone], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers
@html.main("Milestones - " + repository.owner + "/" + repository.name){
@html.header("milestones", repository)
@issuestab("milestones", repository)
<form method="POST" action="@path/@repository.owner/@repository.name/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
<fieldset>
<label for="title"><string>Title</string></label>
<input type="text" id="title" name="title" style="width: 400px;" value="@milestone.map(_.title)"/>
<span id="error-title" class="error"></span>
</fieldset>
<fieldset>
<label for="description"><strong>Description</strong></label>
<textarea id="description" name="description" style="width: 500px; height: 150px;">@milestone.map(_.description)</textarea>
<span id="error-description" class="error"></span>
</fieldset>
<fieldset>
<label for="dueDate"><strong>Due Date</strong></label>
<div id="dueDate" class="input-append date" data-date-format="yyyy-mm-dd" data-date="@milestone.map(_.dueDate.map(helpers.date))">
<input class="span2" name="dueDate" type="text" readonly="" value="@milestone.map(_.dueDate.map(helpers.date))" size="16" name="dueDate"/>
<span class="add-on"><i class="icon-calendar"></i></span>
</div>
<span id="error-dueDate" class="error"></span>
</fieldset>
<hr>
<div class="pull-right">
@if(milestone.isEmpty){
<input type="submit" class="btn" value="Create milestone"/>
} else {
@if(milestone.get.closed){
<input type="button" class="btn" value="Open" id="open"
onclick="location.href='@path/@repository.owner/@repository.name/issues/milestones/@milestone.get.milestoneId/close';"/>
} else {
<input type="button" class="btn" value="Close" id="close"
onclick="location.href='@path/@repository.owner/@repository.name/issues/milestones/@milestone.get.milestoneId/open';"/>
}
<input type="submit" class="btn" value="Update milestone"/>
}
</div>
</form>
}
<script>
$(function(){
$('#dueDate').datepicker();
});
</script>

View File

@@ -0,0 +1,120 @@
@(state: String, milestones: List[model.Milestone], repository: service.RepositoryService.RepositoryInfo, isWritable: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers
@html.main("Milestones - " + repository.owner + "/" + repository.name){
@html.header("milestones", repository)
@issuestab("milestones", repository)
<div class="row-fluid">
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(state == "open"){ class="active"}>
<a href="?state=open">
<span style="float: right; font-weight: bold;">@milestones.filter(!_.closed).size</span>
Open Milestones
</a>
</li>
<li@if(state == "closed"){ class="active"}>
<a href="?state=closed">
<span style="float: right; font-weight: bold;">@milestones.filter(_.closed).size</span>
Closed Milestones
</a>
</li>
</ul>
@if(isWritable){
<hr>
<a href="@path/@repository.owner/@repository.name/issues/milestones/new" class="btn" style="display: block;">Create a new milestone</a>
}
</div>
<div class="span9">
<table class="table table-bordered table-hover">
@defining(milestones.filter(milestone => if(state == "open") !milestone.closed else milestone.closed)){ milestones =>
@milestones.map { milestone =>
<tr>
<td>
<div class="milestone row-fluid">
<div class="span4">
<a href="#" class="milestone-title">@milestone.title</a><br>
@if(milestone.dueDate.isDefined){
<span class="description">@helpers.date(milestone.dueDate.get)</span>
} else {
<span class="description">No due date</span>
}
</div>
<div class="span8">
<div class="milestone-menu">
<div class="pull-right">
@if(isWritable){
<a href="@path/@repository.owner/@repository.name/issues/milestones/@milestone.milestoneId/edit">Edit
@if(milestone.closed){
<a href="@path/@repository.owner/@repository.name/issues/milestones/@milestone.milestoneId/open">Open</a>
} else {
<a href="@path/@repository.owner/@repository.name/issues/milestones/@milestone.milestoneId/close">Close</a>
}
<a href="" class="delete">Delete</a>
}
<a href="">Browse issues</a>
</div>
<span class="description">0 closed - 0 open</span>
</div>
<div class="milestone-progress">10%</div>
</div>
</div>
@if(milestone.description.isDefined){
<div class="milestone-description">
@helpers.markdown(milestone.description.get, repository, false, false, false)
</div>
}
</td>
</tr>
}
@if(milestones.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No milestones to show.
@if(isWritable){
<a href="@path/@repository.owner/@repository.name/issues/milestones/new">Create a new milestone.</a>
}
</td>
</tr>
}
}
</table>
</div>
</div>
}
<style type="text/css">
a.milestone-title {
font-size: 120%;
font-weight: bold;
}
div.milestone-description {
border-top: 1px solid #eee;
color: #666;
}
div.milestone-menu {
font-size: 80%;
}
div.milestone-menu a {
margin-left: 8px;
font-weight: bold;
}
div.milestone-menu a.delete {
color: #b00;
}
div.milestone-progress {
color: white;
padding-left: 4px;
margin-bottom: 4px;
font-weight: bold;
font-size: 12px;
text-shadow: 0px 0px 5px #444;
background-color: silver;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
}
</style>