diff --git a/src/main/resources/update/1_2.sql b/src/main/resources/update/1_2.sql new file mode 100644 index 0000000..0496caf --- /dev/null +++ b/src/main/resources/update/1_2.sql @@ -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); diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala index 9fa0956..de5ed9c 100644 --- a/src/main/scala/app/AccountController.scala +++ b/src/main/scala/app/AccountController.scala @@ -6,10 +6,12 @@ import jp.sf.amateras.scalatra.forms._ 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 { - 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]) @@ -33,8 +35,13 @@ */ get("/:userName") { val userName = params("userName") - getAccountByUserName(userName).map { - account.html.info(_, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName))) + getAccountByUserName(userName).map { x => + 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 } diff --git a/src/main/scala/app/CreateRepositoryController.scala b/src/main/scala/app/CreateRepositoryController.scala index 55df75a..8d50a95 100644 --- a/src/main/scala/app/CreateRepositoryController.scala +++ b/src/main/scala/app/CreateRepositoryController.scala @@ -10,13 +10,15 @@ import jp.sf.amateras.scalatra.forms._ 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. */ 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]) @@ -36,7 +38,8 @@ * Create new repository. */ 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 createRepository(form.name, loginUserName, form.description) @@ -82,7 +85,10 @@ } // 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("/%s/%s".format(loginUserName, form.name)) diff --git a/src/main/scala/model/Activity.scala b/src/main/scala/model/Activity.scala new file mode 100644 index 0000000..8177c57 --- /dev/null +++ b/src/main/scala/model/Activity.scala @@ -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 +) diff --git a/src/main/scala/service/ActivityService.scala b/src/main/scala/service/ActivityService.scala new file mode 100644 index 0000000..2e9102f --- /dev/null +++ b/src/main/scala/service/ActivityService.scala @@ -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) + } + +} diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala index 5453c22..46e8364 100644 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ b/src/main/scala/servlet/AutoUpdateListener.scala @@ -49,8 +49,9 @@ * The history of versions. A head of this sequence is the current BitBucket version. */ val versions = Seq( - Version(1, 1), - Version(1, 0) + Version(1, 2), + Version(1, 1), + Version(1, 0) ) /** diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index 7f4ffdf..88c24d6 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -33,6 +33,13 @@ Html(Markdown.toHtml(value, repository, enableWikiLink, enableCommitLink, enableIssueLink)) } + def activityMessage(message: String)(implicit context: app.Context): Html = { + Html(message + .replaceAll("\\[\\[([^\\s]+?)/([^\\s]+?)\\]\\]", "$1/$2".format(context.path)) + .replaceAll("\\[\\[([^\\s]+?)\\]\\]", "$1".format(context.path)) + ) + } + /** * Generates the url to the repository. */ @@ -61,6 +68,9 @@ // convert commit id to link .replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", "$1$2$3").format(context.path, repository.owner, repository.name)) + /** + * Implicit conversion to add mkHtml() to Seq[Html]. + */ implicit def extendsHtmlSeq(seq: Seq[Html]) = new { def mkHtml(separator: String) = Html(seq.mkString(separator)) def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString)) diff --git a/src/main/twirl/account/activity.scala.html b/src/main/twirl/account/activity.scala.html new file mode 100644 index 0000000..5ef58a9 --- /dev/null +++ b/src/main/twirl/account/activity.scala.html @@ -0,0 +1,31 @@ +@(account: model.Account, activities: List[model.Activity])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main(account.userName){ +