diff --git a/src/main/scala/app/IndexController.scala b/src/main/scala/app/IndexController.scala index 84270b8..c3d1928 100644 --- a/src/main/scala/app/IndexController.scala +++ b/src/main/scala/app/IndexController.scala @@ -1,86 +1,86 @@ -package app - -import util._ -import util.Implicits._ -import service._ -import jp.sf.amateras.scalatra.forms._ - -class IndexController extends IndexControllerBase - with RepositoryService with ActivityService with AccountService with UsersAuthenticator - -trait IndexControllerBase extends ControllerBase { - self: RepositoryService with ActivityService with AccountService with UsersAuthenticator => - - case class SignInForm(userName: String, password: String) - - val form = mapping( - "userName" -> trim(label("Username", text(required))), - "password" -> trim(label("Password", text(required))) - )(SignInForm.apply) - - get("/"){ - val loginAccount = context.loginAccount - - html.index(getRecentActivities(), - getVisibleRepositories(loginAccount, context.baseUrl), - loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl) }.getOrElse(Nil) - ) - } - - get("/signin"){ - val redirect = params.get("redirect") - if(redirect.isDefined && redirect.get.startsWith("/")){ - flash += Keys.Flash.Redirect -> redirect.get - } - html.signin() - } - - post("/signin", form){ form => - authenticate(context.settings, form.userName, form.password) match { - case Some(account) => signin(account) - case None => redirect("/signin") - } - } - - get("/signout"){ - session.invalidate - redirect("/") - } - - get("/activities.atom"){ - contentType = "application/atom+xml; type=feed" - helper.xml.feed(getRecentActivities()) - } - - /** - * Set account information into HttpSession and redirect. - */ - private def signin(account: model.Account) = { - session.setAttribute(Keys.Session.LoginAccount, account) - updateLastLoginDate(account.userName) - - flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl => - if(redirectUrl.stripSuffix("/") == request.getContextPath){ - redirect("/") - } else { - redirect(redirectUrl) - } - }.getOrElse { - redirect("/") - } - } - - /** - * JSON API for collaborator completion. - * - * TODO Move to other controller? - */ - get("/_user/proposals")(usersOnly { - contentType = formats("json") - org.json4s.jackson.Serialization.write( - Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray) - ) - }) - - -} +package app + +import util._ +import util.Implicits._ +import service._ +import jp.sf.amateras.scalatra.forms._ + +class IndexController extends IndexControllerBase + with RepositoryService with ActivityService with AccountService with UsersAuthenticator + +trait IndexControllerBase extends ControllerBase { + self: RepositoryService with ActivityService with AccountService with UsersAuthenticator => + + case class SignInForm(userName: String, password: String) + + val form = mapping( + "userName" -> trim(label("Username", text(required))), + "password" -> trim(label("Password", text(required))) + )(SignInForm.apply) + + get("/"){ + val loginAccount = context.loginAccount + + html.index(getRecentActivities(), + getVisibleRepositories(loginAccount, context.baseUrl), + loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl) }.getOrElse(Nil) + ) + } + + get("/signin"){ + val redirect = params.get("redirect") + if(redirect.isDefined && redirect.get.startsWith("/")){ + flash += Keys.Flash.Redirect -> redirect.get + } + html.signin() + } + + post("/signin", form){ form => + authenticate(context.settings, form.userName, form.password) match { + case Some(account) => signin(account) + case None => redirect("/signin") + } + } + + get("/signout"){ + session.invalidate + redirect("/") + } + + get("/activities.atom"){ + contentType = "application/atom+xml; type=feed" + helper.xml.feed(getRecentActivities()) + } + + /** + * Set account information into HttpSession and redirect. + */ + private def signin(account: model.Account) = { + session.setAttribute(Keys.Session.LoginAccount, account) + updateLastLoginDate(account.userName) + + flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl => + if(redirectUrl.stripSuffix("/") == request.getContextPath){ + redirect("/") + } else { + redirect(redirectUrl) + } + }.getOrElse { + redirect("/") + } + } + + /** + * JSON API for collaborator completion. + * + * TODO Move to other controller? + */ + get("/_user/proposals")(usersOnly { + contentType = formats("json") + org.json4s.jackson.Serialization.write( + Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray) + ) + }) + + +} diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala index 1f2c35e..41450ea 100644 --- a/src/main/scala/app/RepositorySettingsController.scala +++ b/src/main/scala/app/RepositorySettingsController.scala @@ -1,267 +1,267 @@ -package app - -import service._ -import util.Directory._ -import util.ControlUtil._ -import util.Implicits._ -import util.{UsersAuthenticator, OwnerAuthenticator} -import util.JGitUtil.CommitInfo -import jp.sf.amateras.scalatra.forms._ -import org.apache.commons.io.FileUtils -import org.scalatra.i18n.Messages -import service.WebHookService.WebHookPayload -import org.eclipse.jgit.api.Git - -class RepositorySettingsController extends RepositorySettingsControllerBase - with RepositoryService with AccountService with WebHookService - with OwnerAuthenticator with UsersAuthenticator - -trait RepositorySettingsControllerBase extends ControllerBase { - self: RepositoryService with AccountService with WebHookService - with OwnerAuthenticator with UsersAuthenticator => - - // for repository options - case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean) - - val optionsForm = mapping( - "repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))), - "description" -> trim(label("Description" , optional(text()))), - "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), - "isPrivate" -> trim(label("Repository Type", boolean())) - )(OptionsForm.apply) - - // for collaborator addition - case class CollaboratorForm(userName: String) - - val collaboratorForm = mapping( - "userName" -> trim(label("Username", text(required, collaborator))) - )(CollaboratorForm.apply) - - // for web hook url addition - case class WebHookForm(url: String) - - val webHookForm = mapping( - "url" -> trim(label("url", text(required, webHook))) - )(WebHookForm.apply) - - // for transfer ownership - case class TransferOwnerShipForm(newOwner: String) - - val transferForm = mapping( - "newOwner" -> trim(label("New owner", text(required, transferUser))) - )(TransferOwnerShipForm.apply) - - /** - * Redirect to the Options page. - */ - get("/:owner/:repository/settings")(ownerOnly { repository => - redirect(s"/${repository.owner}/${repository.name}/settings/options") - }) - - /** - * Display the Options page. - */ - get("/:owner/:repository/settings/options")(ownerOnly { - settings.html.options(_, flash.get("info")) - }) - - /** - * Save the repository options. - */ - post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => - saveRepositoryOptions( - repository.owner, - repository.name, - form.description, - if(repository.branchList.isEmpty) "master" else form.defaultBranch, - repository.repository.parentUserName.map { _ => - repository.repository.isPrivate - } getOrElse form.isPrivate - ) - // Change repository name - if(repository.name != form.repositoryName){ - // Update database - renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) - // Move git repository - defining(getRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName)) - } - // Move wiki repository - defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) - } - } - flash += "info" -> "Repository settings has been updated." - redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") - }) - - /** - * Display the Collaborators page. - */ - get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => - settings.html.collaborators( - getCollaborators(repository.owner, repository.name), - getAccountByUserName(repository.owner).get.isGroupAccount, - repository) - }) - - /** - * Add the collaborator. - */ - post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => - if(!getAccountByUserName(repository.owner).get.isGroupAccount){ - addCollaborator(repository.owner, repository.name, form.userName) - } - redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") - }) - - /** - * Add the collaborator. - */ - get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => - if(!getAccountByUserName(repository.owner).get.isGroupAccount){ - removeCollaborator(repository.owner, repository.name, params("name")) - } - redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") - }) - - /** - * Display the web hook page. - */ - get("/:owner/:repository/settings/hooks")(ownerOnly { repository => - settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info")) - }) - - /** - * Add the web hook URL. - */ - post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) => - addWebHookURL(repository.owner, repository.name, form.url) - redirect(s"/${repository.owner}/${repository.name}/settings/hooks") - }) - - /** - * Delete the web hook URL. - */ - get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository => - deleteWebHookURL(repository.owner, repository.name, params("url")) - redirect(s"/${repository.owner}/${repository.name}/settings/hooks") - }) - - /** - * Send the test request to registered web hook URLs. - */ - get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - import scala.collection.JavaConverters._ - val commits = git.log - .add(git.getRepository.resolve(repository.repository.defaultBranch)) - .setMaxCount(3) - .call.iterator.asScala.map(new CommitInfo(_)) - - getWebHookURLs(repository.owner, repository.name) match { - case webHookURLs if(webHookURLs.nonEmpty) => - for(ownerAccount <- getAccountByUserName(repository.owner)){ - callWebHook(repository.owner, repository.name, webHookURLs, - WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)) - } - case _ => - } - - flash += "info" -> "Test payload deployed!" - } - redirect(s"/${repository.owner}/${repository.name}/settings/hooks") - }) - - /** - * Display the danger zone. - */ - get("/:owner/:repository/settings/danger")(ownerOnly { - settings.html.danger(_) - }) - - /** - * Transfer repository ownership. - */ - post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => - // Change repository owner - if(repository.owner != form.newOwner){ - // Update database - renameRepository(repository.owner, repository.name, form.newOwner, repository.name) - // Move git repository - defining(getRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name)) - } - // Move wiki repository - defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) - } - } - redirect(s"/${form.newOwner}/${repository.name}") - }) - - /** - * Delete the repository. - */ - post("/:owner/:repository/settings/delete")(ownerOnly { repository => - deleteRepository(repository.owner, repository.name) - - FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name)) - FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name)) - FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name)) - - redirect(s"/${repository.owner}") - }) - - /** - * Provides duplication check for web hook url. - */ - private def webHook: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.") - } - - /** - * Provides Constraint to validate the collaborator name. - */ - private def collaborator: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - getAccountByUserName(value) match { - case None => Some("User does not exist.") - case Some(x) if(x.isGroupAccount) - => Some("User does not exist.") - case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) - => Some("User can access this repository already.") - case _ => None - } - } - - /** - * Duplicate check for the rename repository name. - */ - private def renameRepositoryName: Constraint = new Constraint(){ - override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = - params.get("repository").filter(_ != value).flatMap { _ => - params.get("owner").flatMap { userName => - getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") - } - } - } - - /** - * Provides Constraint to validate the repository transfer user. - */ - private def transferUser: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - getAccountByUserName(value) match { - case None => Some("User does not exist.") - case Some(x) => if(x.userName == params("owner")){ - Some("This is current repository owner.") - } else { - params.get("repository").flatMap { repositoryName => - getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." } - } - } - } - } +package app + +import service._ +import util.Directory._ +import util.ControlUtil._ +import util.Implicits._ +import util.{UsersAuthenticator, OwnerAuthenticator} +import util.JGitUtil.CommitInfo +import jp.sf.amateras.scalatra.forms._ +import org.apache.commons.io.FileUtils +import org.scalatra.i18n.Messages +import service.WebHookService.WebHookPayload +import org.eclipse.jgit.api.Git + +class RepositorySettingsController extends RepositorySettingsControllerBase + with RepositoryService with AccountService with WebHookService + with OwnerAuthenticator with UsersAuthenticator + +trait RepositorySettingsControllerBase extends ControllerBase { + self: RepositoryService with AccountService with WebHookService + with OwnerAuthenticator with UsersAuthenticator => + + // for repository options + case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean) + + val optionsForm = mapping( + "repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))), + "description" -> trim(label("Description" , optional(text()))), + "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), + "isPrivate" -> trim(label("Repository Type", boolean())) + )(OptionsForm.apply) + + // for collaborator addition + case class CollaboratorForm(userName: String) + + val collaboratorForm = mapping( + "userName" -> trim(label("Username", text(required, collaborator))) + )(CollaboratorForm.apply) + + // for web hook url addition + case class WebHookForm(url: String) + + val webHookForm = mapping( + "url" -> trim(label("url", text(required, webHook))) + )(WebHookForm.apply) + + // for transfer ownership + case class TransferOwnerShipForm(newOwner: String) + + val transferForm = mapping( + "newOwner" -> trim(label("New owner", text(required, transferUser))) + )(TransferOwnerShipForm.apply) + + /** + * Redirect to the Options page. + */ + get("/:owner/:repository/settings")(ownerOnly { repository => + redirect(s"/${repository.owner}/${repository.name}/settings/options") + }) + + /** + * Display the Options page. + */ + get("/:owner/:repository/settings/options")(ownerOnly { + settings.html.options(_, flash.get("info")) + }) + + /** + * Save the repository options. + */ + post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => + saveRepositoryOptions( + repository.owner, + repository.name, + form.description, + if(repository.branchList.isEmpty) "master" else form.defaultBranch, + repository.repository.parentUserName.map { _ => + repository.repository.isPrivate + } getOrElse form.isPrivate + ) + // Change repository name + if(repository.name != form.repositoryName){ + // Update database + renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) + // Move git repository + defining(getRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName)) + } + // Move wiki repository + defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) + } + } + flash += "info" -> "Repository settings has been updated." + redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") + }) + + /** + * Display the Collaborators page. + */ + get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => + settings.html.collaborators( + getCollaborators(repository.owner, repository.name), + getAccountByUserName(repository.owner).get.isGroupAccount, + repository) + }) + + /** + * Add the collaborator. + */ + post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => + if(!getAccountByUserName(repository.owner).get.isGroupAccount){ + addCollaborator(repository.owner, repository.name, form.userName) + } + redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") + }) + + /** + * Add the collaborator. + */ + get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => + if(!getAccountByUserName(repository.owner).get.isGroupAccount){ + removeCollaborator(repository.owner, repository.name, params("name")) + } + redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") + }) + + /** + * Display the web hook page. + */ + get("/:owner/:repository/settings/hooks")(ownerOnly { repository => + settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info")) + }) + + /** + * Add the web hook URL. + */ + post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) => + addWebHookURL(repository.owner, repository.name, form.url) + redirect(s"/${repository.owner}/${repository.name}/settings/hooks") + }) + + /** + * Delete the web hook URL. + */ + get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository => + deleteWebHookURL(repository.owner, repository.name, params("url")) + redirect(s"/${repository.owner}/${repository.name}/settings/hooks") + }) + + /** + * Send the test request to registered web hook URLs. + */ + get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + import scala.collection.JavaConverters._ + val commits = git.log + .add(git.getRepository.resolve(repository.repository.defaultBranch)) + .setMaxCount(3) + .call.iterator.asScala.map(new CommitInfo(_)) + + getWebHookURLs(repository.owner, repository.name) match { + case webHookURLs if(webHookURLs.nonEmpty) => + for(ownerAccount <- getAccountByUserName(repository.owner)){ + callWebHook(repository.owner, repository.name, webHookURLs, + WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)) + } + case _ => + } + + flash += "info" -> "Test payload deployed!" + } + redirect(s"/${repository.owner}/${repository.name}/settings/hooks") + }) + + /** + * Display the danger zone. + */ + get("/:owner/:repository/settings/danger")(ownerOnly { + settings.html.danger(_) + }) + + /** + * Transfer repository ownership. + */ + post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => + // Change repository owner + if(repository.owner != form.newOwner){ + // Update database + renameRepository(repository.owner, repository.name, form.newOwner, repository.name) + // Move git repository + defining(getRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name)) + } + // Move wiki repository + defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) + } + } + redirect(s"/${form.newOwner}/${repository.name}") + }) + + /** + * Delete the repository. + */ + post("/:owner/:repository/settings/delete")(ownerOnly { repository => + deleteRepository(repository.owner, repository.name) + + FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name)) + FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name)) + FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name)) + + redirect(s"/${repository.owner}") + }) + + /** + * Provides duplication check for web hook url. + */ + private def webHook: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.") + } + + /** + * Provides Constraint to validate the collaborator name. + */ + private def collaborator: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + getAccountByUserName(value) match { + case None => Some("User does not exist.") + case Some(x) if(x.isGroupAccount) + => Some("User does not exist.") + case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) + => Some("User can access this repository already.") + case _ => None + } + } + + /** + * Duplicate check for the rename repository name. + */ + private def renameRepositoryName: Constraint = new Constraint(){ + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = + params.get("repository").filter(_ != value).flatMap { _ => + params.get("owner").flatMap { userName => + getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") + } + } + } + + /** + * Provides Constraint to validate the repository transfer user. + */ + private def transferUser: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + getAccountByUserName(value) match { + case None => Some("User does not exist.") + case Some(x) => if(x.userName == params("owner")){ + Some("This is current repository owner.") + } else { + params.get("repository").flatMap { repositoryName => + getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." } + } + } + } + } } \ No newline at end of file diff --git a/src/main/scala/model/Account.scala b/src/main/scala/model/Account.scala index 287bc41..7880f3e 100644 --- a/src/main/scala/model/Account.scala +++ b/src/main/scala/model/Account.scala @@ -1,39 +1,39 @@ -package model - -trait AccountComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val Accounts = TableQuery[Accounts] - - class Accounts(tag: Tag) extends Table[Account](tag, "ACCOUNT") { - val userName = column[String]("USER_NAME", O PrimaryKey) - val fullName = column[String]("FULL_NAME") - val mailAddress = column[String]("MAIL_ADDRESS") - val password = column[String]("PASSWORD") - val isAdmin = column[Boolean]("ADMINISTRATOR") - val url = column[String]("URL") - val registeredDate = column[java.util.Date]("REGISTERED_DATE") - val updatedDate = column[java.util.Date]("UPDATED_DATE") - val lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE") - val image = column[String]("IMAGE") - val groupAccount = column[Boolean]("GROUP_ACCOUNT") - val removed = column[Boolean]("REMOVED") - def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply) - } - - case class Account( - userName: String, - fullName: String, - mailAddress: String, - password: String, - isAdmin: Boolean, - url: Option[String], - registeredDate: java.util.Date, - updatedDate: java.util.Date, - lastLoginDate: Option[java.util.Date], - image: Option[String], - isGroupAccount: Boolean, - isRemoved: Boolean - ) -} +package model + +trait AccountComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val Accounts = TableQuery[Accounts] + + class Accounts(tag: Tag) extends Table[Account](tag, "ACCOUNT") { + val userName = column[String]("USER_NAME", O PrimaryKey) + val fullName = column[String]("FULL_NAME") + val mailAddress = column[String]("MAIL_ADDRESS") + val password = column[String]("PASSWORD") + val isAdmin = column[Boolean]("ADMINISTRATOR") + val url = column[String]("URL") + val registeredDate = column[java.util.Date]("REGISTERED_DATE") + val updatedDate = column[java.util.Date]("UPDATED_DATE") + val lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE") + val image = column[String]("IMAGE") + val groupAccount = column[Boolean]("GROUP_ACCOUNT") + val removed = column[Boolean]("REMOVED") + def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply) + } + + case class Account( + userName: String, + fullName: String, + mailAddress: String, + password: String, + isAdmin: Boolean, + url: Option[String], + registeredDate: java.util.Date, + updatedDate: java.util.Date, + lastLoginDate: Option[java.util.Date], + image: Option[String], + isGroupAccount: Boolean, + isRemoved: Boolean + ) +} diff --git a/src/main/scala/model/BasicTemplate.scala b/src/main/scala/model/BasicTemplate.scala index d6800da..e0460a9 100644 --- a/src/main/scala/model/BasicTemplate.scala +++ b/src/main/scala/model/BasicTemplate.scala @@ -1,47 +1,47 @@ -package model - -protected[model] trait TemplateComponent { self: Profile => - import profile.simple._ - - trait BasicTemplate { self: Table[_] => - val userName = column[String]("USER_NAME") - val repositoryName = column[String]("REPOSITORY_NAME") - - def byRepository(owner: String, repository: String) = - (userName is owner.bind) && (repositoryName is repository.bind) - - def byRepository(userName: Column[String], repositoryName: Column[String]) = - (this.userName is userName) && (this.repositoryName is repositoryName) - } - - trait IssueTemplate extends BasicTemplate { self: Table[_] => - val issueId = column[Int]("ISSUE_ID") - - def byIssue(owner: String, repository: String, issueId: Int) = - byRepository(owner, repository) && (this.issueId is issueId.bind) - - def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = - byRepository(userName, repositoryName) && (this.issueId is issueId) - } - - trait LabelTemplate extends BasicTemplate { self: Table[_] => - val labelId = column[Int]("LABEL_ID") - - def byLabel(owner: String, repository: String, labelId: Int) = - byRepository(owner, repository) && (this.labelId is labelId.bind) - - def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = - byRepository(userName, repositoryName) && (this.labelId is labelId) - } - - trait MilestoneTemplate extends BasicTemplate { self: Table[_] => - val milestoneId = column[Int]("MILESTONE_ID") - - def byMilestone(owner: String, repository: String, milestoneId: Int) = - byRepository(owner, repository) && (this.milestoneId is milestoneId.bind) - - def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = - byRepository(userName, repositoryName) && (this.milestoneId is milestoneId) - } - -} +package model + +protected[model] trait TemplateComponent { self: Profile => + import profile.simple._ + + trait BasicTemplate { self: Table[_] => + val userName = column[String]("USER_NAME") + val repositoryName = column[String]("REPOSITORY_NAME") + + def byRepository(owner: String, repository: String) = + (userName is owner.bind) && (repositoryName is repository.bind) + + def byRepository(userName: Column[String], repositoryName: Column[String]) = + (this.userName is userName) && (this.repositoryName is repositoryName) + } + + trait IssueTemplate extends BasicTemplate { self: Table[_] => + val issueId = column[Int]("ISSUE_ID") + + def byIssue(owner: String, repository: String, issueId: Int) = + byRepository(owner, repository) && (this.issueId is issueId.bind) + + def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = + byRepository(userName, repositoryName) && (this.issueId is issueId) + } + + trait LabelTemplate extends BasicTemplate { self: Table[_] => + val labelId = column[Int]("LABEL_ID") + + def byLabel(owner: String, repository: String, labelId: Int) = + byRepository(owner, repository) && (this.labelId is labelId.bind) + + def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = + byRepository(userName, repositoryName) && (this.labelId is labelId) + } + + trait MilestoneTemplate extends BasicTemplate { self: Table[_] => + val milestoneId = column[Int]("MILESTONE_ID") + + def byMilestone(owner: String, repository: String, milestoneId: Int) = + byRepository(owner, repository) && (this.milestoneId is milestoneId.bind) + + def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = + byRepository(userName, repositoryName) && (this.milestoneId is milestoneId) + } + +} diff --git a/src/main/scala/model/Issue.scala b/src/main/scala/model/Issue.scala index bc594e3..d18c098 100644 --- a/src/main/scala/model/Issue.scala +++ b/src/main/scala/model/Issue.scala @@ -1,48 +1,48 @@ -package model - -trait IssueComponent extends TemplateComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val IssueId = TableQuery[IssueId] - lazy val IssueOutline = TableQuery[IssueOutline] - lazy val Issues = TableQuery[Issues] - - class IssueId(tag: Tag) extends Table[(String, String, Int)](tag, "ISSUE_ID") with IssueTemplate { - def * = (userName, repositoryName, issueId) - def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) - } - - class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate { - val commentCount = column[Int]("COMMENT_COUNT") - def * = (userName, repositoryName, issueId, commentCount) - } - - class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate { - val openedUserName = column[String]("OPENED_USER_NAME") - val assignedUserName = column[String]("ASSIGNED_USER_NAME") - val title = column[String]("TITLE") - val content = column[String]("CONTENT") - val closed = column[Boolean]("CLOSED") - val registeredDate = column[java.util.Date]("REGISTERED_DATE") - val updatedDate = column[java.util.Date]("UPDATED_DATE") - val pullRequest = column[Boolean]("PULL_REQUEST") - def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply) - - def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) - } - - case class Issue( - userName: String, - repositoryName: String, - issueId: Int, - openedUserName: String, - milestoneId: Option[Int], - assignedUserName: Option[String], - title: String, - content: Option[String], - closed: Boolean, - registeredDate: java.util.Date, - updatedDate: java.util.Date, - isPullRequest: Boolean) -} +package model + +trait IssueComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val IssueId = TableQuery[IssueId] + lazy val IssueOutline = TableQuery[IssueOutline] + lazy val Issues = TableQuery[Issues] + + class IssueId(tag: Tag) extends Table[(String, String, Int)](tag, "ISSUE_ID") with IssueTemplate { + def * = (userName, repositoryName, issueId) + def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) + } + + class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate { + val commentCount = column[Int]("COMMENT_COUNT") + def * = (userName, repositoryName, issueId, commentCount) + } + + class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate { + val openedUserName = column[String]("OPENED_USER_NAME") + val assignedUserName = column[String]("ASSIGNED_USER_NAME") + val title = column[String]("TITLE") + val content = column[String]("CONTENT") + val closed = column[Boolean]("CLOSED") + val registeredDate = column[java.util.Date]("REGISTERED_DATE") + val updatedDate = column[java.util.Date]("UPDATED_DATE") + val pullRequest = column[Boolean]("PULL_REQUEST") + def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply) + + def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) + } + + case class Issue( + userName: String, + repositoryName: String, + issueId: Int, + openedUserName: String, + milestoneId: Option[Int], + assignedUserName: Option[String], + title: String, + content: Option[String], + closed: Boolean, + registeredDate: java.util.Date, + updatedDate: java.util.Date, + isPullRequest: Boolean) +} diff --git a/src/main/scala/model/IssueComment.scala b/src/main/scala/model/IssueComment.scala index ea02285..8d42702 100644 --- a/src/main/scala/model/IssueComment.scala +++ b/src/main/scala/model/IssueComment.scala @@ -1,34 +1,34 @@ -package model - -trait IssueCommentComponent extends TemplateComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){ - def autoInc = this returning this.map(_.commentId) - } - - class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate { - val commentId = column[Int]("COMMENT_ID", O AutoInc) - val action = column[String]("ACTION") - val commentedUserName = column[String]("COMMENTED_USER_NAME") - val content = column[String]("CONTENT") - val registeredDate = column[java.util.Date]("REGISTERED_DATE") - val updatedDate = column[java.util.Date]("UPDATED_DATE") - def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply) - - def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind - } - - case class IssueComment( - userName: String, - repositoryName: String, - issueId: Int, - commentId: Int = 0, - action: String, - commentedUserName: String, - content: String, - registeredDate: java.util.Date, - updatedDate: java.util.Date - ) -} +package model + +trait IssueCommentComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){ + def autoInc = this returning this.map(_.commentId) + } + + class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate { + val commentId = column[Int]("COMMENT_ID", O AutoInc) + val action = column[String]("ACTION") + val commentedUserName = column[String]("COMMENTED_USER_NAME") + val content = column[String]("CONTENT") + val registeredDate = column[java.util.Date]("REGISTERED_DATE") + val updatedDate = column[java.util.Date]("UPDATED_DATE") + def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply) + + def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind + } + + case class IssueComment( + userName: String, + repositoryName: String, + issueId: Int, + commentId: Int = 0, + action: String, + commentedUserName: String, + content: String, + registeredDate: java.util.Date, + updatedDate: java.util.Date + ) +} diff --git a/src/main/scala/model/package.scala b/src/main/scala/model/package.scala index 36a3504..6c70c66 100644 --- a/src/main/scala/model/package.scala +++ b/src/main/scala/model/package.scala @@ -1,22 +1,22 @@ -package object model extends { - // TODO - val profile = slick.driver.H2Driver - -} with AccountComponent - with ActivityComponent - with CollaboratorComponent - with GroupMemberComponent - with IssueComponent - with IssueCommentComponent - with IssueLabelComponent - with LabelComponent - with MilestoneComponent - with PullRequestComponent - with RepositoryComponent - with SshKeyComponent - with WebHookComponent with Profile { - /** - * Returns system date. - */ - def currentDate = new java.util.Date() -} +package object model extends { + // TODO + val profile = slick.driver.H2Driver + +} with AccountComponent + with ActivityComponent + with CollaboratorComponent + with GroupMemberComponent + with IssueComponent + with IssueCommentComponent + with IssueLabelComponent + with LabelComponent + with MilestoneComponent + with PullRequestComponent + with RepositoryComponent + with SshKeyComponent + with WebHookComponent with Profile { + /** + * Returns system date. + */ + def currentDate = new java.util.Date() +} diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala index 0e6f7d5..6f421be 100644 --- a/src/main/scala/service/IssuesService.scala +++ b/src/main/scala/service/IssuesService.scala @@ -1,385 +1,385 @@ -package service - -import scala.slick.jdbc.{StaticQuery => Q} -import Q.interpolation - -import model._ -import profile.simple._ -import util.Implicits._ -import util.StringUtil._ - -trait IssuesService { - import IssuesService._ - - def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) = - if (issueId forall (_.isDigit)) - Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption - else None - - def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) = - IssueComments filter (_.byIssue(owner, repository, issueId)) list - - def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) = - if (commentId forall (_.isDigit)) - IssueComments filter { t => - t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) - } firstOption - else None - - def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session) = - IssueLabels - .innerJoin(Labels).on { (t1, t2) => - t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) - } - .filter ( _._1.byIssue(owner, repository, issueId) ) - .map ( _._2 ) - .list - - def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = - IssueLabels filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption - - /** - * Returns the count of the search result against issues. - * - * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) - * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. - * @param repos Tuple of the repository owner and the repository name - * @return the count of the search result - */ - def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, - repos: (String, String)*)(implicit s: Session): Int = - // TODO check SQL - Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first - /** - * Returns the Map which contains issue count for each labels. - * - * @param owner the repository owner - * @param repository the repository name - * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) - * @return the Map which contains issue count for each labels (key is label name, value is issue count) - */ - def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, - filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = { - - searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false) - .innerJoin(IssueLabels).on { (t1, t2) => - t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) - } - .innerJoin(Labels).on { case ((t1, t2), t3) => - t2.byLabel(t3.userName, t3.repositoryName, t3.labelId) - } - .groupBy { case ((t1, t2), t3) => - t3.labelName - } - .map { case (labelName, t) => - labelName -> t.length - } - .toMap - } - /** - * Returns list which contains issue count for each repository. - * If the issue does not exist, its repository is not included in the result. - * - * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) - * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. - * @param repos Tuple of the repository owner and the repository name - * @return list which contains issue count for each repository - */ - def countIssueGroupByRepository( - condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, - repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = { - searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) - .groupBy { t => - t.userName -> t.repositoryName - } - .map { case (repo, t) => - (repo._1, repo._2, t.length) - } - .sortBy(_._3 desc) - .list - } - - /** - * Returns the search result against issues. - * - * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name) - * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. - * @param offset the offset for pagination - * @param limit the limit for pagination - * @param repos Tuple of the repository owner and the repository name - * @return the search result (list of tuples which contain issue, labels and comment count) - */ - def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, - offset: Int, limit: Int, repos: (String, String)*) - (implicit s: Session): List[(Issue, List[Label], Int)] = { - - // get issues and comment count and labels - searchIssueQuery(repos, condition, filterUser, onlyPullRequest) - .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } - .sortBy { case (t1, t2) => - (condition.sort match { - case "created" => t1.registeredDate - case "comments" => t2.commentCount - case "updated" => t1.updatedDate - }) match { - case sort => condition.direction match { - case "asc" => sort asc - case "desc" => sort desc - } - } - } - .drop(offset).take(limit) - .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } - .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } - .map { case (((t1, t2), t3), t4) => - (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) - } - .list - .splitWith { (c1, c2) => - c1._1.userName == c2._1.userName && - c1._1.repositoryName == c2._1.repositoryName && - c1._1.issueId == c2._1.issueId - } - .map { issues => issues.head match { - case (issue, commentCount, _,_,_) => - (issue, - issues.flatMap { t => t._3.map ( - Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) - )} toList, - commentCount) - }} toList - } - - /** - * Assembles query for conditional issue searching. - */ - private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, - filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) = - Issues filter { t1 => - condition.repo - .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } - .getOrElse (repos) - .map { case (owner, repository) => t1.byRepository(owner, repository) } - .foldLeft[Column[Boolean]](false) ( _ || _ ) && - (t1.closed is (condition.state == "closed").bind) && - (t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && - (t1.milestoneId isNull, condition.milestoneId == Some(None)) && - (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) && - (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) && - (t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && - (t1.pullRequest is true.bind, onlyPullRequest) && - (IssueLabels filter { t2 => - (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && - (t2.labelId in - (Labels filter { t3 => - (t3.byRepository(t1.userName, t1.repositoryName)) && - (t3.labelName inSetBind condition.labels) - } map(_.labelId))) - } exists, condition.labels.nonEmpty) - } - - def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], - assignedUserName: Option[String], milestoneId: Option[Int], - isPullRequest: Boolean = false)(implicit s: Session) = - // next id number - sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] - .firstOption.filter { id => - Issues insert Issue( - owner, - repository, - id, - loginUser, - milestoneId, - assignedUserName, - title, - content, - false, - currentDate, - currentDate, - isPullRequest) - - // increment issue id - IssueId - .filter (_.byPrimaryKey(owner, repository)) - .map (_.issueId) - .update (id) > 0 - } get - - def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = - IssueLabels insert IssueLabel(owner, repository, issueId, labelId) - - def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = - IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete - - def createComment(owner: String, repository: String, loginUser: String, - issueId: Int, content: String, action: String)(implicit s: Session): Int = - IssueComments.autoInc insert IssueComment( - userName = owner, - repositoryName = repository, - issueId = issueId, - action = action, - commentedUserName = loginUser, - content = content, - registeredDate = currentDate, - updatedDate = currentDate) - - def updateIssue(owner: String, repository: String, issueId: Int, - title: String, content: Option[String])(implicit s: Session) = - Issues - .filter (_.byPrimaryKey(owner, repository, issueId)) - .map { t => - (t.title, t.content.?, t.updatedDate) - } - .update (title, content, currentDate) - - def updateAssignedUserName(owner: String, repository: String, issueId: Int, - assignedUserName: Option[String])(implicit s: Session) = - Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName) - - def updateMilestoneId(owner: String, repository: String, issueId: Int, - milestoneId: Option[Int])(implicit s: Session) = - Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId) - - def updateComment(commentId: Int, content: String)(implicit s: Session) = - IssueComments - .filter (_.byPrimaryKey(commentId)) - .map { t => - t.content -> t.updatedDate - } - .update (content, currentDate) - - def deleteComment(commentId: Int)(implicit s: Session) = - IssueComments filter (_.byPrimaryKey(commentId)) delete - - def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session) = - Issues - .filter (_.byPrimaryKey(owner, repository, issueId)) - .map { t => - t.closed -> t.updatedDate - } - .update (closed, currentDate) - - /** - * Search issues by keyword. - * - * @param owner the repository owner - * @param repository the repository name - * @param query the keywords separated by whitespace. - * @return issues with comment count and matched content of issue or comment - */ - def searchIssuesByKeyword(owner: String, repository: String, query: String) - (implicit s: Session): List[(Issue, Int, String)] = { - import slick.driver.JdbcDriver.likeEncode - val keywords = splitWords(query.toLowerCase) - - // Search Issue - val issues = Issues - .innerJoin(IssueOutline).on { case (t1, t2) => - t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) - } - .filter { case (t1, t2) => - keywords.map { keyword => - (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || - (t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) - } .reduceLeft(_ && _) - } - .map { case (t1, t2) => - (t1, 0, t1.content.?, t2.commentCount) - } - - // Search IssueComment - val comments = IssueComments - .innerJoin(Issues).on { case (t1, t2) => - t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) - } - .innerJoin(IssueOutline).on { case ((t1, t2), t3) => - t2.byIssue(t3.userName, t3.repositoryName, t3.issueId) - } - .filter { case ((t1, t2), t3) => - keywords.map { query => - t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^') - }.reduceLeft(_ && _) - } - .map { case ((t1, t2), t3) => - (t2, t1.commentId, t1.content.?, t3.commentCount) - } - - issues.union(comments).sortBy { case (issue, commentId, _, _) => - issue.issueId -> commentId - }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) => - issue1.issueId == issue2.issueId - }.map { _.head match { - case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse("")) - } - }.toList - } - - def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String)(implicit s: Session) = { - extractCloseId(message).foreach { issueId => - for(issue <- getIssue(owner, repository, issueId) if !issue.closed){ - createComment(owner, repository, userName, issue.issueId, "Close", "close") - updateClosed(owner, repository, issue.issueId, true) - } - } - } -} - -object IssuesService { - import javax.servlet.http.HttpServletRequest - - val IssueLimit = 30 - - case class IssueSearchCondition( - labels: Set[String] = Set.empty, - milestoneId: Option[Option[Int]] = None, - repo: Option[String] = None, - state: String = "open", - sort: String = "created", - direction: String = "desc"){ - - def toURL: String = - "?" + List( - if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))), - milestoneId.map { id => "milestone=" + (id match { - case Some(x) => x.toString - case None => "none" - })}, - repo.map("for=" + urlEncode(_)), - Some("state=" + urlEncode(state)), - Some("sort=" + urlEncode(sort)), - Some("direction=" + urlEncode(direction))).flatten.mkString("&") - - } - - object IssueSearchCondition { - - private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = { - val value = request.getParameter(name) - if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) - } - - def apply(request: HttpServletRequest): IssueSearchCondition = - IssueSearchCondition( - param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), - param(request, "milestone").map{ - case "none" => None - case x => x.toIntOpt - }, - param(request, "for"), - param(request, "state", Seq("open", "closed")).getOrElse("open"), - param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), - param(request, "direction", Seq("asc", "desc")).getOrElse("desc")) - - def page(request: HttpServletRequest) = try { - val i = param(request, "page").getOrElse("1").toInt - if(i <= 0) 1 else i - } catch { - case e: NumberFormatException => 1 - } - } - -} +package service + +import scala.slick.jdbc.{StaticQuery => Q} +import Q.interpolation + +import model._ +import profile.simple._ +import util.Implicits._ +import util.StringUtil._ + +trait IssuesService { + import IssuesService._ + + def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) = + if (issueId forall (_.isDigit)) + Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption + else None + + def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) = + IssueComments filter (_.byIssue(owner, repository, issueId)) list + + def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) = + if (commentId forall (_.isDigit)) + IssueComments filter { t => + t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) + } firstOption + else None + + def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session) = + IssueLabels + .innerJoin(Labels).on { (t1, t2) => + t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) + } + .filter ( _._1.byIssue(owner, repository, issueId) ) + .map ( _._2 ) + .list + + def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = + IssueLabels filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption + + /** + * Returns the count of the search result against issues. + * + * @param condition the search condition + * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) + * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. + * @param repos Tuple of the repository owner and the repository name + * @return the count of the search result + */ + def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, + repos: (String, String)*)(implicit s: Session): Int = + // TODO check SQL + Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first + /** + * Returns the Map which contains issue count for each labels. + * + * @param owner the repository owner + * @param repository the repository name + * @param condition the search condition + * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) + * @return the Map which contains issue count for each labels (key is label name, value is issue count) + */ + def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, + filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = { + + searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false) + .innerJoin(IssueLabels).on { (t1, t2) => + t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) + } + .innerJoin(Labels).on { case ((t1, t2), t3) => + t2.byLabel(t3.userName, t3.repositoryName, t3.labelId) + } + .groupBy { case ((t1, t2), t3) => + t3.labelName + } + .map { case (labelName, t) => + labelName -> t.length + } + .toMap + } + /** + * Returns list which contains issue count for each repository. + * If the issue does not exist, its repository is not included in the result. + * + * @param condition the search condition + * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) + * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. + * @param repos Tuple of the repository owner and the repository name + * @return list which contains issue count for each repository + */ + def countIssueGroupByRepository( + condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, + repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = { + searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) + .groupBy { t => + t.userName -> t.repositoryName + } + .map { case (repo, t) => + (repo._1, repo._2, t.length) + } + .sortBy(_._3 desc) + .list + } + + /** + * Returns the search result against issues. + * + * @param condition the search condition + * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name) + * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. + * @param offset the offset for pagination + * @param limit the limit for pagination + * @param repos Tuple of the repository owner and the repository name + * @return the search result (list of tuples which contain issue, labels and comment count) + */ + def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, + offset: Int, limit: Int, repos: (String, String)*) + (implicit s: Session): List[(Issue, List[Label], Int)] = { + + // get issues and comment count and labels + searchIssueQuery(repos, condition, filterUser, onlyPullRequest) + .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } + .sortBy { case (t1, t2) => + (condition.sort match { + case "created" => t1.registeredDate + case "comments" => t2.commentCount + case "updated" => t1.updatedDate + }) match { + case sort => condition.direction match { + case "asc" => sort asc + case "desc" => sort desc + } + } + } + .drop(offset).take(limit) + .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } + .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } + .map { case (((t1, t2), t3), t4) => + (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) + } + .list + .splitWith { (c1, c2) => + c1._1.userName == c2._1.userName && + c1._1.repositoryName == c2._1.repositoryName && + c1._1.issueId == c2._1.issueId + } + .map { issues => issues.head match { + case (issue, commentCount, _,_,_) => + (issue, + issues.flatMap { t => t._3.map ( + Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) + )} toList, + commentCount) + }} toList + } + + /** + * Assembles query for conditional issue searching. + */ + private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, + filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) = + Issues filter { t1 => + condition.repo + .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } + .getOrElse (repos) + .map { case (owner, repository) => t1.byRepository(owner, repository) } + .foldLeft[Column[Boolean]](false) ( _ || _ ) && + (t1.closed is (condition.state == "closed").bind) && + (t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && + (t1.milestoneId isNull, condition.milestoneId == Some(None)) && + (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) && + (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) && + (t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && + (t1.pullRequest is true.bind, onlyPullRequest) && + (IssueLabels filter { t2 => + (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && + (t2.labelId in + (Labels filter { t3 => + (t3.byRepository(t1.userName, t1.repositoryName)) && + (t3.labelName inSetBind condition.labels) + } map(_.labelId))) + } exists, condition.labels.nonEmpty) + } + + def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], + assignedUserName: Option[String], milestoneId: Option[Int], + isPullRequest: Boolean = false)(implicit s: Session) = + // next id number + sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] + .firstOption.filter { id => + Issues insert Issue( + owner, + repository, + id, + loginUser, + milestoneId, + assignedUserName, + title, + content, + false, + currentDate, + currentDate, + isPullRequest) + + // increment issue id + IssueId + .filter (_.byPrimaryKey(owner, repository)) + .map (_.issueId) + .update (id) > 0 + } get + + def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = + IssueLabels insert IssueLabel(owner, repository, issueId, labelId) + + def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = + IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete + + def createComment(owner: String, repository: String, loginUser: String, + issueId: Int, content: String, action: String)(implicit s: Session): Int = + IssueComments.autoInc insert IssueComment( + userName = owner, + repositoryName = repository, + issueId = issueId, + action = action, + commentedUserName = loginUser, + content = content, + registeredDate = currentDate, + updatedDate = currentDate) + + def updateIssue(owner: String, repository: String, issueId: Int, + title: String, content: Option[String])(implicit s: Session) = + Issues + .filter (_.byPrimaryKey(owner, repository, issueId)) + .map { t => + (t.title, t.content.?, t.updatedDate) + } + .update (title, content, currentDate) + + def updateAssignedUserName(owner: String, repository: String, issueId: Int, + assignedUserName: Option[String])(implicit s: Session) = + Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName) + + def updateMilestoneId(owner: String, repository: String, issueId: Int, + milestoneId: Option[Int])(implicit s: Session) = + Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId) + + def updateComment(commentId: Int, content: String)(implicit s: Session) = + IssueComments + .filter (_.byPrimaryKey(commentId)) + .map { t => + t.content -> t.updatedDate + } + .update (content, currentDate) + + def deleteComment(commentId: Int)(implicit s: Session) = + IssueComments filter (_.byPrimaryKey(commentId)) delete + + def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session) = + Issues + .filter (_.byPrimaryKey(owner, repository, issueId)) + .map { t => + t.closed -> t.updatedDate + } + .update (closed, currentDate) + + /** + * Search issues by keyword. + * + * @param owner the repository owner + * @param repository the repository name + * @param query the keywords separated by whitespace. + * @return issues with comment count and matched content of issue or comment + */ + def searchIssuesByKeyword(owner: String, repository: String, query: String) + (implicit s: Session): List[(Issue, Int, String)] = { + import slick.driver.JdbcDriver.likeEncode + val keywords = splitWords(query.toLowerCase) + + // Search Issue + val issues = Issues + .innerJoin(IssueOutline).on { case (t1, t2) => + t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) + } + .filter { case (t1, t2) => + keywords.map { keyword => + (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || + (t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) + } .reduceLeft(_ && _) + } + .map { case (t1, t2) => + (t1, 0, t1.content.?, t2.commentCount) + } + + // Search IssueComment + val comments = IssueComments + .innerJoin(Issues).on { case (t1, t2) => + t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) + } + .innerJoin(IssueOutline).on { case ((t1, t2), t3) => + t2.byIssue(t3.userName, t3.repositoryName, t3.issueId) + } + .filter { case ((t1, t2), t3) => + keywords.map { query => + t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^') + }.reduceLeft(_ && _) + } + .map { case ((t1, t2), t3) => + (t2, t1.commentId, t1.content.?, t3.commentCount) + } + + issues.union(comments).sortBy { case (issue, commentId, _, _) => + issue.issueId -> commentId + }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) => + issue1.issueId == issue2.issueId + }.map { _.head match { + case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse("")) + } + }.toList + } + + def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String)(implicit s: Session) = { + extractCloseId(message).foreach { issueId => + for(issue <- getIssue(owner, repository, issueId) if !issue.closed){ + createComment(owner, repository, userName, issue.issueId, "Close", "close") + updateClosed(owner, repository, issue.issueId, true) + } + } + } +} + +object IssuesService { + import javax.servlet.http.HttpServletRequest + + val IssueLimit = 30 + + case class IssueSearchCondition( + labels: Set[String] = Set.empty, + milestoneId: Option[Option[Int]] = None, + repo: Option[String] = None, + state: String = "open", + sort: String = "created", + direction: String = "desc"){ + + def toURL: String = + "?" + List( + if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))), + milestoneId.map { id => "milestone=" + (id match { + case Some(x) => x.toString + case None => "none" + })}, + repo.map("for=" + urlEncode(_)), + Some("state=" + urlEncode(state)), + Some("sort=" + urlEncode(sort)), + Some("direction=" + urlEncode(direction))).flatten.mkString("&") + + } + + object IssueSearchCondition { + + private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = { + val value = request.getParameter(name) + if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) + } + + def apply(request: HttpServletRequest): IssueSearchCondition = + IssueSearchCondition( + param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), + param(request, "milestone").map{ + case "none" => None + case x => x.toIntOpt + }, + param(request, "for"), + param(request, "state", Seq("open", "closed")).getOrElse("open"), + param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), + param(request, "direction", Seq("asc", "desc")).getOrElse("desc")) + + def page(request: HttpServletRequest) = try { + val i = param(request, "page").getOrElse("1").toInt + if(i <= 0) 1 else i + } catch { + case e: NumberFormatException => 1 + } + } + +} diff --git a/src/main/scala/servlet/TransactionFilter.scala b/src/main/scala/servlet/TransactionFilter.scala index 5e034d4..6773a59 100644 --- a/src/main/scala/servlet/TransactionFilter.scala +++ b/src/main/scala/servlet/TransactionFilter.scala @@ -1,44 +1,44 @@ -package servlet - -import javax.servlet._ -import org.slf4j.LoggerFactory -import javax.servlet.http.HttpServletRequest -import util.Keys - -/** - * Controls the transaction with the open session in view pattern. - */ -class TransactionFilter extends Filter { - - private val logger = LoggerFactory.getLogger(classOf[TransactionFilter]) - - def init(config: FilterConfig) = {} - - def destroy(): Unit = {} - - def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { - if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){ - // assets don't need transaction - chain.doFilter(req, res) - } else { - Database(req.getServletContext) withTransaction { session => - logger.debug("begin transaction") - req.setAttribute(Keys.Request.DBSession, session) - chain.doFilter(req, res) - logger.debug("end transaction") - } - } - } - -} - -object Database { - def apply(context: ServletContext): slick.jdbc.JdbcBackend.Database = - slick.jdbc.JdbcBackend.Database.forURL(context.getInitParameter("db.url"), - context.getInitParameter("db.user"), - context.getInitParameter("db.password")) - - def getSession(req: ServletRequest): slick.jdbc.JdbcBackend#Session = - req.getAttribute(Keys.Request.DBSession).asInstanceOf[slick.jdbc.JdbcBackend#Session] - -} +package servlet + +import javax.servlet._ +import org.slf4j.LoggerFactory +import javax.servlet.http.HttpServletRequest +import util.Keys + +/** + * Controls the transaction with the open session in view pattern. + */ +class TransactionFilter extends Filter { + + private val logger = LoggerFactory.getLogger(classOf[TransactionFilter]) + + def init(config: FilterConfig) = {} + + def destroy(): Unit = {} + + def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { + if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){ + // assets don't need transaction + chain.doFilter(req, res) + } else { + Database(req.getServletContext) withTransaction { session => + logger.debug("begin transaction") + req.setAttribute(Keys.Request.DBSession, session) + chain.doFilter(req, res) + logger.debug("end transaction") + } + } + } + +} + +object Database { + def apply(context: ServletContext): slick.jdbc.JdbcBackend.Database = + slick.jdbc.JdbcBackend.Database.forURL(context.getInitParameter("db.url"), + context.getInitParameter("db.user"), + context.getInitParameter("db.password")) + + def getSession(req: ServletRequest): slick.jdbc.JdbcBackend#Session = + req.getAttribute(Keys.Request.DBSession).asInstanceOf[slick.jdbc.JdbcBackend#Session] + +} diff --git a/src/main/scala/util/Notifier.scala b/src/main/scala/util/Notifier.scala index 8758ef9..e1bd50b 100644 --- a/src/main/scala/util/Notifier.scala +++ b/src/main/scala/util/Notifier.scala @@ -1,117 +1,117 @@ -package util - -import scala.concurrent._ -import ExecutionContext.Implicits.global -import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail} -import org.slf4j.LoggerFactory - -import app.Context -import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService} -import servlet.Database -import SystemSettingsService.Smtp -import _root_.util.ControlUtil.defining -import model.profile.simple.Session - -trait Notifier extends RepositoryService with AccountService with IssuesService { - def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) - (msg: String => String)(implicit context: Context): Unit - - protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit session: Session, context: Context) = - ( - // individual repository's owner - issue.userName :: - // collaborators - getCollaborators(issue.userName, issue.repositoryName) ::: - // participants - issue.openedUserName :: - getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) - ) - .distinct - .withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded - .foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) ) - -} - -object Notifier { - // TODO We want to be able to switch to mock. - def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match { - case settings if settings.notification => new Mailer(settings.smtp.get) - case _ => new MockMailer - } - - def msgIssue(url: String) = (content: String) => s""" - |${content}
- |--
- |View it on GitBucket - """.stripMargin - - def msgPullRequest(url: String) = (content: String) => s""" - |${content}
- |View, comment on, or merge it at:
- |${url} - """.stripMargin - - def msgComment(url: String) = (content: String) => s""" - |${content}
- |--
- |View it on GitBucket - """.stripMargin - - def msgStatus(url: String) = (content: String) => s""" - |${content} #${url split('/') last} - """.stripMargin -} - -class Mailer(private val smtp: Smtp) extends Notifier { - private val logger = LoggerFactory.getLogger(classOf[Mailer]) - - def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) - (msg: String => String)(implicit context: Context) = { - val database = Database(context.request.getServletContext) - - val f = future { - // TODO Can we use the Database Session in other than Transaction Filter? - database withSession { implicit session => - getIssue(r.owner, r.name, issueId.toString) foreach { issue => - defining( - s"[${r.name}] ${issue.title} (#${issueId})" -> - msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) => - recipients(issue) { to => - val email = new HtmlEmail - email.setHostName(smtp.host) - email.setSmtpPort(smtp.port.get) - smtp.user.foreach { user => - email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse(""))) - } - smtp.ssl.foreach { ssl => - email.setSSLOnConnect(ssl) - } - smtp.fromAddress - .map (_ -> smtp.fromName.orNull) - .orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName)) - .foreach { case (address, name) => - email.setFrom(address, name) - } - email.setCharset("UTF-8") - email.setSubject(subject) - email.setHtmlMsg(msg) - - email.addTo(to).send - } - } - } - } - "Notifications Successful." - } - f onSuccess { - case s => logger.debug(s) - } - f onFailure { - case t => logger.error("Notifications Failed.", t) - } - } -} -class MockMailer extends Notifier { - def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) - (msg: String => String)(implicit context: Context): Unit = {} +package util + +import scala.concurrent._ +import ExecutionContext.Implicits.global +import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail} +import org.slf4j.LoggerFactory + +import app.Context +import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService} +import servlet.Database +import SystemSettingsService.Smtp +import _root_.util.ControlUtil.defining +import model.profile.simple.Session + +trait Notifier extends RepositoryService with AccountService with IssuesService { + def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) + (msg: String => String)(implicit context: Context): Unit + + protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit session: Session, context: Context) = + ( + // individual repository's owner + issue.userName :: + // collaborators + getCollaborators(issue.userName, issue.repositoryName) ::: + // participants + issue.openedUserName :: + getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) + ) + .distinct + .withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded + .foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) ) + +} + +object Notifier { + // TODO We want to be able to switch to mock. + def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match { + case settings if settings.notification => new Mailer(settings.smtp.get) + case _ => new MockMailer + } + + def msgIssue(url: String) = (content: String) => s""" + |${content}
+ |--
+ |View it on GitBucket + """.stripMargin + + def msgPullRequest(url: String) = (content: String) => s""" + |${content}
+ |View, comment on, or merge it at:
+ |${url} + """.stripMargin + + def msgComment(url: String) = (content: String) => s""" + |${content}
+ |--
+ |View it on GitBucket + """.stripMargin + + def msgStatus(url: String) = (content: String) => s""" + |${content} #${url split('/') last} + """.stripMargin +} + +class Mailer(private val smtp: Smtp) extends Notifier { + private val logger = LoggerFactory.getLogger(classOf[Mailer]) + + def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) + (msg: String => String)(implicit context: Context) = { + val database = Database(context.request.getServletContext) + + val f = future { + // TODO Can we use the Database Session in other than Transaction Filter? + database withSession { implicit session => + getIssue(r.owner, r.name, issueId.toString) foreach { issue => + defining( + s"[${r.name}] ${issue.title} (#${issueId})" -> + msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) => + recipients(issue) { to => + val email = new HtmlEmail + email.setHostName(smtp.host) + email.setSmtpPort(smtp.port.get) + smtp.user.foreach { user => + email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse(""))) + } + smtp.ssl.foreach { ssl => + email.setSSLOnConnect(ssl) + } + smtp.fromAddress + .map (_ -> smtp.fromName.orNull) + .orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName)) + .foreach { case (address, name) => + email.setFrom(address, name) + } + email.setCharset("UTF-8") + email.setSubject(subject) + email.setHtmlMsg(msg) + + email.addTo(to).send + } + } + } + } + "Notifications Successful." + } + f onSuccess { + case s => logger.debug(s) + } + f onFailure { + case t => logger.error("Notifications Failed.", t) + } + } +} +class MockMailer extends Notifier { + def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) + (msg: String => String)(implicit context: Context): Unit = {} } \ No newline at end of file