diff --git a/app/controllers/ApiController.scala b/app/controllers/ApiController.scala index e001b9ab9..bb7bda7bf 100644 --- a/app/controllers/ApiController.scala +++ b/app/controllers/ApiController.scala @@ -15,7 +15,7 @@ import models.user.User import ore.permission.EditApiKeys import ore.permission.role.RoleTypes import ore.permission.role.RoleTypes.RoleType -import ore.project.factory.ProjectFactory +import ore.project.factory.{PendingVersion, ProjectFactory} import ore.project.io.{InvalidPluginFileException, PluginUpload, ProjectFiles} import ore.rest.ProjectApiKeyTypes._ import ore.rest.{OreRestfulApi, OreWrites} @@ -23,14 +23,16 @@ import ore.{OreConfig, OreEnv} import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import util.StatusZ +import util.functional.{EitherT, OptionT, Id} +import util.instances.future._ +import util.syntax._ import play.api.libs.json._ import play.api.mvc._ import security.CryptoUtils import security.spauth.SingleSignOnConsumer import slick.lifted.Compiled -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} /** * Ore API (v1) @@ -46,7 +48,7 @@ final class ApiController @Inject()(api: OreRestfulApi, implicit override val bakery: Bakery, implicit override val cache: AsyncCacheApi, implicit override val sso: SingleSignOnConsumer, - implicit override val messagesApi: MessagesApi) + implicit override val messagesApi: MessagesApi)(implicit val ec: ExecutionContext) extends OreBaseController { import writes._ @@ -84,46 +86,37 @@ final class ApiController @Inject()(api: OreRestfulApi, } } - def createKey(version: String, pluginId: String) = { + def createKey(version: String, pluginId: String) = (Action andThen AuthedProjectActionById(pluginId) andThen ProjectPermissionAction(EditApiKeys)) async { implicit request => - val data = request.data - this.forms.ProjectApiKeyCreate.bindFromRequest().fold( _ => Future.successful(BadRequest), - { - case keyType@Deployment => - this.projectApiKeys.exists(k => k.projectId === data.project.id.get && k.keyType === keyType) - .flatMap { exists => - if (exists) Future.successful(BadRequest) - else { - this.projectApiKeys.add(ProjectApiKey( - projectId = data.project.id.get, - keyType = keyType, - value = UUID.randomUUID().toString.replace("-", ""))).map { pak => - Created(Json.toJson(pak)) - } - } - } + val projectId = request.data.project.id.get + val res = for { + keyType <- bindFormOptionT[Future](this.forms.ProjectApiKeyCreate) + if keyType == Deployment + exists <- OptionT.liftF(this.projectApiKeys.exists(k => k.projectId === projectId && k.keyType === keyType)) + if !exists + pak <- OptionT.liftF( + this.projectApiKeys.add(ProjectApiKey( + projectId = projectId, + keyType = keyType, + value = UUID.randomUUID().toString.replace("-", ""))) + ) + } yield Created(Json.toJson(pak)) - case _ => Future.successful(BadRequest) - } - ) + res.getOrElse(BadRequest) } - } - def revokeKey(version: String, pluginId: String) = { + def revokeKey(version: String, pluginId: String) = (AuthedProjectActionById(pluginId) andThen ProjectPermissionAction(EditApiKeys)) { implicit request => - this.forms.ProjectApiKeyRevoke.bindFromRequest().fold( - _ => BadRequest, - key => { - if (key.projectId != request.data.project.id.get) - BadRequest - else { - key.remove() - Ok - } - } - ) + val res = for { + key <- bindFormOptionT[Id](this.forms.ProjectApiKeyRevoke) + if key.projectId == request.data.project.id.get + } yield { + key.remove() + Ok + } + + res.getOrElse(BadRequest) } - } /** * Returns a JSON view of Versions meeting the specified criteria. @@ -138,7 +131,7 @@ final class ApiController @Inject()(api: OreRestfulApi, def listVersions(version: String, pluginId: String, channels: Option[String], limit: Option[Int], offset: Option[Int]) = Action.async { version match { - case "v1" => this.api.getVersionList(pluginId, channels, limit, offset).map(ApiResult) + case "v1" => this.api.getVersionList(pluginId, channels, limit, offset).map(Some.apply).map(ApiResult) case _ => Future.successful(NotFound) } } @@ -164,87 +157,67 @@ final class ApiController @Inject()(api: OreRestfulApi, version match { case "v1" => val projectData = request.data - this.forms.VersionDeploy.bindFromRequest().fold( - hasErrors => Future.successful(BadRequest(Json.obj("errors" -> hasErrors.errorsAsJson))), - formData => { - - val apiKeyTable = TableQuery[ProjectApiKeyTable] - def queryApiKey(deployment: Rep[ProjectApiKeyType], key: Rep[String], pId: Rep[Int]) = { - val query = for { - k <- apiKeyTable if k.value === key && k.projectId === pId && k.keyType === deployment - } yield { - k.id - } - query.exists - } - - val compiled = Compiled(queryApiKey _) - val apiKeyExists: Future[Boolean] = this.service.DB.db.run(compiled(Deployment, formData.apiKey, projectData.project.id.get).result) - val dep = for { - (apiKey, versionExists) <- apiKeyExists zip - projectData.project.versions.exists(_.versionString === name) + bindFormEitherT[Future](this.forms.VersionDeploy)(hasErrors => BadRequest(Json.obj("errors" -> hasErrors.errorsAsJson))).flatMap { formData => + val apiKeyTable = TableQuery[ProjectApiKeyTable] + def queryApiKey(deployment: Rep[ProjectApiKeyType], key: Rep[String], pId: Rep[Int]) = { + val query = for { + k <- apiKeyTable if k.value === key && k.projectId === pId && k.keyType === deployment } yield { - if (!apiKey) Future.successful(Unauthorized(error("apiKey", "api.deploy.invalidKey"))) - else if (versionExists) Future.successful(BadRequest(error("versionName", "api.deploy.versionExists"))) - else { - - def upload(user: User): Future[Result] = { - - val pending = Right(user).flatMap { user => - this.factory.getUploadError(user) - .map(err => BadRequest(error("user", err))) - .toLeft(PluginUpload.bindFromRequest()) - } flatMap { - case None => Left(BadRequest(error("files", "error.noFile"))) - case Some(uploadData) => Right(uploadData) - } match { - case Left(err) => Future.successful(Left(err)) - case Right(data) => - try { - this.factory.processSubsequentPluginUpload(data, user, projectData.project).map { - case Left(err) => Left(BadRequest(error("upload", err))) - case Right(pv) => Right(pv) - } - } catch { - case e: InvalidPluginFileException => - Future.successful(Left(BadRequest(error("upload", e.getMessage)))) - } - } - pending flatMap { - case Left(err) => Future.successful(err) - case Right(pendingVersion) => - pendingVersion.createForumPost = formData.createForumPost - pendingVersion.channelName = formData.channel.name - formData.changelog.foreach(pendingVersion.underlying.setDescription) - pendingVersion.complete().map { newVersion => - if (formData.recommended) - projectData.project.setRecommendedVersion(newVersion._1) - Created(api.writeVersion(newVersion._1, projectData.project, newVersion._2, None, newVersion._3)) - } - } - } + k.id + } + query.exists + } - for { - user <- projectData.project.owner.user - orga <- user.toMaybeOrganization - owner <- orga.map(_.owner.user).getOrElse(Future.successful(user)) - result <- upload(owner) - } yield { - result + val compiled = Compiled(queryApiKey _) + + val apiKeyExists: Future[Boolean] = this.service.DB.db.run(compiled(Deployment, formData.apiKey, projectData.project.id.get).result) + + EitherT.liftF(apiKeyExists) + .filterOrElse(apiKey => !apiKey, Unauthorized(error("apiKey", "api.deploy.invalidKey"))) + .semiFlatMap(_ => projectData.project.versions.exists(_.versionString === name)) + .filterOrElse(identity, BadRequest(error("versionName", "api.deploy.versionExists"))) + .semiFlatMap(_ => projectData.project.owner.user) + .semiFlatMap(user => user.toMaybeOrganization.semiFlatMap(_.owner.user).getOrElse(user)) + .flatMap { owner => + + val pluginUpload = this.factory.getUploadError(owner) + .map(err => BadRequest(error("user", err))) + .toLeft(PluginUpload.bindFromRequest()) + .flatMap(_.toRight(BadRequest(error("files", "error.noFile")))) + + EitherT.fromEither[Future](pluginUpload).flatMap { data => + //TODO: We should get rid of this try + try { + this.factory + .processSubsequentPluginUpload(data, owner, projectData.project) + .leftMap(err => BadRequest(error("upload", err))) + } + catch { + case e: InvalidPluginFileException => + EitherT.leftT[Future, PendingVersion](BadRequest(error("upload", e.getMessage))) } } } - dep.flatten - } - ) + .semiFlatMap { pendingVersion => + pendingVersion.createForumPost = formData.createForumPost + pendingVersion.channelName = formData.channel.name + formData.changelog.foreach(pendingVersion.underlying.setDescription) + pendingVersion.complete() + } + .map { case (newVersion, channel, tags) => + if (formData.recommended) + projectData.project.setRecommendedVersion(newVersion) + Created(api.writeVersion(newVersion, projectData.project, channel, None, tags)) + } + }.merge case _ => Future.successful(NotFound) } } def listPages(version: String, pluginId: String, parentId: Option[Int]) = Action.async { version match { - case "v1" => this.api.getPages(pluginId, parentId).map(ApiResult) + case "v1" => this.api.getPages(pluginId, parentId).value.map(ApiResult) case _ => Future.successful(NotFound) } } @@ -288,7 +261,7 @@ final class ApiController @Inject()(api: OreRestfulApi, */ def listTags(version: String, plugin: String, versionName: String) = Action.async { version match { - case "v1" => this.api.getTags(plugin, versionName).map(ApiResult) + case "v1" => this.api.getTags(plugin, versionName).value.map(ApiResult) case _ => Future.successful(NotFound) } } @@ -308,53 +281,41 @@ final class ApiController @Inject()(api: OreRestfulApi, def showStatusZ = Action(Ok(this.status.json)) def syncSso() = Action.async { implicit request => - this.forms.SyncSso.bindFromRequest.fold( - hasErrors => Future.successful(BadRequest(Json.obj("errors" -> hasErrors.errorsAsJson))), - success = formData => { - val sso = formData._1 - val sig = formData._2 - val apiKey = formData._3 - - - val confApiKey = this.config.security.get[String]("sso.apikey") - val confSecret = this.config.security.get[String]("sso.secret") - - if (apiKey != confApiKey) { - Future.successful(BadRequest("API Key not valid")) - } else if (CryptoUtils.hmac_sha256(confSecret, sso.getBytes("UTF-8")) != sig) { - Future.successful(BadRequest("Signature not matched")) - } else { - val query = Uri.Query(Base64.getMimeDecoder.decode(sso)) - - val external_id = query.get("external_id") + val confApiKey = this.config.security.get[String]("sso.apikey") + val confSecret = this.config.security.get[String]("sso.secret") + + bindFormEitherT[Future](this.forms.SyncSso)(hasErrors => BadRequest(Json.obj("errors" -> hasErrors.errorsAsJson))) + .filterOrElse(_._3 == confApiKey, BadRequest("API Key not valid")) //_3 is apiKey + .filterOrElse( + { case (sso, sig, _) => CryptoUtils.hmac_sha256(confSecret, sso.getBytes("UTF-8")) == sig}, + BadRequest("Signature not matched") + ) + .map(t => Uri.Query(Base64.getMimeDecoder.decode(t._1))) //_1 is sso + .semiFlatMap(q => this.users.get(q.get("external_id").get.toInt).value.tupleLeft(q)) + .map { case (query, optUser) => + optUser.foreach { user => val email = query.get("email") val username = query.get("username") val name = query.get("name") val avatar_url = query.get("avatar_url") val add_groups = query.get("add_groups") - this.users.get(external_id.get.toInt).map { optUser => - if (optUser.isDefined) { - val user = optUser.get - - email.foreach(user.setEmail) - username.foreach(user.setUsername) - name.foreach(user.setFullName) - avatar_url.foreach(user.setAvatarUrl) - add_groups.foreach(groups => - user.setGlobalRoles( - if(groups.trim == "") - Set.empty - else - groups.split(",").map(group => RoleTypes.withInternalName(group)).toSet[RoleType] - ) - ) - } - - Ok(Json.obj("status" -> "success")) + email.foreach(user.setEmail) + username.foreach(user.setUsername) + name.foreach(user.setFullName) + avatar_url.foreach(user.setAvatarUrl) + add_groups.foreach { groups => + user.setGlobalRoles( + if (groups.trim == "") + Set.empty + else + groups.split(",").map(group => RoleTypes.withInternalName(group)).toSet[RoleType] + ) } + } - } - ) + + Ok(Json.obj("status" -> "success")) + }.merge } } diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index 3b4b4c25e..6aecd9fc2 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -28,10 +28,12 @@ import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer import util.DataHelper +import util.functional.OptionT +import util.syntax._ +import util.instances.future._ import views.{html => views} -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} /** * Main entry point for application. @@ -44,7 +46,7 @@ final class Application @Inject()(data: DataHelper, implicit override val env: OreEnv, implicit override val config: OreConfig, implicit override val cache: AsyncCacheApi, - implicit override val service: ModelService) + implicit override val service: ModelService)(implicit val ec: ExecutionContext) extends OreBaseController { private def FlagAction = Authenticated andThen PermissionAction[AuthRequest](ReviewFlags) @@ -215,8 +217,7 @@ final class Application @Inject()(data: DataHelper, def showFlags() = FlagAction.async { implicit request => for { flags <- this.service.access[Flag](classOf[Flag]).filterNot(_.isResolved) - users <- Future.sequence(flags.map(_.user)) - projects <- Future.sequence(flags.map(_.project)) + (users, projects) <- (Future.sequence(flags.map(_.user)), Future.sequence(flags.map(_.project))).parTupled perms <- Future.sequence(projects.map { project => val perms = VisibilityTypes.values.map(_.permission).map { perm => request.user can perm in project map (value => (perm, value)) @@ -239,27 +240,24 @@ final class Application @Inject()(data: DataHelper, * @return Ok */ def setFlagResolved(flagId: Int, resolved: Boolean) = FlagAction.async { implicit request => - this.service.access[Flag](classOf[Flag]).get(flagId) flatMap { - case None => Future.successful(NotFound) - case Some(flag) => - users.current.map { user => - flag.setResolved(resolved, user) - Ok - } - } + this.service.access[Flag](classOf[Flag]).get(flagId).semiFlatMap { flag => + users.current.value.map { user => + flag.setResolved(resolved, user) + Ok + } + }.getOrElse(NotFound) } def showHealth() = (Authenticated andThen PermissionAction[AuthRequest](ViewHealth)) async { implicit request => - - for { - noTopicProjects <- projects.filter(p => p.topicId === -1 || p.postId === -1) - topicDirtyProjects <- projects.filter(_.isTopicDirty) - staleProjects <- projects.stale - notPublic <- projects.filterNot(_.visibility === VisibilityTypes.Public) - missingFileProjects <- projects.missingFile.flatMap { v => + ( + projects.filter(p => p.topicId === -1 || p.postId === -1), + projects.filter(_.isTopicDirty), + projects.stale, + projects.filterNot(_.visibility === VisibilityTypes.Public), + projects.missingFile.flatMap { v => Future.sequence(v.map { v => v.project.map(p => (v, p)) }) } - } yield { + ).parMapN { (noTopicProjects, topicDirtyProjects, staleProjects, notPublic, missingFileProjects) => Ok(views.users.admin.health(noTopicProjects, topicDirtyProjects, staleProjects, notPublic, missingFileProjects)) } } @@ -310,31 +308,29 @@ final class Application @Inject()(data: DataHelper, * Show the activities page for a user */ def showActivities(user: String) = (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => - this.users.withName(user).flatMap { - case None => Future.successful(NotFound) - case Some(u) => - val activities: Future[Seq[(Object, Option[Project])]] = u.id match { - case None => Future.successful(Seq.empty) - case Some(id) => - val reviews = this.service.access[Review](classOf[Review]) - .filter(_.userId === id) - .map(_.take(20).map { review => review -> - this.service.access[Version](classOf[Version]).filter(_.id === review.versionId).flatMap { version => - this.projects.find(_.id === version.head.projectId) - } - }) - val flags = this.service.access[Flag](classOf[Flag]) - .filter(_.resolvedBy === id) - .map(_.take(20).map(flag => flag -> this.projects.find(_.id === flag.projectId))) - - val allActivities = reviews.flatMap(r => flags.map(_ ++ r)) - - allActivities.flatMap(Future.traverse(_) { - case (k, fv) => fv.map(k -> _) - }.map(_.sortWith(sortActivities))) - } - activities.map(a => Ok(views.users.admin.activity(u, a))) - } + this.users.withName(user).semiFlatMap { u => + val activities: Future[Seq[(Object, Option[Project])]] = u.id match { + case None => Future.successful(Seq.empty) + case Some(id) => + val reviews = this.service.access[Review](classOf[Review]) + .filter(_.userId === id) + .map(_.take(20).map { review => review -> + this.service.access[Version](classOf[Version]).filter(_.id === review.versionId).flatMap { version => + this.projects.find(_.id === version.head.projectId).value + } + }) + val flags = this.service.access[Flag](classOf[Flag]) + .filter(_.resolvedBy === id) + .map(_.take(20).map(flag => flag -> this.projects.find(_.id === flag.projectId).value)) + + val allActivities = reviews.flatMap(r => flags.map(_ ++ r)) + + allActivities.flatMap(Future.traverse(_) { + case (k, fv) => fv.map(k -> _) + }.map(_.sortWith(sortActivities))) + } + activities.map(a => Ok(views.users.admin.activity(u, a))) + }.getOrElse(NotFound) } /** @@ -344,13 +340,13 @@ final class Application @Inject()(data: DataHelper, * @return Boolean */ def sortActivities(o1: Object, o2: Object): Boolean = { - var o1Time: Long = 0 - var o2Time: Long = 0 - if (o1.isInstanceOf[Review]) { - o1Time = o1.asInstanceOf[Review].endedAt.getOrElse(Timestamp.from(Instant.EPOCH)).getTime + val o1Time: Long = o1 match { + case review: Review => review.endedAt.getOrElse(Timestamp.from(Instant.EPOCH)).getTime + case _ => 0 } - if (o2.isInstanceOf[Flag]) { - o2Time = o2.asInstanceOf[Flag].resolvedAt.getOrElse(Timestamp.from(Instant.EPOCH)).getTime + val o2Time: Long = o2 match { + case flag: Flag => flag.resolvedAt.getOrElse(Timestamp.from(Instant.EPOCH)).getTime + case _ => 0 } o1Time > o2Time } @@ -384,15 +380,14 @@ final class Application @Inject()(data: DataHelper, ORDER BY days ASC""".as[(String, String)]) } - for { - reviews <- last10DaysCountQuery("project_version_reviews", "ended_at") - uploads <- last10DaysCountQuery("project_versions", "created_at") - totalDownloads <- last10DaysCountQuery("project_version_downloads", "created_at") - unsafeDownloads <- last10DaysCountQuery("project_version_unsafe_downloads", "created_at") - flagsOpen <- last10DaysTotalOpen("project_flags", "created_at", "resolved_at") - flagsClosed <- last10DaysCountQuery("project_flags", "resolved_at") - } - yield { + ( + last10DaysCountQuery("project_version_reviews", "ended_at"), + last10DaysCountQuery("project_versions", "created_at"), + last10DaysCountQuery("project_version_downloads", "created_at"), + last10DaysCountQuery("project_version_unsafe_downloads", "created_at"), + last10DaysTotalOpen("project_flags", "created_at", "resolved_at"), + last10DaysCountQuery("project_flags", "resolved_at") + ).parMapN { (reviews, uploads, totalDownloads, unsafeDownloads, flagsOpen, flagsClosed) => Ok(views.users.admin.stats(reviews, uploads, totalDownloads, unsafeDownloads, flagsOpen, flagsClosed)) } } @@ -400,102 +395,84 @@ final class Application @Inject()(data: DataHelper, def UserAdminAction = Authenticated andThen PermissionAction[AuthRequest](UserAdmin) def userAdmin(user: String) = UserAdminAction.async { implicit request => - this.users.withName(user).flatMap { - case None => Future.successful(notFound) - case Some(u) => - for { - userData <- getUserData(request, user) - isOrga <- u.isOrganization - projectRoles <- if (isOrga) Future.successful(Seq.empty) else u.projectRoles.all - projects <- Future.sequence(projectRoles.map(_.project)) - orga <- if (isOrga) getOrga(request, user) else Future.successful(None) - orgaData <- OrganizationData.of(orga) - scopedOrgaData <- ScopedOrganizationData.of(Some(request.user), orga) - } yield { - val pr = projects zip projectRoles - Ok(views.users.admin.userAdmin(userData.get, orgaData, pr.toSeq)) + this.users.withName(user).semiFlatMap { u => + for { + isOrga <- u.isOrganization + (projectRoles, orga) <- { + if (isOrga) + (Future.successful(Seq.empty), getOrga(request, user).value).parTupled + else + (u.projectRoles.all, Future.successful(None)).parTupled } - } + (userData, projects, orgaData, scopedOrgaData) <- ( + getUserData(request, user).value, + Future.sequence(projectRoles.map(_.project)), + OrganizationData.of(orga).value, + ScopedOrganizationData.of(Some(request.user), orga).value + ).parTupled + } yield { + val pr = projects zip projectRoles + Ok(views.users.admin.userAdmin(userData.get, orgaData, pr.toSeq)) + } + }.getOrElse(notFound) } def updateUser(userName: String) = UserAdminAction.async { implicit request => - this.users.withName(userName).flatMap { - case None => Future.successful(NotFound) - case Some(user) => - this.forms.UserAdminUpdate.bindFromRequest.fold( - _ => Future.successful(BadRequest), - { case (thing, action, data) => - - import play.api.libs.json._ - val json = Json.parse(data) - - def updateRoleTable[M <: RoleModel](modelAccess: ModelAccess[M], allowedType: Class[_ <: Role], ownerType: RoleTypes.RoleType, transferOwner: M => Future[Int]) = { - val id = (json \ "id").as[Int] - val status = action match { - case "setRole" => modelAccess.get(id).map { - case None => BadRequest - case Some(role) => - val roleType = RoleTypes.withId((json \ "role").as[Int]) - if (roleType == ownerType) { - transferOwner(role) - Ok - } else if (roleType.roleClass == allowedType && roleType.isAssignable) { - role.setRoleType(roleType) - Ok - } else BadRequest - } - case "setAccepted" => modelAccess.get(id).map { - case None => BadRequest - case Some(role) => - role.setAccepted((json \ "accepted").as[Boolean]) - Ok - } - case "deleteRole" => modelAccess.get(id).map { - case None => BadRequest - case Some(role) => - if (role.roleType.isAssignable) { - role.remove() - Ok - } else BadRequest - } - } - status + this.users.withName(userName).map { user => + bindFormOptionT[Future](this.forms.UserAdminUpdate).flatMap { case (thing, action, data) => + import play.api.libs.json._ + val json = Json.parse(data) + + def updateRoleTable[M <: RoleModel](modelAccess: ModelAccess[M], allowedType: Class[_ <: Role], ownerType: RoleTypes.RoleType, transferOwner: M => Future[Int]) = { + val id = (json \ "id").as[Int] + action match { + case "setRole" => modelAccess.get(id).map { role => + val roleType = RoleTypes.withId((json \ "role").as[Int]) + if (roleType == ownerType) { + transferOwner(role) + Ok + } else if (roleType.roleClass == allowedType && roleType.isAssignable) { + role.setRoleType(roleType) + Ok + } else BadRequest + } + case "setAccepted" => modelAccess.get(id).map { role => + role.setAccepted((json \ "accepted").as[Boolean]) + Ok + } + case "deleteRole" => modelAccess.get(id).filter(_.roleType.isAssignable).map { role => + role.remove() + Ok } + } + } + + def transferOrgOwner(r: OrganizationRole) = { + r.organization.flatMap { orga => + orga.transferOwner(orga.memberships.newMember(r.userId)) + } + } - def transferOrgOwner(r: OrganizationRole) = { - r.organization.flatMap { orga => - orga.transferOwner(orga.memberships.newMember(r.userId)) + val isOrga = OptionT.liftF(user.isOrganization) + thing match { + case "orgRole" => + isOrga.filterNot(identity).flatMap { _ => + updateRoleTable(user.organizationRoles, classOf[OrganizationRole], RoleTypes.OrganizationOwner, transferOrgOwner) + } + case "memberRole" => + isOrga.filter(identity).flatMap { _ => + OptionT.liftF(user.toOrganization).flatMap { orga => + updateRoleTable(orga.memberships.roles, classOf[OrganizationRole], RoleTypes.OrganizationOwner, transferOrgOwner) } } - - val isOrga = user.isOrganization - thing match { - case "orgRole" => - isOrga.flatMap { - case true => Future.successful(BadRequest) - case false => - updateRoleTable(user.organizationRoles, classOf[OrganizationRole], RoleTypes.OrganizationOwner, transferOrgOwner) - } - case "memberRole" => - isOrga.flatMap { - case false => Future.successful(BadRequest) - case true => - user.toOrganization.flatMap { orga => - updateRoleTable(orga.memberships.roles, classOf[OrganizationRole], RoleTypes.OrganizationOwner, transferOrgOwner) - } - } - case "projectRole" => - isOrga.flatMap { - case true => Future.successful(BadRequest) - case false => - updateRoleTable(user.projectRoles, classOf[ProjectRole], RoleTypes.ProjectOwner, - (r: ProjectRole) => r.project.flatMap(p => p.transferOwner(p.memberships.newMember(r.userId)))) - } - case _ => Future.successful(BadRequest) + case "projectRole" => + isOrga.filterNot(identity).flatMap { _ => + updateRoleTable(user.projectRoles, classOf[ProjectRole], RoleTypes.ProjectOwner, (r: ProjectRole) => r.project.flatMap(p => p.transferOwner(p.memberships.newMember(r.userId)))) } - - }) - } + case _ => OptionT.none[Future, Status] + } + } + }.semiFlatMap(_.getOrElse(BadRequest)).getOrElse(NotFound) } /** @@ -506,39 +483,44 @@ final class Application @Inject()(data: DataHelper, val projectSchema = this.service.getSchema(classOf[ProjectSchema]) for { - projectApprovals <- projectSchema.collect(ModelFilter[Project](_.visibility === VisibilityTypes.NeedsApproval).fn, ProjectSortingStrategies.Default, -1, 0) - perms <- Future.sequence(projectApprovals.map { project => - val perms = VisibilityTypes.values.map(_.permission).map { perm => - request.user can perm in project map (value => (perm, value)) - } - Future.sequence(perms).map(_.toMap) - }) - lastChangeRequests <- Future.sequence(projectApprovals.map(_.lastChangeRequest)) - lastChangeRequesters <- Future.sequence(lastChangeRequests.map { - case None => Future.successful(None) - case Some(lcr) => lcr.created - }) - lastVisibilityChanges <- Future.sequence(projectApprovals.map(_.lastVisibilityChange)) - lastVisibilityChangers <- Future.sequence(lastVisibilityChanges.map { - case None => Future.successful(None) - case Some(lcr) => lcr.created - }) - - projectChanges <- projectSchema.collect(ModelFilter[Project](_.visibility === VisibilityTypes.NeedsChanges).fn, ProjectSortingStrategies.Default, -1, 0) - projectChangeRequests <- Future.sequence(projectChanges.map(_.lastChangeRequest)) - projectVisibilityChanges <- Future.sequence(projectChanges.map(_.lastVisibilityChange)) - projectVisibilityChangers <- Future.sequence(projectVisibilityChanges.map { - case None => Future.successful(None) - case Some(lcr) => lcr.created - }) - + (projectApprovals, projectChanges) <- ( + projectSchema.collect(ModelFilter[Project](_.visibility === VisibilityTypes.NeedsApproval).fn, ProjectSortingStrategies.Default, -1, 0), + projectSchema.collect(ModelFilter[Project](_.visibility === VisibilityTypes.NeedsChanges).fn, ProjectSortingStrategies.Default, -1, 0) + ).parTupled + (lastChangeRequests, lastVisibilityChanges, projectVisibilityChanges) <- ( + Future.sequence(projectApprovals.map(_.lastChangeRequest.value)), + Future.sequence(projectApprovals.map(_.lastVisibilityChange.value)), + Future.sequence(projectChanges.map(_.lastVisibilityChange.value)) + ).parTupled + + (perms, lastChangeRequesters, lastVisibilityChangers, projectChangeRequests, projectVisibilityChangers) <- ( + Future.sequence(projectApprovals.map { project => + val perms = VisibilityTypes.values.map(_.permission).map { perm => + request.user can perm in project map (value => (perm, value)) + } + Future.sequence(perms).map(_.toMap) + }), + Future.sequence(lastChangeRequests.map { + case None => Future.successful(None) + case Some(lcr) => lcr.created.value + }), + Future.sequence(lastVisibilityChanges.map { + case None => Future.successful(None) + case Some(lcr) => lcr.created.value + }), + Future.sequence(projectChanges.map(_.lastChangeRequest.value)), + Future.sequence(projectVisibilityChanges.map { + case None => Future.successful(None) + case Some(lcr) => lcr.created.value + }) + ).parTupled } yield { val needsApproval = projectApprovals zip perms zip lastChangeRequests zip lastChangeRequesters zip lastVisibilityChanges zip lastVisibilityChangers map { case (((((a,b),c),d),e),f) => - (a,b,c,d.map(_.name),e,f.map(_.name)) + (a,b,c,d.fold("Unknown")(_.name),e,f.fold("Unknown")(_.name)) } val waitingProjects = projectChanges zip projectChangeRequests zip projectVisibilityChanges zip projectVisibilityChangers map { case (((a,b), c), d) => - (a,b,c,d.map(_.name)) + (a,b,c,d.fold("Unknown")(_.name)) } Ok(views.users.admin.visibility(needsApproval, waitingProjects)) diff --git a/app/controllers/OreBaseController.scala b/app/controllers/OreBaseController.scala index edcbf1909..db13c8645 100755 --- a/app/controllers/OreBaseController.scala +++ b/app/controllers/OreBaseController.scala @@ -14,9 +14,13 @@ import play.api.i18n.{I18nSupport, Lang} import play.api.mvc._ import security.spauth.SingleSignOnConsumer import util.StringUtils._ +import util.instances.future._ +import scala.concurrent.{ExecutionContext, Future} +import scala.language.higherKinds -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import controllers.OreBaseController.{BindFormEitherTPartiallyApplied, BindFormOptionTPartiallyApplied} +import play.api.data.Form +import util.functional.{EitherT, Monad, OptionT} /** * Represents a Secured base Controller for this application. @@ -43,39 +47,50 @@ abstract class OreBaseController(implicit val env: OreEnv, override def notFound(implicit request: OreRequest[_]) = NotFound(views.html.errors.notFound()) + implicit def ec: ExecutionContext + /** - * Executes the given function with the specified result or returns a - * NotFound if not found. + * Gets a project with the specified author and slug, or returns a notFound. * * @param author Project author * @param slug Project slug - * @param fn Function to execute * @param request Incoming request - * @return NotFound or function result + * @return NotFound or project */ - def withProject(author: String, slug: String)(fn: Project => Result)(implicit request: OreRequest[_]): Future[Result] - = this.projects.withSlug(author, slug).map(_.map(fn).getOrElse(notFound)) - - def withProjectAsync(author: String, slug: String)(fn: Project => Future[Result])(implicit request: OreRequest[_]): Future[Result] - = this.projects.withSlug(author, slug).flatMap(_.map(fn).getOrElse(Future.successful(NotFound))) + def getProject(author: String, slug: String)(implicit request: OreRequest[_]): EitherT[Future, Result, Project] + = this.projects.withSlug(author, slug).toRight(notFound) /** - * Executes the given function with the specified result or returns a - * NotFound if not found. + * Gets a project with the specified versionString, or returns a notFound. * + * @param project Project to get version from * @param versionString VersionString - * @param fn Function to execute * @param request Incoming request - * @param project Project to get version from * @return NotFound or function result */ - def withVersion(versionString: String)(fn: Version => Result) - (implicit request: OreRequest[_], project: Project): Future[Result] - = project.versions.find(equalsIgnoreCase[VersionTable](_.versionString, versionString)).map(_.map(fn).getOrElse(notFound)) + def getVersion(project: Project, versionString: String) + (implicit request: OreRequest[_]): EitherT[Future, Result, Version] + = project.versions.find(equalsIgnoreCase[VersionTable](_.versionString, versionString)).toRight(notFound) - def withVersionAsync(versionString: String)(fn: Version => Future[Result]) - (implicit request: OreRequest[_], project: Project): Future[Result] - = project.versions.find(equalsIgnoreCase[VersionTable](_.versionString, versionString)).flatMap(_.map(fn).getOrElse(Future.successful(notFound))) + /** + * Gets a version with the specified author, project slug and version string + * or returns a notFound. + * + * @param author Project author + * @param slug Project slug + * @param versionString VersionString + * @param request Incoming request + * @return NotFound or project + */ + def getProjectVersion(author: String, slug: String, versionString: String)(implicit request: OreRequest[_]): EitherT[Future, Result, Version] + = for { + project <- getProject(author, slug) + version <- getVersion(project, versionString) + } yield version + + def bindFormEitherT[F[_]] = new BindFormEitherTPartiallyApplied[F] + + def bindFormOptionT[F[_]] = new BindFormOptionTPartiallyApplied[F] def OreAction = Action andThen oreAction @@ -162,3 +177,15 @@ abstract class OreBaseController(implicit val env: OreEnv, = UserAction(username) andThen verifiedAction(sso, sig) } +object OreBaseController { + + final class BindFormEitherTPartiallyApplied[F[_]](val b: Boolean = true) extends AnyVal { + def apply[A, B](form: Form[B])(left: Form[B] => A)(implicit F: Monad[F], request: Request[_]): EitherT[F, A, B] = + form.bindFromRequest().fold(left.andThen(EitherT.leftT[F, B](_)), EitherT.rightT[F, A](_)) + } + + final class BindFormOptionTPartiallyApplied[F[_]](val b: Boolean = true) extends AnyVal { + def apply[A](form: Form[A])(implicit F: Monad[F], request: Request[_]): OptionT[F, A] = + form.bindFromRequest().fold(_ => OptionT.none[F, A], OptionT.some[F](_)) + } +} \ No newline at end of file diff --git a/app/controllers/Organizations.scala b/app/controllers/Organizations.scala index ad24f02cc..3213407f1 100755 --- a/app/controllers/Organizations.scala +++ b/app/controllers/Organizations.scala @@ -13,9 +13,10 @@ import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer import views.{html => views} +import util.instances.future._ +import util.functional.Id -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} /** * Controller for handling Organization based actions. @@ -29,7 +30,7 @@ class Organizations @Inject()(forms: OreForms, implicit override val config: OreConfig, implicit override val service: ModelService, implicit override val cache: AsyncCacheApi, - implicit override val messagesApi: MessagesApi) extends OreBaseController { + implicit override val messagesApi: MessagesApi)(implicit val ec: ExecutionContext) extends OreBaseController { private def EditOrganizationAction(organization: String) = AuthedOrganizationAction(organization, requireUnlock = true) andThen OrganizationPermissionAction(EditSettings) @@ -67,16 +68,12 @@ class Organizations @Inject()(forms: OreForms, else if (!this.config.orgs.get[Boolean]("enabled")) Future.successful(Redirect(failCall).withError("error.org.disabled")) else { - this.forms.OrganizationCreate.bindFromRequest().fold( - hasErrors => Future.successful(FormError(failCall, hasErrors)), - formData => { - this.organizations.create(formData.name, user.id.get, formData.build()) map { - case Left(error) => Redirect(failCall).withError(error) - case Right(organization) => - Redirect(routes.Users.showProjects(organization.name, None)) - } - } - ) + bindFormEitherT[Future](this.forms.OrganizationCreate)(hasErrors => FormError(failCall, hasErrors)).flatMap { formData => + this.organizations.create(formData.name, user.id.get, formData.build()).bimap( + error => Redirect(failCall).withError(error), + organization => Redirect(routes.Users.showProjects(organization.name, None)) + ) + }.merge } } } @@ -90,26 +87,21 @@ class Organizations @Inject()(forms: OreForms, */ def setInviteStatus(id: Int, status: String) = Authenticated.async { implicit request => val user = request.user - user.organizationRoles.get(id).flatMap { - case None => Future.successful(notFound) - case Some(role) => - role.organization.map { orga => - status match { - case STATUS_DECLINE => - orga.memberships.removeRole(role) - Ok - case STATUS_ACCEPT => - role.setAccepted(true) - Ok - case STATUS_UNACCEPT => - role.setAccepted(false) - Ok - case _ => - BadRequest - } - } - - } + user.organizationRoles.get(id).map { role => + status match { + case STATUS_DECLINE => + role.organization.foreach(_.memberships.removeRole(role)) + Ok + case STATUS_ACCEPT => + role.setAccepted(true) + Ok + case STATUS_UNACCEPT => + role.setAccepted(false) + Ok + case _ => + BadRequest + } + }.getOrElse(notFound) } /** @@ -130,12 +122,15 @@ class Organizations @Inject()(forms: OreForms, * @return Redirect to Organization page */ def removeMember(organization: String) = EditOrganizationAction(organization).async { implicit request => - this.users.withName(this.forms.OrganizationMemberRemove.bindFromRequest.get.trim).map { - case None => BadRequest - case Some(user) => - request.data.orga.memberships.removeMember(user) - Redirect(ShowUser(organization)) + val res = for { + name <- bindFormOptionT[Future](this.forms.OrganizationMemberRemove) + user <- this.users.withName(name) + } yield { + request.data.orga.memberships.removeMember(user) + Redirect(ShowUser(organization)) } + + res.getOrElse(BadRequest) } /** @@ -145,8 +140,10 @@ class Organizations @Inject()(forms: OreForms, * @return Redirect to Organization page */ def updateMembers(organization: String) = EditOrganizationAction(organization) { implicit request => - this.forms.OrganizationUpdateMembers.bindFromRequest.get.saveTo(request.data.orga) - Redirect(ShowUser(organization)) + bindFormOptionT[Id](this.forms.OrganizationUpdateMembers).map { update => + update.saveTo(request.data.orga) + Redirect(ShowUser(organization)) + }.getOrElse(BadRequest) } } diff --git a/app/controllers/Reviews.scala b/app/controllers/Reviews.scala index 3004936fd..d619e770e 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -20,13 +20,16 @@ import ore.user.notification.NotificationTypes import ore.{OreConfig, OreEnv} import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi +import play.api.mvc.Result import security.spauth.SingleSignOnConsumer import slick.lifted.{Rep, TableQuery} import util.DataHelper import views.{html => views} +import util.instances.future._ +import util.syntax._ +import util.functional.EitherT -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} /** * Controller for handling Review related actions. @@ -39,95 +42,89 @@ final class Reviews @Inject()(data: DataHelper, implicit override val env: OreEnv, implicit override val cache: AsyncCacheApi, implicit override val config: OreConfig, - implicit override val service: ModelService) + implicit override val service: ModelService)(implicit val ec: ExecutionContext) extends OreBaseController { - def showReviews(author: String, slug: String, versionString: String) = { + def showReviews(author: String, slug: String, versionString: String) = (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects) andThen ProjectAction(author, slug)).async { request => implicit val r = request.request - implicit val data = request.data - implicit val p = data.project - - withVersionAsync(versionString) { implicit version => - version.mostRecentReviews.flatMap { reviews => - val unfinished = reviews.filter(r => r.createdAt.isDefined && r.endedAt.isEmpty).sorted(Review.ordering2).headOption - Future.sequence(reviews.map { r => - r.userBase.get(r.userId).map { u => - (r, u.map(_.name)) - } - }) map { rv => - //implicit val m = messagesApi.preferred(request) - Ok(views.users.admin.reviews(unfinished, rv)) - } - } - } + implicit val p = request.data.project + + val res = for { + version <- getVersion(p, versionString) + reviews <- EitherT.right[Result](version.mostRecentReviews) + rv <- EitherT.right[Result]( + Future.traverse(reviews)(r => r.userBase.get(r.userId).map(_.name).value.tupleLeft(r)) + ) + } yield { + val unfinished = reviews.filter(r => r.createdAt.isDefined && r.endedAt.isEmpty).sorted(Review.ordering2).headOption + implicit val v: Version = version + Ok(views.users.admin.reviews(unfinished, rv)) } + + res.merge } def createReview(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => - withProjectAsync(author, slug) { implicit project => - withVersion(versionString) { implicit version => - val review = new Review(Some(1), Some(Timestamp.from(Instant.now())), version.id.get, request.user.id.get, None, "") - this.service.insert(review) - Redirect(routes.Reviews.showReviews(author, slug, versionString)) - } - } + getProjectVersion(author, slug, versionString).map { version => + val review = new Review(Some(1), Some(Timestamp.from(Instant.now())), version.id.get, request.user.id.get, None, "") + this.service.insert(review) + Redirect(routes.Reviews.showReviews(author, slug, versionString)) + }.merge } } def reopenReview(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => - withProjectAsync(author, slug) { implicit project => - withVersionAsync(versionString) { version => - version.mostRecentReviews.map(_.headOption).map { - case None => NotFound - case Some(review) => - version.setReviewed(false) - version.setApprovedAt(null) - version.setReviewerId(-1) - review.setEnded(None) - review.addMessage(Message("Reopened the review", System.currentTimeMillis(), "start")) - Redirect(routes.Reviews.showReviews(author, slug, versionString)) - } - } + val res = for { + version <- getProjectVersion(author, slug, versionString) + review <- EitherT.fromOptionF(version.mostRecentReviews.map(_.headOption), notFound) + } yield { + version.setReviewed(false) + version.setApprovedAt(null) + version.setReviewerId(-1) + review.setEnded(None) + review.addMessage(Message("Reopened the review", System.currentTimeMillis(), "start")) + Redirect(routes.Reviews.showReviews(author, slug, versionString)) } + + res.merge } } def stopReview(author: String, slug: String, versionString: String) = { Authenticated andThen PermissionAction[AuthRequest](ReviewProjects) async { implicit request => - withProjectAsync(author, slug) { implicit project => - withVersionAsync(versionString) { version => - version.mostRecentUnfinishedReview.map { - case None => NotFound - case Some(review) => - review.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim, System.currentTimeMillis(), "stop")) - review.setEnded(Some(Timestamp.from(Instant.now()))) - Redirect(routes.Reviews.showReviews(author, slug, versionString)) - } - } + val res = for { + version <- getProjectVersion(author, slug, versionString) + review <- version.mostRecentUnfinishedReview.toRight(notFound) + message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest: Result) + } yield { + review.addMessage(Message(message.trim, System.currentTimeMillis(), "stop")) + review.setEnded(Some(Timestamp.from(Instant.now()))) + Redirect(routes.Reviews.showReviews(author, slug, versionString)) } + + res.merge } } def approveReview(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => - withProjectAsync(author, slug) { implicit project => - withVersionAsync(versionString) { version => - version.mostRecentUnfinishedReview.flatMap { - case None => Future.successful(NotFound) - case Some(review) => - for { - (_, _) <- review.setEnded(Some(Timestamp.from(Instant.now()))) zip - // send notification that review happened - sendReviewNotification(project, version, request.user) - } yield { - Redirect(routes.Reviews.showReviews(author, slug, versionString)) - } - } - } - } + val ret = for { + project <- getProject(author, slug) + version <- getVersion(project, versionString) + review <- version.mostRecentUnfinishedReview.toRight(notFound) + _ <- EitherT.right[Result]( + ( + review.setEnded(Some(Timestamp.from(Instant.now()))), + // send notification that review happened + sendReviewNotification(project, version, request.user) + ).parTupled + ) + } yield Redirect(routes.Reviews.showReviews(author, slug, versionString)) + + ret.merge } } @@ -186,63 +183,62 @@ final class Reviews @Inject()(data: DataHelper, def takeoverReview(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => - withProjectAsync(author, slug) { implicit project => - withVersionAsync(versionString) { version => + val ret = for { + version <- getProjectVersion(author, slug, versionString) + message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest) + _ <- { // Close old review - val closeOldReview = version.mostRecentUnfinishedReview.flatMap { - case None => Future.successful(true) - case Some(oldreview) => - for { - (_, _) <- oldreview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim, System.currentTimeMillis(), "takeover")) zip - oldreview.setEnded(Some(Timestamp.from(Instant.now()))) - } yield {} - } + val closeOldReview = version.mostRecentUnfinishedReview.semiFlatMap { oldreview => + ( + oldreview.addMessage(Message(message.trim, System.currentTimeMillis(), "takeover")), + oldreview.setEnded(Some(Timestamp.from(Instant.now()))), + this.service.insert(Review(Some(1), Some(Timestamp.from(Instant.now())), version.id.get, request.user.id.get, None, "")) + ).parMapN { (_, _, _) => () } + }.getOrElse(()) // Then make new one - for { - (_, _) <- closeOldReview zip - this.service.insert(Review(Some(1), Some(Timestamp.from(Instant.now())), version.id.get, request.user.id.get, None, "")) - } yield { - Redirect(routes.Reviews.showReviews(author, slug, versionString)) - } + val result = ( + closeOldReview, + this.service.insert(Review(Some(1), Some(Timestamp.from(Instant.now())), version.id.get, request.user.id.get, None, "")) + ).parTupled + EitherT.right[Result](result) } - } + } yield Redirect(routes.Reviews.showReviews(author, slug, versionString)) + + ret.merge } } def editReview(author: String, slug: String, versionString: String, reviewId: Int) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => - withProjectAsync(author, slug) { implicit project => - withVersionAsync(versionString) { version => - version.reviewById(reviewId).map { - case None => NotFound - case Some(review) => - review.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim)) - Ok("Review" + review) - } - } + val res = for { + version <- getProjectVersion(author, slug, versionString) + review <- version.reviewById(reviewId).toRight(notFound) + message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest: Result) + } yield { + review.addMessage(Message(message.trim)) + Ok("Review" + review) } + + res.merge } } def addMessage(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => - withProjectAsync(author, slug) { implicit project => - withVersionAsync(versionString) { version => - version.mostRecentUnfinishedReview.flatMap { - case None => Future.successful(Ok("Review")) - case Some(recentReview) => - users.current.flatMap { - case None => Future.successful(1) - case Some(currentUser) => - if (recentReview.userId == currentUser.userId) { - recentReview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim)) - } else Future.successful(0) - - }.map( _ => Ok("Review")) - } + val ret = for { + version <- getProjectVersion(author, slug, versionString) + recentReview <- version.mostRecentUnfinishedReview.toRight(Ok("Review")) + currentUser <- users.current.toRight(Ok("Review")) + message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest) + _ <- { + if (recentReview.userId == currentUser.userId) { + EitherT.right[Result](recentReview.addMessage(Message(message.trim))) + } else EitherT.rightT[Future, Result](0) } - } + } yield Ok("Review") + + ret.merge } } } diff --git a/app/controllers/Users.scala b/app/controllers/Users.scala index 127d1628b..a744f3e3d 100755 --- a/app/controllers/Users.scala +++ b/app/controllers/Users.scala @@ -23,9 +23,10 @@ import play.api.i18n.MessagesApi import play.api.mvc._ import security.spauth.SingleSignOnConsumer import views.{html => views} +import util.instances.future._ +import util.syntax._ -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} /** * Controller for general user actions. @@ -42,7 +43,7 @@ class Users @Inject()(fakeUser: FakeUser, implicit override val env: OreEnv, implicit override val config: OreConfig, implicit override val cache: AsyncCacheApi, - implicit override val service: ModelService) extends OreBaseController { + implicit override val service: ModelService)(implicit val ec: ExecutionContext) extends OreBaseController { private val baseUrl = this.config.app.get[String]("baseUrl") @@ -76,17 +77,16 @@ class Users @Inject()(fakeUser: FakeUser, Future.successful(redirectToSso(this.sso.getLoginUrl(this.baseUrl + "/login", nonce))) } else { // Redirected from SpongeSSO, decode SSO payload and convert to Ore user - this.sso.authenticate(sso.get, sig.get)(isNonceValid) flatMap { - case None => - Future.successful(Redirect(ShowHome).withError("error.loginFailed")) - case Some(spongeUser) => - // Complete authentication - User.fromSponge(spongeUser).flatMap(this.users.getOrCreate).flatMap { user => - user.pullForumData().flatMap(_.pullSpongeData()).flatMap { u => - this.redirectBack(request.flash.get("url").getOrElse("/"), u) - } - } - } + this.sso.authenticate(sso.get, sig.get)(isNonceValid).semiFlatMap { spongeUser => + // Complete authentication + val fromSponge = User.fromSponge(spongeUser) + for { + user <- this.users.getOrCreate(fromSponge) + _ <- user.pullForumData() + _ <- user.pullSpongeData() + result <- this.redirectBack(request.flash.get("url").getOrElse("/"), user) + } yield result + }.getOrElse(Redirect(ShowHome).withError("error.loginFailed")) } } @@ -133,27 +133,27 @@ class Users @Inject()(fakeUser: FakeUser, val pageSize = this.config.users.get[Int]("project-page-size") val p = page.getOrElse(1) val offset = (p - 1) * pageSize - this.users.withName(username).flatMap { - case None => Future.successful(notFound) - case Some(user) => - for { - // TODO include orga projects? - projectSeq <- service.DB.db.run(queryUserProjects(user).drop(offset).take(pageSize).result) - tags <- Future.sequence(projectSeq.map(_._2.tags)) - userData <- getUserData(request, username) - starred <- user.starred() - starredRv <- Future.sequence(starred.map(_.recommendedVersion)) - orga <- getOrga(request, username) - orgaData <- OrganizationData.of(orga) - scopedOrgaData <- ScopedOrganizationData.of(request.currentUser, orga) - } yield { - val data = projectSeq zip tags map { case ((p, v), tags) => - (p, user, v, tags) - } - val starredData = starred zip starredRv - Ok(views.users.projects(userData.get, orgaData.flatMap(a => scopedOrgaData.map(b => (a, b))), data, starredData, p)) + this.users.withName(username).semiFlatMap { user => + for { + // TODO include orga projects? + projectSeq <- service.DB.db.run(queryUserProjects(user).drop(offset).take(pageSize).result) + starred <- user.starred() + orga <- getOrga(request, username).value + (tags, userData, starredRv, orgaData, scopedOrgaData) <- ( + Future.sequence(projectSeq.map(_._2.tags)), + getUserData(request, username).value, + Future.sequence(starred.map(_.recommendedVersion)), + OrganizationData.of(orga).value, + ScopedOrganizationData.of(request.currentUser, orga).value + ).parTupled + } yield { + val data = projectSeq zip tags map { case ((p, v), tags) => + (p, user, v, tags) } - } + val starredData = starred zip starredRv + Ok(views.users.projects(userData.get, orgaData.flatMap(a => scopedOrgaData.map(b => (a, b))), data, starredData, p)) + } + }.getOrElse(notFound) } private def queryUserProjects(user: User) = { @@ -183,18 +183,21 @@ class Users @Inject()(fakeUser: FakeUser, * @return View of user page */ def saveTagline(username: String) = UserAction(username).async { implicit request => - val tagline = this.forms.UserTagline.bindFromRequest.get.trim val maxLen = this.config.users.get[Int]("max-tagline-len") - this.users.withName(username).map { - case None => NotFound - case Some(user) => - if (tagline.length > maxLen) { - Redirect(ShowUser(user)).flashing("error" -> this.messagesApi("error.tagline.tooLong", maxLen)) - } else { - user.setTagline(tagline) - Redirect(ShowUser(user)) - } + + val res = for { + user <- this.users.withName(username).toRight(NotFound) + tagline <- bindFormEitherT[Future](this.forms.UserTagline)(_ => BadRequest) + } yield { + if (tagline.length > maxLen) { + Redirect(ShowUser(user)).flashing("error" -> this.messagesApi("error.tagline.tooLong", maxLen)) + } else { + user.setTagline(tagline) + Redirect(ShowUser(user)) + } } + + res.merge } /** @@ -295,29 +298,21 @@ class Users @Inject()(fakeUser: FakeUser, // Get visible notifications val nFilter: NotificationFilter = notificationFilter - .map(str => NotificationFilters.values - .find(_.name.equalsIgnoreCase(str)) - .getOrElse(NotificationFilters.Unread)) + .flatMap(str => NotificationFilters.values.find(_.name.equalsIgnoreCase(str))) .getOrElse(NotificationFilters.Unread) - nFilter(user.notifications).flatMap { l => - Future.sequence(l.map(notif => notif.origin.map((notif, _)))) - } flatMap { notifications => - // Get visible invites - val iFilter: InviteFilter = inviteFilter - .map(str => InviteFilters.values - .find(_.name.equalsIgnoreCase(str)) - .getOrElse(InviteFilters.All)) - .getOrElse(InviteFilters.All) - - iFilter(user).flatMap { invites => - Future.sequence(invites.map {invite => invite.subject.map((invite, _))}) - } map { invites => - Ok(views.users.notifications( - notifications, - invites, - nFilter, iFilter)) - } + val iFilter: InviteFilter = inviteFilter + .flatMap(str => InviteFilters.values.find(_.name.equalsIgnoreCase(str))) + .getOrElse(InviteFilters.All) + + val notificationsFut = nFilter(user.notifications).flatMap(l => Future.sequence(l.map(notif => notif.origin.map((notif, _))))) + val invitesFut = iFilter(user).flatMap(invites => Future.sequence(invites.map {invite => invite.subject.map((invite, _))})) + + (notificationsFut, invitesFut).parMapN { (notifications, invites) => + Ok(views.users.notifications( + notifications, + invites, + nFilter, iFilter)) } } } @@ -329,12 +324,10 @@ class Users @Inject()(fakeUser: FakeUser, * @return Ok if marked as read, NotFound if notification does not exist */ def markNotificationRead(id: Int) = Authenticated.async { implicit request => - request.user.notifications.get(id) map { - case None => notFound - case Some(notification) => - notification.setRead(read = true) - Ok - } + request.user.notifications.get(id).map { notification => + notification.setRead(read = true) + Ok + }.getOrElse(notFound) } /** diff --git a/app/controllers/project/Channels.scala b/app/controllers/project/Channels.scala index 05d73dba1..4aaf4465a 100755 --- a/app/controllers/project/Channels.scala +++ b/app/controllers/project/Channels.scala @@ -12,9 +12,12 @@ import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer import views.html.projects.{channels => views} +import util.instances.future._ +import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import models.project.Project +import util.functional.EitherT +import util.syntax._ /** * Controller for handling Channel related actions. @@ -27,7 +30,7 @@ class Channels @Inject()(forms: OreForms, implicit override val messagesApi: MessagesApi, implicit override val env: OreEnv, implicit override val config: OreConfig, - implicit override val service: ModelService) + implicit override val service: ModelService)(implicit val ec: ExecutionContext) extends OreBaseController { private val self = controllers.project.routes.Channels @@ -61,19 +64,12 @@ class Channels @Inject()(forms: OreForms, * @return Redirect to view of channels */ def create(author: String, slug: String) = ChannelEditAction(author, slug).async { implicit request => - this.forms.ChannelEdit.bindFromRequest.fold( - hasErrors => Future.successful(Redirect(self.showList(author, slug)).withError(hasErrors.errors.head.message)), - channelData => { - channelData.addTo(request.data.project).map { _.fold( - error => Redirect(self.showList(author, slug)).withError(error), - _ => { - Redirect(self.showList(author, slug)) - } + val res = for { + channelData <- bindFormEitherT[Future](this.forms.ChannelEdit)(hasErrors => Redirect(self.showList(author, slug)).withError(hasErrors.errors.head.message)) + _ <- channelData.addTo(request.data.project).leftMap(error => Redirect(self.showList(author, slug)).withError(error)) + } yield Redirect(self.showList(author, slug)) - ) - } - } - ) + res.merge } /** @@ -85,20 +81,14 @@ class Channels @Inject()(forms: OreForms, * @return View of channels */ def save(author: String, slug: String, channelName: String) = ChannelEditAction(author, slug).async { implicit request => - implicit val data = request.data - this.forms.ChannelEdit.bindFromRequest.fold( - hasErrors => - Future.successful(Redirect(self.showList(author, slug)).withError(hasErrors.errors.head.message)), - channelData => { - implicit val p = data.project - channelData.saveTo(channelName).map { _.map { error => - Redirect(self.showList(author, slug)).withError(error) - } getOrElse { - Redirect(self.showList(author, slug)) - } - } - } - ) + implicit val project: Project = request.data.project + + val res = for { + channelData <- bindFormEitherT[Future](this.forms.ChannelEdit)(hasErrors => Redirect(self.showList(author, slug)).withErrors(hasErrors.errors.flatMap(_.messages))) + _ <- channelData.saveTo(channelName).leftMap(errors => Redirect(self.showList(author, slug)).withErrors(errors)) + } yield Redirect(self.showList(author, slug)) + + res.merge } /** @@ -112,32 +102,27 @@ class Channels @Inject()(forms: OreForms, */ def delete(author: String, slug: String, channelName: String) = ChannelEditAction(author, slug).async { implicit request => implicit val data = request.data - data.project.channels.all.flatMap { channels => - if (channels.size == 1) { - Future.successful(Redirect(self.showList(author, slug)).withError("error.channel.last")) - } else { - channels.find(c => c.name.equals(channelName)) match { - case None => Future.successful(NotFound) - case Some(channel) => - for { - nonEmpty <- channel.versions.nonEmpty - channelCount <- Future.sequence(channels.map(_.versions.nonEmpty)).map(l => l.count(_ == true)) - } yield { - if (nonEmpty && channelCount == 1) { - Redirect(self.showList(author, slug)).withError("error.channel.lastNonEmpty") - } else { - val reviewedChannels = channels.filter(!_.isNonReviewed) - if (!channel.isNonReviewed && reviewedChannels.size <= 1 && reviewedChannels.contains(channel)) { - Redirect(self.showList(author, slug)).withError("error.channel.lastReviewed") - } else { - this.projects.deleteChannel(channel) - Redirect(self.showList(author, slug)) - } - } - } - - } - } - } + EitherT.right[Status](data.project.channels.all) + .filterOrElse(_.size != 1, Redirect(self.showList(author, slug)).withError("error.channel.last")) + .flatMap { channels => + EitherT.fromEither[Future](channels.find(_.name == channelName).toRight(NotFound)) + .semiFlatMap { channel => + (channel.versions.isEmpty, Future.traverse(channels.toSeq)(_.versions.nonEmpty).map(_.count(identity))) + .parTupled + .tupleRight(channel) + } + .filterOrElse( + { case ((emptyChannel, nonEmptyChannelCount), _) => + emptyChannel || nonEmptyChannelCount > 1}, + Redirect(self.showList(author, slug)).withError("error.channel.lastNonEmpty") + ) + .map(_._2) + .filterOrElse( + channel => channel.isNonReviewed || channels.count(_.isReviewed) > 1, + Redirect(self.showList(author, slug)).withError("error.channel.lastReviewed") + ) + .semiFlatMap(channel => this.projects.deleteChannel(channel)) + .map(_ => Redirect(self.showList(author, slug))) + }.merge } } diff --git a/app/controllers/project/Pages.scala b/app/controllers/project/Pages.scala index f8486fb99..011153445 100755 --- a/app/controllers/project/Pages.scala +++ b/app/controllers/project/Pages.scala @@ -14,9 +14,9 @@ import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer import util.StringUtils._ import views.html.projects.{pages => views} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import util.instances.future._ +import util.syntax._ +import scala.concurrent.{ExecutionContext, Future} /** * Controller for handling Page related actions. @@ -29,7 +29,7 @@ class Pages @Inject()(forms: OreForms, implicit override val messagesApi: MessagesApi, implicit override val env: OreEnv, implicit override val config: OreConfig, - implicit override val service: ModelService) + implicit override val service: ModelService)(implicit val ec: ExecutionContext) extends OreBaseController { private val self = controllers.project.routes.Pages @@ -45,17 +45,20 @@ class Pages @Inject()(forms: OreForms, * @return Tuple: Optional Page, true if using legacy fallback */ def withPage(project: Project, page: String): Future[(Option[Page], Boolean)] = { + //TODO: Can the return type here be changed to OptionT[Future (Page, Boolean)]? val parts = page.split("/") if (parts.size == 2) { - project.pages.find(equalsIgnoreCase(_.slug, parts(0))).map { - _.flatMap(_.id).getOrElse(-1) - } flatMap { parentId => - project.pages.filter(equalsIgnoreCase(_.slug, parts(1))).map(seq => seq.find(_.parentId == parentId)).map((_, false)) - } + project.pages + .find(equalsIgnoreCase(_.slug, parts(0))) + .subflatMap(_.id) + .getOrElse(-1) + .flatMap { parentId => + project.pages.filter(equalsIgnoreCase(_.slug, parts(1))).map(seq => seq.find(_.parentId == parentId)).map((_, false)) + } } else { - project.pages.find((ModelFilter[Page](_.slug === parts(0)) +&& ModelFilter[Page](_.parentId === -1)).fn).flatMap { + project.pages.find((ModelFilter[Page](_.slug === parts(0)) +&& ModelFilter[Page](_.parentId === -1)).fn).value.flatMap { case Some(r) => Future.successful((Some(r), false)) - case None => project.pages.find(ModelFilter[Page](_.slug === parts(0)).fn).map((_, true)) + case None => project.pages.find(ModelFilter[Page](_.slug === parts(0)).fn).value.map((_, true)) } } } @@ -98,25 +101,19 @@ class Pages @Inject()(forms: OreForms, implicit val r = request.request val data = request.data val parts = pageName.split("/") - val p = if (parts.size != 2) Future.successful((parts(0), -1)) - else { - data.project.pages.find(equalsIgnoreCase(_.slug, parts(0))).map(_.flatMap(_.id).getOrElse(-1)) - .map((parts(1), _)) - } - p.flatMap { - case (name, parentId) => - data.project.pages.find(equalsIgnoreCase(_.slug, name)).flatMap { - case Some(page) => Future.successful(page) - case None => - data.project.getOrCreatePage(name, parentId) - } - } flatMap { p => - projects.queryProjectPages(data.project) map { pages => - val pageCount = pages.size + pages.map(_._2.size).sum - val parentPage = if (pages.contains(p)) None - else pages.collectFirst { case (pp, page) if page.contains(p) => pp } - Ok(views.view(data, request.scoped, pages, p, parentPage, pageCount, editorOpen = true)) + + for { + (name, parentId) <- if (parts.size != 2) Future.successful((parts(0), -1)) else { + data.project.pages.find(equalsIgnoreCase(_.slug, parts(0))).subflatMap(_.id).getOrElse(-1).map((parts(1), _)) } + (p, pages) <- ( + data.project.pages.find(equalsIgnoreCase(_.slug, name)).getOrElseF(data.project.getOrCreatePage(name, parentId)), + projects.queryProjectPages(data.project) + ).parTupled + } yield { + val pageCount = pages.size + pages.map(_._2.size).sum + val parentPage = pages.collectFirst { case (pp, page) if page.contains(p) => pp } + Ok(views.view(data, request.scoped, pages, p, parentPage, pageCount, editorOpen = true)) } } @@ -156,8 +153,7 @@ class Pages @Inject()(forms: OreForms, val parts = page.split("/") val created = if (parts.size == 2) { - data.project.pages.find(equalsIgnoreCase(_.slug, parts(0))).flatMap { parent => - val parentId = parent.flatMap(_.id).getOrElse(-1) + data.project.pages.find(equalsIgnoreCase(_.slug, parts(0))).subflatMap(_.id).getOrElse(-1).flatMap { parentId => val pageName = pageData.name.getOrElse(parts(1)) data.project.getOrCreatePage(pageName, parentId, pageData.content) } diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index f83391a86..f5976dcf4 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -24,10 +24,13 @@ import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer import views.html.{projects => views} import db.impl.OrePostgresDriver.api._ - import scala.collection.JavaConverters._ -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} + +import play.api.mvc.Result +import util.functional.{EitherT, Id, OptionT} +import util.instances.future._ +import util.syntax._ /** * Controller for handling Project related actions. @@ -43,7 +46,7 @@ class Projects @Inject()(stats: StatTracker, implicit override val messagesApi: MessagesApi, implicit override val env: OreEnv, implicit override val config: OreConfig, - implicit override val service: ModelService) + implicit override val service: ModelService)(implicit val ec: ExecutionContext) extends OreBaseController { @@ -114,8 +117,7 @@ class Projects @Inject()(stats: StatTracker, Future.successful(Redirect(self.showCreator())) case Some(pending) => for { - orgas <- request.user.organizations.all - owner <- pending.underlying.owner.user + (orgas, owner) <- (request.user.organizations.all, pending.underlying.owner.user).parTupled createOrga <- Future.sequence(orgas.map(orga => owner can CreateProject in orga)) } yield { val createdOrgas = orgas zip createOrga filter (_._2) map (_._1) @@ -150,11 +152,11 @@ class Projects @Inject()(stats: StatTracker, implicit val currentUser = request.user val authors = pendingProject.file.meta.get.getAuthors.asScala - for { - users <- Future.sequence(authors.filterNot(_.equals(currentUser.username)).map(this.users.withName)) - registered <- this.forums.countUsers(authors.toList) - owner <- pendingProject.underlying.owner.user - } yield { + ( + Future.sequence(authors.filterNot(_.equals(currentUser.username)).map(this.users.withName(_).value)), + this.forums.countUsers(authors.toList), + pendingProject.underlying.owner.user + ).parMapN { (users, registered, owner) => Ok(views.invite(owner, pendingProject, users.flatten.toList, registered)) } } @@ -164,17 +166,16 @@ class Projects @Inject()(stats: StatTracker, } private def orgasUserCanUploadTo(user: User): Future[Set[Int]] = { - user.organizations.all.flatMap { all => - - Future.sequence(all.map { org => - user can CreateProject in org map { perm => - (org.id.get, perm) - } - }) map { - _.filter(_._2).map(_._1) // Filter by can Create Project - } map { - _ + user.id.get // Add self + for { + all <- user.organizations.all + canCreate <- Future.traverse(all)(org => user can CreateProject in org map { perm => (org.id.get, perm)}) + } yield { + // Filter by can Create Project + val others = canCreate.collect { + case (id, perm) if perm => id } + + others + user.id.get // Add self } } @@ -187,14 +188,16 @@ class Projects @Inject()(stats: StatTracker, * @return Redirection to project page if successful */ def showFirstVersionCreator(author: String, slug: String) = UserLock() { implicit request => - this.factory.getPendingProject(author, slug) match { - case None => - Redirect(self.showCreator()) - case Some(pendingProject) => - pendingProject.roles = this.forms.ProjectMemberRoles.bindFromRequest.get.build() - val pendingVersion = pendingProject.pendingVersion - Redirect(routes.Versions.showCreatorWithMeta(author, slug, pendingVersion.underlying.versionString)) + val res = for { + pendingProject <- EitherT.fromOption[Id](this.factory.getPendingProject(author, slug), Redirect(self.showCreator())) + roles <- bindFormEitherT[Id](this.forms.ProjectMemberRoles)(_ => BadRequest: Result) + } yield { + pendingProject.roles = roles.build() + val pendingVersion = pendingProject.pendingVersion + Redirect(routes.Versions.showCreatorWithMeta(author, slug, pendingVersion.underlying.versionString)) } + + res.merge } /** @@ -221,9 +224,8 @@ class Projects @Inject()(stats: StatTracker, * @return Redirect to project page. */ def showProjectById(pluginId: String) = OreAction async { implicit request => - this.projects.withPluginId(pluginId).map { - case None => notFound - case Some(project) => Redirect(self.show(project.ownerName, project.slug)) + this.projects.withPluginId(pluginId).fold(notFound) { project => + Redirect(self.show(project.ownerName, project.slug)) } } @@ -256,20 +258,16 @@ class Projects @Inject()(stats: StatTracker, Future.successful(BadRequest) else { // Do forum post and display errors to user if any - val poster = formData.poster match { - case None => Future.successful(request.user) - case Some(posterName) => - this.users.requestPermission(request.user, posterName, PostAsOrganization).map { - case None => request.user // No Permission ; Post as self instead - case Some(user) => user // Permission granted - } - } - val errors = poster.flatMap { post => - this.forums.postDiscussionReply(data.project, post, formData.content) - } - errors.map { errList => + for { + poster <- { + OptionT.fromOption[Future](formData.poster) + .flatMap(posterName => this.users.requestPermission(request.user, posterName, PostAsOrganization)) + .getOrElse(request.user) + } + errors <- this.forums.postDiscussionReply(data.project, poster, formData.content) + } yield { val result = Redirect(self.showDiscussion(author, slug)) - if (errList.nonEmpty) result.withError(errList.head) else result + if (errors.nonEmpty) result.withError(errors.head) else result } } } @@ -316,16 +314,14 @@ class Projects @Inject()(stats: StatTracker, */ def showIcon(author: String, slug: String) = Action async { implicit request => // TODO maybe instead of redirect cache this on ore? - this.projects.withSlug(author, slug).flatMap { - case None => Future.successful(NotFound) - case Some(project) => - this.projects.fileManager.getIconPath(project) match { - case None => - project.owner.user.map(_.avatarUrl.map(Redirect(_)).getOrElse(NotFound)) - case Some(iconPath) => - Future.successful(showImage(iconPath)) - } - } + this.projects.withSlug(author, slug).semiFlatMap { project => + this.projects.fileManager.getIconPath(project) match { + case None => + project.owner.user.map(_.avatarUrl.map(Redirect(_)).getOrElse(NotFound)) + case Some(iconPath) => + Future.successful(showImage(iconPath)) + } + }.getOrElse(NotFound) } private def showImage(path: Path) = Ok(Files.readAllBytes(path)).as("image/jpeg") @@ -396,26 +392,24 @@ class Projects @Inject()(stats: StatTracker, */ def setInviteStatus(id: Int, status: String) = Authenticated.async { implicit request => val user = request.user - user.projectRoles.get(id).flatMap { - case None => Future.successful(NotFound) - case Some(role) => - role.project.map { project => - val dossier = project.memberships - status match { - case STATUS_DECLINE => - dossier.removeRole(role) - Ok - case STATUS_ACCEPT => - role.setAccepted(true) - Ok - case STATUS_UNACCEPT => - role.setAccepted(false) - Ok - case _ => - BadRequest - } + user.projectRoles.get(id).semiFlatMap { role => + role.project.map { project => + val dossier = project.memberships + status match { + case STATUS_DECLINE => + dossier.removeRole(role) + Ok + case STATUS_ACCEPT => + role.setAccepted(true) + Ok + case STATUS_UNACCEPT => + role.setAccepted(false) + Ok + case _ => + BadRequest } - } + } + }.getOrElse(NotFound) } /** @@ -428,7 +422,7 @@ class Projects @Inject()(stats: StatTracker, def showSettings(author: String, slug: String) = SettingsEditAction(author, slug) async { request => implicit val r = request.request val projectData = request.data - projectData.project.apiKeys.find(_.keyType === ProjectApiKeyTypes.Deployment).map { deployKey => + projectData.project.apiKeys.find(_.keyType === ProjectApiKeyTypes.Deployment).value.map { deployKey => Ok(views.settings(projectData, request.scoped, deployKey)) } } @@ -495,12 +489,15 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug */ def removeMember(author: String, slug: String) = SettingsEditAction(author, slug).async { implicit request => - this.users.withName(this.forms.ProjectMemberRemove.bindFromRequest.get.trim).map { - case None => BadRequest - case Some(user) => - request.data.project.memberships.removeMember(user) - Redirect(self.showSettings(author, slug)) + val res = for { + name <- bindFormOptionT[Future](this.forms.ProjectMemberRemove) + user <- this.users.withName(name) + } yield { + request.data.project.memberships.removeMember(user) + Redirect(self.showSettings(author, slug)) } + + res.getOrElse(BadRequest) } /** @@ -533,15 +530,16 @@ class Projects @Inject()(stats: StatTracker, * @return Project homepage */ def rename(author: String, slug: String) = SettingsEditAction(author, slug).async { implicit request => - val newName = compact(this.forms.ProjectRename.bindFromRequest.get) - projects.isNamespaceAvailable(author, slugify(newName)).flatMap { - case false => Future.successful(Redirect(self.showSettings(author, slug)).withError("error.nameUnavailable")) - case true => - val data = request.data - this.projects.rename(data.project, newName).map { _ => - Redirect(self.show(author, data.project.slug)) - } - } + val project = request.data.project + + val res = for { + newName <- bindFormEitherT[Future](this.forms.ProjectRename)(_ => BadRequest).map(compact) + available <- EitherT.right[Result](projects.isNamespaceAvailable(author, slugify(newName))) + _ <- EitherT.cond[Future](available, (), Redirect(self.showSettings(author, slug)).withError("error.nameUnavailable")) + _ <- EitherT.right[Result](this.projects.rename(project, newName)) + } yield Redirect(self.show(author, project.slug)) + + res.merge } /** @@ -605,10 +603,8 @@ class Projects @Inject()(stats: StatTracker, implicit val r = request.request val project = request.data.project for { - changes <- project.visibilityChangesByDate - changedBy <- Future.sequence(changes.map(_.created)) - logger <- project.logger - logs <- logger.entries.all + (changes, logger) <- (project.visibilityChangesByDate, project.logger).parTupled + (changedBy, logs) <- (Future.sequence(changes.map(_.created.value)), logger.entries.all).parTupled } yield { val visChanges = changes zip changedBy Ok(views.log(project, visChanges, logs.toSeq)) @@ -625,10 +621,10 @@ class Projects @Inject()(stats: StatTracker, */ def delete(author: String, slug: String) = { (Authenticated andThen PermissionAction[AuthRequest](HardRemoveProject)).async { implicit request => - withProject(author, slug) { project => + getProject(author, slug).map { project => this.projects.delete(project) Redirect(ShowHome).withSuccess(this.messagesApi("project.deleted", project.name)) - } + }.merge } } @@ -656,9 +652,9 @@ class Projects @Inject()(stats: StatTracker, def showFlags(author: String, slug: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewFlags)) andThen ProjectAction(author, slug) async { request => implicit val r = request.request - withProject(author, slug) { project => + getProject(author, slug).map { project => Ok(views.admin.flags(request.data)) - } + }.merge } } @@ -670,20 +666,25 @@ class Projects @Inject()(stats: StatTracker, */ def showNotes(author: String, slug: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewFlags)).async { implicit request => - withProjectAsync(author, slug) { project => - Future.sequence(project.getNotes().map(note => users.get(note.user).map(user => (note, user)))) map { notes => + getProject(author, slug).semiFlatMap { project => + Future.sequence(project.getNotes().map(note => users.get(note.user).value.map(user => (note, user)))) map { notes => Ok(views.admin.notes(project, notes)) } - } + }.merge } } def addMessage(author: String, slug: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => - withProject(author, slug) { project => - project.addNote(Note(this.forms.NoteDescription.bindFromRequest.get.trim, request.user.userId)) + val res = for { + project <- getProject(author, slug) + description <- bindFormEitherT[Future](this.forms.NoteDescription)(_ => BadRequest: Result) + } yield { + project.addNote(Note(description.trim, request.user.userId)) Ok("Review") } + + res.merge } } } \ No newline at end of file diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index 8c2dac1fb..756450f3f 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -18,7 +18,7 @@ import models.project._ import models.viewhelper.{ProjectData, VersionData} import ore.permission.{EditVersions, ReviewProjects} import ore.project.factory.TagAlias.ProjectTag -import ore.project.factory.{PendingProject, ProjectFactory} +import ore.project.factory.{PendingProject, PendingVersion, ProjectFactory} import ore.project.io.DownloadTypes._ import ore.project.io.{DownloadTypes, InvalidPluginFileException, PluginFile, PluginUpload} import ore.{OreConfig, OreEnv, StatTracker} @@ -31,10 +31,12 @@ import play.filters.csrf.CSRF import security.spauth.SingleSignOnConsumer import util.JavaUtils.autoClose import util.StringUtils._ +import util.syntax._ import views.html.projects.{versions => views} +import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import util.functional.{EitherT, OptionT} +import util.instances.future._ /** * Controller for handling Version related actions. @@ -49,7 +51,7 @@ class Versions @Inject()(stats: StatTracker, implicit override val messagesApi: MessagesApi, implicit override val env: OreEnv, implicit override val config: OreConfig, - implicit override val service: ModelService) + implicit override val service: ModelService)(implicit val ec: ExecutionContext) extends OreBaseController { private val fileManager = this.projects.fileManager @@ -68,19 +70,14 @@ class Versions @Inject()(stats: StatTracker, * @return Version view */ def show(author: String, slug: String, versionString: String) = ProjectAction(author, slug) async { request => - implicit val data = request.data implicit val r = request.request - implicit val p = data.project - withVersionAsync(versionString) { version => - for { - data <- VersionData.of(request, version) - response <- this.stats.projectViewed(request) { request => - Ok(views.view(data, request.scoped)) - } - } yield { - response - } - } + val res = for { + version <- getVersion(request.data.project, versionString) + data <- EitherT.right[Result](VersionData.of(request, version)) + response <- EitherT.right[Result](this.stats.projectViewed(request)(request => Ok(views.view(data, request.scoped)))) + } yield response + + res.merge } /** @@ -94,12 +91,15 @@ class Versions @Inject()(stats: StatTracker, def saveDescription(author: String, slug: String, versionString: String) = { VersionEditAction(author, slug).async { request => implicit val r = request.request - implicit val data = request.data - implicit val p = data.project - withVersion(versionString) { version => - version.setDescription(this.forms.VersionDescription.bindFromRequest.get.trim) + val res = for { + version <- getVersion(request.data.project, versionString) + description <- bindFormEitherT[Future](this.forms.VersionDescription)(_ => BadRequest: Result) + } yield { + version.setDescription(description.trim) Redirect(self.show(author, slug, versionString)) } + + res.merge } } @@ -114,12 +114,10 @@ class Versions @Inject()(stats: StatTracker, def setRecommended(author: String, slug: String, versionString: String) = { VersionEditAction(author, slug).async { implicit request => implicit val r = request.request - implicit val data = request.data - implicit val p = data.project - withVersion(versionString) { version => - data.project.setRecommendedVersion(version) + getVersion(request.data.project, versionString).map { version => + request.data.project.setRecommendedVersion(version) Redirect(self.show(author, slug, versionString)) - } + }.merge } } @@ -134,15 +132,13 @@ class Versions @Inject()(stats: StatTracker, def approve(author: String, slug: String, versionString: String) = { (AuthedProjectAction(author, slug, requireUnlock = true) andThen ProjectPermissionAction(ReviewProjects)).async { implicit request => - implicit val project = request.data implicit val r = request.request - implicit val p = project.project - withVersion(versionString) { version => + getVersion(request.data.project, versionString).map { version => version.setReviewed(reviewed = true) version.setReviewer(request.user) version.setApprovedAt(this.service.theTime) Redirect(self.show(author, slug, versionString)) - } + }.merge } } @@ -215,28 +211,27 @@ class Versions @Inject()(stats: StatTracker, def upload(author: String, slug: String) = VersionEditAction(author, slug).async { implicit request => val call = self.showCreator(author, slug) val user = request.user - this.factory.getUploadError(user) match { - case Some(error) => - Future.successful(Redirect(call).withError(error)) - case None => - PluginUpload.bindFromRequest() match { - case None => - Future.successful(Redirect(call).withError("error.noFile")) - case Some(uploadData) => - try { - this.factory.processSubsequentPluginUpload(uploadData, user, request.data.project).map(_.fold( - err => Redirect(call).withError(err), - version => { - version.underlying.setAuthorId(user.id.getOrElse(-1)) - Redirect(self.showCreatorWithMeta(request.data.project.ownerName, slug, version.underlying.versionString)) - } - )) - } catch { - case e: InvalidPluginFileException => - Future.successful(Redirect(call).withError(Option(e.getMessage).getOrElse(""))) - } - } - } + + val uploadData = this.factory.getUploadError(user) + .map(error => Redirect(call).withError(error)) + .toLeft(()) + .flatMap(_ => PluginUpload.bindFromRequest().toRight(Redirect(call).withError("error.noFile"))) + + EitherT.fromEither[Future](uploadData).flatMap { data => + //TODO: We should get rid of this try + try { + this.factory + .processSubsequentPluginUpload(data, user, request.data.project) + .leftMap(err => Redirect(call).withError(err)) + } + catch { + case e: InvalidPluginFileException => + EitherT.leftT[Future, PendingVersion](Redirect(call).withError(Option(e.getMessage).getOrElse(""))) + } + }.map { pendingVersion => + pendingVersion.underlying.setAuthorId(user.id.getOrElse(-1)) + Redirect(self.showCreatorWithMeta(request.data.project.ownerName, slug, pendingVersion.underlying.versionString)) + }.merge } /** @@ -247,41 +242,30 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return Version create view */ - def showCreatorWithMeta(author: String, slug: String, versionString: String) = { + def showCreatorWithMeta(author: String, slug: String, versionString: String) = UserLock(ShowProject(author, slug)).async { implicit request => - // Get pending version - this.factory.getPendingVersion(author, slug, versionString) match { - case None => - Future.successful(Redirect(self.showCreator(author, slug))) - case Some(pendingVersion) => - // Get project - pendingOrReal(author, slug).flatMap { - case None => - Future.successful(Redirect(self.showCreator(author, slug))) - case Some(p) => p match { - case pending: PendingProject => - ProjectData.of(request, pending).map { data => - Ok(views.create(data, data.settings.forumSync, Some(pendingVersion), None, showFileControls = false)) - } - case real: Project => - for { - channels <- real.channels.toSeq - data <- ProjectData.of(real) - } yield { - Ok(views.create(data, data.settings.forumSync, Some(pendingVersion), Some(channels), showFileControls = true)) - } - } - } - } + val success = OptionT.fromOption[Future](this.factory.getPendingVersion(author, slug, versionString)) + // Get pending version + .flatMap(pendingVersion => pendingOrReal(author, slug).map(pendingVersion -> _)) + .semiFlatMap { + case (pendingVersion, Left(pending)) => + Future.successful((None, ProjectData.of(request, pending), pendingVersion)) + case (pendingVersion, Right(real)) => + (real.channels.toSeq, ProjectData.of(real)) + .parMapN((channels, data) => (Some(channels), data, pendingVersion)) + } + .map { case (channels, data, pendingVersion) => + Ok(views.create(data, data.settings.forumSync, Some(pendingVersion), channels, showFileControls = channels.isDefined)) + } + + success.getOrElse(Redirect(self.showCreator(author, slug))) } - } - private def pendingOrReal(author: String, slug: String): Future[Option[Any]] = { + private def pendingOrReal(author: String, slug: String): OptionT[Future, Either[PendingProject, Project]] = { // Returns either a PendingProject or existing Project - this.projects.withSlug(author, slug) map { - case None => this.factory.getPendingProject(author, slug) - case Some(project) => Some(project) - } + this.projects.withSlug(author, slug) + .map[Either[PendingProject, Project]](Right.apply) + .orElse(OptionT.fromOption[Future](this.factory.getPendingProject(author, slug)).map(Left.apply)) } /** @@ -320,33 +304,26 @@ class Versions @Inject()(stats: StatTracker, this.factory.getPendingProject(author, slug) match { case None => // No pending project, create version for existing project - withProjectAsync(author, slug) { project => - project.channels.find { - equalsIgnoreCase(_.name, pendingVersion.channelName) - } flatMap { - case None => versionData.addTo(project) - case Some(channel) => Future.successful(Right(channel)) - } flatMap { channelResult => - channelResult.fold( - error => { - Future.successful(Redirect(self.showCreatorWithMeta(author, slug, versionString)).withError(error)) - }, - _ => { - // Update description - versionData.content.foreach { content => - pendingVersion.underlying.setDescription(content.trim) - } - - pendingVersion.complete.map { newVersion => - if (versionData.recommended) - project.setRecommendedVersion(newVersion._1) - addUnstableTag(newVersion._1, versionData.unstable) - Redirect(self.show(author, slug, versionString)) - } + getProject(author, slug).flatMap { project => + project.channels + .find(equalsIgnoreCase(_.name, pendingVersion.channelName)) + .toRight(versionData.addTo(project)) + .leftFlatMap(identity) + .semiFlatMap { _ => + // Update description + versionData.content.foreach { content => + pendingVersion.underlying.setDescription(content.trim) } - ) - } - } + + pendingVersion.complete.map { newVersion => + if (versionData.recommended) + project.setRecommendedVersion(newVersion._1) + addUnstableTag(newVersion._1, versionData.unstable) + Redirect(self.show(author, slug, versionString)) + } + } + .leftMap(error => Redirect(self.showCreatorWithMeta(author, slug, versionString)).withError(error)) + }.merge case Some(pendingProject) => // Found a pending project, create it with first version pendingProject.complete.map { created => @@ -396,13 +373,12 @@ class Versions @Inject()(stats: StatTracker, */ def delete(author: String, slug: String, versionString: String) = { VersionEditAction(author, slug).async { implicit request => - implicit val data = request.data implicit val r = request.request - implicit val p = data.project - withVersion(versionString) { version => + implicit val p = request.data.project + getVersion(p, versionString).map { version => this.projects.deleteVersion(version) Redirect(self.showList(author, slug, None, None)) - } + }.merge } } @@ -416,12 +392,11 @@ class Versions @Inject()(stats: StatTracker, */ def download(author: String, slug: String, versionString: String, token: Option[String]) = { ProjectAction(author, slug).async { implicit request => - implicit val project = request.data - implicit val p: Project = project.project + val project = request.data.project implicit val r = request.request - withVersionAsync(versionString) { version => - sendVersion(project.project, version, token) - } + getVersion(project, versionString).semiFlatMap { version => + sendVersion(project, version, token) + }.merge } } @@ -433,11 +408,10 @@ class Versions @Inject()(stats: StatTracker, if (passed) _sendVersion(project, version) else - Future.successful( - Redirect(self.showDownloadConfirm( - project.ownerName, project.slug, version.name, Some(UploadedFile.id), api = Some(false)))) + Future.successful( + Redirect(self.showDownloadConfirm( + project.ownerName, project.slug, version.name, Some(UploadedFile.id), api = Some(false)))) } - } private def checkConfirmation(project: Project, @@ -446,27 +420,24 @@ class Versions @Inject()(stats: StatTracker, (implicit req: ProjectRequest[_]): Future[Boolean] = { if (version.isReviewed) return Future.successful(true) + // check for confirmation - req.cookies.get(DownloadWarning.COOKIE).map(_.value).orElse(token) match { - case None => - // unconfirmed - Future.successful(false) - case Some(tkn) => + OptionT + .fromOption[Future](req.cookies.get(DownloadWarning.COOKIE).map(_.value).orElse(token)) + .flatMap { tkn => this.warnings.find { warn => (warn.token === tkn) && (warn.versionId === version.id.get) && (warn.address === InetString(StatTracker.remoteAddress)) && warn.isConfirmed - } map { - case None => false - case Some(warn) => - if (!warn.hasExpired) true - else { - warn.remove() - false - } } - } + }.exists { warn => + if (!warn.hasExpired) true + else { + warn.remove() + false + } + } } private def _sendVersion(project: Project, version: Version)(implicit req: ProjectRequest[_]): Future[Result] = { @@ -496,13 +467,11 @@ class Versions @Inject()(stats: StatTracker, api: Option[Boolean]) = { ProjectAction(author, slug).async { request => val dlType = downloadType.flatMap(i => DownloadTypes.values.find(_.id == i)).getOrElse(DownloadTypes.UploadedFile) - implicit val data = request.data - implicit val p = data.project implicit val r = request.request - withVersionAsync(target) { version => - if (version.isReviewed) - Future.successful(Redirect(ShowProject(author, slug))) - else { + val project = request.data.project + getVersion(project, target) + .filterOrElse(v => !v.isReviewed, Redirect(ShowProject(author, slug))) + .semiFlatMap { version => // generate a unique "warning" object to ensure the user has landed // on the warning before downloading val token = UUID.randomUUID().toString @@ -522,7 +491,7 @@ class Versions @Inject()(stats: StatTracker, MultipleChoices(Json.obj( "message" -> this.messagesApi("version.download.confirm.body.api").split('\n'), "post" -> self.confirmDownload(author, slug, target, Some(dlType.id), token).absoluteURL(), - "url" -> self.downloadJarById(p.pluginId, version.name, Some(token)).absoluteURL(), + "url" -> self.downloadJarById(project.pluginId, version.name, Some(token)).absoluteURL(), "token" -> token) ) } @@ -545,51 +514,44 @@ class Versions @Inject()(stats: StatTracker, CSRF.getToken.get.value) + "\n") .withHeaders("Content-Disposition" -> "inline; filename=\"README.txt\"")) } else { - for { - warn <- warning - nonReviewed <- version.channel.map(_.isNonReviewed) - } yield MultipleChoices(views.unsafeDownload(data.project, version, nonReviewed , dlType, token)).withCookies(warn.cookie) + (warning, version.channel.map(_.isNonReviewed)).parMapN { (warn, nonReviewed) => + MultipleChoices(views.unsafeDownload(project, version, nonReviewed, dlType, token)) + .withCookies(warn.cookie) + } } } - } - } + }.merge } } def confirmDownload(author: String, slug: String, target: String, downloadType: Option[Int], token: String) = { ProjectAction(author, slug) async { request => - implicit val data = request.data - implicit val p = data.project implicit val r: OreRequest[_] = request.request - withVersionAsync(target) { version => - if (version.isReviewed) - Future.successful(Redirect(ShowProject(author, slug))) - else { - confirmDownload0(version.id.get, downloadType, token).map { - case None => Redirect(ShowProject(author, slug)) - case Some(dl) => - dl.downloadType match { - case UploadedFile => - Redirect(self.download(author, slug, target, Some(token))) - case JarFile => - Redirect(self.downloadJar(author, slug, target, Some(token))) - case SignatureFile => - // Note: Shouldn't get here in the first place since sig files - // don't need confirmation, but added as a failsafe. - Redirect(self.downloadSignature(author, slug, target)) - case _ => - throw new Exception("unknown download type: " + downloadType) - } + getVersion(request.data.project, target) + .filterOrElse(v => !v.isReviewed, Redirect(ShowProject(author, slug))) + .flatMap(version => confirmDownload0(version.id.get, downloadType, token).toRight(Redirect(ShowProject(author, slug)))) + .map { dl => + dl.downloadType match { + case UploadedFile => + Redirect(self.download(author, slug, target, Some(token))) + case JarFile => + Redirect(self.downloadJar(author, slug, target, Some(token))) + case SignatureFile => + // Note: Shouldn't get here in the first place since sig files + // don't need confirmation, but added as a failsafe. + Redirect(self.downloadSignature(author, slug, target)) + case _ => + throw new Exception("unknown download type: " + downloadType) } } - } + .merge } } /** * Confirms the download and prepares the unsafe download. */ - private def confirmDownload0(versionId: Int, downloadType: Option[Int],token: String)(implicit requestHeader: Request[_]): Future[Option[UnsafeDownload]] = { + private def confirmDownload0(versionId: Int, downloadType: Option[Int],token: String)(implicit requestHeader: Request[_]): OptionT[Future, UnsafeDownload] = { val addr = InetString(StatTracker.remoteAddress) val dlType = downloadType .flatMap(i => DownloadTypes.values.find(_.id == i)) @@ -601,28 +563,24 @@ class Versions @Inject()(stats: StatTracker, (warn.versionId === versionId) && !warn.isConfirmed && (warn.downloadId === -1) - } flatMap { - case None => Future.successful(None) - case Some(warn) => - if (warn.hasExpired) { - // warning has expired - warn.remove() - Future.successful(None) - } else { - // warning confirmed and redirect to download - val downloads = this.service.access[UnsafeDownload](classOf[UnsafeDownload]) - for { - user <- this.users.current - _ <- warn.setConfirmed() - unsafeDownload <- downloads.add(UnsafeDownload( - userId = user.flatMap(_.id), - address = addr, - downloadType = dlType)) - _ <- warn.setDownload(unsafeDownload) - } yield { - Some(unsafeDownload) - } - } + }.filterNot { warn => + val isInvalid = warn.hasExpired + // warning has expired + if(isInvalid) warn.remove() + + isInvalid + }.semiFlatMap { warn => + // warning confirmed and redirect to download + val downloads = this.service.access[UnsafeDownload](classOf[UnsafeDownload]) + for { + user <- this.users.current.value + _ <- warn.setConfirmed() + unsafeDownload <- downloads.add(UnsafeDownload( + userId = user.flatMap(_.id), + address = addr, + downloadType = dlType)) + _ <- warn.setDownload(unsafeDownload) + } yield unsafeDownload } } @@ -654,11 +612,9 @@ class Versions @Inject()(stats: StatTracker, */ def downloadJar(author: String, slug: String, versionString: String, token: Option[String]) = { ProjectAction(author, slug).async { implicit request => - implicit val data = request.data - implicit val project = data.project + val project = request.data.project implicit val r = request.request - withVersionAsync(versionString)(version => - sendJar(project, version, token)) + getVersion(project, versionString).semiFlatMap(version => sendJar(project, version, token)).merge } } @@ -727,21 +683,17 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return Sent file */ - def downloadJarById(pluginId: String, versionString: String, token: Option[String]) = { + def downloadJarById(pluginId: String, versionString: String, optToken: Option[String]) = { ProjectAction(pluginId).async { implicit request => - implicit val data = request.data - implicit val p = data.project + val project = request.data.project implicit val r = request.request - withVersionAsync(versionString) { version => - if (token.isDefined) { - request.headers - confirmDownload0(version.id.get, Some(JarFile.id), token.get)(request.request).flatMap { _ => - sendJar(data.project, version, token, api = true) + getVersion(project, versionString).semiFlatMap { version => + optToken.map { token => + confirmDownload0(version.id.get, Some(JarFile.id), token)(request.request).value.flatMap { _ => + sendJar(project, version, optToken, api = true) } - } else { - sendJar(data.project, version, token, api = true) - } - } + }.getOrElse(sendJar(project, version, optToken, api = true)) + }.merge } } @@ -771,10 +723,9 @@ class Versions @Inject()(stats: StatTracker, */ def downloadSignature(author: String, slug: String, versionString: String) = { ProjectAction(author, slug).async { implicit request => - implicit val data = request.data - implicit val project = data.project + val project = request.data.project implicit val r = request.request - withVersion(versionString)(sendSignatureFile(_, project)) + getVersion(project, versionString).map(sendSignatureFile(_, project)).merge } } @@ -786,10 +737,9 @@ class Versions @Inject()(stats: StatTracker, * @return Sent file */ def downloadSignatureById(pluginId: String, versionString: String) = ProjectAction(pluginId).async { implicit request => - implicit val data = request.data - implicit val project = data.project + val project = request.data.project implicit val r = request.request - withVersion(versionString)(sendSignatureFile(_, project)) + getVersion(project, versionString).map(sendSignatureFile(_, project)).merge } /** diff --git a/app/controllers/sugar/ActionHelpers.scala b/app/controllers/sugar/ActionHelpers.scala index 952b87acf..277fc66c2 100644 --- a/app/controllers/sugar/ActionHelpers.scala +++ b/app/controllers/sugar/ActionHelpers.scala @@ -1,7 +1,9 @@ package controllers.sugar import com.google.common.base.Preconditions.{checkArgument, checkNotNull} + import play.api.data.Form +import play.api.i18n.Messages import play.api.mvc.Results.Redirect import play.api.mvc.{Call, Result} @@ -36,6 +38,20 @@ trait ActionHelpers { */ def withError(error: String) = result.flashing("error" -> error) + /** + * Adds one or more errors messages to the result. + * + * @param errors Error messages + * @return Result with errors + */ + //TODO: Use NEL[String] if we get the type + def withErrors(errors: Seq[String])(implicit messages: Messages): Result = errors match { + case Seq() => result + case Seq(single) => withError(messages(single)) + case multiple => + result.flashing("error" -> multiple.map(s => messages(s)).mkString("• ", "
• ", ""), "error-israw" -> "true") + } + /** * Adds a success message to the result. * diff --git a/app/controllers/sugar/Actions.scala b/app/controllers/sugar/Actions.scala index 47c40307b..bfddd58ac 100644 --- a/app/controllers/sugar/Actions.scala +++ b/app/controllers/sugar/Actions.scala @@ -22,6 +22,11 @@ import slick.jdbc.JdbcBackend import scala.concurrent.{ExecutionContext, Future} import scala.language.higherKinds +import util.FutureUtils +import util.functional.OptionT +import util.instances.future._ +import util.syntax._ + /** * A set of actions used by Ore. */ @@ -42,8 +47,8 @@ trait Actions extends Calls with ActionHelpers { /** Called when a [[User]] tries to make a request they do not have permission for */ def onUnauthorized(implicit request: Request[_], ec: ExecutionContext): Future[Result] = { val noRedirect = request.flash.get("noRedirect") - this.users.current.map { currentUser => - if (noRedirect.isEmpty && currentUser.isEmpty) + this.users.current.isEmpty.map { currentUserEmpty => + if(noRedirect.isEmpty && currentUserEmpty) Redirect(routes.Users.logIn(None, None, Some(request.path))) else Redirect(ShowHome) @@ -134,16 +139,14 @@ trait Actions extends Calls with ActionHelpers { * @param nonce Nonce to check * @return True if valid */ - def isNonceValid(nonce: String)(implicit ec: ExecutionContext): Future[Boolean] = this.signOns.find(_.nonce === nonce).map { - _.exists { - signOn => - if (signOn.isCompleted || new Date().getTime - signOn.createdAt.get.getTime > 600000) - false - else { - signOn.setCompleted() - true - } - } + def isNonceValid(nonce: String)(implicit ec: ExecutionContext): Future[Boolean] = this.signOns.find(_.nonce === nonce).exists { + signOn => + if (signOn.isCompleted || new Date().getTime - signOn.createdAt.get.getTime > 600000) + false + else { + signOn.setCompleted() + true + } } /** @@ -167,29 +170,28 @@ trait Actions extends Calls with ActionHelpers { def verifiedAction(sso: Option[String], sig: Option[String])(implicit ec: ExecutionContext) = new ActionFilter[AuthRequest] { def executionContext = ec - def filter[A](request: AuthRequest[A]): Future[Option[Result]] = - if (sso.isEmpty || sig.isEmpty) - Future.successful(Some(Unauthorized)) - else { - Actions.this.sso.authenticate(sso.get, sig.get)(isNonceValid) map { - case None => Some(Unauthorized) - case Some(spongeUser) => - if (spongeUser.id == request.user.id.get) - None - else - Some(Unauthorized) - } - } + def filter[A](request: AuthRequest[A]): Future[Option[Result]] = { + val auth = for { + ssoSome <- sso + sigSome <- sig + } yield Actions.this.sso.authenticate(ssoSome, sigSome)(isNonceValid) + + OptionT.fromOption[Future](auth).flatMap(identity).cata(Some(Unauthorized), spongeUser => + if (spongeUser.id == request.user.id.get) + None + else + Some(Unauthorized)) + } } def userAction(username: String)(implicit ec: ExecutionContext) = new ActionFilter[AuthRequest] { def executionContext = ec def filter[A](request: AuthRequest[A]): Future[Option[Result]] = { - Actions.this.users.requestPermission(request.user, username, EditSettings).map { + Actions.this.users.requestPermission(request.user, username, EditSettings).transform { case None => Some(Unauthorized) // No Permission case Some(_) => None // Permission granted => No Filter - } + }.value } } @@ -210,15 +212,12 @@ trait Actions extends Calls with ActionHelpers { } - private def maybeAuthRequest[A](request: Request[A], futUser: Future[Option[User]])(implicit ec: ExecutionContext, + private def maybeAuthRequest[A](request: Request[A], futUser: OptionT[Future, User])(implicit ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): Future[Either[Result, AuthRequest[A]]] = { - futUser.flatMap { - case None => onUnauthorized(request, ec).map(Left(_)) - case Some(user) => { - implicit val service = users.service - HeaderData.of(request).map(hd => Right(new AuthRequest[A](user, hd, request))) - } - } + futUser.semiFlatMap { user => + implicit val service = users.service + HeaderData.of(request).map(hd => new AuthRequest[A](user, hd, request)) + }.toRight(onUnauthorized(request, ec)).leftSemiFlatMap(identity).value } def projectAction(author: String, slug: String)(implicit modelService: ModelService, ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = new ActionRefiner[OreRequest, ProjectRequest] { @@ -233,45 +232,39 @@ trait Actions extends Calls with ActionHelpers { def refine[A](request: OreRequest[A]) = maybeProjectRequest(request, Actions.this.projects.withPluginId(pluginId)) } - private def maybeProjectRequest[A](r: OreRequest[A], project: Future[Option[Project]])(implicit modelService: ModelService, + private def maybeProjectRequest[A](r: OreRequest[A], project: OptionT[Future, Project])(implicit modelService: ModelService, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext): Future[Either[Result, ProjectRequest[A]]] = { implicit val request = r - val pr = project.flatMap { - case None => Future.successful(None) - case Some(p) => processProject(p, request.data.currentUser) - } - pr.flatMap { - case None => Future.successful(Left(notFound)) - case Some(p) => - toProjectRequest(p) { case (data, scoped) => - Right(new ProjectRequest[A](data, scoped, r)) - } - } + project.flatMap { p => + processProject(p, request.data.currentUser) + }.semiFlatMap { p => + toProjectRequest(p) { case (data, scoped) => + new ProjectRequest[A](data, scoped, r) + } + }.toRight(notFound).value } private def toProjectRequest[T](project: Project)(f: (ProjectData, ScopedProjectData) => T)(implicit request: OreRequest[_], modelService: ModelService, ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = { - for { - (data, scoped) <- ProjectData.of(project) zip ScopedProjectData.of(request.data.currentUser, project) - } yield { - f(data, scoped) - } + (ProjectData.of(project), ScopedProjectData.of(request.data.currentUser, project)).parMapN(f) } - private def processProject(project: Project, user: Option[User])(implicit ec: ExecutionContext) : Future[Option[Project]] = { + private def processProject(project: Project, user: Option[User])(implicit ec: ExecutionContext) : OptionT[Future, Project] = { if (project.visibility == VisibilityTypes.Public || project.visibility == VisibilityTypes.New) { - Future.successful(Some(project)) + OptionT.pure[Future](project) } else { if (user.isDefined) { - Future.firstCompletedOf(Seq()) - for { - check1 <- canEditAndNeedChangeOrApproval(project, user) - check2 <- user.get can HideProjects in GlobalScope - } yield { - if (check1 || check2) Some(project) else None - } + val check1 = canEditAndNeedChangeOrApproval(project, user) + val check2 = user.get can HideProjects in GlobalScope + + OptionT( + FutureUtils.raceBoolean(check1, check2).map { + case true => Some(project) + case false => None + } + ) } else { - Future.successful(None) + OptionT.none[Future, Project] } } } @@ -284,26 +277,21 @@ trait Actions extends Calls with ActionHelpers { } } - def authedProjectActionImpl(project: Future[Option[Project]])(implicit modelService: ModelService, ec: ExecutionContext, + def authedProjectActionImpl(project: OptionT[Future, Project])(implicit modelService: ModelService, ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = new ActionRefiner[AuthRequest, AuthedProjectRequest] { def executionContext = ec - def refine[A](request: AuthRequest[A]) = project.flatMap { p => + def refine[A](request: AuthRequest[A]) = { implicit val r = request - p match { - case None => Future.successful(Left(notFound)) - case Some(pr) => - val processed = processProject(pr, Some(request.user)) - processed.flatMap { - case None => Future.successful(Left(notFound)) - case Some(p) => - toProjectRequest(p) { case (data, scoped) => - Right(new AuthedProjectRequest[A](data, scoped, request)) - } + project.flatMap { pr => + processProject(pr, Some(request.user)).semiFlatMap { p => + toProjectRequest(p) { case (data, scoped) => + new AuthedProjectRequest[A](data, scoped, request) } - } + } + }.toRight(notFound).value } } @@ -320,7 +308,7 @@ trait Actions extends Calls with ActionHelpers { def refine[A](request: OreRequest[A]) = { implicit val r = request - getOrga(request, organization).flatMap { + getOrga(request, organization).value.flatMap { maybeOrgaRequest(_) { case (data, scoped) => new OrganizationRequest[A](data, scoped, request) } @@ -335,7 +323,7 @@ trait Actions extends Calls with ActionHelpers { def refine[A](request: AuthRequest[A]) = { implicit val r = request - getOrga(request, organization).flatMap { + getOrga(request, organization).value.flatMap { maybeOrgaRequest(_) { case (data, scoped) => new AuthedOrganizationRequest[A](data, scoped, request) } @@ -349,27 +337,19 @@ trait Actions extends Calls with ActionHelpers { maybeOrga match { case None => Future.successful(Left(notFound)) case Some(orga) => - for { - (data, scoped) <- OrganizationData.of(orga) zip - ScopedOrganizationData.of(request.data.currentUser, orga) - } yield { - Right(f(data, scoped)) - } + val rf = Function.untupled(f.tupled.andThen(Right.apply)) + (OrganizationData.of(orga), ScopedOrganizationData.of(request.data.currentUser, orga)).parMapN(rf) } } def getOrga(request: OreRequest[_], organization: String)(implicit modelService: ModelService, ec: ExecutionContext, - asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): Future[Option[Organization]] = { + asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): OptionT[Future, Organization] = { this.organizations.withName(organization) } def getUserData(request: OreRequest[_], userName: String)(implicit modelService: ModelService, ec: ExecutionContext, - asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): Future[Option[UserData]] = { - this.users.withName(userName).flatMap { - case None => Future.successful(None) - case Some(user) => - UserData.of(request, user).map(Some(_)) - } + asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): OptionT[Future, UserData] = { + this.users.withName(userName).semiFlatMap(user => UserData.of(request, user)) } } diff --git a/app/db/ModelSchema.scala b/app/db/ModelSchema.scala index 2cd997e4a..e89367eae 100644 --- a/app/db/ModelSchema.scala +++ b/app/db/ModelSchema.scala @@ -4,10 +4,12 @@ import db.access.{ImmutableModelAccess, ModelAccess, ModelAssociationAccess} import db.impl.OrePostgresDriver.api._ import db.table.{AssociativeTable, ModelAssociation, ModelTable} -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.{Future, Promise} +import scala.concurrent.{ExecutionContext, Future, Promise} import scala.util.{Failure, Success} +import util.functional.OptionT +import util.instances.future._ + /** * Defines a set of [[Model]] behaviors such as relationships between other * Models or any other specialized database actions. Every [[Model]] has @@ -130,7 +132,7 @@ class ModelSchema[M <: Model](val service: ModelService, * @tparam S Sibling model type * @return Sibling */ - def getSibling[S <: Model](siblingClass: Class[S], model: M): Future[Option[S]] = { + def getSibling[S <: Model](siblingClass: Class[S], model: M)(implicit ec: ExecutionContext): OptionT[Future, S] = { val ref: M => Int = this.siblings(siblingClass) this.service.get[S](siblingClass, ref(model)) } @@ -141,9 +143,9 @@ class ModelSchema[M <: Model](val service: ModelService, * @param model Model to get or create * @return Existing or newly created model */ - def getOrInsert(model: M): Future[M] = { + def getOrInsert(model: M)(implicit ec: ExecutionContext): Future[M] = { val modelPromise = Promise[M] - like(model).onComplete { + like(model).value.onComplete { case Failure(thrown) => modelPromise.failure(thrown) case Success(modelOpt) => modelOpt match { case Some(existing) => modelPromise.success(existing) @@ -159,6 +161,6 @@ class ModelSchema[M <: Model](val service: ModelService, * @param model Model to find * @return Model if found */ - def like(model: M): Future[Option[M]] = Future.successful(None) + def like(model: M)(implicit ec: ExecutionContext): OptionT[Future, M] = OptionT.none[Future, M] } diff --git a/app/db/ModelService.scala b/app/db/ModelService.scala index 5d032f378..b476ffa21 100644 --- a/app/db/ModelService.scala +++ b/app/db/ModelService.scala @@ -13,11 +13,12 @@ import slick.jdbc.{JdbcProfile, JdbcType} import slick.lifted.{ColumnOrdered, WrappingQuery} import slick.util.ConstArray -import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.Duration -import scala.concurrent.{Await, Future, Promise} +import scala.concurrent.{Await, ExecutionContext, Future, Promise} import scala.util.{Failure, Success, Try} +import util.functional.OptionT + /** * Represents a service that creates, deletes, and manipulates Models. */ @@ -112,7 +113,7 @@ trait ModelService { * @param action Action to run * @return Processed result */ - def doAction[R](action: AbstractModelAction[R]): Future[R] + def doAction[R](action: AbstractModelAction[R])(implicit ec: ExecutionContext): Future[R] = DB.db.run(action).map(r => action.processResult(this, r)) /** @@ -132,7 +133,7 @@ trait ModelService { * @param model Model to create * @return Newly created model */ - def insert[M <: Model](model: M): Future[M] = { + def insert[M <: Model](model: M)(implicit ec: ExecutionContext): Future[M] = { val toInsert = model.copyWith(None, Some(theTime)).asInstanceOf[M] val models = newAction[M](model.getClass) doAction { @@ -182,14 +183,14 @@ trait ModelService { * @param filter Filter * @return Optional result */ - def find[M <: Model](modelClass: Class[M], filter: M#T => Rep[Boolean]): Future[Option[M]] = { + def find[M <: Model](modelClass: Class[M], filter: M#T => Rep[Boolean])(implicit ec: ExecutionContext): OptionT[Future, M] = { val modelPromise = Promise[Option[M]] val query = newAction[M](modelClass).filter(filter).take(1) doAction(query.result).andThen { case Failure(thrown) => modelPromise.failure(thrown) case Success(result) => modelPromise.success(result.headOption) } - modelPromise.future + OptionT(modelPromise.future) } /** @@ -227,7 +228,7 @@ trait ModelService { * @param id Model with ID * @return Model if present, None otherwise */ - def get[M <: Model](modelClass: Class[M], id: Int, filter: M#T => Rep[Boolean] = null): Future[Option[M]] + def get[M <: Model](modelClass: Class[M], id: Int, filter: M#T => Rep[Boolean] = null)(implicit ec: ExecutionContext): OptionT[Future, M] = find[M](modelClass, (IdFilter[M](id) && filter).fn) /** @@ -239,7 +240,7 @@ trait ModelService { * @tparam M Model type * @return Seq of models in ID set */ - def in[M <: Model](modelClass: Class[M], ids: Set[Int], filter: M#T => Rep[Boolean] = null): Future[Seq[M]] + def in[M <: Model](modelClass: Class[M], ids: Set[Int], filter: M#T => Rep[Boolean] = null)(implicit ec: ExecutionContext): Future[Seq[M]] = this.filter[M](modelClass, (ModelFilter[M](_.id inSetBind ids) && filter).fn) /** @@ -250,7 +251,7 @@ trait ModelService { * @return Collection of models */ def collect[M <: Model](modelClass: Class[M], filter: M#T => Rep[Boolean] = null, - sort: M#T => ColumnOrdered[_] = null, limit: Int = -1, offset: Int = -1): Future[Seq[M]] = { + sort: M#T => ColumnOrdered[_] = null, limit: Int = -1, offset: Int = -1)(implicit ec: ExecutionContext): Future[Seq[M]] = { var query = newAction[M](modelClass) if (filter != null) query = query.filter(filter) if (sort != null) query = query.sortBy(sort) @@ -263,7 +264,7 @@ trait ModelService { * Same as collect but with multiple sorted columns */ def collectMultipleSorts[M <: Model](modelClass: Class[M], filter: M#T => Rep[Boolean] = null, - sort: M#T => List[ColumnOrdered[_]] = null, limit: Int = -1, offset: Int = -1): Future[Seq[M]] = { + sort: M#T => List[ColumnOrdered[_]] = null, limit: Int = -1, offset: Int = -1)(implicit ec: ExecutionContext): Future[Seq[M]] = { var query = newAction[M](modelClass) if (filter != null) query = query.filter(filter) if (sort != null) { @@ -288,7 +289,7 @@ trait ModelService { * @return Filtered models */ def filter[M <: Model](modelClass: Class[M], filter: M#T => Rep[Boolean], limit: Int = -1, - offset: Int = -1): Future[Seq[M]] + offset: Int = -1)(implicit ec: ExecutionContext): Future[Seq[M]] = collect(modelClass, filter, null.asInstanceOf[M#T => ColumnOrdered[_]], limit, offset) /** @@ -302,14 +303,14 @@ trait ModelService { * @return Sorted models */ def sorted[M <: Model](modelClass: Class[M], sort: M#T => ColumnOrdered[_], filter: M#T => Rep[Boolean] = null, - limit: Int = -1, offset: Int = -1): Future[Seq[M]] + limit: Int = -1, offset: Int = -1)(implicit ec: ExecutionContext): Future[Seq[M]] = collect(modelClass, filter, sort, limit, offset) /** * Same as sorted but with multiple sorts */ def sortedMultipleOrders[M <: Model](modelClass: Class[M], sorts: M#T => List[ColumnOrdered[_]], filter: M#T => Rep[Boolean] = null, - limit: Int = -1, offset: Int = -1): Future[Seq[M]] + limit: Int = -1, offset: Int = -1)(implicit ec: ExecutionContext): Future[Seq[M]] = collectMultipleSorts(modelClass, filter, sorts, limit, offset) } diff --git a/app/db/access/ModelAccess.scala b/app/db/access/ModelAccess.scala index 43f4bb87d..497ae68be 100644 --- a/app/db/access/ModelAccess.scala +++ b/app/db/access/ModelAccess.scala @@ -7,6 +7,8 @@ import slick.lifted.ColumnOrdered import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT + /** * Provides simple, synchronous, access to a ModelTable. */ @@ -20,7 +22,7 @@ class ModelAccess[M <: Model](val service: ModelService, * @param id ID to lookup * @return Model with ID or None if not found */ - def get(id: Int): Future[Option[M]] = this.service.get[M](this.modelClass, id, this.baseFilter.fn) + def get(id: Int)(implicit ec: ExecutionContext): OptionT[Future, M] = this.service.get[M](this.modelClass, id, this.baseFilter.fn) /** * Returns a set of Models that have an ID that is in the specified Int set. @@ -102,7 +104,7 @@ class ModelAccess[M <: Model](val service: ModelService, * @param filter Filter to use * @return Model matching filter, if any */ - def find(filter: M#T => Rep[Boolean]): Future[Option[M]] = this.service.find[M](this.modelClass, (this.baseFilter && filter).fn) + def find(filter: M#T => Rep[Boolean])(implicit ec: ExecutionContext): OptionT[Future, M] = this.service.find[M](this.modelClass, (this.baseFilter && filter).fn) /** * Returns a sorted Seq by the specified [[ColumnOrdered]]. @@ -114,14 +116,14 @@ class ModelAccess[M <: Model](val service: ModelService, * @return Sorted models */ def sorted(ordering: M#T => ColumnOrdered[_], filter: M#T => Rep[Boolean] = null, limit: Int = -1, - offset: Int = -1): Future[Seq[M]] + offset: Int = -1)(implicit ec: ExecutionContext): Future[Seq[M]] = this.service.sorted[M](this.modelClass, ordering, (this.baseFilter && filter).fn, limit, offset) /** * Same as sorted but with multiple orderings */ def sortedMultipleOrders(orderings: M#T => List[ColumnOrdered[_]], filter: M#T => Rep[Boolean] = null, limit: Int = -1, - offset: Int = -1): Future[Seq[M]] + offset: Int = -1)(implicit ec: ExecutionContext): Future[Seq[M]] = this.service.sortedMultipleOrders[M](this.modelClass, orderings, (this.baseFilter && filter).fn, limit, offset) /** @@ -132,7 +134,7 @@ class ModelAccess[M <: Model](val service: ModelService, * @param offset Amount to drop * @return Filtered models */ - def filter(filter: M#T => Rep[Boolean], limit: Int = -1, offset: Int = -1): Future[Seq[M]] + def filter(filter: M#T => Rep[Boolean], limit: Int = -1, offset: Int = -1)(implicit ec: ExecutionContext): Future[Seq[M]] = this.service.filter[M](this.modelClass, (this.baseFilter && filter).fn, limit, offset) /** @@ -143,7 +145,7 @@ class ModelAccess[M <: Model](val service: ModelService, * @param offset Amount to drop * @return Filtered models */ - def filterNot(filter: M#T => Rep[Boolean], limit: Int = -1, offset: Int = -1): Future[Seq[M]] = this.filter(!filter(_), limit, offset) + def filterNot(filter: M#T => Rep[Boolean], limit: Int = -1, offset: Int = -1)(implicit ec: ExecutionContext): Future[Seq[M]] = this.filter(!filter(_), limit, offset) /** * Returns a Seq of this set. diff --git a/app/db/impl/access/OrganizationBase.scala b/app/db/impl/access/OrganizationBase.scala index f6d274256..cc31eecca 100644 --- a/app/db/impl/access/OrganizationBase.scala +++ b/app/db/impl/access/OrganizationBase.scala @@ -11,9 +11,11 @@ import play.api.cache.AsyncCacheApi import play.api.i18n.{Lang, MessagesApi} import security.spauth.SpongeAuthApi import util.StringUtils - +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import util.functional.{EitherT, OptionT} + class OrganizationBase(override val service: ModelService, forums: OreDiscourseApi, auth: SpongeAuthApi, @@ -35,7 +37,7 @@ class OrganizationBase(override val service: ModelService, * @param ownerId User ID of the organization owner * @return New organization if successful, None otherwise */ - def create(name: String, ownerId: Int, members: Set[OrganizationRole])(implicit cache: AsyncCacheApi, ec: ExecutionContext): Future[Either[String, Organization]] = { + def create(name: String, ownerId: Int, members: Set[OrganizationRole])(implicit cache: AsyncCacheApi, ec: ExecutionContext): EitherT[Future, String, Organization] = { Logger.info("Creating Organization...") Logger.info("Name : " + name) Logger.info("Owner ID : " + ownerId) @@ -48,38 +50,35 @@ class OrganizationBase(override val service: ModelService, Logger.info("Creating on SpongeAuth...") val dummyEmail = name + '@' + this.config.orgs.get[String]("dummyEmailDomain") val spongeResult = this.auth.createDummyUser(name, dummyEmail, verified = true) + // Check for error - spongeResult flatMap { - case Left(err) => - Logger.info(" " + err) - Future.successful(Left(err)) - case Right(spongeUser) => - Logger.info(" " + spongeUser) - // Next we will create the Organization on Ore itself. This contains a - // reference to the Sponge user ID, the organization's username and a - // reference to the User owner of the organization. - Logger.info("Creating on Ore...") - this.add(Organization(id = Some(spongeUser.id), username = name, _ownerId = ownerId)).map(Right(_)) - } flatMap { - case Left(err) => Future.successful(Left(err)) - case Right(org) => - // Every organization model has a regular User companion. Organizations - // are just normal users with additional information. Adding the - // Organization global role signifies that this User is an Organization - // and should be treated as such. - org.toUser.map { - case None => throw new IllegalStateException("User not created") - case Some(userOrg) => userOrg.pullForumData().flatMap(_.pullSpongeData()) - userOrg.setGlobalRoles(userOrg.globalRoles + RoleTypes.Organization) - userOrg - } flatMap { orga => - // Add the owner + spongeResult.leftMap { err => + Logger.info(" " + err) + err + }.semiFlatMap { spongeUser => + Logger.info(" " + spongeUser) + // Next we will create the Organization on Ore itself. This contains a + // reference to the Sponge user ID, the organization's username and a + // reference to the User owner of the organization. + Logger.info("Creating on Ore...") + this.add(Organization(id = Some(spongeUser.id), username = name, _ownerId = ownerId)) + }.semiFlatMap { org => + // Every organization model has a regular User companion. Organizations + // are just normal users with additional information. Adding the + // Organization global role signifies that this User is an Organization + // and should be treated as such. + for { + userOrg <- org.toUser.getOrElse(throw new IllegalStateException("User not created")) + _ <- userOrg.pullForumData() + _ <- userOrg.pullSpongeData() + _ = userOrg.setGlobalRoles(userOrg.globalRoles + RoleTypes.Organization) + _ <- // Add the owner org.memberships.addRole(OrganizationRole( userId = ownerId, organizationId = org.id.get, _roleType = RoleTypes.OrganizationOwner, _isAccepted = true)) - } flatMap { _ => + _ <- { // Invite the User members that the owner selected during creation. Logger.info("Inviting members...") @@ -92,12 +91,12 @@ class OrganizationBase(override val service: ModelService, message = this.messages("notification.organization.invite", role.roleType.title, org.username) )) } - } - ) - } map { _ => - Logger.info(" " + org) - Right(org) + }) } + } yield { + Logger.info(" " + org) + org + } } } @@ -107,6 +106,6 @@ class OrganizationBase(override val service: ModelService, * @param name Organization name * @return Organization with name if exists, None otherwise */ - def withName(name: String): Future[Option[Organization]] = this.find(StringUtils.equalsIgnoreCase(_.name, name)) + def withName(name: String)(implicit ec: ExecutionContext): OptionT[Future, Organization] = this.find(StringUtils.equalsIgnoreCase(_.name, name)) } diff --git a/app/db/impl/access/ProjectBase.scala b/app/db/impl/access/ProjectBase.scala index e85389efb..a1c1904dd 100644 --- a/app/db/impl/access/ProjectBase.scala +++ b/app/db/impl/access/ProjectBase.scala @@ -16,9 +16,11 @@ import ore.{OreConfig, OreEnv} import slick.lifted.TableQuery import util.FileUtils import util.StringUtils._ - +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT + class ProjectBase(override val service: ModelService, env: OreEnv, config: OreConfig, @@ -55,7 +57,7 @@ class ProjectBase(override val service: ModelService, * * @return Stale projects */ - def stale: Future[Seq[Project]] + def stale(implicit ec: ExecutionContext): Future[Seq[Project]] = this.filter(_.lastUpdated > new Timestamp(new Date().getTime - this.config.projects.get[Int]("staleAge"))) /** @@ -65,7 +67,7 @@ class ProjectBase(override val service: ModelService, * @param name Project name * @return Project with name */ - def withName(owner: String, name: String): Future[Option[Project]] + def withName(owner: String, name: String)(implicit ec: ExecutionContext): OptionT[Future, Project] = this.find(p => p.ownerName.toLowerCase === owner.toLowerCase && p.name.toLowerCase === name.toLowerCase) /** @@ -75,7 +77,7 @@ class ProjectBase(override val service: ModelService, * @param slug URL slug * @return Project if found, None otherwise */ - def withSlug(owner: String, slug: String): Future[Option[Project]] + def withSlug(owner: String, slug: String)(implicit ec: ExecutionContext): OptionT[Future, Project] = this.find(p => p.ownerName.toLowerCase === owner.toLowerCase && p.slug.toLowerCase === slug.toLowerCase) /** @@ -84,14 +86,14 @@ class ProjectBase(override val service: ModelService, * @param pluginId Plugin ID * @return Project if found, None otherwise */ - def withPluginId(pluginId: String): Future[Option[Project]] = this.find(equalsIgnoreCase(_.pluginId, pluginId)) + def withPluginId(pluginId: String)(implicit ec: ExecutionContext): OptionT[Future, Project] = this.find(equalsIgnoreCase(_.pluginId, pluginId)) /** * Returns true if the Project's desired slug is available. * * @return True if slug is available */ - def isNamespaceAvailable(owner: String, slug: String)(implicit ec: ExecutionContext): Future[Boolean] = withSlug(owner, slug).map(_.isEmpty) + def isNamespaceAvailable(owner: String, slug: String)(implicit ec: ExecutionContext): Future[Boolean] = withSlug(owner, slug).isEmpty /** * Returns true if the specified project exists. @@ -99,7 +101,7 @@ class ProjectBase(override val service: ModelService, * @param project Project to check * @return True if exists */ - def exists(project: Project)(implicit ec: ExecutionContext): Future[Boolean] = this.withName(project.ownerName, project.name).map(_.isDefined) + def exists(project: Project)(implicit ec: ExecutionContext): Future[Boolean] = this.withName(project.ownerName, project.name).isDefined /** * Saves any pending icon that has been uploaded for the specified [[Project]]. @@ -128,22 +130,21 @@ class ProjectBase(override val service: ModelService, val newName = compact(name) val newSlug = slugify(newName) checkArgument(this.config.isValidProjectName(name), "invalid name", "") - val future = for { + for { isAvailable <- this.isNamespaceAvailable(project.ownerName, newSlug) - } yield { - checkArgument(isAvailable, "slug not available", "") - } - future.flatMap { _ => - this.fileManager.renameProject(project.ownerName, project.name, newName) - project.setName(newName) - project.setSlug(newSlug) - - // Project's name alter's the topic title, update it - if (project.topicId != -1 && this.forums.isEnabled) - this.forums.updateProjectTopic(project) - else - Future.successful(false) - } + _ = checkArgument(isAvailable, "slug not available", "") + res <- { + this.fileManager.renameProject(project.ownerName, project.name, newName) + project.setName(newName) + project.setSlug(newSlug) + + // Project's name alter's the topic title, update it + if (project.topicId != -1 && this.forums.isEnabled) + this.forums.updateProjectTopic(project) + else + Future.successful(false) + } + } yield res } /** @@ -152,30 +153,24 @@ class ProjectBase(override val service: ModelService, * @param context Project context */ def deleteChannel(channel: Channel)(implicit context: Project = null, ec: ExecutionContext): Future[Unit] = { - val project = if (context != null) Future.successful(context) else channel.project - project.map { project => - checkArgument(project.id.get == channel.projectId, "invalid project id", "") - val checks = for { - channels <- project.channels.all - noVersion <- channel.versions.isEmpty - nonEmptyChannels <- Future.sequence(channels.map(_.versions.nonEmpty)).map(_.count(_ == true)) - } yield { - checkArgument(channels.size > 1, "only one channel", "") - checkArgument(noVersion || nonEmptyChannels > 1, "last non-empty channel", "") - val reviewedChannels = channels.filter(!_.isNonReviewed) - checkArgument(channel.isNonReviewed || reviewedChannels.size > 1 || !reviewedChannels.contains(channel), - "last reviewed channel", "") - } - for { - _ <- checks - _ <- channel.remove() - versions <- channel.versions.all - } yield { - versions.foreach { version => - val versionFolder = this.fileManager.getVersionDir(project.ownerName, project.name, version.name) - FileUtils.deleteDirectory(versionFolder) - version.remove() - } + for { + project <- if (context != null) Future.successful(context) else channel.project + _ = checkArgument(project.id.get == channel.projectId, "invalid project id", "") + channels <- project.channels.all + noVersion <- channel.versions.isEmpty + nonEmptyChannels <- Future.traverse(channels.toSeq)(_.versions.nonEmpty).map(_.count(identity)) + _ = checkArgument(channels.size > 1, "only one channel", "") + _ = checkArgument(noVersion || nonEmptyChannels > 1, "last non-empty channel", "") + reviewedChannels = channels.filter(!_.isNonReviewed) + _ = checkArgument(channel.isNonReviewed || reviewedChannels.size > 1 || !reviewedChannels.contains(channel), + "last reviewed channel", "") + _ <- channel.remove() + versions <- channel.versions.all + } yield { + versions.foreach { version => + val versionFolder = this.fileManager.getVersionDir(project.ownerName, project.name, version.name) + FileUtils.deleteDirectory(versionFolder) + version.remove() } } } @@ -186,26 +181,14 @@ class ProjectBase(override val service: ModelService, * @param project Project context */ def deleteVersion(version: Version)(implicit project: Project = null, ec: ExecutionContext) = { - val checks = for { + for { proj <- if (project != null) Future.successful(project) else version.project size <- proj.versions.size - } yield { - checkArgument(size > 1, "only one version", "") - checkArgument(proj.id.get == version.projectId, "invalid context id", "") - proj - } - - val rcUpdate = for { - proj <- checks + _ = checkArgument(size > 1, "only one version", "") + _ = checkArgument(proj.id.get == version.projectId, "invalid context id", "") rv <- proj.recommendedVersion projects <- proj.versions.sorted(_.createdAt.desc) // TODO optimize: only query one version - } yield { - if (version.equals(rv)) proj.setRecommendedVersion(projects.filterNot(_.equals(version)).head) - proj - } - - val channelCleanup = for { - proj <- rcUpdate + _ = if (version.equals(rv)) proj.setRecommendedVersion(projects.filterNot(_.equals(version)).head) channel <- version.channel noVersions <- channel.versions.isEmpty _ <- { @@ -225,7 +208,7 @@ class ProjectBase(override val service: ModelService, * * @param project Project to delete */ - def delete(project: Project) = { + def delete(project: Project)(implicit ec: ExecutionContext) = { FileUtils.deleteDirectory(this.fileManager.getProjectDir(project.ownerName, project.name)) if (project.topicId != -1) this.forums.deleteProjectTopic(project) diff --git a/app/db/impl/access/UserBase.scala b/app/db/impl/access/UserBase.scala index 9229f7203..f38f2898c 100755 --- a/app/db/impl/access/UserBase.scala +++ b/app/db/impl/access/UserBase.scala @@ -12,11 +12,14 @@ import ore.permission.Permission import play.api.mvc.Request import security.spauth.SpongeAuthApi import util.StringUtils._ + import scala.concurrent.{ExecutionContext, Future} import ore.permission.role import ore.permission.role.RoleTypes import ore.permission.role.RoleTypes.RoleType +import util.functional.OptionT +import util.instances.future._ /** * Represents a central location for all Users. @@ -40,13 +43,9 @@ class UserBase(override val service: ModelService, * @param username Username of user * @return User if found, None otherwise */ - def withName(username: String)(implicit ec: ExecutionContext): Future[Option[User]] = { - this.find(equalsIgnoreCase(_.name, username)).flatMap { - case Some(u) => Future.successful(Some(u)) - case None => this.auth.getUser(username) flatMap { - case None => Future.successful(None) - case Some(u) => User.fromSponge(u).flatMap(getOrCreate).map(Some(_)) - } + def withName(username: String)(implicit ec: ExecutionContext): OptionT[Future, User] = { + this.find(equalsIgnoreCase(_.name, username)).orElse { + this.auth.getUser(username).map(User.fromSponge).semiFlatMap(getOrCreate) } } @@ -59,21 +58,12 @@ class UserBase(override val service: ModelService, * * @return the requested user */ - def requestPermission(user: User, name: String, perm: Permission)(implicit ec: ExecutionContext): Future[Option[User]] = { - this.withName(name).flatMap { - case None => Future.successful(None) // Name not found - case Some(toCheck) => - if (user.equals(toCheck)) Future.successful(Some(user)) // Same user - else { - // TODO remove double DB access for orga check - toCheck.isOrganization.flatMap { - case false => Future.successful(None) // Not an orga - case true => toCheck.toOrganization.flatMap { orga => - user can perm in orga map { perm => - if (perm) Some(toCheck) // Has Orga perm - else None // Has not Orga perm - } - } + def requestPermission(user: User, name: String, perm: Permission)(implicit ec: ExecutionContext): OptionT[Future, User] = { + this.withName(name).flatMap { toCheck => + if(user == toCheck) OptionT.pure[Future](user) // Same user + else toCheck.toMaybeOrganization.flatMap { orga => + OptionT.liftF(user can perm in orga).collect { + case true => toCheck // Has Orga perm } } } @@ -153,7 +143,7 @@ class UserBase(override val service: ModelService, * * @return Found or new User */ - def getOrCreate(user: User): Future[User] = user.schema(this.service).getOrInsert(user) + def getOrCreate(user: User)(implicit ec: ExecutionContext): Future[User] = user.schema(this.service).getOrInsert(user) /** * Creates a new [[Session]] for the specified [[User]]. @@ -177,15 +167,14 @@ class UserBase(override val service: ModelService, * @param token Token of session * @return Session if found and has not expired */ - private def getSession(token: String)(implicit ec: ExecutionContext): Future[Option[Session]] = - this.service.access[Session](classOf[Session]).find(_.token === token).map { _.flatMap { session => + private def getSession(token: String)(implicit ec: ExecutionContext): OptionT[Future, Session] = + this.service.access[Session](classOf[Session]).find(_.token === token).subflatMap { session => if (session.hasExpired) { session.remove() None - } else - Some(session) + } else Some(session) } - } + /** * Returns the currently authenticated User.c @@ -193,14 +182,10 @@ class UserBase(override val service: ModelService, * @param session Current session * @return Authenticated user, if any, None otherwise */ - def current(implicit session: Request[_], ec: ExecutionContext): Future[Option[User]] = { - session.cookies.get("_oretoken") match { - case None => Future.successful(None) - case Some(cookie) => getSession(cookie.value).flatMap { - case None => Future.successful(None) - case Some(s) => s.user - } - } + def current(implicit session: Request[_], ec: ExecutionContext): OptionT[Future, User] = { + OptionT.fromOption[Future](session.cookies.get("_oretoken")) + .flatMap(cookie => getSession(cookie.value)) + .flatMap(_.user) } } diff --git a/app/db/impl/schema/PageSchema.scala b/app/db/impl/schema/PageSchema.scala index 8b63a3388..edd49919f 100644 --- a/app/db/impl/schema/PageSchema.scala +++ b/app/db/impl/schema/PageSchema.scala @@ -4,8 +4,9 @@ import db.impl.OrePostgresDriver.api._ import db.impl.PageTable import db.{ModelSchema, ModelService} import models.project.Page +import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.Future +import util.functional.OptionT /** * Page related queries. @@ -13,7 +14,7 @@ import scala.concurrent.Future class PageSchema(override val service: ModelService) extends ModelSchema[Page](service, classOf[Page], TableQuery[PageTable]) { - override def like(page: Page): Future[Option[Page]] = { + override def like(page: Page)(implicit ec: ExecutionContext): OptionT[Future, Page] = { this.service.find[Page](this.modelClass, p => p.projectId === page.projectId && p.name.toLowerCase === page.name.toLowerCase && p.parentId === page.parentId ) diff --git a/app/db/impl/schema/ProjectSchema.scala b/app/db/impl/schema/ProjectSchema.scala index 54377b3d8..474e07510 100755 --- a/app/db/impl/schema/ProjectSchema.scala +++ b/app/db/impl/schema/ProjectSchema.scala @@ -9,9 +9,9 @@ import models.user.User import ore.Platforms.Platform import ore.project.Categories.Category import ore.project.ProjectSortingStrategies.ProjectSortingStrategy +import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import util.functional.OptionT /** * Project related queries @@ -24,12 +24,11 @@ class ProjectSchema(override val service: ModelService, implicit val users: User * * @return Project authors */ - def distinctAuthors: Future[Seq[User]] = { - service.DB.db.run { - (for (project <- this.baseQuery) yield project.userId).distinct.result - } flatMap { userIds => - this.users.in(userIds.toSet) - } map(_.toSeq) + def distinctAuthors(implicit ec: ExecutionContext): Future[Seq[User]] = { + for { + userIds <- service.DB.db.run(this.baseQuery.map(_.userId).distinct.result) + inIds <- this.users.in(userIds.toSet) + } yield inIds.toSeq } /** @@ -78,10 +77,10 @@ class ProjectSchema(override val service: ModelService, implicit val users: User * @return Projects matching criteria */ def collect(filter: Project#T => Rep[Boolean], sort: ProjectSortingStrategy, - limit: Int, offset: Int): Future[Seq[Project]] + limit: Int, offset: Int)(implicit ec: ExecutionContext): Future[Seq[Project]] = this.service.collect[Project](this.modelClass, filter, Option(sort).map(_.fn).orNull, limit, offset) - override def like(model: Project): Future[Option[Project]] = { + override def like(model: Project)(implicit ec: ExecutionContext): OptionT[Future, Project] = { this.service.find[Project](this.modelClass, p => p.ownerName.toLowerCase === model.ownerName.toLowerCase && p.name.toLowerCase === model.name.toLowerCase) } diff --git a/app/db/impl/schema/StatSchema.scala b/app/db/impl/schema/StatSchema.scala index fc3dd4d4c..1a15b31d4 100644 --- a/app/db/impl/schema/StatSchema.scala +++ b/app/db/impl/schema/StatSchema.scala @@ -3,9 +3,10 @@ package db.impl.schema import db.impl.OrePostgresDriver.api._ import db.{ModelFilter, ModelSchema, ModelService} import models.statistic.StatEntry +import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.{Future, Promise} +import util.functional.OptionT +import util.instances.future._ /** * Records and determines uniqueness of StatEntries in a StatTable. @@ -25,9 +26,9 @@ trait StatSchema[M <: StatEntry[_]] extends ModelSchema[M] { * @param entry Entry to check * @return True if recorded in database */ - def record(entry: M): Future[Boolean] = { + def record(entry: M)(implicit ec: ExecutionContext): Future[Boolean] = { val promise = Promise[Boolean] - this.like(entry).andThen { + this.like(entry).value.andThen { case result => result.get match { case None => // No previous entry found, insert new entry @@ -43,11 +44,11 @@ trait StatSchema[M <: StatEntry[_]] extends ModelSchema[M] { promise.future } - override def like(entry: M): Future[Option[M]] = { + override def like(entry: M)(implicit ec: ExecutionContext): OptionT[Future, M] = { val baseFilter: ModelFilter[M] = ModelFilter[M](_.modelId === entry.modelId) val filter: M#T => Rep[Boolean] = e => e.address === entry.address || e.cookie === entry.cookie - val userFilter = entry.user.map(_.map[M#T => Rep[Boolean]](u => e => filter(e) || e.userId === u.id.get).getOrElse(filter)) - userFilter.flatMap { uFilter => + val userFilter = entry.user.map[M#T => Rep[Boolean]](u => e => filter(e) || e.userId === u.id.get).getOrElse(filter) + OptionT.liftF(userFilter).flatMap { uFilter => this.service.find(this.modelClass, (baseFilter && uFilter).fn) } } diff --git a/app/db/impl/schema/UserSchema.scala b/app/db/impl/schema/UserSchema.scala index 288d82029..fad42c36b 100644 --- a/app/db/impl/schema/UserSchema.scala +++ b/app/db/impl/schema/UserSchema.scala @@ -4,8 +4,9 @@ import db.impl.OrePostgresDriver.api._ import db.impl.UserTable import db.{ModelSchema, ModelService} import models.user.User +import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.Future +import util.functional.OptionT /** * User related queries. @@ -15,7 +16,7 @@ import scala.concurrent.Future class UserSchema(override val service: ModelService) extends ModelSchema[User](service, classOf[User], TableQuery[UserTable]) { - override def like(user: User): Future[Option[User]] + override def like(user: User)(implicit ec: ExecutionContext): OptionT[Future, User] = this.service.find[User](this.modelClass, _.name.toLowerCase === user.username.toLowerCase) } diff --git a/app/discourse/OreDiscourseApi.scala b/app/discourse/OreDiscourseApi.scala index 992c11b0a..944e412f1 100644 --- a/app/discourse/OreDiscourseApi.scala +++ b/app/discourse/OreDiscourseApi.scala @@ -10,9 +10,8 @@ import models.user.User import org.spongepowered.play.discourse.DiscourseApi import util.StringUtils._ -import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{Future, Promise} +import scala.concurrent.{ExecutionContext, Future, Promise} import scala.util.{Failure, Success} /** @@ -45,6 +44,9 @@ trait OreDiscourseApi extends DiscourseApi { private var recovery: RecoveryTask = _ + //This executionContext should only be used in start() which is called from Bootstrap + def bootstrapExecutionContext: ExecutionContext + /** * Initializes and starts this API instance. */ @@ -54,7 +56,7 @@ trait OreDiscourseApi extends DiscourseApi { return } checkNotNull(this.projects, "projects are null", "") - this.recovery = new RecoveryTask(this.scheduler, this.retryRate, this, this.projects) + this.recovery = new RecoveryTask(this.scheduler, this.retryRate, this, this.projects)(bootstrapExecutionContext) this.recovery.start() Logger.info("Discourse API initialized.") } @@ -65,7 +67,7 @@ trait OreDiscourseApi extends DiscourseApi { * @param project Project to create topic for. * @return True if successful */ - def createProjectTopic(project: Project): Future[Boolean] = { + def createProjectTopic(project: Project)(implicit ec: ExecutionContext): Future[Boolean] = { if (!this.isEnabled) return Future.successful(true) checkArgument(project.id.isDefined, "undefined project", "") @@ -124,7 +126,7 @@ trait OreDiscourseApi extends DiscourseApi { * @param project Project to update topic for * @return True if successful */ - def updateProjectTopic(project: Project): Future[Boolean] = { + def updateProjectTopic(project: Project)(implicit ec: ExecutionContext): Future[Boolean] = { if (!this.isEnabled) return Future.successful(true) checkArgument(project.id.isDefined, "undefined project", "") @@ -206,7 +208,7 @@ trait OreDiscourseApi extends DiscourseApi { * @param content Post content * @return List of errors Discourse returns */ - def postDiscussionReply(project: Project, user: User, content: String): Future[List[String]] = { + def postDiscussionReply(project: Project, user: User, content: String)(implicit ec: ExecutionContext): Future[List[String]] = { if (!this.isEnabled) { Logger.warn("Tried to post discussion with API disabled?") // Shouldn't be reachable return Future.successful(List.empty) @@ -230,7 +232,7 @@ trait OreDiscourseApi extends DiscourseApi { * @param version Version of project * @return */ - def postVersionRelease(project: Project, version: Version, content: Option[String]): Future[List[String]] = { + def postVersionRelease(project: Project, version: Version, content: Option[String])(implicit ec: ExecutionContext): Future[List[String]] = { if (!this.isEnabled) return Future.successful(List.empty) checkArgument(project.id.isDefined, "undefined project", "") @@ -255,7 +257,7 @@ trait OreDiscourseApi extends DiscourseApi { * @param project Project to delete topic for * @return True if deleted */ - def deleteProjectTopic(project: Project): Future[Boolean] = { + def deleteProjectTopic(project: Project)(implicit ec: ExecutionContext): Future[Boolean] = { if (!this.isEnabled) return Future.successful(true) checkArgument(project.id.isDefined, "undefined project", "") @@ -290,7 +292,7 @@ trait OreDiscourseApi extends DiscourseApi { * @param users Users to check * @return Amount on discourse */ - def countUsers(users: List[String]): Future[Int] = { + def countUsers(users: List[String])(implicit ec: ExecutionContext): Future[Int] = { if (!this.isEnabled) return Future.successful(0) var futures: Seq[Future[Boolean]] = Seq.empty @@ -311,7 +313,7 @@ trait OreDiscourseApi extends DiscourseApi { def projectTitle(project: Project) = project.name + project.description.map(d => s" - $d").getOrElse("") /** Generates the content for a project topic. */ - def projectTopic(project: Project) = readAndFormatFile( + def projectTopic(project: Project)(implicit ec: ExecutionContext) = readAndFormatFile( OreDiscourseApi.this.topicTemplatePath, project.name, OreDiscourseApi.this.baseUrl + '/' + project.url, diff --git a/app/discourse/RecoveryTask.scala b/app/discourse/RecoveryTask.scala index 1f3041212..bc2972291 100644 --- a/app/discourse/RecoveryTask.scala +++ b/app/discourse/RecoveryTask.scala @@ -4,7 +4,7 @@ import akka.actor.Scheduler import db.impl.OrePostgresDriver.api._ import db.impl.access.ProjectBase -import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration /** @@ -13,7 +13,7 @@ import scala.concurrent.duration.FiniteDuration class RecoveryTask(scheduler: Scheduler, retryRate: FiniteDuration, api: OreDiscourseApi, - projects: ProjectBase) extends Runnable { + projects: ProjectBase)(implicit ec: ExecutionContext) extends Runnable { val Logger = this.api.Logger diff --git a/app/discourse/SpongeForums.scala b/app/discourse/SpongeForums.scala index 5023c57c8..0cb961fc0 100644 --- a/app/discourse/SpongeForums.scala +++ b/app/discourse/SpongeForums.scala @@ -7,6 +7,7 @@ import javax.inject.{Inject, Singleton} import ore.{OreConfig, OreEnv} import play.api.libs.ws.WSClient +import scala.concurrent.ExecutionContext import scala.concurrent.duration._ /** @@ -33,6 +34,7 @@ class SpongeForums @Inject()(env: OreEnv, override val topicTemplatePath: Path = this.env.conf.resolve("discourse/project_topic.md") override val versionReleasePostTemplatePath: Path = this.env.conf.resolve("discourse/version_post.md") override val scheduler = this.actorSystem.scheduler + override val bootstrapExecutionContext: ExecutionContext = this.actorSystem.dispatcher override val retryRate = this.conf.get[FiniteDuration]("embed.retryRate") } diff --git a/app/form/OreForms.scala b/app/form/OreForms.scala index b7bd4db30..56eec11bd 100755 --- a/app/form/OreForms.scala +++ b/app/form/OreForms.scala @@ -21,6 +21,7 @@ import play.api.data.format.Formatter import play.api.data.validation.{Constraint, Invalid, Valid, ValidationError} import play.api.data.{Form, FormError} +import scala.concurrent.ExecutionContext import scala.util.Try /** @@ -226,7 +227,7 @@ class OreForms @Inject()(implicit config: OreConfig, factory: ProjectFactory, se def required(key: String) = Seq(FormError(key, "error.required", Nil)) - val projectApiKey = of[ProjectApiKey](new Formatter[ProjectApiKey] { + def projectApiKey(implicit ec: ExecutionContext) = of[ProjectApiKey](new Formatter[ProjectApiKey] { def bind(key: String, data: Map[String, String]) = { data.get(key). flatMap(id => Try(id.toInt).toOption.flatMap(evilAwaitpProjectApiKey(_))) @@ -236,15 +237,15 @@ class OreForms @Inject()(implicit config: OreConfig, factory: ProjectFactory, se def unbind(key: String, value: ProjectApiKey): Map[String, String] = Map(key -> value.id.get.toString) }) - def evilAwaitpProjectApiKey(key: Int): Option[ProjectApiKey] = { + def evilAwaitpProjectApiKey(key: Int)(implicit ec: ExecutionContext): Option[ProjectApiKey] = { val projectApiKeys = this.service.access[ProjectApiKey](classOf[ProjectApiKey]) // TODO remvove await - this.service.await(projectApiKeys.get(key)).getOrElse(None) + this.service.await(projectApiKeys.get(key).value).getOrElse(None) } - lazy val ProjectApiKeyRevoke = Form(single("id" -> projectApiKey)) + def ProjectApiKeyRevoke(implicit ec: ExecutionContext) = Form(single("id" -> projectApiKey)) - def channel(implicit request: ProjectRequest[_]) = of[Channel](new Formatter[Channel] { + def channel(implicit request: ProjectRequest[_], ec: ExecutionContext) = of[Channel](new Formatter[Channel] { def bind(key: String, data: Map[String, String]) = { data.get(key) .flatMap(evilAwaitChannel(_)) @@ -254,13 +255,13 @@ class OreForms @Inject()(implicit config: OreConfig, factory: ProjectFactory, se def unbind(key: String, value: Channel) = Map(key -> value.name.toLowerCase) }) - def evilAwaitChannel(c: String)(implicit request: ProjectRequest[_]): Option[Channel] = { + def evilAwaitChannel(c: String)(implicit request: ProjectRequest[_], ec: ExecutionContext): Option[Channel] = { val value = request.data.project.channels.find(_.name.toLowerCase === c.toLowerCase) // TODO remvove await - this.service.await(value).getOrElse(None) + this.service.await(value.value).getOrElse(None) } - def VersionDeploy(implicit request: ProjectRequest[_]) = Form(mapping( + def VersionDeploy(implicit request: ProjectRequest[_], ec: ExecutionContext) = Form(mapping( "apiKey" -> nonEmptyText, "channel" -> channel, "recommended" -> default(boolean, true), diff --git a/app/form/project/TChannelData.scala b/app/form/project/TChannelData.scala index 2e22e018e..3cd0ae163 100644 --- a/app/form/project/TChannelData.scala +++ b/app/form/project/TChannelData.scala @@ -4,6 +4,10 @@ import models.project.{Channel, Project} import ore.Colors.Color import ore.OreConfig import ore.project.factory.ProjectFactory +import util.functional.{EitherT, OptionT} +import util.instances.future._ +import util.syntax._ +import util.StringUtils._ import scala.concurrent.{ExecutionContext, Future} @@ -33,23 +37,12 @@ trait TChannelData { * @param project Project to add Channel to * @return Either the new channel or an error message */ - def addTo(project: Project)(implicit ec: ExecutionContext): Future[Either[String, Channel]] = { - project.channels.all.flatMap { channels => - if (channels.size >= config.projects.get[Int]("max-channels")) { - Future.successful(Left("A project may only have up to five channels.")) - } else { - channels.find(_.name.equalsIgnoreCase(this.channelName)) match { - case Some(_) => - Future.successful(Left("A channel with that name already exists.")) - case None => channels.find(_.color.equals(this.color)) match { - case Some(_) => - Future.successful(Left("A channel with that color already exists.")) - case None => - this.factory.createChannel(project, this.channelName, this.color, this.nonReviewed).map(Right(_)) - } - } - } - } + def addTo(project: Project)(implicit ec: ExecutionContext): EitherT[Future, String, Channel] = { + EitherT.liftF(project.channels.all) + .filterOrElse(_.size <= config.projects.get[Int]("max-channels"), "A project may only have up to five channels.") + .filterOrElse(_.forall(ch => !ch.name.equalsIgnoreCase(this.channelName)), "A channel with that name already exists.") + .filterOrElse(_.forall(_.color != this.color), "A channel with that color already exists.") + .semiFlatMap(_ => this.factory.createChannel(project, this.channelName, this.color, this.nonReviewed)) } /** @@ -60,29 +53,25 @@ trait TChannelData { * @param project Project of channel * @return Error, if any */ - def saveTo(oldName: String)(implicit project: Project, ec: ExecutionContext): Future[Option[String]] = { - project.channels.all.map { channels => - val channel = channels.find(_.name.equalsIgnoreCase(oldName)).get - val colorChan = channels.find(_.color.equals(this.color)) - val colorTaken = colorChan.isDefined && !colorChan.get.equals(channel) - if (colorTaken) { - Some("A channel with that color already exists.") - } else { - val nameChan = channels.find(_.name.equalsIgnoreCase(this.channelName)) - val nameTaken = nameChan.isDefined && !nameChan.get.equals(channel) - if (nameTaken) { - Some("A channel with that name already exists.") - } else { - val reviewedChannels = channels.filter(!_.isNonReviewed) - if (this.nonReviewed && reviewedChannels.size <= 1 && reviewedChannels.contains(channel)) { - Some("There must be at least one reviewed channel.") - } else { - channel.setName(this.channelName) - channel.setColor(this.color) - channel.setNonReviewed(this.nonReviewed) - None - } - } + //TODO: Return NEL[String] if we get the type + def saveTo(oldName: String)(implicit project: Project, ec: ExecutionContext): EitherT[Future, List[String], Unit] = { + EitherT.liftF(project.channels.all).flatMap { allChannels => + val (channelChangeSet, channels) = allChannels.partition(_.name.equalsIgnoreCase(oldName)) + val channel = channelChangeSet.toSeq.head + //TODO: Rewrite this nicer if we ever get a Validated/Validation type + val e1 = if(channels.exists(_.color == this.color)) List("error.channel.duplicateColor") else Nil + val e2 = if(channels.exists(_.name.equalsIgnoreCase(this.channelName))) List("error.channel.duplicateName") else Nil + val e3 = if(nonReviewed && channels.count(_.isReviewed) < 1) List("error.channel.minOneReviewed") else Nil + val errors = e1 ::: e2 ::: e3 + + if(errors.nonEmpty) { + EitherT.leftT[Future, Unit](errors) + } + else { + val effects = channel.setName(this.channelName) *> + channel.setColor(this.color) *> + channel.setNonReviewed(this.nonReviewed) + EitherT.right[List[String]](effects).map(_ => ()) } } } diff --git a/app/mail/Mailer.scala b/app/mail/Mailer.scala index cc03e442f..ffa3327a7 100644 --- a/app/mail/Mailer.scala +++ b/app/mail/Mailer.scala @@ -11,7 +11,7 @@ import javax.mail.Session import javax.mail.internet.{InternetAddress, MimeMessage} import play.api.Configuration -import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.ExecutionContext import scala.concurrent.duration._ /** @@ -52,7 +52,7 @@ trait Mailer extends Runnable { /** * Configures, initializes, and starts this Mailer. */ - def start() = { + def start()(implicit ec: ExecutionContext) = { Security.addProvider(new Provider) val props = System.getProperties for (prop <- this.properties.keys) @@ -104,7 +104,7 @@ trait Mailer extends Runnable { } @Singleton -final class SpongeMailer @Inject()(config: Configuration, actorSystem: ActorSystem) extends Mailer { +final class SpongeMailer @Inject()(config: Configuration, actorSystem: ActorSystem)(implicit ec: ExecutionContext) extends Mailer { private val conf = config.get[Configuration]("mail") diff --git a/app/models/admin/ProjectLog.scala b/app/models/admin/ProjectLog.scala index d6fcb4293..c9732d4c4 100644 --- a/app/models/admin/ProjectLog.scala +++ b/app/models/admin/ProjectLog.scala @@ -8,6 +8,7 @@ import db.impl.OrePostgresDriver.api._ import db.impl.ProjectLogTable import db.impl.model.OreModel import ore.project.ProjectOwned +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} @@ -43,17 +44,17 @@ case class ProjectLog(override val id: Option[Int] = None, val entries = this.service.access[ProjectLogEntry]( classOf[ProjectLogEntry], ModelFilter[ProjectLogEntry](_.logId === this.id.get)) val tag = "error" - entries.find(e => e.message === message && e.tag === tag).flatMap(_.map { entry => + entries.find(e => e.message === message && e.tag === tag).map { entry => entry.setOoccurrences(entry.occurrences + 1) entry.setLastOccurrence(this.service.theTime) - Future.successful(entry) - } getOrElse { + entry + }.getOrElseF { entries.add(ProjectLogEntry( logId = this.id.get, tag = tag, message = message, _lastOccurrence = this.service.theTime)) - }) + } } def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) diff --git a/app/models/admin/VisibilityChange.scala b/app/models/admin/VisibilityChange.scala index c7801ebbd..4dbea7d92 100644 --- a/app/models/admin/VisibilityChange.scala +++ b/app/models/admin/VisibilityChange.scala @@ -8,9 +8,11 @@ import db.impl.model.OreModel import db.impl.table.ModelKeys._ import models.project.Page import models.user.User +import util.functional.OptionT +import util.instances.future._ import play.twirl.api.Html -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} case class VisibilityChange(override val id: Option[Int] = None, override val createdAt: Option[Timestamp] = None, @@ -31,11 +33,8 @@ case class VisibilityChange(override val id: Option[Int] = None, /** Check if the change has been dealt with */ def isResolved: Boolean = !resolvedAt.isEmpty - def created: Future[Option[User]] = { - if (createdBy.isEmpty) Future.successful(None) - else { - userBase.get(createdBy.get) - } + def created(implicit ec: ExecutionContext): OptionT[Future, User] = { + OptionT.fromOption[Future](createdBy).flatMap(userBase.get(_)) } /** diff --git a/app/models/project/Channel.scala b/app/models/project/Channel.scala index a89196389..e33888c72 100644 --- a/app/models/project/Channel.scala +++ b/app/models/project/Channel.scala @@ -2,6 +2,8 @@ package models.project import java.sql.Timestamp +import scala.concurrent.Future + import com.google.common.base.Preconditions._ import db.Named import db.impl.ChannelTable @@ -76,12 +78,15 @@ case class Channel(override val id: Option[Int] = None, update(ModelKeys.Color) } + def isReviewed: Boolean = !this._isNonReviewed + def isNonReviewed: Boolean = this._isNonReviewed def setNonReviewed(isNonReviewed: Boolean) = { this._isNonReviewed = isNonReviewed if (isDefined) update(IsNonReviewed) + else Future.unit } /** diff --git a/app/models/project/DownloadWarning.scala b/app/models/project/DownloadWarning.scala index c5978ecb7..1bad9976a 100644 --- a/app/models/project/DownloadWarning.scala +++ b/app/models/project/DownloadWarning.scala @@ -10,6 +10,8 @@ import db.impl.DownloadWarningsTable import db.impl.model.OreModel import db.impl.table.ModelKeys._ import models.project.DownloadWarning.COOKIE +import util.functional.OptionT +import util.instances.future._ import play.api.mvc.Cookie import scala.concurrent.{ExecutionContext, Future} @@ -57,9 +59,9 @@ case class DownloadWarning(override val id: Option[Int] = None, * * @return Download */ - def download(implicit ec: ExecutionContext): Future[Option[UnsafeDownload]] = { + def download(implicit ec: ExecutionContext): OptionT[Future, UnsafeDownload] = { if (this._downloadId == -1) - Future.successful(None) + OptionT.none[Future, UnsafeDownload] else this.service.access[UnsafeDownload](classOf[UnsafeDownload]).get(this._downloadId) } diff --git a/app/models/project/Page.scala b/app/models/project/Page.scala index fe9469523..72a60e1b2 100644 --- a/app/models/project/Page.scala +++ b/app/models/project/Page.scala @@ -27,6 +27,8 @@ import ore.OreConfig import ore.permission.scope.ProjectScope import play.twirl.api.Html import util.StringUtils._ +import util.instances.future._ +import util.functional.OptionT import scala.concurrent.{ExecutionContext, Future} @@ -119,18 +121,14 @@ case class Page(override val id: Option[Int] = None, * * @return Optional Project */ - def parentProject(implicit ec: ExecutionContext): Future[Option[Project]] = this.projectBase.get(projectId) + def parentProject(implicit ec: ExecutionContext): OptionT[Future, Project] = this.projectBase.get(projectId) /** * * @return */ - def parentPage(implicit ec: ExecutionContext): Future[Option[Page]] = { - parentProject.flatMap { - case None => Future.successful(None) - case Some(pp) => - pp.pages.find(ModelFilter[Page](_.id === parentId).fn) - } + def parentPage(implicit ec: ExecutionContext): OptionT[Future, Page] = { + parentProject.flatMap(_.pages.find(ModelFilter[Page](_.id === parentId).fn)) } /** diff --git a/app/models/project/Project.scala b/app/models/project/Project.scala index 7e12eb668..f384a73a6 100755 --- a/app/models/project/Project.scala +++ b/app/models/project/Project.scala @@ -5,6 +5,8 @@ import java.time.Instant import _root_.util.StringUtils import _root_.util.StringUtils._ +import _root_.util.instances.future._ +import _root_.util.functional.OptionT import com.google.common.base.Preconditions._ import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ @@ -160,17 +162,19 @@ case class Project(override val id: Option[Int] = None, override def transferOwner(member: ProjectMember)(implicit ex: ExecutionContext): Future[Int] = { // Down-grade current owner to "Developer" - this.owner.user.flatMap(u => this.memberships.getRoles(u)).map { roles => - roles.filter(_.roleType == RoleTypes.ProjectOwner) - .foreach(_.setRoleType(RoleTypes.ProjectDev)) - } flatMap { _ => - member.user.flatMap { user => - this.memberships.getRoles(user).map { roles => - roles.foreach(_.setRoleType(RoleTypes.ProjectOwner)) - } flatMap { _ => - this.setOwner(user) - } - } + for { + owner <- this.owner.user + ownerRoles <- this.memberships.getRoles(owner) + user <- member.user + userRoles <- this.memberships.getRoles(user) + setOwner <- this.setOwner(user) + } yield { + ownerRoles.filter(_.roleType == RoleTypes.ProjectOwner) + .foreach(_.setRoleType(RoleTypes.ProjectDev)) + + userRoles.foreach(_.setRoleType(RoleTypes.ProjectOwner)) + + setOwner } } @@ -285,7 +289,7 @@ case class Project(override val id: Option[Int] = None, * @return Project settings */ def settings(implicit ec: ExecutionContext): Future[ProjectSettings] - = this.service.access[ProjectSettings](classOf[ProjectSettings]).find(_.projectId === this.id.get).map(_.get) + = this.service.access[ProjectSettings](classOf[ProjectSettings]).find(_.projectId === this.id.get).getOrElse(throw new NoSuchElementException("Get on None")) /** * Sets this [[Project]]'s [[ProjectSettings]]. @@ -326,12 +330,10 @@ case class Project(override val id: Option[Int] = None, this._visibility = visibility if (isDefined) update(ModelKeys.Visibility) - val cnt = lastVisibilityChange.flatMap { - case Some(vc) => + val cnt = lastVisibilityChange.fold(0) { vc => vc.setResolvedAt(Timestamp.from(Instant.now())) vc.setResolvedById(creator) - Future.successful(0) - case None => Future.successful(0) + 0 } cnt.flatMap { _ => val change = VisibilityChange(None, Some(Timestamp.from(Instant.now())), Some(creator), this.id.get, comment, None, None, visibility.id) @@ -345,8 +347,10 @@ case class Project(override val id: Option[Int] = None, def visibilityChanges = this.schema.getChildren[VisibilityChange](classOf[VisibilityChange], this) def visibilityChangesByDate(implicit ec: ExecutionContext) = visibilityChanges.all.map(_.toSeq.sortWith(byCreationDate)) def byCreationDate(first: VisibilityChange, second: VisibilityChange) = first.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime < second.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime - def lastVisibilityChange(implicit ec: ExecutionContext): Future[Option[VisibilityChange]] = visibilityChanges.all.map(_.toSeq.filter(cr => !cr.isResolved).sortWith(byCreationDate).headOption) - def lastChangeRequest(implicit ec: ExecutionContext): Future[Option[VisibilityChange]] = visibilityChanges.all.map(_.toSeq.filter(cr => cr.visibility == VisibilityTypes.NeedsChanges.id).sortWith(byCreationDate).lastOption) + def lastVisibilityChange(implicit ec: ExecutionContext): OptionT[Future, VisibilityChange] = + OptionT(visibilityChanges.all.map(_.toSeq.filter(cr => !cr.isResolved).sortWith(byCreationDate).headOption)) + def lastChangeRequest(implicit ec: ExecutionContext): OptionT[Future, VisibilityChange] = + OptionT(visibilityChanges.all.map(_.toSeq.filter(cr => cr.visibility == VisibilityTypes.NeedsChanges.id).sortWith(byCreationDate).lastOption)) /** * Returns the last time this [[Project]] was updated. @@ -486,7 +490,8 @@ case class Project(override val id: Option[Int] = None, * * @return Recommended version */ - def recommendedVersion(implicit ec: ExecutionContext): Future[Version] = this.versions.get(this.recommendedVersionId.get).map(_.get) + def recommendedVersion(implicit ec: ExecutionContext): Future[Version] = + this.versions.get(this.recommendedVersionId.get).getOrElse(throw new NoSuchElementException("Get on None")) /** * Updates this project's recommended version. @@ -513,7 +518,7 @@ case class Project(override val id: Option[Int] = None, * * @return Project home page */ - def homePage: Page = Defined { + def homePage(implicit ec: ExecutionContext): Page = Defined { val page = new Page(this.id.get, Page.HomeName, Page.Template(this.name, Page.HomeMessage), false, -1) this.service.await(page.schema.getOrInsert(page)).get } @@ -532,7 +537,7 @@ case class Project(override val id: Option[Int] = None, * @param name Page name * @return Page with name or new name if it doesn't exist */ - def getOrCreatePage(name: String, parentId: Int = -1, content: Option[String] = None): Future[Page] = Defined { + def getOrCreatePage(name: String, parentId: Int = -1, content: Option[String] = None)(implicit ec: ExecutionContext): Future[Page] = Defined { checkNotNull(name, "null name", "") val c = content match { case None => Page.Template(name, Page.HomeMessage) @@ -550,16 +555,13 @@ case class Project(override val id: Option[Int] = None, * * @return Root pages of project */ - def rootPages: Future[Seq[Page]] = { + def rootPages(implicit ec: ExecutionContext): Future[Seq[Page]] = { this.service.access[Page](classOf[Page]).sorted(_.name, p => p.projectId === this.id.get && p.parentId === -1) } def logger(implicit ec: ExecutionContext): Future[ProjectLog] = { val loggers = this.service.access[ProjectLog](classOf[ProjectLog]) - loggers.find(_.projectId === this.id.get).flatMap { - case None => loggers.add(ProjectLog(projectId = this.id.get)) - case Some(l) => Future.successful(l) - } + loggers.find(_.projectId === this.id.get).getOrElseF(loggers.add(ProjectLog(projectId = this.id.get))) } /** diff --git a/app/models/project/ProjectSettings.scala b/app/models/project/ProjectSettings.scala index ce540e3d8..6abf584ab 100644 --- a/app/models/project/ProjectSettings.scala +++ b/app/models/project/ProjectSettings.scala @@ -8,6 +8,7 @@ import db.impl.OrePostgresDriver.api._ import db.impl._ import db.impl.model.OreModel import db.impl.table.ModelKeys._ +import util.instances.future._ import form.project.ProjectSettingsForm import models.user.Notification import models.user.role.ProjectRole @@ -158,7 +159,7 @@ case class ProjectSettings(override val id: Option[Int] = None, // Update the owner if needed val ownerSet = formData.ownerId.find(_ != project.ownerId) match { case None => Future.successful(true) - case Some(ownerId) => this.userBase.get(ownerId).flatMap(user => project.setOwner(user.get)) + case Some(ownerId) => this.userBase.get(ownerId).semiFlatMap(project.setOwner).value } ownerSet.flatMap { _ => // Update icon diff --git a/app/models/project/Version.scala b/app/models/project/Version.scala index 6cbe8baad..d0d13a65d 100755 --- a/app/models/project/Version.scala +++ b/app/models/project/Version.scala @@ -19,6 +19,8 @@ import ore.permission.scope.ProjectScope import ore.project.Dependency import play.twirl.api.Html import util.FileUtils +import util.instances.future._ +import util.functional.OptionT import scala.concurrent.{ExecutionContext, Future} @@ -74,7 +76,8 @@ case class Version(override val id: Option[Int] = None, * * @return Channel */ - def channel(implicit ec: ExecutionContext): Future[Channel] = this.service.access[Channel](classOf[Channel]).get(this.channelId).map(_.get) + def channel(implicit ec: ExecutionContext): Future[Channel] = + this.service.access[Channel](classOf[Channel]).get(this.channelId).getOrElse(throw new NoSuchElementException("None of Option")) /** * Returns the channel this version belongs to from the specified collection @@ -140,7 +143,7 @@ case class Version(override val id: Option[Int] = None, def authorId: Int = this._authorId - def author: Future[Option[User]] = this.userBase.get(this._authorId) + def author(implicit ec: ExecutionContext): OptionT[Future, User] = this.userBase.get(this._authorId) def setAuthorId(authorId: Int) = { this._authorId = authorId @@ -152,7 +155,7 @@ case class Version(override val id: Option[Int] = None, def reviewerId: Int = this._reviewerId - def reviewer: Future[Option[User]] = this.userBase.get(this._reviewerId) + def reviewer(implicit ec: ExecutionContext): OptionT[Future, User] = this.userBase.get(this._reviewerId) def setReviewer(reviewer: User) = Defined { this._reviewerId = reviewer.id.get @@ -274,9 +277,9 @@ case class Version(override val id: Option[Int] = None, def byCreationDate(first: Review, second: Review) = first.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime < second.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime def reviewEntries = this.schema.getChildren[Review](classOf[Review], this) def unfinishedReviews(implicit ec: ExecutionContext): Future[Seq[Review]] = reviewEntries.all.map(_.toSeq.filter(rev => rev.createdAt.isDefined && rev.endedAt.isEmpty).sortWith(byCreationDate)) - def mostRecentUnfinishedReview(implicit ec: ExecutionContext): Future[Option[Review]] = unfinishedReviews.map(_.headOption) + def mostRecentUnfinishedReview(implicit ec: ExecutionContext): OptionT[Future, Review] = OptionT(unfinishedReviews.map(_.headOption)) def mostRecentReviews(implicit ec: ExecutionContext): Future[Seq[Review]] = reviewEntries.toSeq.map(_.sortWith(byCreationDate)) - def reviewById(id: Int): Future[Option[Review]] = reviewEntries.find(equalsInt[ReviewTable](_.id, id)) + def reviewById(id: Int)(implicit ec: ExecutionContext): OptionT[Future, Review] = reviewEntries.find(equalsInt[ReviewTable](_.id, id)) def equalsInt[T <: Table[_]](int1: T => Rep[Int], int2: Int): T => Rep[Boolean] = int1(_) === int2 } diff --git a/app/models/statistic/ProjectView.scala b/app/models/statistic/ProjectView.scala index 10648e523..97e8eb212 100644 --- a/app/models/statistic/ProjectView.scala +++ b/app/models/statistic/ProjectView.scala @@ -10,6 +10,7 @@ import db.impl.access.UserBase import models.project.Project import ore.StatTracker._ import ore.permission.scope.ProjectScope +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} @@ -53,7 +54,7 @@ object ProjectView { implicit val r = request.request checkNotNull(request, "null request", "") checkNotNull(users, "null user base", "") - users.current.map { _.flatMap(_.id) } map { userId => + users.current.subflatMap(_.id).value.map { userId => val view = ProjectView( modelId = request.data.project.id.get, address = InetString(remoteAddress), diff --git a/app/models/statistic/StatEntry.scala b/app/models/statistic/StatEntry.scala index 9e1ffc4b7..16b3afb1a 100644 --- a/app/models/statistic/StatEntry.scala +++ b/app/models/statistic/StatEntry.scala @@ -8,9 +8,11 @@ import db.Model import db.impl.model.OreModel import db.impl.table.ModelKeys._ import db.impl.table.StatTable +import util.instances.future._ +import util.functional.OptionT import models.user.User -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} /** * Represents a statistic entry in a StatTable. @@ -41,11 +43,8 @@ abstract class StatEntry[Subject <: Model](override val id: Option[Int] = None, * * @return User of entry */ - def user: Future[Option[User]] = { - this._userId match { - case None => Future.successful(None) - case Some(id) => this.userBase.get(id) - } + def user(implicit ec: ExecutionContext): OptionT[Future, User] = { + OptionT.fromOption[Future](this._userId).flatMap(this.userBase.get(_)) } def userId = _userId diff --git a/app/models/statistic/VersionDownload.scala b/app/models/statistic/VersionDownload.scala index da06de294..a8da2434a 100644 --- a/app/models/statistic/VersionDownload.scala +++ b/app/models/statistic/VersionDownload.scala @@ -9,6 +9,7 @@ import db.impl.VersionDownloadsTable import db.impl.access.UserBase import models.project.Version import ore.StatTracker._ +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} @@ -52,8 +53,7 @@ object VersionDownload { checkArgument(version.isDefined, "undefined version", "") checkNotNull(request, "null request", "") checkNotNull(users, "null user base", "") - users.current.map { user => - val userId = user.flatMap(_.id) + users.current.subflatMap(_.id).value.map { userId => val dl = VersionDownload( modelId = version.id.get, address = InetString(remoteAddress), diff --git a/app/models/user/Notification.scala b/app/models/user/Notification.scala index d40660182..3e15f7792 100644 --- a/app/models/user/Notification.scala +++ b/app/models/user/Notification.scala @@ -8,6 +8,7 @@ import db.impl.model.OreModel import db.impl.table.ModelKeys._ import ore.user.UserOwned import ore.user.notification.NotificationTypes.NotificationType +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} @@ -41,7 +42,8 @@ case class Notification(override val id: Option[Int] = None, * * @return User from which this originated from */ - def origin(implicit ec: ExecutionContext): Future[User] = this.userBase.get(this.originId).map(_.get) + def origin(implicit ec: ExecutionContext): Future[User] = + this.userBase.get(this.originId).getOrElse(throw new NoSuchElementException("Get on None")) /** * Returns true if this notification has been read. diff --git a/app/models/user/Organization.scala b/app/models/user/Organization.scala index 449f5cdc8..87557da6d 100644 --- a/app/models/user/Organization.scala +++ b/app/models/user/Organization.scala @@ -87,19 +87,19 @@ case class Organization(override val id: Option[Int] = None, override def transferOwner(member: OrganizationMember)(implicit ec: ExecutionContext): Future[Int] = { // Down-grade current owner to "Admin" - this.owner.user.flatMap { owner => - this.memberships.getRoles(owner).map { roles => - roles.filter(_.roleType == RoleTypes.OrganizationOwner) - .foreach(_.setRoleType(RoleTypes.OrganizationAdmin)) - } - } flatMap { _ => - member.user.flatMap { memberUser => - this.memberships.getRoles(memberUser).map { memberRoles => - memberRoles.foreach(_.setRoleType(RoleTypes.OrganizationOwner)) - } flatMap { _ => - this.setOwner(memberUser) - } - } + for { + owner <- this.owner.user + roles <- this.memberships.getRoles(owner) + memberUser <- member.user + memberRoles <- this.memberships.getRoles(memberUser) + setOwner <- this.setOwner(memberUser) + } yield { + roles.filter(_.roleType == RoleTypes.OrganizationOwner) + .foreach(_.setRoleType(RoleTypes.OrganizationAdmin)) + + memberRoles.foreach(_.setRoleType(RoleTypes.OrganizationOwner)) + + setOwner } } diff --git a/app/models/user/User.scala b/app/models/user/User.scala index b6e306ef6..2f429a182 100644 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -25,8 +25,9 @@ import security.pgp.PGPPublicKeyInfo import security.spauth.SpongeUser import slick.lifted.{QueryBase, TableQuery} import util.StringUtils._ +import util.instances.future._ +import util.functional.OptionT -import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{ExecutionContext, Future} import scala.util.control.Breaks._ @@ -284,7 +285,7 @@ case class User(override val id: Option[Int] = None, * * @return Highest level of trust */ - def trustIn(scope: Scope = GlobalScope): Future[Trust] = Defined { + def trustIn(scope: Scope = GlobalScope)(implicit ec: ExecutionContext): Future[Trust] = Defined { scope match { case GlobalScope => Future.successful(this.globalRoles.map(_.trust).toList.sorted.lastOption.getOrElse(Default)) @@ -334,7 +335,7 @@ case class User(override val id: Option[Int] = None, * @param page Page of user stars * @return Projects user has starred */ - def starred(page: Int = -1): Future[Seq[Project]] = Defined { + def starred(page: Int = -1)(implicit ec: ExecutionContext): Future[Seq[Project]] = Defined { val starsPerPage = this.config.users.get[Int]("stars-per-page") val limit = if (page < 1) -1 else starsPerPage val offset = (page - 1) * starsPerPage @@ -348,23 +349,12 @@ case class User(override val id: Option[Int] = None, * * @return True if currently authenticated user */ - def isCurrent(implicit request: Request[_]): Future[Boolean] = { + def isCurrent(implicit request: Request[_], ec: ExecutionContext): Future[Boolean] = { checkNotNull(request, "null request", "") - this.service.getModelBase(classOf[UserBase]).current.flatMap { - case None => Future.successful(false) - case Some(user) => - if (user.equals(this)) Future.successful(true) - else { - this.isOrganization.flatMap { - case false => Future.successful(false) - case true => this.toOrganization.flatMap { orga => - orga.owner.user - } map { orgaOwner => - orgaOwner.equals(user) - } - } - } - } + this.service.getModelBase(classOf[UserBase]).current.semiFlatMap { user => + if(user == this) Future.successful(true) + else this.toMaybeOrganization.semiFlatMap(_.owner.user).contains(user) + }.exists(identity) } /** @@ -372,41 +362,37 @@ case class User(override val id: Option[Int] = None, * non-missing mutable fields. * * @param user User to fill with - * @return This user - */ - def fill(user: DiscourseUser): Future[User] = { - if (user == null) - return Future.successful(this) - this.setUsername(user.username) - user.createdAt.foreach(this.setJoinDate) - user.email.foreach(this.setEmail) - user.fullName.foreach(this.setFullName) - user.avatarTemplate.foreach(this.setAvatarUrl) - this.setGlobalRoles(user.groups - .flatMap(group => RoleTypes.values.find(_.roleId == group.id).map(_.asInstanceOf[RoleType])) - .toSet[RoleType]) - Future.successful(this) + */ + def fill(user: DiscourseUser): Unit = { + if (user != null) { + this.setUsername(user.username) + user.createdAt.foreach(this.setJoinDate) + user.email.foreach(this.setEmail) + user.fullName.foreach(this.setFullName) + user.avatarTemplate.foreach(this.setAvatarUrl) + this.setGlobalRoles(user.groups + .flatMap(group => RoleTypes.values.find(_.roleId == group.id).map(_.asInstanceOf[RoleType])) + .toSet[RoleType]) + } } /** * Fills this User with the information SpongeUser provides. * * @param user Sponge User - * @return This user - */ - def fill(user: SpongeUser)(implicit config: OreConfig): Future[User] = { - if (user == null) - return Future.successful(this) - this.setUsername(user.username) - this.setEmail(user.email) - user.avatarUrl.map { url => - if (!url.startsWith("http")) { - val baseUrl = config.security.get[String]("api.url") - baseUrl + url - } else - url - }.foreach(this.setAvatarUrl(_)) - Future.successful(this) + */ + def fill(user: SpongeUser)(implicit config: OreConfig): Unit = { + if (user != null) { + this.setUsername(user.username) + this.setEmail(user.email) + user.avatarUrl.map { url => + if (!url.startsWith("http")) { + val baseUrl = config.security.get[String]("api.url") + baseUrl + url + } else + url + }.foreach(this.setAvatarUrl) + } } /** @@ -414,9 +400,9 @@ case class User(override val id: Option[Int] = None, * * @return This user */ - def pullForumData(): Future[User] = { + def pullForumData()(implicit ec: ExecutionContext): Future[Unit] = { // Exceptions are ignored - this.forums.fetchUser(this.name).recover{case _: Exception => None}.flatMap(_.map(fill).getOrElse(Future.successful(this))) + OptionT(this.forums.fetchUser(this.name).recover{case _: Exception => None}).cata((), fill) } /** @@ -424,8 +410,8 @@ case class User(override val id: Option[Int] = None, * * @return This user */ - def pullSpongeData(): Future[User] = { - this.auth.getUser(this.name).flatMap(_.map(fill).getOrElse(Future.failed(new Exception("user doesn't exist on SpongeAuth?")))) + def pullSpongeData()(implicit ec: ExecutionContext): Future[Unit] = { + this.auth.getUser(this.name).cata((), fill) } /** @@ -441,7 +427,7 @@ case class User(override val id: Option[Int] = None, * @param name Name of project * @return Owned project, if any, None otherwise */ - def getProject(name: String)(implicit ec: ExecutionContext): Future[Option[Project]] = this.projects.find(equalsIgnoreCase(_.name, name)) + def getProject(name: String)(implicit ec: ExecutionContext): OptionT[Future, Project] = this.projects.find(equalsIgnoreCase(_.name, name)) /** * Returns a [[ModelAccess]] of [[ProjectRole]]s. @@ -477,7 +463,7 @@ case class User(override val id: Option[Int] = None, * * @return True if organization */ - def isOrganization: Future[Boolean] = Defined { + def isOrganization(implicit ec: ExecutionContext): Future[Boolean] = Defined { this.service.getModelBase(classOf[OrganizationBase]).exists(_.id === this.id.get) } @@ -486,12 +472,12 @@ case class User(override val id: Option[Int] = None, * * @return Organization */ - def toOrganization: Future[Organization] = Defined { - this.service.getModelBase(classOf[OrganizationBase]).get(this.id.get).map( - _.getOrElse(throw new IllegalStateException("user is not an organization"))) + def toOrganization(implicit ec: ExecutionContext): Future[Organization] = Defined { + this.service.getModelBase(classOf[OrganizationBase]).get(this.id.get) + .getOrElse(throw new IllegalStateException("user is not an organization")) } - def toMaybeOrganization: Future[Option[Organization]] = Defined { + def toMaybeOrganization(implicit ec: ExecutionContext): OptionT[Future, Organization] = Defined { this.service.getModelBase(classOf[OrganizationBase]).get(this.id.get) } @@ -508,7 +494,7 @@ case class User(override val id: Option[Int] = None, * @param project Project to update status on * @param watching True if watching */ - def setWatching(project: Project, watching: Boolean) = { + def setWatching(project: Project, watching: Boolean)(implicit ec: ExecutionContext) = { checkNotNull(project, "null project", "") checkArgument(project.isDefined, "undefined project", "") val contains = this.watching.contains(project) @@ -532,7 +518,7 @@ case class User(override val id: Option[Int] = None, * @param project Project to check * @return True if has pending flag on Project */ - def hasUnresolvedFlagFor(project: Project): Future[Boolean] = { + def hasUnresolvedFlagFor(project: Project)(implicit ec: ExecutionContext): Future[Boolean] = { checkNotNull(project, "null project", "") checkArgument(project.isDefined, "undefined project", "") this.flags.exists(f => f.projectId === project.id.get && !f.isResolved) @@ -551,7 +537,7 @@ case class User(override val id: Option[Int] = None, * @param notification Notification to send * @return Future result */ - def sendNotification(notification: Notification) = { + def sendNotification(notification: Notification)(implicit ec: ExecutionContext) = { checkNotNull(notification, "null notification", "") this.config.debug("Sending notification: " + notification, -1) this.service.access[Notification](classOf[Notification]).add(notification.copy(userId = this.id.get)) @@ -562,7 +548,7 @@ case class User(override val id: Option[Int] = None, * * @return True if has unread notifications */ - def hasNotice: Future[Boolean] = Defined { + def hasNotice(implicit ec: ExecutionContext): Future[Boolean] = Defined { val flags = this.service.access[Flag](classOf[Flag]) val versions = this.service.access[Version](classOf[Version]) @@ -611,18 +597,26 @@ object User { /** * Creates a new [[User]] from the specified [[DiscourseUser]]. * - * @param user User to convert - * @return Ore User + * @param toConvert User to convert + * @return Ore User */ @deprecated("use fromSponge instead", "Oct 14, 2016, 1:45 PM PDT") - def fromDiscourse(user: DiscourseUser) = User().fill(user).map(_.copy(id = Some(user.id))) + def fromDiscourse(toConvert: DiscourseUser)(implicit ec: ExecutionContext): User = { + val user = User() + user.fill(toConvert) + user.copy(id = Some(toConvert.id)) + } /** * Create a new [[User]] from the specified [[SpongeUser]]. * - * @param user User to convert - * @return Ore user + * @param toConvert User to convert + * @return Ore user */ - def fromSponge(user: SpongeUser)(implicit config: OreConfig) = User().fill(user).map(_.copy(id = Some(user.id))) + def fromSponge(toConvert: SpongeUser)(implicit config: OreConfig, ec: ExecutionContext): User = { + val user = User() + user.fill(toConvert) + user.copy(id = Some(toConvert.id)) + } } diff --git a/app/models/viewhelper/HeaderData.scala b/app/models/viewhelper/HeaderData.scala index c36784248..414313871 100644 --- a/app/models/viewhelper/HeaderData.scala +++ b/app/models/viewhelper/HeaderData.scala @@ -13,6 +13,9 @@ import play.api.cache.AsyncCacheApi import play.api.mvc.Request import slick.jdbc.JdbcBackend import slick.lifted.TableQuery +import util.functional.OptionT +import util.instances.future._ +import util.syntax._ import scala.concurrent.{ExecutionContext, Future} @@ -62,17 +65,14 @@ object HeaderData { def cacheKey(user: User) = s"""user${user.id.get}""" def of[A](request: Request[A])(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext, service: ModelService): Future[HeaderData] = { - request.cookies.get("_oretoken") match { - case None => Future.successful(unAuthenticated) - case Some(cookie) => - getSessionUser(cookie.value).flatMap { - case None => Future.successful(unAuthenticated) - case Some(user) => - user.service = service - user.organizationBase = service.getModelBase(classOf[OrganizationBase]) - getHeaderData(user) - } - } + OptionT.fromOption[Future](request.cookies.get("_oretoken")) + .flatMap(cookie => getSessionUser(cookie.value)) + .semiFlatMap { user => + user.service = service + user.organizationBase = service.getModelBase(classOf[OrganizationBase]) + getHeaderData(user) + } + .getOrElse(unAuthenticated) } private def getSessionUser(token: String)(implicit ec: ExecutionContext, db: JdbcBackend#DatabaseDef) = { @@ -86,10 +86,8 @@ object HeaderData { (s, u) } - db.run(query.result.headOption).map { - case None => None - case Some((session, user)) => - if (session.hasExpired) None else Some(user) + OptionT(db.run(query.result.headOption)).collect { + case (session, user) if !session.hasExpired => user } } @@ -119,21 +117,22 @@ object HeaderData { private def getHeaderData(user: User)(implicit ec: ExecutionContext, db: JdbcBackend#DatabaseDef) = { - for { - perms <- perms(Some(user)) - hasNotice <- user.hasNotice - unreadNotif <- user.notifications.filterNot(_.read).map(_.nonEmpty) - unresolvedFlags <- user.flags.filterNot(_.isResolved).map(_.nonEmpty) - hasProjectApprovals <- projectApproval(user) - hasReviewQueue <- if (perms(ReviewProjects)) reviewQueue() else Future.successful(false) - } yield { + perms(Some(user)).flatMap { perms => + ( + user.hasNotice, + user.notifications.filterNot(_.read).map(_.nonEmpty), + user.flags.filterNot(_.isResolved).map(_.nonEmpty), + projectApproval(user), + if (perms(ReviewProjects)) reviewQueue() else Future.successful(false) + ).parMapN { (hasNotice, unreadNotif, unresolvedFlags, hasProjectApprovals, hasReviewQueue) => HeaderData(Some(user), - perms, - hasNotice, - unreadNotif, - unresolvedFlags, - hasProjectApprovals, - hasReviewQueue) + perms, + hasNotice, + unreadNotif, + unresolvedFlags, + hasProjectApprovals, + hasReviewQueue) + } } } @@ -142,20 +141,20 @@ object HeaderData { if (currentUser.isEmpty) Future.successful(noPerms) else { val user = currentUser.get - for { - reviewFlags <- user can ReviewFlags in GlobalScope map ((ReviewFlags, _)) - reviewVisibility <- user can ReviewVisibility in GlobalScope map ((ReviewVisibility, _)) - reviewProjects <- user can ReviewProjects in GlobalScope map ((ReviewProjects, _)) - viewStats <- user can ViewStats in GlobalScope map ((ViewStats, _)) - viewHealth <- user can ViewHealth in GlobalScope map ((ViewHealth, _)) - viewLogs <- user can ViewLogs in GlobalScope map ((ViewLogs, _)) - hideProjects <- user can HideProjects in GlobalScope map ((HideProjects, _)) - hardRemoveProject <- user can HardRemoveProject in GlobalScope map ((HardRemoveProject, _)) - userAdmin <- user can UserAdmin in GlobalScope map ((UserAdmin, _)) - hideProjects <- user can HideProjects in GlobalScope map ((HideProjects, _)) - } yield { - val perms = Seq(reviewFlags, reviewVisibility, reviewProjects, viewStats, viewHealth, viewLogs, hideProjects, hardRemoveProject, userAdmin, hideProjects) - perms toMap + ( + user can ReviewFlags in GlobalScope map ((ReviewFlags, _)), + user can ReviewVisibility in GlobalScope map ((ReviewVisibility, _)), + user can ReviewProjects in GlobalScope map ((ReviewProjects, _)), + user can ViewStats in GlobalScope map ((ViewStats, _)), + user can ViewHealth in GlobalScope map ((ViewHealth, _)), + user can ViewLogs in GlobalScope map ((ViewLogs, _)), + user can HideProjects in GlobalScope map ((HideProjects, _)), + user can HardRemoveProject in GlobalScope map ((HardRemoveProject, _)), + user can UserAdmin in GlobalScope map ((UserAdmin, _)), + ).parMapN { + case (reviewFlags, reviewVisibility, reviewProjects, viewStats, viewHealth, viewLogs, hideProjects, hardRemoveProject, userAdmin) => + val perms = Seq(reviewFlags, reviewVisibility, reviewProjects, viewStats, viewHealth, viewLogs, hideProjects, hardRemoveProject, userAdmin) + perms.toMap } } } diff --git a/app/models/viewhelper/OrganizationData.scala b/app/models/viewhelper/OrganizationData.scala index 649812dc5..abc7a10e3 100644 --- a/app/models/viewhelper/OrganizationData.scala +++ b/app/models/viewhelper/OrganizationData.scala @@ -10,6 +10,8 @@ import slick.jdbc.JdbcBackend import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT +import util.instances.future._ case class OrganizationData(joinable: Organization, ownerRole: OrganizationRole, @@ -31,7 +33,8 @@ object OrganizationData { implicit val users = orga.userBase for { role <- orga.owner.headRole - memberRoles <- orga.memberships.members.flatMap(m => Future.sequence(m.map(_.headRole))) + members <- orga.memberships.members + memberRoles <- Future.sequence(members.map(_.headRole)) memberUser <- Future.sequence(memberRoles.map(_.user)) } yield { val members = memberRoles zip memberUser @@ -41,10 +44,7 @@ object OrganizationData { def of[A](orga: Option[Organization])(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext, - service: ModelService): Future[Option[OrganizationData]] = { - orga match { - case None => Future.successful(None) - case Some(o) => of(o).map(Some(_)) - } + service: ModelService): OptionT[Future, OrganizationData] = { + OptionT.fromOption[Future](orga).semiFlatMap(of) } } diff --git a/app/models/viewhelper/ProjectData.scala b/app/models/viewhelper/ProjectData.scala index 970138b14..70885603e 100644 --- a/app/models/viewhelper/ProjectData.scala +++ b/app/models/viewhelper/ProjectData.scala @@ -15,6 +15,9 @@ import slick.lifted.TableQuery import scala.concurrent.{ExecutionContext, Future} +import util.syntax._ +import util.instances.future._ + /** * Holds ProjetData that is the same for all users */ @@ -46,7 +49,7 @@ object ProjectData { def cacheKey(project: Project) = "project" + project.id.get - def of[A](request: OreRequest[A], project: PendingProject)(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext): Future[ProjectData] = { + def of[A](request: OreRequest[A], project: PendingProject)(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext): ProjectData = { val projectOwner = request.data.currentUser.get @@ -73,44 +76,53 @@ object ProjectData { lastVisibilityChange, lastVisibilityChangeUser) - Future.successful(data) + data } + def of[A](project: Project)(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext): Future[ProjectData] = { implicit val userBase = project.userBase - for { - settings <- project.settings - projectOwner <- project.owner.user - - ownerRole <- project.owner.headRole - versions <- project.versions.size - members <- members(project) - - logSize <- project.logger.flatMap(_.entries.size) - flags <- project.flags.all - flagUsers <- Future.sequence(flags.map(_.user)) - flagResolved <- Future.sequence(flags.map(flag => flag.userBase.get(flag.resolvedBy.getOrElse(-1)))) - lastVisibilityChange <- project.lastVisibilityChange - lastVisibilityChangeUser <- if (lastVisibilityChange.isEmpty) Future.successful("Unknown") - else lastVisibilityChange.get.created.map(_.map(_.name).getOrElse("Unknown")) - } yield { - val noteCount = project.getNotes().size - val flagData = flags zip flagUsers zip flagResolved map { case ((fl, user), resolved) => - (fl, user.name, resolved.map(_.username)) - } - - new ProjectData( - project, - projectOwner, - ownerRole, - versions, - settings, - members.sortBy(_._1.roleType.trust).reverse, - logSize, - flagData.toSeq, - noteCount, - lastVisibilityChange, - lastVisibilityChangeUser) + + val flagsFut = project.flags.all + val flagUsersFut = flagsFut.flatMap(flags => Future.sequence(flags.map(_.user))) + val flagResolvedFut = flagsFut.flatMap(flags => Future.sequence(flags.map(flag => flag.userBase.get(flag.resolvedBy.getOrElse(-1)).value))) + + val lastVisibilityChangeFut = project.lastVisibilityChange.value + val lastVisibilityChangeUserFut = lastVisibilityChangeFut.flatMap { lastVisibilityChange => + if (lastVisibilityChange.isEmpty) Future.successful("Unknown") else lastVisibilityChange.get.created.fold("Unknown")(_.name) + } + + ( + project.settings, + project.owner.user, + project.owner.headRole, + project.versions.size, + members(project), + project.logger.flatMap(_.entries.size), + flagsFut, + flagUsersFut, + flagResolvedFut, + lastVisibilityChangeFut, + lastVisibilityChangeUserFut + ).parMapN { + case (settings, projectOwner, ownerRole, versions, members, logSize, flags, flagUsers, flagResolved, lastVisibilityChange, lastVisibilityChangeUser) => + val noteCount = project.getNotes().size + val flagData = flags zip flagUsers zip flagResolved map { case ((fl, user), resolved) => + (fl, user.name, resolved.map(_.username)) + } + + new ProjectData( + project, + projectOwner, + ownerRole, + versions, + settings, + members.sortBy(_._1.roleType.trust).reverse, + logSize, + flagData.toSeq, + noteCount, + lastVisibilityChange, + lastVisibilityChangeUser) } } diff --git a/app/models/viewhelper/ScopedOrganizationData.scala b/app/models/viewhelper/ScopedOrganizationData.scala index 88001a659..03d919107 100644 --- a/app/models/viewhelper/ScopedOrganizationData.scala +++ b/app/models/viewhelper/ScopedOrganizationData.scala @@ -8,6 +8,9 @@ import slick.jdbc.JdbcBackend import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT +import util.instances.future._ + case class ScopedOrganizationData(permissions: Map[Permission, Boolean] = Map.empty) object ScopedOrganizationData { @@ -32,10 +35,7 @@ object ScopedOrganizationData { } def of[A](currentUser: Option[User], orga: Option[Organization])(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext, - service: ModelService): Future[Option[ScopedOrganizationData]] = { - orga match { - case None => Future.successful(None) - case Some(o) => of(currentUser, o).map(Some(_)) - } + service: ModelService): OptionT[Future, ScopedOrganizationData] = { + OptionT.fromOption[Future](orga).semiFlatMap(of(currentUser, _)) } } \ No newline at end of file diff --git a/app/models/viewhelper/ScopedProjectData.scala b/app/models/viewhelper/ScopedProjectData.scala index efa3d1dd4..5a5e45576 100644 --- a/app/models/viewhelper/ScopedProjectData.scala +++ b/app/models/viewhelper/ScopedProjectData.scala @@ -7,6 +7,8 @@ import play.api.cache.AsyncCacheApi import scala.concurrent.{ExecutionContext, Future} +import util.syntax._ + /** * Holds ProjectData that is specific to a user */ @@ -16,23 +18,22 @@ object ScopedProjectData { def of(currentUser: Option[User], project: Project)(implicit ec: ExecutionContext, cache: AsyncCacheApi): Future[ScopedProjectData] = { currentUser.map { user => - for { - projectOwner <- project.owner.user - orgaOwner <- projectOwner.toMaybeOrganization - - canPostAsOwnerOrga <- user can PostAsOrganization in orgaOwner - uProjectFlags <- user.hasUnresolvedFlagFor(project) - starred <- project.stars.contains(user) - watching <- project.watchers.contains(user) - - editPages <- user can EditPages in project map ((EditPages, _)) - editSettings <- user can EditSettings in project map ((EditSettings, _)) - editChannels <- user can EditChannels in project map ((EditChannels, _)) - editVersions <- user can EditVersions in project map ((EditVersions, _)) - visibilities <- Future.sequence(VisibilityTypes.values.map(_.permission).map(p => user can p in project map ((p, _)))) - } yield { - val perms = visibilities + editPages + editSettings + editChannels + editVersions - ScopedProjectData(canPostAsOwnerOrga, uProjectFlags, starred, watching, perms.toMap) + ( + project.owner.user.flatMap(_.toMaybeOrganization.value).flatMap(orgaOwner => user can PostAsOrganization in orgaOwner), + + user.hasUnresolvedFlagFor(project), + project.stars.contains(user), + project.watchers.contains(user), + + user can EditPages in project map ((EditPages, _)), + user can EditSettings in project map ((EditSettings, _)), + user can EditChannels in project map ((EditChannels, _)), + user can EditVersions in project map ((EditVersions, _)), + Future.sequence(VisibilityTypes.values.map(_.permission).map(p => user can p in project map ((p, _)))) + ).parMapN { + case (canPostAsOwnerOrga, uProjectFlags, starred, watching, editPages, editSettings, editChannels, editVersions, visibilities) => + val perms = visibilities + editPages + editSettings + editChannels + editVersions + ScopedProjectData(canPostAsOwnerOrga, uProjectFlags, starred, watching, perms.toMap) } } getOrElse Future.successful(noScope) } diff --git a/app/models/viewhelper/UserData.scala b/app/models/viewhelper/UserData.scala index 26dcf2c24..c28e124c6 100644 --- a/app/models/viewhelper/UserData.scala +++ b/app/models/viewhelper/UserData.scala @@ -13,6 +13,8 @@ import slick.lifted.TableQuery import db.impl.OrePostgresDriver.api._ import scala.concurrent.{ExecutionContext, Future} +import util.syntax._ + // TODO separate Scoped UserData case class UserData(headerData: HeaderData, @@ -86,16 +88,16 @@ object UserData { if (currentUser.isEmpty) Future.successful((Map.empty, Map.empty)) else { val user = currentUser.get - for { - orga <- user.toMaybeOrganization - viewActivity <- user can ViewActivity in user map ((ViewActivity, _)) - reviewFlags <- user can ReviewFlags in user map ((ReviewFlags, _)) - reviewProjects <- user can ReviewProjects in user map ((ReviewProjects, _)) - editSettings <- user can EditSettings in orga map ((EditSettings, _)) - } yield { - val userPerms: Map[Permission, Boolean] = Seq(viewActivity, reviewFlags, reviewProjects).toMap - val orgaPerms: Map[Permission, Boolean] = Seq(editSettings).toMap - (userPerms, orgaPerms) + val viewActivityFut = user can ViewActivity in user map ((ViewActivity, _)) + val reviewFlagsFut = user can ReviewFlags in user map ((ReviewFlags, _)) + val reviewProjectsFut = user can ReviewProjects in user map ((ReviewProjects, _)) + val editSettingsFut = user.toMaybeOrganization.value.flatMap(orga => user can EditSettings in orga map ((EditSettings, _))) + + (viewActivityFut, reviewFlagsFut, reviewProjectsFut, editSettingsFut).parMapN { + case (viewActivity, reviewFlags, reviewProjects, editSettings) => + val userPerms: Map[Permission, Boolean] = Seq(viewActivity, reviewFlags, reviewProjects).toMap + val orgaPerms: Map[Permission, Boolean] = Seq(editSettings).toMap + (userPerms, orgaPerms) } } } diff --git a/app/models/viewhelper/VersionData.scala b/app/models/viewhelper/VersionData.scala index 9dba55aec..7dd2eccbd 100644 --- a/app/models/viewhelper/VersionData.scala +++ b/app/models/viewhelper/VersionData.scala @@ -10,6 +10,9 @@ import slick.jdbc.JdbcBackend import scala.concurrent.{ExecutionContext, Future} +import util.syntax._ +import util.instances.future._ + case class VersionData(p: ProjectData, v: Version, c: Channel, approvedBy: Option[String], // Reviewer if present dependencies: Seq[(Dependency, Option[Project])]) { @@ -29,16 +32,11 @@ case class VersionData(p: ProjectData, v: Version, c: Channel, object VersionData { def of[A](request: ProjectRequest[A], version: Version)(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext, service: ModelService): Future[VersionData] = { implicit val base = version.projectBase - for { - channel <- version.channel - approvedBy <- version.reviewer - deps <- Future.sequence(version.dependencies.map(dep => dep.project.map((dep, _)))) - } yield { - VersionData(request.data, - version, - channel, - approvedBy.map(_.name), - deps) + val depsFut = Future.sequence(version.dependencies.map(dep => dep.project.value.map((dep, _)))) + + (version.channel, version.reviewer.map(_.name).value, depsFut).parMapN { + case (channel, approvedBy, deps) => + VersionData(request.data, version, channel, approvedBy, deps) } } } diff --git a/app/ore/StatTracker.scala b/app/ore/StatTracker.scala index 2b38444e7..77edf5aa2 100755 --- a/app/ore/StatTracker.scala +++ b/app/ore/StatTracker.scala @@ -14,8 +14,7 @@ import ore.StatTracker.COOKIE_NAME import play.api.cache.AsyncCacheApi import play.api.mvc.{RequestHeader, Result} -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} /** * Helper class for handling tracking of statistics. @@ -36,7 +35,8 @@ trait StatTracker { * * @param request Request to view the project */ - def projectViewed(projectRequest: ProjectRequest[_])(f: ProjectRequest[_] => Result)(implicit cache: AsyncCacheApi, request: OreRequest[_]): Future[Result] = { + def projectViewed(projectRequest: ProjectRequest[_])(f: ProjectRequest[_] => Result)(implicit cache: AsyncCacheApi, request: OreRequest[_], + ec: ExecutionContext): Future[Result] = { ProjectView.bindFromRequest(projectRequest).map { statEntry => this.viewSchema.record(statEntry).andThen { case recorded => if (recorded.get) { @@ -55,7 +55,8 @@ trait StatTracker { * @param version Version to check downloads for * @param request Request to download the version */ - def versionDownloaded(version: Version)(f: ProjectRequest[_] => Result)(implicit cache: AsyncCacheApi,request: ProjectRequest[_]): Future[Result] = { + def versionDownloaded(version: Version)(f: ProjectRequest[_] => Result)(implicit cache: AsyncCacheApi,request: ProjectRequest[_], + ec: ExecutionContext): Future[Result] = { VersionDownload.bindFromRequest(version).map { statEntry => this.downloadSchema.record(statEntry).andThen { case recorded => if (recorded.get) { diff --git a/app/ore/organization/OrganizationOwned.scala b/app/ore/organization/OrganizationOwned.scala index efa331212..a16c20e7e 100644 --- a/app/ore/organization/OrganizationOwned.scala +++ b/app/ore/organization/OrganizationOwned.scala @@ -2,6 +2,7 @@ package ore.organization import db.impl.access.OrganizationBase import models.user.Organization +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} @@ -12,5 +13,6 @@ trait OrganizationOwned { /** Returns the Organization's ID */ def organizationId: Int /** Returns the Organization */ - def organization(implicit organizations: OrganizationBase, ec: ExecutionContext): Future[Organization] = organizations.get(this.organizationId).map(_.get) + def organization(implicit organizations: OrganizationBase, ec: ExecutionContext): Future[Organization] = + organizations.get(this.organizationId).getOrElse(throw new NoSuchElementException("Get on None")) } diff --git a/app/ore/permission/PermissionPredicate.scala b/app/ore/permission/PermissionPredicate.scala index d3253ec7b..42e6c73ae 100644 --- a/app/ore/permission/PermissionPredicate.scala +++ b/app/ore/permission/PermissionPredicate.scala @@ -8,6 +8,9 @@ import ore.permission.scope.ScopeSubject import scala.concurrent.{ExecutionContext, Future} +import util.FutureUtils +import util.instances.future._ + /** * Permission wrapper used for chaining permission checks. * @@ -50,13 +53,15 @@ case class PermissionPredicate(user: User, not: Boolean = false) { } private def checkProjectPerm(project: Project): Future[Boolean] = { - for { - pp <- project.service.getModelBase(classOf[OrganizationBase]).get(project.ownerId) - orgTest <- if (pp.isEmpty) Future.successful(false) else pp.get.scope.test(user, p) - projectTest <- project.scope.test(user, p) - } yield { - orgTest || projectTest - } + val orgTest = project.service + .getModelBase(classOf[OrganizationBase]) + .get(project.ownerId) + .fold(Future.successful(false))(_.scope.test(user, p)) + .flatten + + val projectTest = project.scope.test(user, p) + + FutureUtils.raceBoolean(orgTest, projectTest) } def in(subject: Option[ScopeSubject]): Future[Boolean] = { diff --git a/app/ore/project/Dependency.scala b/app/ore/project/Dependency.scala index 0e1f41c26..50d98ccdd 100755 --- a/app/ore/project/Dependency.scala +++ b/app/ore/project/Dependency.scala @@ -2,8 +2,9 @@ package ore.project import db.impl.access.ProjectBase import models.project.Project +import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.Future +import util.functional.OptionT /** * Represents a dependency to another plugin. Either on or not on Ore. @@ -18,7 +19,7 @@ case class Dependency(pluginId: String, version: String) { * * @return Project if dependency is on Ore, empty otherwise. */ - def project(implicit projects: ProjectBase): Future[Option[Project]] = projects.withPluginId(this.pluginId) + def project(implicit projects: ProjectBase, ec: ExecutionContext): OptionT[Future, Project] = projects.withPluginId(this.pluginId) } diff --git a/app/ore/project/ProjectOwned.scala b/app/ore/project/ProjectOwned.scala index c4ae95527..e00374489 100644 --- a/app/ore/project/ProjectOwned.scala +++ b/app/ore/project/ProjectOwned.scala @@ -2,6 +2,7 @@ package ore.project import db.impl.access.ProjectBase import models.project.Project +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} @@ -12,5 +13,6 @@ trait ProjectOwned { /** Returns the Project ID */ def projectId: Int /** Returns the Project */ - def project(implicit projects: ProjectBase, ec: ExecutionContext): Future[Project] = projects.get(this.projectId).map(_.get) + def project(implicit projects: ProjectBase, ec: ExecutionContext): Future[Project] = + projects.get(this.projectId).getOrElse(throw new NoSuchElementException("Get on None")) } diff --git a/app/ore/project/ProjectTask.scala b/app/ore/project/ProjectTask.scala index a66aa0730..9c990c211 100644 --- a/app/ore/project/ProjectTask.scala +++ b/app/ore/project/ProjectTask.scala @@ -6,7 +6,7 @@ import javax.inject.{Inject, Singleton} import akka.actor.ActorSystem import scala.concurrent.duration._ -import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.ExecutionContext import db.impl.OrePostgresDriver.api._ import db.impl.schema.ProjectSchema import db.{ModelFilter, ModelService} @@ -17,7 +17,7 @@ import ore.OreConfig * Task that is responsible for publishing New projects */ @Singleton -class ProjectTask @Inject()(models: ModelService, actorSystem: ActorSystem, config: OreConfig) extends Runnable { +class ProjectTask @Inject()(models: ModelService, actorSystem: ActorSystem, config: OreConfig)(implicit ec: ExecutionContext) extends Runnable { val Logger = play.api.Logger("ProjectTask") val interval = this.config.projects.get[FiniteDuration]("check-interval") diff --git a/app/ore/project/factory/ProjectFactory.scala b/app/ore/project/factory/ProjectFactory.scala index 75f63ad44..bf0fa1ecc 100755 --- a/app/ore/project/factory/ProjectFactory.scala +++ b/app/ore/project/factory/ProjectFactory.scala @@ -27,10 +27,12 @@ import play.api.cache.SyncCacheApi import play.api.i18n.{Lang, MessagesApi} import security.pgp.PGPVerifier import util.StringUtils._ +import util.functional.{EitherT, OptionT} +import util.instances.future._ +import util.syntax._ import scala.collection.JavaConverters._ -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration.Duration import scala.util.Try @@ -106,26 +108,25 @@ trait ProjectFactory { def processSubsequentPluginUpload(uploadData: PluginUpload, owner: User, - project: Project): Future[Either[String, PendingVersion]] = { + project: Project)(implicit ec: ExecutionContext): EitherT[Future, String, PendingVersion] = { val plugin = this.processPluginUpload(uploadData, owner) if (!plugin.meta.get.getId.equals(project.pluginId)) - return Future.successful(Left("error.version.invalidPluginId")) - val version = for { - (channels, settings) <- project.channels.all zip - project.settings - } yield { - this.startVersion(plugin, project, settings, channels.head.name) - } - version.flatMap { version => - val model = version.underlying - model.exists.map { exists => - if (exists && this.config.projects.get[Boolean]("file-validate")) - Left("error.version.duplicate") - else { - version.cache() - Right(version) + EitherT.leftT("error.version.invalidPluginId") + else { + EitherT( + for { + (channels, settings) <- (project.channels.all, project.settings).parTupled + version = this.startVersion(plugin, project, settings, channels.head.name) + modelExists <- version.underlying.exists + } yield { + if(modelExists && this.config.projects.get[Boolean]("file-validate")) + Left("error.version.duplicate") + else { + version.cache() + Right(version) + } } - } + ) } } @@ -252,21 +253,18 @@ trait ProjectFactory { * @return New Project * @throws IllegalArgumentException if the project already exists */ - def createProject(pending: PendingProject): Future[Project] = { + def createProject(pending: PendingProject)(implicit ec: ExecutionContext): Future[Project] = { val project = pending.underlying - val checks = for { - (exists, available) <- this.projects.exists(project) zip - this.projects.isNamespaceAvailable(project.ownerName, project.slug) - } yield { - checkArgument(!exists, "project already exists", "") - checkArgument(available, "slug not available", "") - checkArgument(this.config.isValidProjectName(pending.underlying.name), "invalid name", "") - } - - // Create the project and it's settings for { - _ <- checks + (exists, available) <- ( + this.projects.exists(project), + this.projects.isNamespaceAvailable(project.ownerName, project.slug) + ).parTupled + _ = checkArgument(!exists, "project already exists", "") + _ = checkArgument(available, "slug not available", "") + _ = checkArgument(this.config.isValidProjectName(pending.underlying.name), "invalid name", "") + // Create the project and it's settings newProject <- this.projects.add(pending.underlying) } yield { newProject.updateSettings(pending.settings) @@ -303,21 +301,17 @@ trait ProjectFactory { * @param color Channel color * @return New channel */ - def createChannel(project: Project, name: String, color: Color, nonReviewed: Boolean): Future[Channel] = { + def createChannel(project: Project, name: String, color: Color, nonReviewed: Boolean)(implicit ec: ExecutionContext): Future[Channel] = { checkNotNull(project, "null project", "") checkArgument(project.isDefined, "undefined project", "") checkNotNull(name, "null name", "") checkArgument(this.config.isValidChannelName(name), "invalid name", "") checkNotNull(color, "null color", "") - val checks = for { + for { channelCount <- project.channels.size - } yield { - checkState(channelCount < this.config.projects.get[Int]("max-channels"), "channel limit reached", "") - } - - checks.flatMap { _ => - this.service.access[Channel](classOf[Channel]).add(new Channel(name, color, project.id.get)) - } + _ = checkState(channelCount < this.config.projects.get[Int]("max-channels"), "channel limit reached", "") + channel <- this.service.access[Channel](classOf[Channel]).add(new Channel(name, color, project.id.get)) + } yield channel } /** @@ -326,43 +320,34 @@ trait ProjectFactory { * @param pending PendingVersion * @return New version */ - def createVersion(pending: PendingVersion): Future[(Version, Channel, Seq[ProjectTag])] = { + def createVersion(pending: PendingVersion)(implicit ec: ExecutionContext): Future[(Version, Channel, Seq[ProjectTag])] = { val project = pending.project val pendingVersion = pending.underlying - val channel = for { - // Create channel if not exists - (channel, exists) <- getOrCreateChannel(pending, project) zip pendingVersion.exists - } yield { - if (exists && this.config.projects.get[Boolean]("file-validate")) - throw new IllegalArgumentException("Version already exists.") - channel - } - - // Create version - val newVersion = channel.flatMap { channel => - val newVersion = Version( - versionString = pendingVersion.versionString, - dependencyIds = pendingVersion.dependencyIds, - _description = pendingVersion.description, - assets = pendingVersion.assets, - projectId = project.id.get, - channelId = channel.id.get, - fileSize = pendingVersion.fileSize, - hash = pendingVersion.hash, - _authorId = pendingVersion.authorId, - fileName = pendingVersion.fileName, - signatureFileName = pendingVersion.signatureFileName - ) - this.service.access[Version](classOf[Version]).add(newVersion) - } - for { - channel <- channel - newVersion <- newVersion - spongeTag <- addTags(newVersion, SpongeApiId, "Sponge", TagColors.Sponge) - forgeTag <- addTags(newVersion, ForgeId, "Forge", TagColors.Forge) + // Create channel if not exists + (channel, exists) <- (getOrCreateChannel(pending, project), pendingVersion.exists).parTupled + _ = if (exists && this.config.projects.get[Boolean]("file-validate")) throw new IllegalArgumentException("Version already exists.") + // Create version + newVersion <- { + val newVersion = Version( + versionString = pendingVersion.versionString, + dependencyIds = pendingVersion.dependencyIds, + _description = pendingVersion.description, + assets = pendingVersion.assets, + projectId = project.id.get, + channelId = channel.id.get, + fileSize = pendingVersion.fileSize, + hash = pendingVersion.hash, + _authorId = pendingVersion.authorId, + fileName = pendingVersion.fileName, + signatureFileName = pendingVersion.signatureFileName + ) + this.service.access[Version](classOf[Version]).add(newVersion) + } + spongeTag <- addTags(newVersion, SpongeApiId, "Sponge", TagColors.Sponge).value + forgeTag <- addTags(newVersion, ForgeId, "Forge", TagColors.Forge).value } yield { val tags = spongeTag ++ forgeTag @@ -381,18 +366,15 @@ trait ProjectFactory { } } - private def addTags(newVersion: Version, dependencyName: String, tagName: String, tagColor: TagColor): Future[Option[ProjectTag]] = { + private def addTags(newVersion: Version, dependencyName: String, tagName: String, tagColor: TagColor)(implicit ec: ExecutionContext): OptionT[Future, ProjectTag] = { val dependenciesMatchingName = newVersion.dependencies.filter(_.pluginId == dependencyName) - dependenciesMatchingName.headOption match { - case None => Future.successful(None) - case Some(dep) => - if (!dependencyVersionRegex.pattern.matcher(dep.version).matches()) - Future.successful(None) - else { - val tagToAdd = for { - tagsWithVersion <- service.access(classOf[ProjectTag]) - .filter(t => t.name === tagName && t.data === dep.version) - } yield { + OptionT.fromOption[Future](dependenciesMatchingName.headOption) + .filter(dep => dependencyVersionRegex.pattern.matcher(dep.version).matches()) + .semiFlatMap { dep => + for { + tagsWithVersion <- service.access(classOf[ProjectTag]) + .filter(t => t.name === tagName && t.data === dep.version) + tag <- { if (tagsWithVersion.isEmpty) { val tag = Tag( _versionIds = List(newVersion.id.get), @@ -410,20 +392,17 @@ trait ProjectFactory { Future.successful(tag) } } - tagToAdd.flatten.map { tag => - newVersion.addTag(tag) - Some(tag) - } + } yield { + newVersion.addTag(tag) + tag } } } - private def getOrCreateChannel(pending: PendingVersion, project: Project) = { - project.channels.find(equalsIgnoreCase(_.name, pending.channelName)) flatMap { - case Some(existing) => Future.successful(existing) - case None => createChannel(project, pending.channelName, pending.channelColor, nonReviewed = false) - } + private def getOrCreateChannel(pending: PendingVersion, project: Project)(implicit ec: ExecutionContext) = { + project.channels.find(equalsIgnoreCase(_.name, pending.channelName)) + .getOrElseF(createChannel(project, pending.channelName, pending.channelColor, nonReviewed = false)) } private def uploadPlugin(project: Project, channel: Channel, plugin: PluginFile, version: Version): Try[Unit] = Try { diff --git a/app/ore/rest/OreRestfulApi.scala b/app/ore/rest/OreRestfulApi.scala index e896989f9..c1de580c7 100755 --- a/app/ore/rest/OreRestfulApi.scala +++ b/app/ore/rest/OreRestfulApi.scala @@ -18,9 +18,11 @@ import ore.project.{Categories, ProjectSortingStrategies} import play.api.libs.json.Json.{obj, toJson} import play.api.libs.json.{JsArray, JsObject, JsString, JsValue} import util.StringUtils._ - +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT + /** * The Ore API */ @@ -76,13 +78,12 @@ trait OreRestfulApi { val query = filteredProjects(offset, lim) - val all = for { + for { projects <- service.DB.db.run(query.result) json <- writeProjects(projects) } yield { - json.map(_._2) + toJson(json.map(_._2)) } - all.map(toJson(_)) } private def getMembers(projects: Seq[Int]) = { @@ -223,7 +224,7 @@ trait OreRestfulApi { * @return JSON list of versions */ def getVersionList(pluginId: String, channels: Option[String], - limit: Option[Int], offset: Option[Int])(implicit ec: ExecutionContext): Future[Option[JsValue]] = { + limit: Option[Int], offset: Option[Int])(implicit ec: ExecutionContext): Future[JsValue] = { val filtered = channels.map { chan => queryVersions.filter { case (p, v, vId, c, uName) => @@ -249,7 +250,7 @@ trait OreRestfulApi { val list = data.map { case (p, v, vId, c, uName) => writeVersion(v, p, c, uName, vTags.getOrElse(vId, Seq.empty)) } - Some(toJson(list)) + toJson(list) } } @@ -300,23 +301,21 @@ trait OreRestfulApi { * @param parentId Optional parent ID filter * @return List of project pages */ - def getPages(pluginId: String, parentId: Option[Int])(implicit ec: ExecutionContext): Future[Option[JsValue]] = { - this.projects.withPluginId(pluginId).flatMap { - case None => Future.successful(None) - case Some(project) => + def getPages(pluginId: String, parentId: Option[Int])(implicit ec: ExecutionContext): OptionT[Future, JsValue] = { + this.projects.withPluginId(pluginId).semiFlatMap { project => for { pages <- project.pages.sorted(_.name) } yield { val seq = if (parentId.isDefined) pages.filter(_.parentId == parentId.get) else pages val pageById = pages.map(p => (p.id.get, p)).toMap - Some(toJson(seq.map(page => obj( + toJson(seq.map(page => obj( "createdAt" -> page.createdAt, "id" -> page.id, "name" -> page.name, "parentId" -> page.parentId, "slug" -> page.slug, "fullSlug" -> page.fullSlug(pageById.get(page.parentId)) - )))) + ))) } } } @@ -390,9 +389,7 @@ trait OreRestfulApi { for { user <- service.DB.db.run(queryOneUser.result) json <- writeUsers(user) - } yield { - json.headOption - } + } yield json.headOption } /** @@ -402,18 +399,15 @@ trait OreRestfulApi { * @param version Version name * @return Tags on the Version */ - def getTags(pluginId: String, version: String)(implicit ec: ExecutionContext): Future[Option[JsValue]] = { - this.projects.withPluginId(pluginId).flatMap { - case None => Future.successful(None) - case Some(project) => project.versions.find(equalsIgnoreCase(_.versionString, version)).flatMap { - case None => Future.successful(None) - case Some(v) => - v.tags.map { tags => - Some(obj( - "pluginId" -> pluginId, - "version" -> version, - "tags" -> tags.map(toJson(_)))) - } + def getTags(pluginId: String, version: String)(implicit ec: ExecutionContext): OptionT[Future, JsValue] = { + this.projects.withPluginId(pluginId).flatMap { project => + project.versions.find(equalsIgnoreCase(_.versionString, version)).semiFlatMap { v => + v.tags.map { tags => + obj( + "pluginId" -> pluginId, + "version" -> version, + "tags" -> tags.map(toJson(_))): JsValue + } } } } diff --git a/app/ore/user/MembershipDossier.scala b/app/ore/user/MembershipDossier.scala index 0cb9ecb08..4c9c67337 100644 --- a/app/ore/user/MembershipDossier.scala +++ b/app/ore/user/MembershipDossier.scala @@ -70,13 +70,12 @@ trait MembershipDossier { * @param role Role to add */ def addRole(role: RoleType)(implicit ec: ExecutionContext) = { - role.user.flatMap { user => - this.roles.exists(_.userId === user.id.get).flatMap { exists => - if (!exists) addMember(user) else Future.successful(user) - } flatMap { _ => - this.roleAccess.add(role) - } - } + for { + user <- role.user + exists <- this.roles.exists(_.userId === user.id.get) + _ <- if(!exists) addMember(user) else Future.successful(user) + ret <- this.roleAccess.add(role) + } yield ret } /** @@ -101,16 +100,12 @@ trait MembershipDossier { * @param role Role to remove */ def removeRole(role: RoleType)(implicit ec: ExecutionContext) = { - val checks = for { + for { _ <- this.roleAccess.remove(role) user <- role.user exists <- this.roles.exists(_.userId === user.id.get) - } yield { - (exists, user) - } - checks.flatMap { - case (exists, user) => if (!exists) removeMember(user) else Future.successful(0) - } + _ <- if(!exists) removeMember(user) else Future.successful(0) + } yield () } /** diff --git a/app/ore/user/UserOwned.scala b/app/ore/user/UserOwned.scala index b6da47b00..77096b2a8 100644 --- a/app/ore/user/UserOwned.scala +++ b/app/ore/user/UserOwned.scala @@ -2,6 +2,7 @@ package ore.user import db.impl.access.UserBase import models.user.User +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} @@ -10,5 +11,6 @@ trait UserOwned { /** Returns the User ID */ def userId: Int /** Returns the User */ - def user(implicit users: UserBase, ec: ExecutionContext): Future[User] = users.get(this.userId).map(_.get) + def user(implicit users: UserBase, ec: ExecutionContext): Future[User] = + users.get(this.userId).getOrElse(throw new NoSuchElementException("None on get")) } diff --git a/app/ore/user/UserSyncTask.scala b/app/ore/user/UserSyncTask.scala index de34a8779..d748b4e3f 100644 --- a/app/ore/user/UserSyncTask.scala +++ b/app/ore/user/UserSyncTask.scala @@ -6,8 +6,7 @@ import db.impl.access.UserBase import javax.inject.{Inject, Singleton} import ore.OreConfig -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ /** @@ -17,7 +16,7 @@ import scala.concurrent.duration._ * @param models ModelService instance */ @Singleton -final class UserSyncTask @Inject()(models: ModelService, actorSystem: ActorSystem, config: OreConfig) extends Runnable { +final class UserSyncTask @Inject()(models: ModelService, actorSystem: ActorSystem, config: OreConfig)(implicit ec: ExecutionContext) extends Runnable { val Logger = play.api.Logger("UserSync") val interval = this.config.users.get[Long]("syncRate").millis @@ -37,7 +36,8 @@ final class UserSyncTask @Inject()(models: ModelService, actorSystem: ActorSyste this.models.getModelBase(classOf[UserBase]).all.map { users => Logger.info(s"Synchronizing ${users.size} users with external site data...") Future.sequence(users.map { user => - user.pullForumData().flatMap(_.pullSpongeData()) + user.pullForumData() + user.pullSpongeData() }).map { _ => Logger.info("Done") } diff --git a/app/ore/user/notification/InviteFilters.scala b/app/ore/user/notification/InviteFilters.scala index 25a5a70a4..aa91afe91 100644 --- a/app/ore/user/notification/InviteFilters.scala +++ b/app/ore/user/notification/InviteFilters.scala @@ -3,8 +3,7 @@ package ore.user.notification import models.user.User import models.user.role.RoleModel -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.language.implicitConversions /** @@ -12,24 +11,24 @@ import scala.language.implicitConversions */ object InviteFilters extends Enumeration { - val All = InviteFilter(0, "all", "notification.invite.all", user => { + val All = InviteFilter(0, "all", "notification.invite.all", implicit ec => user => { user.projectRoles.filterNot(_.isAccepted).flatMap(q1 => user.organizationRoles.filterNot(_.isAccepted).map(q1 ++ _)) }) - val Projects = InviteFilter(1, "projects", "notification.invite.projects", user => { + val Projects = InviteFilter(1, "projects", "notification.invite.projects", implicit ec => user => { user.projectRoles.filterNot(_.isAccepted) }) - val Organizations = InviteFilter(2, "organizations", "notification.invite.organizations", user => { + val Organizations = InviteFilter(2, "organizations", "notification.invite.organizations", implicit ec => user => { user.organizationRoles.filterNot(_.isAccepted) }) case class InviteFilter(i: Int, name: String, title: String, - filter: User => Future[Seq[RoleModel]]) extends super.Val(i, name) { + filter: ExecutionContext => User => Future[Seq[RoleModel]]) extends super.Val(i, name) { - def apply(user: User): Future[Seq[RoleModel]] = this.filter(user) + def apply(user: User)(implicit ec: ExecutionContext): Future[Seq[RoleModel]] = this.filter(ec)(user) } diff --git a/app/ore/user/notification/NotificationFilters.scala b/app/ore/user/notification/NotificationFilters.scala index f1a00f2b0..8c4d296ca 100644 --- a/app/ore/user/notification/NotificationFilters.scala +++ b/app/ore/user/notification/NotificationFilters.scala @@ -4,7 +4,7 @@ import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ import models.user.Notification -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.language.implicitConversions /** @@ -22,7 +22,7 @@ object NotificationFilters extends Enumeration { title: String, filter: Notification#T => Rep[Boolean]) extends super.Val(i, name) { - def apply(notifications: ModelAccess[Notification]): Future[Seq[Notification]] = notifications.filter(this.filter) + def apply(notifications: ModelAccess[Notification])(implicit ec: ExecutionContext): Future[Seq[Notification]] = notifications.filter(this.filter) } diff --git a/app/security/NonceFilter.scala b/app/security/NonceFilter.scala index 7584f02d2..b675494e4 100644 --- a/app/security/NonceFilter.scala +++ b/app/security/NonceFilter.scala @@ -8,7 +8,6 @@ import javax.inject.Inject import play.api.libs.typedmap.TypedKey import play.api.mvc.{Filter, RequestHeader, Result} -import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future object NonceFilter { @@ -30,6 +29,7 @@ class NonceFilter @Inject() (implicit val mat: Materializer) extends Filter { private val random = new SecureRandom() override def apply(next: (RequestHeader) => Future[Result])(request: RequestHeader): Future[Result] = { + import mat.executionContext val nonce = generateNonce next(request.addAttr(NonceFilter.NonceKey, nonce)).map { result => result.withHeaders("Content-Security-Policy" -> result.header.headers("Content-Security-Policy") diff --git a/app/security/spauth/SingleSignOnConsumer.scala b/app/security/spauth/SingleSignOnConsumer.scala index a74cb8fad..6fbbd16e1 100644 --- a/app/security/spauth/SingleSignOnConsumer.scala +++ b/app/security/spauth/SingleSignOnConsumer.scala @@ -12,10 +12,11 @@ import org.apache.commons.codec.binary.Hex import play.api.Configuration import play.api.http.Status import play.api.libs.ws.WSClient +import util.functional.OptionT +import util.instances.future._ -import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ -import scala.concurrent.{Await, Future} +import scala.concurrent.{Await, ExecutionContext, Future} /** * Manages authentication to Sponge services. @@ -39,7 +40,7 @@ trait SingleSignOnConsumer { * * @return True if available */ - def isAvailable: Boolean = Await.result(this.ws.url(this.loginUrl).get().map(_.status == Status.OK).recover { + def isAvailable(implicit ec: ExecutionContext): Boolean = Await.result(this.ws.url(this.loginUrl).get().map(_.status == Status.OK).recover { case _: Exception => false }, this.timeout) @@ -105,13 +106,13 @@ trait SingleSignOnConsumer { * marks the nonce as invalid so it cannot be used again * @return [[SpongeUser]] if successful */ - def authenticate(payload: String, sig: String)(isNonceValid: String => Future[Boolean]): Future[Option[SpongeUser]] = { + def authenticate(payload: String, sig: String)(isNonceValid: String => Future[Boolean])(implicit ec: ExecutionContext): OptionT[Future, SpongeUser] = { Logger.info("Authenticating SSO payload...") Logger.info(payload) Logger.info("Signed with : " + sig) if (!hmac_sha256(payload.getBytes(this.CharEncoding)).equals(sig)) { Logger.info(" Could not verify payload against signature.") - return Future.successful(None) + return OptionT.none[Future, SpongeUser] } // decode payload @@ -142,10 +143,10 @@ trait SingleSignOnConsumer { if (externalId == -1 || username == null || email == null || nonce == null) { Logger.info(" Incomplete payload.") - return Future.successful(None) + return OptionT.none[Future, SpongeUser] } - isNonceValid(nonce).map { + OptionT.liftF(isNonceValid(nonce)).subflatMap { case false => Logger.info(" Invalid nonce.") None diff --git a/app/security/spauth/SpongeAuthApi.scala b/app/security/spauth/SpongeAuthApi.scala index e410340b1..caf2e996f 100644 --- a/app/security/spauth/SpongeAuthApi.scala +++ b/app/security/spauth/SpongeAuthApi.scala @@ -5,14 +5,15 @@ import java.util.concurrent.TimeoutException import com.google.common.base.Preconditions._ import javax.inject.Inject import ore.OreConfig +import util.WSUtils.parseJson +import util.functional.{EitherT, OptionT} +import util.instances.future._ import play.api.libs.functional.syntax._ import play.api.libs.json.Reads._ -import util.WSUtils.parseJson import play.api.libs.json._ import play.api.libs.ws.{WSClient, WSResponse} -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ /** @@ -48,7 +49,7 @@ trait SpongeAuthApi { def createUser(username: String, email: String, password: String, - verified: Boolean = false): Future[Either[String, SpongeUser]] + verified: Boolean = false)(implicit ec: ExecutionContext): EitherT[Future, String, SpongeUser] = doCreateUser(username, email, password, verified) /** @@ -59,14 +60,14 @@ trait SpongeAuthApi { * @param verified True if should bypass email verification * @return Newly created user */ - def createDummyUser(username: String, email: String, verified: Boolean = false): Future[Either[String, SpongeUser]] + def createDummyUser(username: String, email: String, verified: Boolean = false)(implicit ec: ExecutionContext): EitherT[Future, String, SpongeUser] = doCreateUser(username, email, null, verified, dummy = true) private def doCreateUser(username: String, email: String, password: String, verified: Boolean = false, - dummy: Boolean = false): Future[Either[String, SpongeUser]] = { + dummy: Boolean = false)(implicit ec: ExecutionContext): EitherT[Future, String, SpongeUser] = { checkNotNull(username, "null username", "") checkNotNull(email, "null email", "") var params = Map( @@ -86,10 +87,10 @@ trait SpongeAuthApi { * @param username Username to lookup * @return User with username */ - def getUser(username: String): Future[Option[SpongeUser]] = { + def getUser(username: String)(implicit ec: ExecutionContext): OptionT[Future, SpongeUser] = { checkNotNull(username, "null username", "") val url = route("/api/users/" + username) + s"?apiKey=$apiKey" - readUser(this.ws.url(url).withRequestTimeout(timeout).get()).map(_.right.toOption) + readUser(this.ws.url(url).withRequestTimeout(timeout).get()).toOption } /** @@ -98,28 +99,28 @@ trait SpongeAuthApi { * @param username Username to lookup * @return Deleted user */ - def deleteUser(username: String): Future[Either[String, SpongeUser]] = { + def deleteUser(username: String)(implicit ec: ExecutionContext): EitherT[Future, String, SpongeUser] = { checkNotNull(username, "null username", "") val url = route("/api/users") + s"?username=$username&apiKey=$apiKey" readUser(this.ws.url(url).withRequestTimeout(timeout).delete()) } - private def readUser(response: Future[WSResponse], nullable: Boolean = false): Future[Either[String, SpongeUser]] = { - response.map(parseJson(_, Logger)).map(_.map { json => - val obj = json.as[JsObject] - if (obj.keys.contains("error")) - Left((obj \ "error").as[String]) - else - Right(obj.as[SpongeUser]) - } getOrElse { - Left("error.spongeauth.parse") - }) recover { - case toe: TimeoutException => - Left("error.spongeauth.connect") - case e => - Logger.error("An unexpected error occured while handling a response", e) - Left("error.spongeauth.unexpected") - } + private def readUser(response: Future[WSResponse], nullable: Boolean = false)(implicit ec: ExecutionContext): EitherT[Future, String, SpongeUser] = { + EitherT( + OptionT(response.map(parseJson(_, Logger))).map { json => + val obj = json.as[JsObject] + if (obj.keys.contains("error")) + Left((obj \ "error").as[String]) + else + Right(obj.as[SpongeUser]) + }.getOrElse(Left("error.spongeauth.parse")).recover { + case toe: TimeoutException => + Left("error.spongeauth.connect") + case e => + Logger.error("An unexpected error occured while handling a response", e) + Left("error.spongeauth.unexpected") + } + ) } private def route(route: String) = this.url + route diff --git a/app/util/FutureUtils.scala b/app/util/FutureUtils.scala new file mode 100644 index 000000000..0277d8073 --- /dev/null +++ b/app/util/FutureUtils.scala @@ -0,0 +1,17 @@ +package util + +import scala.concurrent.{ExecutionContext, Future} + +object FutureUtils { + + def race[A, B](fa: Future[A], fb: Future[B])( + implicit ec: ExecutionContext): Future[Either[A, B]] = + Future.firstCompletedOf(Seq(fa.map(Left.apply), fb.map(Right.apply))) + + def raceBoolean(fa: Future[Boolean], fb: Future[Boolean])( + implicit ec: ExecutionContext): Future[Boolean] = race(fa, fb).flatMap { + case Left(false) => fb + case Right(false) => fa + case _ => Future.successful(true) + } +} diff --git a/app/util/functional/Applicative.scala b/app/util/functional/Applicative.scala new file mode 100644 index 000000000..9b8a1d53e --- /dev/null +++ b/app/util/functional/Applicative.scala @@ -0,0 +1,22 @@ +package util.functional + +import scala.language.higherKinds + +trait Applicative[F[_]] extends Functor[F] { + + def pure[A](a: A): F[A] + + def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] + + override def map[A, B](fa: F[A])(f: A => B): F[B] = ap(pure(f))(fa) + + def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] = ap(map(fa)(a => (b: B) => (a, b)))(fb) + + def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] = map(product(fa, fb))(f.tupled) + + def <*>[A, B](ff: F[A => B])(fa: F[A]): F[B] = ap(ff)(fa) + + def *>[A, B](fa: F[A])(fb: F[B]): F[B] = map2(fa, fb)((_, b) => b) + + def <*[A, B](fa: F[A])(fb: F[B]): F[A] = map2(fa, fb)((a, _) => a) +} diff --git a/app/util/functional/EitherT.scala b/app/util/functional/EitherT.scala new file mode 100644 index 000000000..69178aa2f --- /dev/null +++ b/app/util/functional/EitherT.scala @@ -0,0 +1,155 @@ +package util.functional + +import scala.language.higherKinds +import util.syntax._ + +case class EitherT[F[_], A, B](value: F[Either[A, B]]) { + + def fold[C](fa: A => C, fb: B => C)(implicit F: Functor[F]): F[C] = value.map(_.fold(fa, fb)) + + def swap(implicit F: Functor[F]): EitherT[F, B, A] = EitherT(value.map(_.swap)) + + def getOrElse[B1 >: B](or: => B1)(implicit F: Functor[F]): F[B1] = value.map(_.getOrElse(or)) + + def merge[A1 >: A](implicit ev: B <:< A1, F: Functor[F]): F[A1] = value.map(_.fold(identity, ev.apply)) + + def getOrElseF[B1 >: B](default: => F[B1])(implicit F: Monad[F]): F[B1] = + value.flatMap { + case Left(_) => default + case Right(b) => F.pure(b) + } + + def orElse[A1, B1 >: B](default: => EitherT[F, A1, B1])(implicit F: Monad[F]): EitherT[F, A1, B1] = { + EitherT( + value.flatMap { + case Left(_) => default.value + case r @ Right(_) => + F.pure(r.asInstanceOf[Either[A1, B1]]) //This is safe as B1 >: B and the left is uninhabited + } + ) + } + + def contains[B1 >: B](elem: B1)(implicit F: Functor[F]): F[Boolean] = value.map(_.contains(elem)) + + def forall(f: B => Boolean)(implicit F: Functor[F]): F[Boolean] = value.map(_.forall(f)) + + def exists(f: B => Boolean)(implicit F: Functor[F]): F[Boolean] = value.map(_.exists(f)) + + def transform[C, D](f: Either[A, B] => Either[C, D])(implicit F: Functor[F]): EitherT[F, C, D] = + EitherT(value.map(f)) + + def flatMap[A1 >: A, D](f: B => EitherT[F, A1, D])(implicit F: Monad[F]): EitherT[F, A1, D] = + EitherT( + value.flatMap { + case l @ Left(_) => + F.pure(l.asInstanceOf[Either[A1, D]]) //This is safe as A1 >: A and the right is uninhabited + case Right(b) => f(b).value + } + ) + + def flatMapF[A1 >: A, D](f: B => F[Either[A1, D]])(implicit F: Monad[F]): EitherT[F, A1, D] = + flatMap(f andThen EitherT.apply) + + def subflatMap[A1 >: A, D](f: B => Either[A1, D])(implicit F: Functor[F]): EitherT[F, A1, D] = + transform(_.flatMap(f)) + + def semiFlatMap[D](f: B => F[D])(implicit F: Monad[F]): EitherT[F, A, D] = + flatMap(b => EitherT.right(f(b))) + + def leftFlatMap[B1 >: B, D](f: A => EitherT[F, D, B1])(implicit F: Monad[F]): EitherT[F, D, B1] = + EitherT( + value.flatMap { + case Left(a) => f(a).value + case r @ Right(_) => F.pure(r.asInstanceOf[Either[D, B1]]) //This is safe as B1 >: B and the left is uninhabited + } + ) + + def leftSemiFlatMap[D](f: A => F[D])(implicit F: Monad[F]): EitherT[F, D, B] = + EitherT( + value.flatMap { + case Left(a) => f(a).map(d => Left(d)) + case r @ Right(_) => F.pure(r.asInstanceOf[Either[D, B]]) //This is safe as the left is uninhabited + } + ) + + def map[B1](f: B => B1)(implicit F: Functor[F]): EitherT[F, A, B1] = bimap(identity, f) + + def leftMap[C](f: A => C)(implicit F: Functor[F]): EitherT[F, C, B] = bimap(f, identity) + + def bimap[C, D](fa: A => C, fb: B => D)(implicit F: Functor[F]): EitherT[F, C, D] = + EitherT( + value.map { + case Left(a) => Left(fa(a)) + case Right(b) => Right(fb(b)) + } + ) + + def filterOrElse[A1 >: A](p: B => Boolean, zero: => A1)(implicit F: Functor[F]): EitherT[F, A1, B] = + EitherT(value.map(_.filterOrElse(p, zero))) + + def toOption(implicit F: Functor[F]): OptionT[F, B] = OptionT(value.map(_.toOption)) + + def isLeft(implicit F: Functor[F]): F[Boolean] = value.map(_.isLeft) + + def isRight(implicit F: Functor[F]): F[Boolean] = value.map(_.isRight) +} +object EitherT { + + final class LeftPartiallyApplied[B](val b: Boolean = true) extends AnyVal { + def apply[F[_], A](fa: F[A])(implicit F: Functor[F]): EitherT[F, A, B] = EitherT(fa.map(Left.apply)) + } + + final def left[B]: LeftPartiallyApplied[B] = new LeftPartiallyApplied[B] + + final class LeftTPartiallyApplied[F[_], B](val b: Boolean = true) extends AnyVal { + def apply[A](a: A)(implicit F: Applicative[F]): EitherT[F, A, B] = EitherT(F.pure(Left(a))) + } + + final def leftT[F[_], B]: LeftTPartiallyApplied[F, B] = new LeftTPartiallyApplied[F, B] + + final class RightPartiallyApplied[A](val b: Boolean = true) extends AnyVal { + def apply[F[_], B](fb: F[B])(implicit F: Functor[F]): EitherT[F, A, B] = EitherT(fb.map(Right.apply)) + } + + final def right[A]: RightPartiallyApplied[A] = new RightPartiallyApplied[A] + + final class PurePartiallyApplied[F[_], A](val b: Boolean = true) extends AnyVal { + def apply[B](b: B)(implicit F: Applicative[F]): EitherT[F, A, B] = EitherT(F.pure(Right(b))) + } + + final def pure[F[_], A]: PurePartiallyApplied[F, A] = new PurePartiallyApplied[F, A] + + final def rightT[F[_], A]: PurePartiallyApplied[F, A] = pure + + final def liftF[F[_], A, B](fb: F[B])(implicit F: Functor[F]): EitherT[F, A, B] = right(fb) + + final def fromEither[F[_]]: FromEitherPartiallyApplied[F] = new FromEitherPartiallyApplied + + final class FromEitherPartiallyApplied[F[_]](val b: Boolean = true) extends AnyVal { + def apply[E, A](either: Either[E, A])(implicit F: Applicative[F]): EitherT[F, E, A] = + EitherT(F.pure(either)) + } + + final def fromOption[F[_]]: FromOptionPartiallyApplied[F] = new FromOptionPartiallyApplied + + final class FromOptionPartiallyApplied[F[_]](val b: Boolean = true) extends AnyVal { + def apply[E, A](opt: Option[A], ifNone: => E)(implicit F: Applicative[F]): EitherT[F, E, A] = + fromEither(opt.toRight(ifNone)) + } + + final def fromOptionF[F[_], E, A](fopt: F[Option[A]], ifNone: => E)(implicit F: Functor[F]): EitherT[F, E, A] = + EitherT(fopt.map(_.toRight(ifNone))) + + final def cond[F[_]]: CondPartiallyApplied[F] = new CondPartiallyApplied + + final class CondPartiallyApplied[F[_]](val b: Boolean = true) extends AnyVal { + def apply[E, A](test: Boolean, right: => A, left: => E)(implicit F: Applicative[F]): EitherT[F, E, A] = + EitherT(F.pure(Either.cond(test, right, left))) + } + + implicit def eitherTMonad[F[_]: Monad, L]: Monad[({type λ[R] = EitherT[F, L, R]})#λ] = new Monad[({type λ[B] = EitherT[F, L, B]})#λ] { + override def flatMap[A, B](fa: EitherT[F, L, A])(f: A => EitherT[F, L, B]): EitherT[F, L, B] = fa.flatMap(f) + override def pure[A](a: A): EitherT[F, L, A] = EitherT.pure[F, L](a) + override def map[A, B](fa: EitherT[F, L, A])(f: A => B): EitherT[F, L, B] = fa.map(f) + } +} \ No newline at end of file diff --git a/app/util/functional/Functor.scala b/app/util/functional/Functor.scala new file mode 100644 index 000000000..304b75630 --- /dev/null +++ b/app/util/functional/Functor.scala @@ -0,0 +1,17 @@ +package util.functional + +import scala.language.higherKinds + +trait Functor[F[_]] { + + def map[A, B](fa: F[A])(f: A => B): F[B] + + def as[A, B](fa: F[A], b: B): F[B] = map(fa)(_ => b) + + def fproduct[A, B](fa: F[A])(f: A => B): F[(A, B)] = map(fa)(a => (a, f(a))) + + def tupleLeft[A, B](fa: F[A], b: B): F[(B, A)] = map(fa)(a => (b, a)) + + def tupleRight[A, B](fa: F[A], b: B): F[(A, B)] = map(fa)(a => (a, b)) + +} diff --git a/app/util/functional/Monad.scala b/app/util/functional/Monad.scala new file mode 100644 index 000000000..576583302 --- /dev/null +++ b/app/util/functional/Monad.scala @@ -0,0 +1,12 @@ +package util.functional + +import scala.language.higherKinds + +trait Monad[F[_]] extends Applicative[F] { + + def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] + + override def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] = flatMap(ff)(f => map(fa)(f)) + + def flatten[A](ffa: F[F[A]]): F[A] = flatMap(ffa)(fa => fa) +} \ No newline at end of file diff --git a/app/util/functional/OptionT.scala b/app/util/functional/OptionT.scala new file mode 100644 index 000000000..3565bdf0e --- /dev/null +++ b/app/util/functional/OptionT.scala @@ -0,0 +1,93 @@ +package util.functional + +import scala.language.higherKinds +import util.syntax._ + +case class OptionT[F[_], A](value: F[Option[A]]) { + + def isEmpty(implicit F: Functor[F]): F[Boolean] = value.map(_.isEmpty) + + def isDefined(implicit F: Functor[F]): F[Boolean] = value.map(_.isDefined) + + def getOrElse[B >: A](default: => B)(implicit F: Functor[F]): F[B] = value.map(_.getOrElse(default)) + + def getOrElseF[B >: A](default: => F[B])(implicit F: Monad[F]): F[B] = + value.flatMap(_.fold(default)(F.pure)) + + def map[B](f: A => B)(implicit F: Functor[F]): OptionT[F, B] = OptionT(value.map(_.map(f))) + + def fold[B](ifEmpty: => B)(f: A => B)(implicit F: Functor[F]): F[B] = value.map(_.fold(ifEmpty)(f)) + + def cata[B](ifEmpty: => B, f: A => B)(implicit F: Functor[F]): F[B] = fold(ifEmpty)(f) + + def semiFlatMap[B](f: A => F[B])(implicit F: Monad[F]): OptionT[F, B] = flatMap(a => OptionT.liftF(f(a))) + + def flatMap[B](f: A => OptionT[F, B])(implicit F: Monad[F]): OptionT[F, B] = flatMapF(a => f(a).value) + + def flatMapF[B](f: A => F[Option[B]])(implicit F: Monad[F]): OptionT[F, B] = + OptionT(value.flatMap(_.fold(F.pure[Option[B]](None))(f))) + + def transform[B](f: Option[A] => Option[B])(implicit F: Functor[F]): OptionT[F, B] = OptionT(value.map(f)) + + def subflatMap[B](f: A => Option[B])(implicit F: Functor[F]): OptionT[F, B] = transform(_.flatMap(f)) + + def filter(f: A => Boolean)(implicit F: Functor[F]): OptionT[F, A] = OptionT(value.map(_.filter(f))) + + def withFilter(f: A => Boolean)(implicit F: Functor[F]): OptionT[F, A] = filter(f) + + def filterNot(f: A => Boolean)(implicit F: Functor[F]): OptionT[F, A] = OptionT(value.map(_.filterNot(f))) + + def contains[A1 >: A](elem: A1)(implicit F: Functor[F]): F[Boolean] = value.map(_.contains(elem)) + + def exists(f: A => Boolean)(implicit F: Functor[F]): F[Boolean] = value.map(_.exists(f)) + + def forall(f: A => Boolean)(implicit F: Functor[F]): F[Boolean] = value.map(_.forall(f)) + + def collect[B](f: PartialFunction[A, B])(implicit F: Functor[F]): OptionT[F, B] = + OptionT(value.map(_.collect(f))) + + def orElse(alternative: OptionT[F, A])(implicit F: Monad[F]): OptionT[F, A] = + orElseF(alternative.value) + + def orElseF(default: => F[Option[A]])(implicit F: Monad[F]): OptionT[F, A] = + OptionT( + value.flatMap { + case s @ Some(_) => F.pure(s) + case None => default + } + ) + + def toRight[X](left: => X)(implicit F: Functor[F]): EitherT[F, X, A] = EitherT(cata(Left(left), Right.apply)) + + def toLeft[X](right: => X)(implicit F: Functor[F]): EitherT[F, A, X] = EitherT(cata(Right(right), Left.apply)) +} +object OptionT { + + //To overcome type interference + final class PurePartiallyApplied[F[_]](val b: Boolean = true) extends AnyVal { + def apply[A](value: A)(implicit F: Applicative[F]): OptionT[F, A] = + OptionT(F.pure(Some(value))) + } + + def pure[F[_]]: PurePartiallyApplied[F] = new PurePartiallyApplied[F] + + def some[F[_]]: PurePartiallyApplied[F] = pure + + def none[F[_], A](implicit F: Applicative[F]): OptionT[F, A] = OptionT(F.pure(None)) + + def fromOption[F[_]]: FromOptionPartiallyApplied[F] = new FromOptionPartiallyApplied + + //To overcome type interference + final class FromOptionPartiallyApplied[F[_]](val b: Boolean = true ) extends AnyVal { + def apply[A](value: Option[A])(implicit F: Applicative[F]): OptionT[F, A] = + OptionT(F.pure(value)) + } + + def liftF[F[_], A](fa: F[A])(implicit F: Functor[F]): OptionT[F, A] = OptionT(fa.map(Some(_))) + + implicit def optionTMonad[F[_]: Monad]: Monad[({type λ[A] = OptionT[F, A]})#λ] = new Monad[({type λ[A] = OptionT[F, A]})#λ] { + override def flatMap[A, B](fa: OptionT[F, A])(f: A => OptionT[F, B]): OptionT[F, B] = fa.flatMap(f) + override def pure[A](a: A): OptionT[F, A] = OptionT.pure[F](a) + override def map[A, B](fa: OptionT[F, A])(f: A => B): OptionT[F, B] = fa.map(f) + } +} \ No newline at end of file diff --git a/app/util/functional/package.scala b/app/util/functional/package.scala new file mode 100644 index 000000000..8e96c620c --- /dev/null +++ b/app/util/functional/package.scala @@ -0,0 +1,25 @@ +package util + +package object functional { + + type Id[A] = A + + implicit val idInstance: Monad[Id] = new Monad[Id] { + override def flatMap[A, B](fa: A)(f: A => B): B = f(fa) + override def pure[A](a: A): A = a + + override def ap[A, B](ff: Id[A => B])(fa: A): B = ff(fa) + override def flatten[A](ffa: A): A = ffa: A + override def map[A, B](fa: A)(f: A => B): B = f(fa) + override def product[A, B](fa: A, fb: B): (A, B) = (fa, fb) + override def map2[A, B, C](fa: A, fb: B)(f: (A, B) => C): Id[C] = f(fa, fb) + override def <*>[A, B](ff: Id[A => B])(fa: A): B = ff(fa) + override def *>[A, B](fa: A)(fb: B): B = fb + override def <*[A, B](fa: A)(fb: B): A = fa + override def as[A, B](fa: A, b: B): B = b + override def fproduct[A, B](fa: A)(f: A => B): (A, B) = (fa, f(fa)) + override def tupleLeft[A, B](fa: A, b: B): (B, A) = (b, fa) + override def tupleRight[A, B](fa: A, b: B): (A, B) = (fa, b) + } + +} diff --git a/app/util/instances/AllInstances.scala b/app/util/instances/AllInstances.scala new file mode 100644 index 000000000..66167a19e --- /dev/null +++ b/app/util/instances/AllInstances.scala @@ -0,0 +1,8 @@ +package util.instances + +trait AllInstances + extends FutureInstances + with OptionInstances + with EitherInstances + with DBIOInstances + with SeqInstances \ No newline at end of file diff --git a/app/util/instances/DBIOInstances.scala b/app/util/instances/DBIOInstances.scala new file mode 100644 index 000000000..232d1a412 --- /dev/null +++ b/app/util/instances/DBIOInstances.scala @@ -0,0 +1,19 @@ +package util.instances + +import scala.concurrent.ExecutionContext + +import slick.dbio.DBIO +import util.functional.Monad + +trait DBIOInstances { + + implicit def dbioInstance(implicit ec: ExecutionContext): Monad[DBIO] = new Monad[DBIO] { + override def flatMap[A, B](fa: DBIO[A])(f: A => DBIO[B]): DBIO[B] = fa.flatMap(f) + override def pure[A](a: A): DBIO[A] = DBIO.successful(a) + override def flatten[A](ffa: DBIO[DBIO[A]]): DBIO[A] = ffa.flatten + override def map[A, B](fa: DBIO[A])(f: A => B): DBIO[B] = fa.map(f) + override def product[A, B](fa: DBIO[A], fb: DBIO[B]): DBIO[(A, B)] = fa.zip(fb) + override def map2[A, B, C](fa: DBIO[A], fb: DBIO[B])(f: (A, B) => C): DBIO[C] = fa.zipWith(fb)(f) + override def *>[A, B](fa: DBIO[A])(fb: DBIO[B]): DBIO[B] = fa >> fb + } +} diff --git a/app/util/instances/EitherInstances.scala b/app/util/instances/EitherInstances.scala new file mode 100644 index 000000000..c9654847b --- /dev/null +++ b/app/util/instances/EitherInstances.scala @@ -0,0 +1,12 @@ +package util.instances + +import util.functional.Monad + +trait EitherInstances { + + implicit def eitherInstance[L]: Monad[({ type λ[R] = Either[L, R] })#λ] = new Monad[({ type λ[R] = Either[L, R] })#λ] { + override def flatMap[A, B](fa: Either[L, A])(f: A => Either[L, B]): Either[L, B] = fa.flatMap(f) + override def pure[A](a: A): Either[L, A] = Right(a) + override def map[A, B](fa: Either[L, A])(f: A => B): Either[L, B] = fa.map(f) + } +} diff --git a/app/util/instances/FutureInstances.scala b/app/util/instances/FutureInstances.scala new file mode 100644 index 000000000..43ac91bdb --- /dev/null +++ b/app/util/instances/FutureInstances.scala @@ -0,0 +1,18 @@ +package util.instances + +import scala.concurrent.{ExecutionContext, Future} + +import util.functional.Monad + +trait FutureInstances { + + implicit def futureInstance(implicit ec: ExecutionContext): Monad[Future] = + new Monad[Future] { + override def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = fa.flatMap(f) + override def flatten[A](ffa: Future[Future[A]]): Future[A] = ffa.flatten + override def pure[A](a: A): Future[A] = Future.successful(a) + override def map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f) + override def product[A, B](fa: Future[A], fb: Future[B]): Future[(A, B)] = fa.zip(fb) + } + +} diff --git a/app/util/instances/OptionInstances.scala b/app/util/instances/OptionInstances.scala new file mode 100644 index 000000000..6e780768f --- /dev/null +++ b/app/util/instances/OptionInstances.scala @@ -0,0 +1,13 @@ +package util.instances + +import util.functional.Monad + +trait OptionInstances { + + implicit val optionInstance: Monad[Option] = new Monad[Option] { + override def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = fa.flatMap(f) + override def pure[A](a: A): Option[A] = Some(a) + override def flatten[A](ffa: Option[Option[A]]): Option[A] = ffa.flatten + override def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f) + } +} diff --git a/app/util/instances/SeqInstances.scala b/app/util/instances/SeqInstances.scala new file mode 100644 index 000000000..626754e4e --- /dev/null +++ b/app/util/instances/SeqInstances.scala @@ -0,0 +1,12 @@ +package util.instances + +import util.functional.Monad + +trait SeqInstances { + + implicit val seqInstance: Monad[Seq] = new Monad[Seq] { + override def flatMap[A, B](fa: Seq[A])(f: A => Seq[B]): Seq[B] = fa.flatMap(f) + override def pure[A](a: A): Seq[A] = Seq(a) + override def map[A, B](fa: Seq[A])(f: A => B): Seq[B] = fa.map(f) + } +} diff --git a/app/util/instances/package.scala b/app/util/instances/package.scala new file mode 100644 index 000000000..48930710a --- /dev/null +++ b/app/util/instances/package.scala @@ -0,0 +1,11 @@ +package util + +package object instances { + + object all extends AllInstances + object future extends FutureInstances + object option extends OptionInstances + object either extends EitherInstances + object dbio extends DBIOInstances + object seq extends SeqInstances +} diff --git a/app/util/syntax/ParallelSyntax.scala b/app/util/syntax/ParallelSyntax.scala new file mode 100644 index 000000000..185adf9e1 --- /dev/null +++ b/app/util/syntax/ParallelSyntax.scala @@ -0,0 +1,117 @@ +package util.syntax + +import scala.concurrent.{ExecutionContext, Future} +import scala.language.implicitConversions + +trait ParallelSyntax { + implicit def tuple1ParallelSyntax[A0](t1: Tuple1[Future[A0]]): Tuple1ParallelOps[A0] = new Tuple1ParallelOps(t1) + implicit def tuple2ParallelSyntax[A0, A1](t2: Tuple2[Future[A0], Future[A1]]): Tuple2ParallelOps[A0, A1] = new Tuple2ParallelOps(t2) + implicit def tuple3ParallelSyntax[A0, A1, A2](t3: Tuple3[Future[A0], Future[A1], Future[A2]]): Tuple3ParallelOps[A0, A1, A2] = new Tuple3ParallelOps(t3) + implicit def tuple4ParallelSyntax[A0, A1, A2, A3](t4: Tuple4[Future[A0], Future[A1], Future[A2], Future[A3]]): Tuple4ParallelOps[A0, A1, A2, A3] = new Tuple4ParallelOps(t4) + implicit def tuple5ParallelSyntax[A0, A1, A2, A3, A4](t5: Tuple5[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4]]): Tuple5ParallelOps[A0, A1, A2, A3, A4] = new Tuple5ParallelOps(t5) + implicit def tuple6ParallelSyntax[A0, A1, A2, A3, A4, A5](t6: Tuple6[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5]]): Tuple6ParallelOps[A0, A1, A2, A3, A4, A5] = new Tuple6ParallelOps(t6) + implicit def tuple7ParallelSyntax[A0, A1, A2, A3, A4, A5, A6](t7: Tuple7[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6]]): Tuple7ParallelOps[A0, A1, A2, A3, A4, A5, A6] = new Tuple7ParallelOps(t7) + implicit def tuple8ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7](t8: Tuple8[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7]]): Tuple8ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7] = new Tuple8ParallelOps(t8) + implicit def tuple9ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8](t9: Tuple9[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8]]): Tuple9ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8] = new Tuple9ParallelOps(t9) + implicit def tuple10ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9](t10: Tuple10[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9]]): Tuple10ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9] = new Tuple10ParallelOps(t10) + implicit def tuple11ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10](t11: Tuple11[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10]]): Tuple11ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10] = new Tuple11ParallelOps(t11) + implicit def tuple12ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11](t12: Tuple12[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11]]): Tuple12ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11] = new Tuple12ParallelOps(t12) + implicit def tuple13ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12](t13: Tuple13[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12]]): Tuple13ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12] = new Tuple13ParallelOps(t13) + implicit def tuple14ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13](t14: Tuple14[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13]]): Tuple14ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13] = new Tuple14ParallelOps(t14) + implicit def tuple15ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14](t15: Tuple15[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14]]): Tuple15ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14] = new Tuple15ParallelOps(t15) + implicit def tuple16ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15](t16: Tuple16[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15]]): Tuple16ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15] = new Tuple16ParallelOps(t16) + implicit def tuple17ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16](t17: Tuple17[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16]]): Tuple17ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16] = new Tuple17ParallelOps(t17) + implicit def tuple18ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17](t18: Tuple18[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16], Future[A17]]): Tuple18ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17] = new Tuple18ParallelOps(t18) + implicit def tuple19ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18](t19: Tuple19[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16], Future[A17], Future[A18]]): Tuple19ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18] = new Tuple19ParallelOps(t19) + implicit def tuple20ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19](t20: Tuple20[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16], Future[A17], Future[A18], Future[A19]]): Tuple20ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19] = new Tuple20ParallelOps(t20) + implicit def tuple21ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20](t21: Tuple21[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16], Future[A17], Future[A18], Future[A19], Future[A20]]): Tuple21ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20] = new Tuple21ParallelOps(t21) + implicit def tuple22ParallelSyntax[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21](t22: Tuple22[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16], Future[A17], Future[A18], Future[A19], Future[A20], Future[A21]]): Tuple22ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21] = new Tuple22ParallelOps(t22) +} + +final class Tuple1ParallelOps[A0](t1: Tuple1[Future[A0]]) { + def parMap[Z](f: (A0) => Z)(implicit ec: ExecutionContext): Future[Z] = t1._1.map(f) +} +final class Tuple2ParallelOps[A0, A1](t2: Tuple2[Future[A0], Future[A1]]) { + def parMapN[Z](f: (A0, A1) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1)] = t2._1.zip(t2._2) +} +final class Tuple3ParallelOps[A0, A1, A2](t3: Tuple3[Future[A0], Future[A1], Future[A2]]) { + def parMapN[Z](f: (A0, A1, A2) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2)] = t3._1.zip(t3._2).zipWith(t3._3) { case ((a0, a1), a2) => (a0, a1, a2) } +} +final class Tuple4ParallelOps[A0, A1, A2, A3](t4: Tuple4[Future[A0], Future[A1], Future[A2], Future[A3]]) { + def parMapN[Z](f: (A0, A1, A2, A3) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3)] = t4._1.zip(t4._2).zip(t4._3).zipWith(t4._4) { case (((a0, a1), a2), a3) => (a0, a1, a2, a3) } +} +final class Tuple5ParallelOps[A0, A1, A2, A3, A4](t5: Tuple5[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4)] = t5._1.zip(t5._2).zip(t5._3).zip(t5._4).zipWith(t5._5) { case ((((a0, a1), a2), a3), a4) => (a0, a1, a2, a3, a4) } +} +final class Tuple6ParallelOps[A0, A1, A2, A3, A4, A5](t6: Tuple6[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5)] = t6._1.zip(t6._2).zip(t6._3).zip(t6._4).zip(t6._5).zipWith(t6._6) { case (((((a0, a1), a2), a3), a4), a5) => (a0, a1, a2, a3, a4, a5) } +} +final class Tuple7ParallelOps[A0, A1, A2, A3, A4, A5, A6](t7: Tuple7[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6)] = t7._1.zip(t7._2).zip(t7._3).zip(t7._4).zip(t7._5).zip(t7._6).zipWith(t7._7) { case ((((((a0, a1), a2), a3), a4), a5), a6) => (a0, a1, a2, a3, a4, a5, a6) } +} +final class Tuple8ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7](t8: Tuple8[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7)] = t8._1.zip(t8._2).zip(t8._3).zip(t8._4).zip(t8._5).zip(t8._6).zip(t8._7).zipWith(t8._8) { case (((((((a0, a1), a2), a3), a4), a5), a6), a7) => (a0, a1, a2, a3, a4, a5, a6, a7) } +} +final class Tuple9ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8](t9: Tuple9[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8)] = t9._1.zip(t9._2).zip(t9._3).zip(t9._4).zip(t9._5).zip(t9._6).zip(t9._7).zip(t9._8).zipWith(t9._9) { case ((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8) => (a0, a1, a2, a3, a4, a5, a6, a7, a8) } +} +final class Tuple10ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9](t10: Tuple10[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9)] = t10._1.zip(t10._2).zip(t10._3).zip(t10._4).zip(t10._5).zip(t10._6).zip(t10._7).zip(t10._8).zip(t10._9).zipWith(t10._10) { case (((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) } +} +final class Tuple11ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10](t11: Tuple11[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10)] = t11._1.zip(t11._2).zip(t11._3).zip(t11._4).zip(t11._5).zip(t11._6).zip(t11._7).zip(t11._8).zip(t11._9).zip(t11._10).zipWith(t11._11) { case ((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) } +} +final class Tuple12ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11](t12: Tuple12[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11)] = t12._1.zip(t12._2).zip(t12._3).zip(t12._4).zip(t12._5).zip(t12._6).zip(t12._7).zip(t12._8).zip(t12._9).zip(t12._10).zip(t12._11).zipWith(t12._12) { case (((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10), a11) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11) } +} +final class Tuple13ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12](t13: Tuple13[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12)] = t13._1.zip(t13._2).zip(t13._3).zip(t13._4).zip(t13._5).zip(t13._6).zip(t13._7).zip(t13._8).zip(t13._9).zip(t13._10).zip(t13._11).zip(t13._12).zipWith(t13._13) { case ((((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12) } +} +final class Tuple14ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13](t14: Tuple14[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13)] = t14._1.zip(t14._2).zip(t14._3).zip(t14._4).zip(t14._5).zip(t14._6).zip(t14._7).zip(t14._8).zip(t14._9).zip(t14._10).zip(t14._11).zip(t14._12).zip(t14._13).zipWith(t14._14) { case (((((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12), a13) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13) } +} +final class Tuple15ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14](t15: Tuple15[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14)] = t15._1.zip(t15._2).zip(t15._3).zip(t15._4).zip(t15._5).zip(t15._6).zip(t15._7).zip(t15._8).zip(t15._9).zip(t15._10).zip(t15._11).zip(t15._12).zip(t15._13).zip(t15._14).zipWith(t15._15) { case ((((((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12), a13), a14) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14) } +} +final class Tuple16ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15](t16: Tuple16[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15)] = t16._1.zip(t16._2).zip(t16._3).zip(t16._4).zip(t16._5).zip(t16._6).zip(t16._7).zip(t16._8).zip(t16._9).zip(t16._10).zip(t16._11).zip(t16._12).zip(t16._13).zip(t16._14).zip(t16._15).zipWith(t16._16) { case (((((((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12), a13), a14), a15) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) } +} +final class Tuple17ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16](t17: Tuple17[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16)] = t17._1.zip(t17._2).zip(t17._3).zip(t17._4).zip(t17._5).zip(t17._6).zip(t17._7).zip(t17._8).zip(t17._9).zip(t17._10).zip(t17._11).zip(t17._12).zip(t17._13).zip(t17._14).zip(t17._15).zip(t17._16).zipWith(t17._17) { case ((((((((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12), a13), a14), a15), a16) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16) } +} +final class Tuple18ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17](t18: Tuple18[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16], Future[A17]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17)] = t18._1.zip(t18._2).zip(t18._3).zip(t18._4).zip(t18._5).zip(t18._6).zip(t18._7).zip(t18._8).zip(t18._9).zip(t18._10).zip(t18._11).zip(t18._12).zip(t18._13).zip(t18._14).zip(t18._15).zip(t18._16).zip(t18._17).zipWith(t18._18) { case (((((((((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12), a13), a14), a15), a16), a17) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17) } +} +final class Tuple19ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18](t19: Tuple19[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16], Future[A17], Future[A18]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18)] = t19._1.zip(t19._2).zip(t19._3).zip(t19._4).zip(t19._5).zip(t19._6).zip(t19._7).zip(t19._8).zip(t19._9).zip(t19._10).zip(t19._11).zip(t19._12).zip(t19._13).zip(t19._14).zip(t19._15).zip(t19._16).zip(t19._17).zip(t19._18).zipWith(t19._19) { case ((((((((((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12), a13), a14), a15), a16), a17), a18) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18) } +} +final class Tuple20ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19](t20: Tuple20[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16], Future[A17], Future[A18], Future[A19]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19)] = t20._1.zip(t20._2).zip(t20._3).zip(t20._4).zip(t20._5).zip(t20._6).zip(t20._7).zip(t20._8).zip(t20._9).zip(t20._10).zip(t20._11).zip(t20._12).zip(t20._13).zip(t20._14).zip(t20._15).zip(t20._16).zip(t20._17).zip(t20._18).zip(t20._19).zipWith(t20._20) { case (((((((((((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12), a13), a14), a15), a16), a17), a18), a19) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19) } +} +final class Tuple21ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20](t21: Tuple21[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16], Future[A17], Future[A18], Future[A19], Future[A20]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20)] = t21._1.zip(t21._2).zip(t21._3).zip(t21._4).zip(t21._5).zip(t21._6).zip(t21._7).zip(t21._8).zip(t21._9).zip(t21._10).zip(t21._11).zip(t21._12).zip(t21._13).zip(t21._14).zip(t21._15).zip(t21._16).zip(t21._17).zip(t21._18).zip(t21._19).zip(t21._20).zipWith(t21._21) { case ((((((((((((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12), a13), a14), a15), a16), a17), a18), a19), a20) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) } +} +final class Tuple22ParallelOps[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21](t22: Tuple22[Future[A0], Future[A1], Future[A2], Future[A3], Future[A4], Future[A5], Future[A6], Future[A7], Future[A8], Future[A9], Future[A10], Future[A11], Future[A12], Future[A13], Future[A14], Future[A15], Future[A16], Future[A17], Future[A18], Future[A19], Future[A20], Future[A21]]) { + def parMapN[Z](f: (A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21) => Z)(implicit ec: ExecutionContext): Future[Z] = parTupled.map(f.tupled) + def parTupled(implicit ec: ExecutionContext): Future[(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21)] = t22._1.zip(t22._2).zip(t22._3).zip(t22._4).zip(t22._5).zip(t22._6).zip(t22._7).zip(t22._8).zip(t22._9).zip(t22._10).zip(t22._11).zip(t22._12).zip(t22._13).zip(t22._14).zip(t22._15).zip(t22._16).zip(t22._17).zip(t22._18).zip(t22._19).zip(t22._20).zip(t22._21).zipWith(t22._22) { case (((((((((((((((((((((a0, a1), a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12), a13), a14), a15), a16), a17), a18), a19), a20), a21) => (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21) } +} diff --git a/app/util/syntax/package.scala b/app/util/syntax/package.scala new file mode 100644 index 000000000..c1b4ec0e0 --- /dev/null +++ b/app/util/syntax/package.scala @@ -0,0 +1,49 @@ +package util + +import scala.language.higherKinds + +import util.functional.{Applicative, Functor, Monad} + +package object syntax extends ParallelSyntax { + + implicit class FunctorOps[F[_], A](private val fa: F[A]) extends AnyVal { + def map[B](f: A => B)(implicit F: Functor[F]): F[B] = F.map(fa)(f) + + def as[B](b: B)(implicit F: Functor[F]): F[B] = F.as(fa, b) + + def fproduct[B](f: A => B)(implicit F: Functor[F]): F[(A, B)] = F.fproduct(fa)(f) + + def tupleLeft[B](b: B)(implicit F: Functor[F]): F[(B, A)] = F.tupleLeft(fa, b) + + def tupleRight[B](b: B)(implicit F: Functor[F]): F[(A, B)] = F.tupleRight(fa, b) + } + + implicit class ApplicativeTupleOps[F[_], A, B](private val tfa: (F[A], F[B])) + extends AnyVal { + + def product(implicit F: Applicative[F]): F[(A, B)] = F.product(tfa._1, tfa._2) + + def map2[C](f: (A, B) => C)(implicit F: Applicative[F]): F[C] = F.map2(tfa._1, tfa._2)(f) + } + + implicit class ApplicativeOps[F[_], A](private val fa: F[A]) extends AnyVal { + + def *>[B](fb: F[B])(implicit F: Applicative[F]): F[B] = F.*>(fa)(fb) + + def <*[B](fb: F[B])(implicit F: Applicative[F]): F[A] = F.<*(fa)(fb) + } + + implicit class ApplicativeApOps[F[_], A, B](private val ff: F[A => B]) + extends AnyVal { + def <*>(fa: F[A])(implicit F: Applicative[F]): F[B] = F.ap(ff)(fa) + } + + implicit class MonadOps[F[_], A](private val fa: F[A]) extends AnyVal { + def flatMap[B](f: A => F[B])(implicit F: Monad[F]): F[B] = F.flatMap(fa)(f) + } + + implicit class MonadFlattenOps[F[_], A](private val ffa: F[F[A]]) + extends AnyVal { + def flatten(implicit F: Monad[F]): F[A] = F.flatten(ffa) + } +} diff --git a/app/views/users/admin/visibility.scala.html b/app/views/users/admin/visibility.scala.html index 33200209d..8d8125752 100644 --- a/app/views/users/admin/visibility.scala.html +++ b/app/views/users/admin/visibility.scala.html @@ -7,8 +7,8 @@ @* visChange2 is users.get(project.lastVisibilityChange.get.createdBy.getOrElse(1)).map(_.username) *@ @* visChange4 is users.get(lastVisibilityChange.get.createdBy.getOrElse(1)).map(_.username) *@ @import ore.permission.Permission -@(needsApproval: Seq[(Project, Map[Permission, Boolean], Option[VisibilityChange], Option[String], Option[VisibilityChange], Option[String])], - waitingProjects: Seq[(Project, Option[VisibilityChange], Option[VisibilityChange], Option[String])])(implicit messages: Messages, request: OreRequest[_], config: OreConfig) +@(needsApproval: Seq[(Project, Map[Permission, Boolean], Option[VisibilityChange], String, Option[VisibilityChange], String)], + waitingProjects: Seq[(Project, Option[VisibilityChange], Option[VisibilityChange], String)])(implicit messages: Messages, request: OreRequest[_], config: OreConfig) @projectRoutes = @{controllers.project.routes.Projects} @@ -36,12 +36,12 @@

Needs Approval

} @needsApproval.map { case (project, projectPerms, lastChangeRequest, lastChangeRequester, lastVisibilityChange, lastVisibilityChanger) => - @if(lastChangeRequest.isDefined) { -
  • -
    -
    +
  • + -
  • - } else { -
  • -
    -
    + } else { @if(lastVisibilityChange.isDefined) { - @lastVisibilityChanger.getOrElse("Unknown") + @lastVisibilityChanger } else { Unknown } @@ -77,15 +66,15 @@

    Needs Approval

    No requests specified

    -
    -
    - - @projects.helper.btnHide(project, projectPerms, request.data.currentUser.get) - -
    + }
    -
  • - } +
    + + @projects.helper.btnHide(project, projectPerms, request.data.currentUser.get) + +
    + + } @@ -110,12 +99,12 @@

    Waiting Changes

    } @waitingProjects.map { case (project, lastChangeRequest, lastVisibilityChange, lastVisibilityChanger) => - @if(lastChangeRequest.isDefined) { + @lastChangeRequest.map { lastRequest =>
  • diff --git a/app/views/utils/alert.scala.html b/app/views/utils/alert.scala.html index 440ede7ab..92bfa964d 100644 --- a/app/views/utils/alert.scala.html +++ b/app/views/utils/alert.scala.html @@ -13,6 +13,11 @@ - @(messages(message)) + + @if(flash.get(s"$alertType-israw").contains("true")) { + @Html(message) + } else { + @(messages(message)) + } } diff --git a/conf/messages b/conf/messages index eb3269da8..012eb508d 100755 --- a/conf/messages +++ b/conf/messages @@ -80,6 +80,9 @@ error.project.invalidPluginFile = Invalid plugin file. error.channel.last = You cannot delete your only channel. error.channel.lastNonEmpty = You cannot delete your only non-empty channel. error.channel.lastReviewed = You cannot delete your only reviewed channel. +error.channel.duplicateColor = A channel with that color already exists. +error.channel.duplicateName = A channel with that name already exists. +error.channel.minOneReviewed = There must be at least one reviewed channel. error.tagline.tooLong = Tagline is too long (max {0}). error.org.disabled = Apologies, creation of Organizations is temporarily disabled. error.org.createLimit = You may only create up to {0} organizations!