(refs #4)Add 'Public Activity' tab to the account information page.

This commit is contained in:
takezoe
2013-07-06 20:03:34 +09:00
parent eba81a6065
commit f84078c7ca
10 changed files with 143 additions and 23 deletions

View File

@@ -0,0 +1,12 @@
CREATE TABLE ACTIVITY(
ACTIVITY_ID INT AUTO_INCREMENT,
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
ACTIVITY_USER_NAME VARCHAR(100) NOT NULL,
MESSAGE TEXT NOT NULL,
ACTIVITY_DATE TIMESTAMP NOT NULL
);
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_PK PRIMARY KEY (ACTIVITY_ID);
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_FK1 FOREIGN KEY (ACTIVITY_USER_NAME) REFERENCES ACCOUNT (USER_NAME);

View File

@@ -6,10 +6,12 @@ import util.StringUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
class AccountController extends AccountControllerBase class AccountController extends AccountControllerBase
with SystemSettingsService with AccountService with RepositoryService with OneselfAuthenticator with SystemSettingsService with AccountService with RepositoryService with ActivityService
with OneselfAuthenticator
trait AccountControllerBase extends ControllerBase { trait AccountControllerBase extends ControllerBase {
self: SystemSettingsService with AccountService with RepositoryService with OneselfAuthenticator => self: SystemSettingsService with AccountService with RepositoryService with ActivityService
with OneselfAuthenticator =>
case class AccountNewForm(userName: String, password: String,mailAddress: String, url: Option[String]) case class AccountNewForm(userName: String, password: String,mailAddress: String, url: Option[String])
@@ -33,8 +35,13 @@ trait AccountControllerBase extends ControllerBase {
*/ */
get("/:userName") { get("/:userName") {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { getAccountByUserName(userName).map { x =>
account.html.info(_, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName))) params.getOrElse("tab", "repositories") match {
// Public Activity
case "activity" => account.html.activity(x, getActivitiesByUser(userName, true))
// Repositories
case _ => account.html.repositories(x, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
}
} getOrElse NotFound } getOrElse NotFound
} }

View File

@@ -10,13 +10,15 @@ import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
class CreateRepositoryController extends CreateRepositoryControllerBase class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with UsersAuthenticator with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator
/** /**
* Creates new repository. * Creates new repository.
*/ */
trait CreateRepositoryControllerBase extends ControllerBase { trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with WikiService with LabelsService with UsersAuthenticator => self: RepositoryService with WikiService with LabelsService with ActivityService
with UsersAuthenticator =>
case class RepositoryCreationForm(name: String, description: Option[String]) case class RepositoryCreationForm(name: String, description: Option[String])
@@ -36,7 +38,8 @@ trait CreateRepositoryControllerBase extends ControllerBase {
* Create new repository. * Create new repository.
*/ */
post("/new", form)(usersOnly { form => post("/new", form)(usersOnly { form =>
val loginUserName = context.loginAccount.get.userName val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first // Insert to the database at first
createRepository(form.name, loginUserName, form.description) createRepository(form.name, loginUserName, form.description)
@@ -82,7 +85,10 @@ trait CreateRepositoryControllerBase extends ControllerBase {
} }
// Create Wiki repository // Create Wiki repository
createWikiRepository(context.loginAccount.get, form.name) createWikiRepository(loginAccount, form.name)
// Record activity
recordCreateRepository(loginUserName, form.name, loginUserName)
// redirect to the repository // redirect to the repository
redirect("/%s/%s".format(loginUserName, form.name)) redirect("/%s/%s".format(loginUserName, form.name))

View File

@@ -0,0 +1,21 @@
package model
import scala.slick.driver.H2Driver.simple._
object Activities extends Table[Activity]("ACTIVITY") with BasicTemplate with Functions {
def activityId = column[Int]("ACTIVITY_ID", O AutoInc)
def activityUserName = column[String]("ACTIVITY_USER_NAME")
def message = column[String]("MESSAGE")
def activityDate = column[java.util.Date]("ACTIVITY_DATE")
def * = activityId ~ userName ~ repositoryName ~ activityUserName ~ message ~ activityDate <> (Activity, Activity.unapply _)
def autoInc = userName ~ repositoryName ~ activityUserName ~ message ~ activityDate returning activityId
}
case class Activity(
activityId: Int,
userName: String,
repositoryName: String,
activityUserName: String,
message: String,
activityDate: java.util.Date
)

View File

@@ -0,0 +1,30 @@
package service
import model._
import Activities._
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
trait ActivityService {
def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] = {
val q = Query(Activities)
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
(if(isPublic){
q filter { case (t1, t2) => (t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind) }
} else {
q filter { case (t1, t2) => t1.activityUserName is activityUserName.bind }
})
.sortBy { case (t1, t2) => t1.activityId desc }
.map { case (t1, t2) => t1 }
.list
}
def recordCreateRepository(userName: String, repositoryName: String, activityUserName: String): Unit = {
Activities.autoInc insert(userName, repositoryName, activityUserName,
"[[%s]] created [[%s/%s]]".format(activityUserName, userName, repositoryName),
currentDate)
}
}

View File

@@ -49,6 +49,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
Version(1, 2),
Version(1, 1), Version(1, 1),
Version(1, 0) Version(1, 0)
) )

View File

@@ -33,6 +33,13 @@ object helpers {
Html(Markdown.toHtml(value, repository, enableWikiLink, enableCommitLink, enableIssueLink)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableCommitLink, enableIssueLink))
} }
def activityMessage(message: String)(implicit context: app.Context): Html = {
Html(message
.replaceAll("\\[\\[([^\\s]+?)/([^\\s]+?)\\]\\]", "<a href=\"%s/$1/$2\">$1/$2</a>".format(context.path))
.replaceAll("\\[\\[([^\\s]+?)\\]\\]", "<a href=\"%s/$1\">$1</a>".format(context.path))
)
}
/** /**
* Generates the url to the repository. * Generates the url to the repository.
*/ */
@@ -61,6 +68,9 @@ object helpers {
// convert commit id to link // convert commit id to link
.replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", "$1<a href=\"%s/%s/%s/commit/$2\">$2</a>$3").format(context.path, repository.owner, repository.name)) .replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", "$1<a href=\"%s/%s/%s/commit/$2\">$2</a>$3").format(context.path, repository.owner, repository.name))
/**
* Implicit conversion to add mkHtml() to Seq[Html].
*/
implicit def extendsHtmlSeq(seq: Seq[Html]) = new { implicit def extendsHtmlSeq(seq: Seq[Html]) = new {
def mkHtml(separator: String) = Html(seq.mkString(separator)) def mkHtml(separator: String) = Html(seq.mkString(separator))
def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString)) def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString))

View File

@@ -0,0 +1,31 @@
@(account: model.Account, activities: List[model.Activity])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="block-header">@account.userName</div>
</div>
<div class="block">
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
</div>
<div class="span8">
@tab(account, "activity")
@if(activities.isEmpty){
No activity
} else {
@activities.map { activity =>
<div class="block">
<div class="muted small">@datetime(activity.activityDate)</div>
<div>@activityMessage(activity.message)</div>
</div>
}
}
</div>
</div>
</div>
}

View File

@@ -14,19 +14,7 @@
</div> </div>
</div> </div>
<div class="span8"> <div class="span8">
<ul class="nav nav-tabs"> @tab(account, "repositories")
<li class="active"><a href="#">Repositories</a></li>
<!--
<li><a href="#">Activity</a></li>
-->
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>
@if(repositories.isEmpty){ @if(repositories.isEmpty){
No repositories No repositories
} else { } else {

View File

@@ -0,0 +1,14 @@
@(account: model.Account, active: String)(implicit context: app.Context)
@import context._
@import view.helpers._
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>