From ef9d0ada426fc8cb19670e7700eedf4dc9f59209 Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 27 Mar 2018 05:38:54 +0200 Subject: [PATCH 01/18] Initial work on OptionT, parallel stuff and no global execution context --- app/controllers/ApiController.scala | 15 +- app/controllers/Application.scala | 273 ++++++++---------- app/controllers/OreBaseController.scala | 15 +- app/controllers/Organizations.scala | 63 ++-- app/controllers/Reviews.scala | 89 +++--- app/controllers/Users.scala | 95 +++--- app/controllers/project/Channels.scala | 30 +- app/controllers/project/Pages.scala | 39 ++- app/controllers/project/Projects.scala | 98 +++---- app/controllers/project/Versions.scala | 146 +++++----- app/controllers/sugar/Actions.scala | 151 +++++----- app/db/ModelSchema.scala | 15 +- app/db/ModelService.scala | 27 +- app/db/access/ModelAccess.scala | 14 +- app/db/impl/access/OrganizationBase.scala | 68 ++--- app/db/impl/access/ProjectBase.scala | 19 +- app/db/impl/access/UserBase.scala | 56 ++-- app/db/impl/schema/PageSchema.scala | 5 +- app/db/impl/schema/ProjectSchema.scala | 10 +- app/db/impl/schema/StatSchema.scala | 15 +- app/db/impl/schema/UserSchema.scala | 5 +- app/discourse/OreDiscourseApi.scala | 25 +- app/discourse/RecoveryTask.scala | 6 +- app/discourse/SpongeForums.scala | 1 + app/form/OreForms.scala | 20 +- app/form/project/TChannelData.scala | 73 ++--- app/mail/Mailer.scala | 9 +- app/models/admin/ProjectLog.scala | 9 +- app/models/admin/VisibilityChange.scala | 11 +- app/models/project/DownloadWarning.scala | 9 +- app/models/project/Page.scala | 14 +- app/models/project/Project.scala | 32 +- app/models/project/ProjectSettings.scala | 5 +- app/models/project/Version.scala | 16 +- app/models/statistic/ProjectView.scala | 3 +- app/models/statistic/StatEntry.scala | 13 +- app/models/statistic/VersionDownload.scala | 4 +- app/models/user/Notification.scala | 4 +- app/models/user/User.scala | 64 ++-- app/models/viewhelper/HeaderData.scala | 30 +- app/models/viewhelper/OrganizationData.scala | 10 +- app/models/viewhelper/ProjectData.scala | 77 ++--- .../viewhelper/ScopedOrganizationData.scala | 11 +- app/models/viewhelper/ScopedProjectData.scala | 35 +-- app/models/viewhelper/UserData.scala | 22 +- app/models/viewhelper/VersionData.scala | 18 +- app/ore/StatTracker.scala | 11 +- app/ore/organization/OrganizationOwned.scala | 4 +- app/ore/permission/PermissionPredicate.scala | 18 +- app/ore/project/Dependency.scala | 5 +- app/ore/project/ProjectOwned.scala | 4 +- app/ore/project/ProjectTask.scala | 7 +- app/ore/project/factory/ProjectFactory.scala | 58 ++-- app/ore/rest/OreRestfulApi.scala | 40 ++- app/ore/user/UserOwned.scala | 4 +- app/ore/user/UserSyncTask.scala | 7 +- app/ore/user/notification/InviteFilters.scala | 14 +- .../notification/NotificationFilters.scala | 5 +- app/security/NonceFilter.scala | 2 +- .../spauth/SingleSignOnConsumer.scala | 19 +- app/security/spauth/SpongeAuthApi.scala | 52 ++-- app/util/EitherT.scala | 152 ++++++++++ app/util/FutureUtils.scala | 17 ++ app/util/Monad.scala | 14 + app/util/OptionT.scala | 90 ++++++ app/util/instances/AllInstances.scala | 3 + app/util/instances/FutureInstances.scala | 17 ++ app/util/instances/package.scala | 7 + app/util/syntax/ParallelSyntax.scala | 117 ++++++++ app/util/syntax/package.scala | 14 + 70 files changed, 1406 insertions(+), 1044 deletions(-) create mode 100644 app/util/EitherT.scala create mode 100644 app/util/FutureUtils.scala create mode 100644 app/util/Monad.scala create mode 100644 app/util/OptionT.scala create mode 100644 app/util/instances/AllInstances.scala create mode 100644 app/util/instances/FutureInstances.scala create mode 100644 app/util/instances/package.scala create mode 100644 app/util/syntax/ParallelSyntax.scala create mode 100644 app/util/syntax/package.scala diff --git a/app/controllers/ApiController.scala b/app/controllers/ApiController.scala index e001b9ab9..3c49bcb60 100644 --- a/app/controllers/ApiController.scala +++ b/app/controllers/ApiController.scala @@ -8,8 +8,10 @@ import controllers.sugar.Bakery import db.ModelService import db.impl.OrePostgresDriver.api._ import db.impl.ProjectApiKeyTable +import util.instances.future._ import form.OreForms import javax.inject.Inject + import models.api.ProjectApiKey import models.user.User import ore.permission.EditApiKeys @@ -28,9 +30,7 @@ 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 +46,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._ @@ -227,8 +227,7 @@ final class ApiController @Inject()(api: OreRestfulApi, for { user <- projectData.project.owner.user - orga <- user.toMaybeOrganization - owner <- orga.map(_.owner.user).getOrElse(Future.successful(user)) + owner <- user.toMaybeOrganization.semiFlatMap(_.owner.user).getOrElse(user) result <- upload(owner) } yield { result @@ -244,7 +243,7 @@ final class ApiController @Inject()(api: OreRestfulApi, 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 +287,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) } } diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index 3b4b4c25e..74ee20a84 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -2,6 +2,7 @@ package controllers import java.sql.Timestamp import java.time.Instant + import javax.inject.Inject import controllers.sugar.Bakery @@ -27,11 +28,12 @@ import play.api.Logger import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer -import util.DataHelper +import util.{DataHelper, OptionT} import views.{html => views} +import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import util.syntax._ +import util.instances.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) @@ -239,14 +241,12 @@ 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 => @@ -310,31 +310,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))) + 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)) + 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))) - } + 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) } /** @@ -400,102 +398,83 @@ 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 { + userData <- getUserData(request, user).value + 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).value else Future.successful(None) + orgaData <- OrganizationData.of(orga).value + scopedOrgaData <- ScopedOrganizationData.of(Some(request.user), orga).value + } 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 - } + this.users.withName(userName).map { user => + this.forms.UserAdminUpdate.bindFromRequest.fold( + _ => OptionT.none[Future, Status], + { 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 } - status - } - - def transferOrgOwner(r: OrganizationRole) = { - r.organization.flatMap { orga => - orga.transferOwner(orga.memberships.newMember(r.userId)) + case "setAccepted" => modelAccess.get(id).map { role => + role.setAccepted((json \ "accepted").as[Boolean]) + Ok + } + case "deleteRole" => modelAccess.get(id).map { role => + if (role.roleType.isAssignable) { + role.remove() + Ok + } else BadRequest } } + } - 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) + 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) + } + } + 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) } /** @@ -507,31 +486,35 @@ final class Application @Inject()(data: DataHelper, 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 - }) + lastChangeRequests <- Future.sequence(projectApprovals.map(_.lastChangeRequest.value)) + lastVisibilityChanges <- Future.sequence(projectApprovals.map(_.lastVisibilityChange.value)) 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 - }) - + projectVisibilityChanges <- Future.sequence(projectChanges.map(_.lastVisibilityChange.value)) + + ( + 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) => diff --git a/app/controllers/OreBaseController.scala b/app/controllers/OreBaseController.scala index edcbf1909..9bb3da9ff 100755 --- a/app/controllers/OreBaseController.scala +++ b/app/controllers/OreBaseController.scala @@ -14,9 +14,8 @@ import play.api.i18n.{I18nSupport, Lang} import play.api.mvc._ import security.spauth.SingleSignOnConsumer import util.StringUtils._ - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import util.instances.future._ +import scala.concurrent.{ExecutionContext, Future} /** * Represents a Secured base Controller for this application. @@ -43,6 +42,8 @@ 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. @@ -54,10 +55,10 @@ abstract class OreBaseController(implicit val env: OreEnv, * @return NotFound or function result */ def withProject(author: String, slug: String)(fn: Project => Result)(implicit request: OreRequest[_]): Future[Result] - = this.projects.withSlug(author, slug).map(_.map(fn).getOrElse(notFound)) + = this.projects.withSlug(author, slug).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))) + = this.projects.withSlug(author, slug).semiFlatMap(fn).getOrElse(NotFound) /** * Executes the given function with the specified result or returns a @@ -71,11 +72,11 @@ abstract class OreBaseController(implicit val env: OreEnv, */ 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)) + = project.versions.find(equalsIgnoreCase[VersionTable](_.versionString, versionString)).map(fn).getOrElse(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))) + = project.versions.find(equalsIgnoreCase[VersionTable](_.versionString, versionString)).semiFlatMap(fn).getOrElse(notFound) def OreAction = Action andThen oreAction diff --git a/app/controllers/Organizations.scala b/app/controllers/Organizations.scala index ad24f02cc..0fd8ccfe7 100755 --- a/app/controllers/Organizations.scala +++ b/app/controllers/Organizations.scala @@ -5,6 +5,7 @@ import db.ModelService import discourse.OreDiscourseApi import form.OreForms import javax.inject.Inject + import ore.permission.EditSettings import ore.rest.OreWrites import ore.user.MembershipDossier._ @@ -13,9 +14,8 @@ import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer import views.{html => views} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import util.instances.future._ +import scala.concurrent.{ExecutionContext, Future} /** * Controller for handling Organization based actions. @@ -29,7 +29,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) @@ -70,11 +70,10 @@ class Organizations @Inject()(forms: OreForms, 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)) - } + 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 +89,24 @@ 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).semiFlatMap { role => + //TODO: Why access the organization when it's only used for one of the statuses? + 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 } - - } + } + }.getOrElse(notFound) } /** @@ -130,12 +127,10 @@ 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)) - } + this.users.withName(this.forms.OrganizationMemberRemove.bindFromRequest.get.trim).map { user => + request.data.orga.memberships.removeMember(user) + Redirect(ShowUser(organization)) + }.getOrElse(BadRequest) } /** diff --git a/app/controllers/Reviews.scala b/app/controllers/Reviews.scala index 3004936fd..372d47d92 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -10,6 +10,7 @@ import db.impl.OrePostgresDriver.api._ import db.impl._ import form.OreForms import javax.inject.Inject + import models.admin.{Message, Review} import models.project.{Project, Version} import models.user.{Notification, User} @@ -24,9 +25,8 @@ import security.spauth.SingleSignOnConsumer import slick.lifted.{Rep, TableQuery} import util.DataHelper import views.{html => views} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import util.instances.future._ +import scala.concurrent.{ExecutionContext, Future} /** * Controller for handling Review related actions. @@ -39,7 +39,7 @@ 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) = { @@ -52,8 +52,8 @@ final class Reviews @Inject()(data: DataHelper, 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)) + r.userBase.get(r.userId).map(_.name).value.map { u => + (r, u) } }) map { rv => //implicit val m = messagesApi.preferred(request) @@ -99,13 +99,11 @@ final class Reviews @Inject()(data: DataHelper, 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)) - } + version.mostRecentUnfinishedReview.map { review => + review.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim, System.currentTimeMillis(), "stop")) + review.setEnded(Timestamp.from(Instant.now())) + Redirect(routes.Reviews.showReviews(author, slug, versionString)) + }.getOrElse(NotFound) } } } @@ -115,17 +113,15 @@ final class Reviews @Inject()(data: DataHelper, (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)) - } - } + version.mostRecentUnfinishedReview.semiFlatMap { review => + for { + (_, _) <- review.setEnded(Timestamp.from(Instant.now())) zip + // send notification that review happened + sendReviewNotification(project, version, request.user) + } yield { + Redirect(routes.Reviews.showReviews(author, slug, versionString)) + } + }.getOrElse(NotFound) } } } @@ -189,14 +185,12 @@ final class Reviews @Inject()(data: DataHelper, withProjectAsync(author, slug) { implicit project => withVersionAsync(versionString) { version => // 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 => + for { + (_, _) <- oldreview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim, System.currentTimeMillis(), "takeover")) zip + oldreview.setEnded(Timestamp.from(Instant.now())) + } yield {} + }.getOrElse(()) // Then make new one for { @@ -213,13 +207,11 @@ final class Reviews @Inject()(data: DataHelper, 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) - } + withVersionAsync(versionString) { version => + version.reviewById(reviewId).map { review => + review.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim)) + Ok("Review" + review) + }.getOrElse(NotFound) } } } @@ -229,18 +221,13 @@ final class Reviews @Inject()(data: DataHelper, (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")) - } + version.mostRecentUnfinishedReview.semiFlatMap { recentReview => + users.current.semiFlatMap { currentUser => + if (recentReview.userId == currentUser.userId) { + recentReview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim)) + } else Future.successful(0) + }.getOrElse(1).map( _ => Ok("Review")) + }.getOrElse(Ok("Review")) } } } diff --git a/app/controllers/Users.scala b/app/controllers/Users.scala index 127d1628b..6418914ff 100755 --- a/app/controllers/Users.scala +++ b/app/controllers/Users.scala @@ -8,6 +8,7 @@ import db.impl.{ProjectTableMain, VersionTable} import discourse.OreDiscourseApi import form.OreForms import javax.inject.Inject + import mail.{EmailFactory, Mailer} import models.user.{SignOn, User} import models.viewhelper.{OrganizationData, ScopedOrganizationData} @@ -23,9 +24,8 @@ import play.api.i18n.MessagesApi import play.api.mvc._ import security.spauth.SingleSignOnConsumer import views.{html => views} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import util.instances.future._ +import scala.concurrent.{ExecutionContext, Future} /** * Controller for general user actions. @@ -42,7 +42,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 +76,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 + for { + fromSponge <- User.fromSponge(spongeUser) + getOrCreate <- this.users.getOrCreate(fromSponge) + pulledForum <- getOrCreate.pullForumData() //TODO These two pull methods at the moment just return this at the end. + pulledSponge <- pulledForum.pullSpongeData() //Should their results be ignored and getOrCreate be used from there on? + result <- this.redirectBack(request.flash.get("url").getOrElse("/"), pulledSponge) + } yield result + }.getOrElse(Redirect(ShowHome).withError("error.loginFailed")) } } @@ -133,27 +132,25 @@ 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) + tags <- Future.sequence(projectSeq.map(_._2.tags)) + userData <- getUserData(request, username).value + starred <- user.starred() + starredRv <- Future.sequence(starred.map(_.recommendedVersion)) + orga <- getOrga(request, username).value + orgaData <- OrganizationData.of(orga).value + scopedOrgaData <- ScopedOrganizationData.of(request.currentUser, orga).value + } 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) = { @@ -185,16 +182,14 @@ class Users @Inject()(fakeUser: FakeUser, 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)) - } - } + this.users.withName(username).map { user => + if (tagline.length > maxLen) { + Redirect(ShowUser(user)).flashing("error" -> this.messagesApi("error.tagline.tooLong", maxLen)) + } else { + user.setTagline(tagline) + Redirect(ShowUser(user)) + } + }.getOrElse(NotFound) } /** @@ -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..9205f7806 100755 --- a/app/controllers/project/Channels.scala +++ b/app/controllers/project/Channels.scala @@ -5,6 +5,7 @@ import controllers.sugar.Bakery import db.ModelService import form.OreForms import javax.inject.Inject + import ore.permission.EditChannels import ore.project.factory.ProjectFactory import ore.{OreConfig, OreEnv} @@ -12,9 +13,8 @@ import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer import views.html.projects.{channels => views} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import util.instances.future._ +import scala.concurrent.{ExecutionContext, Future} /** * Controller for handling Channel related actions. @@ -27,7 +27,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 @@ -64,14 +64,10 @@ class Channels @Inject()(forms: OreForms, 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)) - } - - ) - } + channelData.addTo(request.data.project).fold( + error => Redirect(self.showList(author, slug)).withError(error), + _ => Redirect(self.showList(author, slug)) + ) } ) } @@ -91,12 +87,10 @@ class Channels @Inject()(forms: OreForms, 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)) - } - } + channelData.saveTo(channelName).cata( + Redirect(self.showList(author, slug)), + error => Redirect(self.showList(author, slug)).withError(error) + ) } ) } diff --git a/app/controllers/project/Pages.scala b/app/controllers/project/Pages.scala index f8486fb99..398435b42 100755 --- a/app/controllers/project/Pages.scala +++ b/app/controllers/project/Pages.scala @@ -6,6 +6,7 @@ import db.impl.OrePostgresDriver.api._ import db.{ModelFilter, ModelService} import form.OreForms import javax.inject.Inject + import models.project.{Page, Project} import ore.permission.EditPages import ore.{OreConfig, OreEnv, StatTracker} @@ -14,9 +15,8 @@ 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 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 @@ -47,15 +47,17 @@ class Pages @Inject()(forms: OreForms, def withPage(project: Project, page: String): Future[(Option[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)) } } } @@ -100,21 +102,15 @@ class Pages @Inject()(forms: OreForms, 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), _)) + data.project.pages.find(equalsIgnoreCase(_.slug, parts(0))).subflatMap(_.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) - } + data.project.pages.find(equalsIgnoreCase(_.slug, name)).getOrElseF(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 } + 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 +152,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..0819e9359 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -10,6 +10,7 @@ import db.ModelService import discourse.OreDiscourseApi import form.OreForms import javax.inject.Inject + import models.project.{Note, VisibilityTypes} import models.user.User import ore.permission._ @@ -24,10 +25,11 @@ 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 util.OptionT +import util.instances.future._ /** * Controller for handling Project related actions. @@ -43,7 +45,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 { @@ -151,7 +153,7 @@ class Projects @Inject()(stats: StatTracker, val authors = pendingProject.file.meta.get.getAuthors.asScala for { - users <- Future.sequence(authors.filterNot(_.equals(currentUser.username)).map(this.users.withName)) + users <- Future.sequence(authors.filterNot(_.equals(currentUser.username)).map(this.users.withName(_).value)) registered <- this.forums.countUsers(authors.toList) owner <- pendingProject.underlying.owner.user } yield { @@ -221,9 +223,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,14 +257,9 @@ 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 poster = OptionT.fromOption[Future](formData.poster) + .flatMap(posterName => this.users.requestPermission(request.user, posterName, PostAsOrganization)) + .getOrElse(request.user) val errors = poster.flatMap { post => this.forums.postDiscussionReply(data.project, post, formData.content) } @@ -316,16 +312,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 +390,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 +420,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 +487,10 @@ 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)) - } + this.users.withName(this.forms.ProjectMemberRemove.bindFromRequest.get.trim).map { user => + request.data.project.memberships.removeMember(user) + Redirect(self.showSettings(author, slug)) + }.getOrElse(BadRequest) } /** @@ -606,7 +596,7 @@ class Projects @Inject()(stats: StatTracker, val project = request.data.project for { changes <- project.visibilityChangesByDate - changedBy <- Future.sequence(changes.map(_.created)) + changedBy <- Future.sequence(changes.map(_.created.value)) logger <- project.logger logs <- logger.entries.all } yield { @@ -671,7 +661,7 @@ 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 => + Future.sequence(project.getNotes().map(note => users.get(note.user).value.map(user => (note, user)))) map { notes => Ok(views.admin.notes(project, notes)) } } diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index 8c2dac1fb..e3a0333c8 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -6,6 +6,7 @@ import java.sql.Timestamp import java.util.{Date, UUID} import com.github.tminglei.slickpg.InetString + import controllers.OreBaseController import controllers.sugar.Bakery import controllers.sugar.Requests.{OreRequest, ProjectRequest} @@ -14,6 +15,7 @@ import db.impl.OrePostgresDriver.api._ import discourse.OreDiscourseApi import form.OreForms import javax.inject.Inject + import models.project._ import models.viewhelper.{ProjectData, VersionData} import ore.permission.{EditVersions, ReviewProjects} @@ -31,10 +33,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.{EitherT, OptionT} +import util.instances.future._ /** * Controller for handling Version related actions. @@ -49,7 +53,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 @@ -249,36 +253,29 @@ class Versions @Inject()(stats: StatTracker, */ 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)) - } - } + //TODO: Does order matter here. If not, the code could be rewritten a bit clearer + val success = OptionT.fromOption[Future](this.factory.getPendingVersion(author, slug, versionString)) + .flatMap { pendingVersion => + // Get pending version + pendingOrReal(author, slug).semiFlatMap { + case pending: PendingProject => + ProjectData.of(request, pending).map(data => + Ok(views.create(data, data.settings.forumSync, Some(pendingVersion), None, showFileControls = false))) + case real: Project => + (real.channels.toSeq, ProjectData.of(real)).parMapN { case (channels, data) => + Ok(views + .create(data, data.settings.forumSync, Some(pendingVersion), Some(channels), showFileControls = true)) + } } - } + } + + 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, Any] = { // Returns either a PendingProject or existing Project - this.projects.withSlug(author, slug) map { + this.projects.withSlug(author, slug) transform { case None => this.factory.getPendingProject(author, slug) case Some(project) => Some(project) } @@ -321,31 +318,25 @@ class Versions @Inject()(stats: StatTracker, 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)) - } + project.channels + .find(equalsIgnoreCase(_.name, pendingVersion.channelName)) + .toRight(versionData.addTo(project).value) + .leftFlatMap(EitherT.apply) + .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 @@ -457,9 +448,8 @@ class Versions @Inject()(stats: StatTracker, (warn.versionId === version.id.get) && (warn.address === InetString(StatTracker.remoteAddress)) && warn.isConfirmed - } map { - case None => false - case Some(warn) => + } exists { + warn => if (!warn.hasExpired) true else { warn.remove() @@ -565,7 +555,7 @@ class Versions @Inject()(stats: StatTracker, if (version.isReviewed) Future.successful(Redirect(ShowProject(author, slug))) else { - confirmDownload0(version.id.get, downloadType, token).map { + confirmDownload0(version.id.get, downloadType, token).value.map { case None => Redirect(ShowProject(author, slug)) case Some(dl) => dl.downloadType match { @@ -589,7 +579,7 @@ class Versions @Inject()(stats: StatTracker, /** * 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 +591,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 } } @@ -735,7 +721,7 @@ class Versions @Inject()(stats: StatTracker, withVersionAsync(versionString) { version => if (token.isDefined) { request.headers - confirmDownload0(version.id.get, Some(JarFile.id), token.get)(request.request).flatMap { _ => + confirmDownload0(version.id.get, Some(JarFile.id), token.get)(request.request).value.flatMap { _ => sendJar(data.project, version, token, api = true) } } else { diff --git a/app/controllers/sugar/Actions.scala b/app/controllers/sugar/Actions.scala index 47c40307b..b40fd87f4 100644 --- a/app/controllers/sugar/Actions.scala +++ b/app/controllers/sugar/Actions.scala @@ -18,10 +18,12 @@ import play.api.mvc.Results.{Redirect, Unauthorized} import play.api.mvc._ import security.spauth.SingleSignOnConsumer import slick.jdbc.JdbcBackend - import scala.concurrent.{ExecutionContext, Future} import scala.language.higherKinds +import util.{FutureUtils, OptionT} +import util.instances.future._ + /** * A set of actions used by Ore. */ @@ -42,8 +44,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 +136,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 +167,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 +209,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,20 +229,16 @@ 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[_], @@ -258,20 +250,22 @@ trait Actions extends Calls with ActionHelpers { } } - 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 +278,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 +309,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 +324,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) } @@ -351,25 +340,19 @@ trait Actions extends Calls with ActionHelpers { case Some(orga) => for { (data, scoped) <- OrganizationData.of(orga) zip - ScopedOrganizationData.of(request.data.currentUser, orga) - } yield { - Right(f(data, scoped)) - } + ScopedOrganizationData.of(request.data.currentUser, orga) + } yield Right(f(data, scoped)) } } 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..ee8d4710b 100644 --- a/app/db/ModelSchema.scala +++ b/app/db/ModelSchema.scala @@ -3,11 +3,12 @@ package db 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.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 +131,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 +142,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 +160,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..8a76d48a1 100644 --- a/app/db/ModelService.scala +++ b/app/db/ModelService.scala @@ -7,15 +7,14 @@ import db.ModelAction._ import db.ModelFilter.IdFilter import db.access.ModelAccess import db.table.{MappedType, ModelTable} +import util.OptionT import slick.ast.{AnonSymbol, Ref, SortBy} import slick.basic.DatabaseConfig 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} /** @@ -112,7 +111,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 +131,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 +181,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 +226,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 +238,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 +249,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 +262,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 +287,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 +301,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..81f277449 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.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..bdcceb1e3 100644 --- a/app/db/impl/access/OrganizationBase.scala +++ b/app/db/impl/access/OrganizationBase.scala @@ -10,8 +10,8 @@ import ore.user.notification.NotificationTypes import play.api.cache.AsyncCacheApi import play.api.i18n.{Lang, MessagesApi} import security.spauth.SpongeAuthApi -import util.StringUtils - +import util.{EitherT, OptionT, StringUtils} +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} class OrganizationBase(override val service: ModelService, @@ -35,7 +35,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 +48,40 @@ 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 => + 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. + org + .toUser + .getOrElse(throw new IllegalStateException("User not created")) + .map { userOrg => + userOrg.pullForumData().flatMap(_.pullSpongeData()) + userOrg.setGlobalRoles(userOrg.globalRoles + RoleTypes.Organization) + userOrg + } + .flatMap { _ => // Add the owner org.memberships.addRole(OrganizationRole( userId = ownerId, organizationId = org.id.get, _roleType = RoleTypes.OrganizationOwner, _isAccepted = true)) - } flatMap { _ => + } + .flatMap { _ => // Invite the User members that the owner selected during creation. Logger.info("Inviting members...") @@ -92,11 +94,11 @@ class OrganizationBase(override val service: ModelService, message = this.messages("notification.organization.invite", role.roleType.title, org.username) )) } - } - ) - } map { _ => + }) + } + .map { _ => Logger.info(" " + org) - Right(org) + org } } } @@ -107,6 +109,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..33b4d1770 100644 --- a/app/db/impl/access/ProjectBase.scala +++ b/app/db/impl/access/ProjectBase.scala @@ -6,6 +6,7 @@ import java.sql.Timestamp import java.util.Date import com.google.common.base.Preconditions._ + import db.impl.OrePostgresDriver.api._ import db.impl.{PageTable, ProjectTableMain, VersionTable} import db.{ModelBase, ModelService} @@ -14,9 +15,9 @@ import models.project.{Channel, Project, Version} import ore.project.io.ProjectFiles import ore.{OreConfig, OreEnv} import slick.lifted.TableQuery -import util.FileUtils +import util.{FileUtils, OptionT} import util.StringUtils._ - +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} class ProjectBase(override val service: ModelService, @@ -55,7 +56,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 +66,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 +76,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 +85,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 +100,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]]. @@ -225,7 +226,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..fd3da0eec 100755 --- a/app/db/impl/access/UserBase.scala +++ b/app/db/impl/access/UserBase.scala @@ -17,6 +17,8 @@ import scala.concurrent.{ExecutionContext, Future} import ore.permission.role import ore.permission.role.RoleTypes import ore.permission.role.RoleTypes.RoleType +import util.OptionT +import util.instances.future._ /** * Represents a central location for all Users. @@ -40,13 +42,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).semiFlatMap(User.fromSponge).semiFlatMap(getOrCreate) } } @@ -59,21 +57,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 +142,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 +166,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 +181,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..d4bba4427 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.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..770a5f2aa 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.OptionT /** * Project related queries @@ -24,7 +24,7 @@ class ProjectSchema(override val service: ModelService, implicit val users: User * * @return Project authors */ - def distinctAuthors: Future[Seq[User]] = { + def distinctAuthors(implicit ec: ExecutionContext): Future[Seq[User]] = { service.DB.db.run { (for (project <- this.baseQuery) yield project.userId).distinct.result } flatMap { userIds => @@ -78,10 +78,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..7b2233aa6 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.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..4e56fa78b 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.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..aa4f4a212 100644 --- a/app/discourse/OreDiscourseApi.scala +++ b/app/discourse/OreDiscourseApi.scala @@ -4,15 +4,15 @@ import java.nio.file.Path import akka.actor.Scheduler import com.google.common.base.Preconditions.{checkArgument, checkNotNull} + import db.impl.access.ProjectBase import models.project.{Project, Version} import models.user.User import org.spongepowered.play.discourse.DiscourseApi -import util.StringUtils._ -import scala.concurrent.ExecutionContext.Implicits.global +import util.StringUtils._ import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{Future, Promise} +import scala.concurrent.{ExecutionContext, Future, Promise} import scala.util.{Failure, Success} /** @@ -45,6 +45,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 +57,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 +68,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 +127,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 +209,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 +233,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 +258,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 +293,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 +314,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..38f10fb56 100644 --- a/app/discourse/RecoveryTask.scala +++ b/app/discourse/RecoveryTask.scala @@ -1,10 +1,10 @@ package discourse +import scala.concurrent.ExecutionContext + import akka.actor.Scheduler import db.impl.OrePostgresDriver.api._ import db.impl.access.ProjectBase - -import scala.concurrent.ExecutionContext.Implicits.global 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..c38f9d3c4 100644 --- a/app/discourse/SpongeForums.scala +++ b/app/discourse/SpongeForums.scala @@ -33,6 +33,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 = 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..53ca84a40 100755 --- a/app/form/OreForms.scala +++ b/app/form/OreForms.scala @@ -8,6 +8,9 @@ import db.impl.OrePostgresDriver.api._ import form.organization.{OrganizationAvatarUpdate, OrganizationMembersUpdate, OrganizationRoleSetBuilder} import form.project._ import javax.inject.Inject + +import scala.concurrent.ExecutionContext + import models.api.ProjectApiKey import models.project.{Channel, Page} import models.project.Page._ @@ -20,7 +23,6 @@ import play.api.data.Forms._ import play.api.data.format.Formatter import play.api.data.validation.{Constraint, Invalid, Valid, ValidationError} import play.api.data.{Form, FormError} - import scala.util.Try /** @@ -226,7 +228,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 +238,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 +256,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..dfc51c4d2 100644 --- a/app/form/project/TChannelData.scala +++ b/app/form/project/TChannelData.scala @@ -4,9 +4,10 @@ import models.project.{Channel, Project} import ore.Colors.Color import ore.OreConfig import ore.project.factory.ProjectFactory - import scala.concurrent.{ExecutionContext, Future} +import util.{EitherT, OptionT} + /** * Represents submitted [[Channel]] data. */ @@ -33,23 +34,25 @@ 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 { + def addTo(project: Project)(implicit ec: ExecutionContext): EitherT[Future, String, Channel] = { + EitherT( + 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 color already exists.")) - case None => - this.factory.createChannel(project, this.channelName, this.color, this.nonReviewed).map(Right(_)) + 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(_)) + } } } } - } + ) } /** @@ -60,31 +63,33 @@ 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.") + def saveTo(oldName: String)(implicit project: Project, ec: ExecutionContext): OptionT[Future, String] = { + OptionT( + project.channels.all.map { channels => + val channel = channels.find(_.name.equalsIgnoreCase(oldName)).get + val colorChan = channels.find(_.color.equals(this.color)) + val colorTaken = colorChan.exists(_ != channel) + if (colorTaken) { + Some("A channel with that color 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.") + val nameChan = channels.find(_.name.equalsIgnoreCase(this.channelName)) + val nameTaken = nameChan.exists(_ != channel) + if (nameTaken) { + Some("A channel with that name already exists.") } else { - channel.setName(this.channelName) - channel.setColor(this.color) - channel.setNonReviewed(this.nonReviewed) - None + 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 + } } } } - } + ) } } diff --git a/app/mail/Mailer.scala b/app/mail/Mailer.scala index cc03e442f..20dc44391 100644 --- a/app/mail/Mailer.scala +++ b/app/mail/Mailer.scala @@ -9,9 +9,10 @@ import javax.inject.{Inject, Singleton} import javax.mail.Message.RecipientType 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 play.api.Configuration import scala.concurrent.duration._ /** @@ -52,7 +53,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 +105,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..655ea12a3 100644 --- a/app/models/admin/VisibilityChange.scala +++ b/app/models/admin/VisibilityChange.scala @@ -9,8 +9,10 @@ import db.impl.table.ModelKeys._ import models.project.Page import models.user.User import play.twirl.api.Html +import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.Future +import util.OptionT +import util.instances.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/DownloadWarning.scala b/app/models/project/DownloadWarning.scala index c5978ecb7..d9efad47c 100644 --- a/app/models/project/DownloadWarning.scala +++ b/app/models/project/DownloadWarning.scala @@ -4,6 +4,7 @@ import java.sql.Timestamp import com.github.tminglei.slickpg.InetString import com.google.common.base.Preconditions._ + import controllers.sugar.Bakery import db.Expirable import db.impl.DownloadWarningsTable @@ -11,9 +12,11 @@ import db.impl.model.OreModel import db.impl.table.ModelKeys._ import models.project.DownloadWarning.COOKIE import play.api.mvc.Cookie - import scala.concurrent.{ExecutionContext, Future} +import util.OptionT +import util.instances.future._ + /** * Represents an instance of a warning that a client has landed on. Warnings * will expire and are associated with a certain inet address. @@ -57,9 +60,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..807ae5d68 100644 --- a/app/models/project/Page.scala +++ b/app/models/project/Page.scala @@ -16,6 +16,7 @@ import com.vladsch.flexmark.html.renderer._ import com.vladsch.flexmark.html.{HtmlRenderer, LinkResolver, LinkResolverFactory} import com.vladsch.flexmark.parser.Parser import com.vladsch.flexmark.util.options.MutableDataSet + import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ import db.impl.PageTable @@ -27,7 +28,8 @@ import ore.OreConfig import ore.permission.scope.ProjectScope import play.twirl.api.Html import util.StringUtils._ - +import util.OptionT +import util.instances.future._ 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..893f92450 100755 --- a/app/models/project/Project.scala +++ b/app/models/project/Project.scala @@ -3,9 +3,12 @@ package models.project import java.sql.Timestamp import java.time.Instant -import _root_.util.StringUtils +import _root_.util.{StringUtils, OptionT} import _root_.util.StringUtils._ +import _root_.util.instances.future._ + import com.google.common.base.Preconditions._ + import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ import db.impl._ @@ -33,9 +36,7 @@ import play.api.libs.json._ import play.twirl.api.Html import slick.lifted import slick.lifted.{Rep, TableQuery} - import scala.concurrent.{ExecutionContext, Future} - /** * Represents an Ore package. * @@ -285,7 +286,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 +327,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 +344,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 +487,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 +515,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 +534,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,13 +552,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 { + loggers.find(_.projectId === this.id.get).value.flatMap { case None => loggers.add(ProjectLog(projectId = this.id.get)) case Some(l) => Future.successful(l) } diff --git a/app/models/project/ProjectSettings.scala b/app/models/project/ProjectSettings.scala index ce540e3d8..0c4b7de82 100644 --- a/app/models/project/ProjectSettings.scala +++ b/app/models/project/ProjectSettings.scala @@ -8,6 +8,8 @@ import db.impl.OrePostgresDriver.api._ import db.impl._ import db.impl.model.OreModel import db.impl.table.ModelKeys._ +import util.OptionT +import util.instances.future._ import form.project.ProjectSettingsForm import models.user.Notification import models.user.role.ProjectRole @@ -20,7 +22,6 @@ import play.api.cache.AsyncCacheApi import play.api.i18n.{Lang, MessagesApi} import slick.lifted.TableQuery import util.StringUtils._ - import scala.concurrent.{ExecutionContext, Future} /** @@ -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..bde69a714 100755 --- a/app/models/project/Version.scala +++ b/app/models/project/Version.scala @@ -4,6 +4,7 @@ import java.sql.Timestamp import java.time.Instant import com.google.common.base.Preconditions.{checkArgument, checkNotNull} + import db.ModelService import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ @@ -18,8 +19,8 @@ import models.user.User import ore.permission.scope.ProjectScope import ore.project.Dependency import play.twirl.api.Html -import util.FileUtils - +import util.{FileUtils, OptionT} +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} /** @@ -74,7 +75,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 +142,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 +154,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 +276,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..92b0598fe 100644 --- a/app/models/statistic/StatEntry.scala +++ b/app/models/statistic/StatEntry.scala @@ -4,13 +4,15 @@ import java.sql.Timestamp import com.github.tminglei.slickpg.InetString import com.google.common.base.Preconditions._ + import db.Model import db.impl.model.OreModel import db.impl.table.ModelKeys._ import db.impl.table.StatTable +import util.OptionT +import util.instances.future._ 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/User.scala b/app/models/user/User.scala index b6e306ef6..931fd4500 100644 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -3,6 +3,7 @@ package models.user import java.sql.Timestamp import com.google.common.base.Preconditions._ + import db.{ModelFilter, Named} import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ @@ -20,16 +21,18 @@ import ore.permission.scope._ import ore.user.Prompts.Prompt import ore.user.UserOwned import org.spongepowered.play.discourse.model.DiscourseUser + import play.api.mvc.Request import security.pgp.PGPPublicKeyInfo import security.spauth.SpongeUser import slick.lifted.{QueryBase, TableQuery} import util.StringUtils._ - -import scala.concurrent.ExecutionContext.Implicits.global +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} import scala.util.control.Breaks._ +import util.OptionT + /** * Represents a Sponge user. * @@ -284,7 +287,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 +337,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 +351,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) } /** @@ -414,9 +406,9 @@ case class User(override val id: Option[Int] = None, * * @return This user */ - def pullForumData(): Future[User] = { + def pullForumData()(implicit ec: ExecutionContext): Future[User] = { // 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}).semiFlatMap(fill).getOrElse(this) } /** @@ -424,8 +416,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[User] = { + this.auth.getUser(this.name).semiFlatMap(fill).getOrElse(throw new Exception("user doesn't exist on SpongeAuth?")) } /** @@ -441,7 +433,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 +469,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 +478,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 +500,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 +524,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 +543,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 +554,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]) @@ -615,7 +607,7 @@ object User { * @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(user: DiscourseUser)(implicit ec: ExecutionContext) = User().fill(user).map(_.copy(id = Some(user.id))) /** * Create a new [[User]] from the specified [[SpongeUser]]. @@ -623,6 +615,6 @@ object User { * @param user User to convert * @return Ore user */ - def fromSponge(user: SpongeUser)(implicit config: OreConfig) = User().fill(user).map(_.copy(id = Some(user.id))) + def fromSponge(user: SpongeUser)(implicit config: OreConfig, ec: ExecutionContext) = User().fill(user).map(_.copy(id = Some(user.id))) } diff --git a/app/models/viewhelper/HeaderData.scala b/app/models/viewhelper/HeaderData.scala index c36784248..4e1a4c95b 100644 --- a/app/models/viewhelper/HeaderData.scala +++ b/app/models/viewhelper/HeaderData.scala @@ -16,6 +16,8 @@ import slick.lifted.TableQuery import scala.concurrent.{ExecutionContext, Future} +import util.syntax._ + /** * Holds global user specific data - When a User is not authenticated a dummy is used */ @@ -142,20 +144,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..54e285a4b 100644 --- a/app/models/viewhelper/OrganizationData.scala +++ b/app/models/viewhelper/OrganizationData.scala @@ -7,9 +7,10 @@ import ore.organization.OrganizationMember import ore.permission._ import play.api.cache.AsyncCacheApi import slick.jdbc.JdbcBackend - import scala.concurrent.{ExecutionContext, Future} +import util.OptionT +import util.instances.future._ case class OrganizationData(joinable: Organization, ownerRole: OrganizationRole, @@ -41,10 +42,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..5859a0176 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 */ @@ -78,39 +81,47 @@ object ProjectData { 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..4b0a3c00f 100644 --- a/app/models/viewhelper/ScopedOrganizationData.scala +++ b/app/models/viewhelper/ScopedOrganizationData.scala @@ -5,9 +5,11 @@ import models.user.{Organization, User} import ore.permission.{Permission, _} import play.api.cache.AsyncCacheApi import slick.jdbc.JdbcBackend - import scala.concurrent.{ExecutionContext, Future} +import util.OptionT +import util.instances.future._ + case class ScopedOrganizationData(permissions: Map[Permission, Boolean] = Map.empty) object ScopedOrganizationData { @@ -32,10 +34,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..d07f523bd 100755 --- a/app/ore/StatTracker.scala +++ b/app/ore/StatTracker.scala @@ -8,14 +8,13 @@ import db.ModelService import db.impl.access.{ProjectBase, UserBase} import db.impl.schema.StatSchema import javax.inject.Inject + import models.project.Version import models.statistic.{ProjectView, VersionDownload} 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..1917afd8a 100644 --- a/app/ore/permission/PermissionPredicate.scala +++ b/app/ore/permission/PermissionPredicate.scala @@ -5,9 +5,10 @@ import models.project.Project import models.user.User import ore.permission.role.RoleTypes import ore.permission.scope.ScopeSubject - import scala.concurrent.{ExecutionContext, Future} +import util.FutureUtils + /** * Permission wrapper used for chaining permission checks. * @@ -50,13 +51,14 @@ 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).value + .flatMap(_.fold(Future.successful(false))(_.scope.test(user, p))) + + 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..611f9fef1 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.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..4d17f5dba 100644 --- a/app/ore/project/ProjectTask.scala +++ b/app/ore/project/ProjectTask.scala @@ -2,11 +2,14 @@ package ore.project import java.sql.Timestamp import java.time.Instant + import javax.inject.{Inject, Singleton} +import scala.concurrent.ExecutionContext + import akka.actor.ActorSystem import scala.concurrent.duration._ -import scala.concurrent.ExecutionContext.Implicits.global + import db.impl.OrePostgresDriver.api._ import db.impl.schema.ProjectSchema import db.{ModelFilter, ModelService} @@ -17,7 +20,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..9b1bff13c 100755 --- a/app/ore/project/factory/ProjectFactory.scala +++ b/app/ore/project/factory/ProjectFactory.scala @@ -5,11 +5,13 @@ import java.nio.file.StandardCopyOption import akka.actor.ActorSystem import com.google.common.base.Preconditions._ + import db.ModelService import db.impl.OrePostgresDriver.api._ import db.impl.access.{ProjectBase, UserBase} import discourse.OreDiscourseApi import javax.inject.Inject + import models.project.TagColors.TagColor import models.project._ import models.user.role.ProjectRole @@ -23,17 +25,19 @@ import ore.project.factory.TagAlias.ProjectTag import ore.project.io.{InvalidPluginFileException, PluginFile, PluginUpload, ProjectFiles} import ore.user.notification.NotificationTypes import org.spongepowered.plugin.meta.PluginMetadata + import play.api.cache.SyncCacheApi import play.api.i18n.{Lang, MessagesApi} import security.pgp.PGPVerifier import util.StringUtils._ - +import util.instances.future._ 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 +import util.OptionT + /** * Manages the project and version creation pipeline. */ @@ -106,7 +110,7 @@ trait ProjectFactory { def processSubsequentPluginUpload(uploadData: PluginUpload, owner: User, - project: Project): Future[Either[String, PendingVersion]] = { + project: Project)(implicit ec: ExecutionContext): Future[Either[String, PendingVersion]] = { val plugin = this.processPluginUpload(uploadData, owner) if (!plugin.meta.get.getId.equals(project.pluginId)) return Future.successful(Left("error.version.invalidPluginId")) @@ -252,7 +256,7 @@ 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 { @@ -303,7 +307,7 @@ 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", "") @@ -326,7 +330,7 @@ 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 @@ -361,8 +365,8 @@ trait ProjectFactory { for { channel <- channel newVersion <- newVersion - spongeTag <- addTags(newVersion, SpongeApiId, "Sponge", TagColors.Sponge) - forgeTag <- addTags(newVersion, ForgeId, "Forge", TagColors.Forge) + spongeTag <- addTags(newVersion, SpongeApiId, "Sponge", TagColors.Sponge).value + forgeTag <- addTags(newVersion, ForgeId, "Forge", TagColors.Forge).value } yield { val tags = spongeTag ++ forgeTag @@ -381,18 +385,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 => + val tagToAdd = 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 +411,19 @@ trait ProjectFactory { Future.successful(tag) } } - tagToAdd.flatten.map { tag => - newVersion.addTag(tag) - Some(tag) - } + } yield tag + + tagToAdd.map { tag => + 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..a427f26ec 100755 --- a/app/ore/rest/OreRestfulApi.scala +++ b/app/ore/rest/OreRestfulApi.scala @@ -8,6 +8,7 @@ import db.impl._ import db.impl.access.{ProjectBase, UserBase} import db.impl.schema.{ProjectSchema, ProjectTag} import javax.inject.Inject + import models.project._ import models.user.User import models.user.role.ProjectRole @@ -18,7 +19,8 @@ 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.OptionT +import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} /** @@ -242,6 +244,7 @@ trait OreRestfulApi { val limited = filtered.drop(offset.getOrElse(0)).take(lim) + //TODO: Why is this an Option again? for { data <- service.DB.db.run(limited.result) // Get Project Version Channel and AuthorName vTags <- service.DB.db.run(queryVersionTags(data.map(_._3)).result).map { p => p.groupBy(_._1) mapValues (_.map(_._2)) } @@ -300,23 +303,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 +391,7 @@ trait OreRestfulApi { for { user <- service.DB.db.run(queryOneUser.result) json <- writeUsers(user) - } yield { - json.headOption - } + } yield json.headOption } /** @@ -402,18 +401,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/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..5b176334c 100644 --- a/app/ore/user/UserSyncTask.scala +++ b/app/ore/user/UserSyncTask.scala @@ -4,10 +4,9 @@ import akka.actor.ActorSystem import db.ModelService import db.impl.access.UserBase import javax.inject.{Inject, Singleton} -import ore.OreConfig -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import ore.OreConfig +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 diff --git a/app/ore/user/notification/InviteFilters.scala b/app/ore/user/notification/InviteFilters.scala index 25a5a70a4..d8032fd92 100644 --- a/app/ore/user/notification/InviteFilters.scala +++ b/app/ore/user/notification/InviteFilters.scala @@ -2,9 +2,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 +10,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..36d636858 100644 --- a/app/ore/user/notification/NotificationFilters.scala +++ b/app/ore/user/notification/NotificationFilters.scala @@ -3,8 +3,7 @@ package ore.user.notification 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 +21,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..629c64423 100644 --- a/app/security/spauth/SingleSignOnConsumer.scala +++ b/app/security/spauth/SingleSignOnConsumer.scala @@ -8,14 +8,17 @@ import java.util.Base64 import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import javax.inject.Inject + import org.apache.commons.codec.binary.Hex + import play.api.Configuration import play.api.http.Status import play.api.libs.ws.WSClient - -import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ -import scala.concurrent.{Await, Future} +import scala.concurrent.{Await, ExecutionContext, Future} + +import util.OptionT +import util.instances.future._ /** * Manages authentication to Sponge services. @@ -39,7 +42,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 +108,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 +145,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..1fe579bda 100644 --- a/app/security/spauth/SpongeAuthApi.scala +++ b/app/security/spauth/SpongeAuthApi.scala @@ -4,17 +4,19 @@ import java.util.concurrent.TimeoutException import com.google.common.base.Preconditions._ import javax.inject.Inject + import ore.OreConfig 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._ +import _root_.util.{EitherT, OptionT} +import _root_.util.instances.future._ + /** * Interfaces with the SpongeAuth Web API */ @@ -48,7 +50,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 +61,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 +88,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 +100,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/EitherT.scala b/app/util/EitherT.scala new file mode 100644 index 000000000..88a0a5f82 --- /dev/null +++ b/app/util/EitherT.scala @@ -0,0 +1,152 @@ +package util + +import scala.language.higherKinds + +import play.api.libs.functional.{Applicative, Functor} +import play.api.libs.functional.syntax._ +import 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.fmap(_.fold(fa, fb)) + + def swap(implicit F: Functor[F]): EitherT[F, B, A] = EitherT(value.fmap(_.swap)) + + def getOrElse[B1 >: B](or: => B1)(implicit F: Functor[F]): F[B1] = value.fmap(_.getOrElse(or)) + + def merge[A1 >: A](implicit ev: B <:< A1, F: Functor[F]): F[A1] = value.fmap(_.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.fmap(_.contains(elem)) + + def forall(f: B => Boolean)(implicit F: Functor[F]): F[Boolean] = value.fmap(_.forall(f)) + + def exists(f: B => Boolean)(implicit F: Functor[F]): F[Boolean] = value.fmap(_.exists(f)) + + def transform[C, D](f: Either[A, B] => Either[C, D])(implicit F: Functor[F]): EitherT[F, C, D] = + EitherT(value.fmap(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).fmap(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.fmap { + 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.fmap(_.filterOrElse(p, zero))) + + def toOption(implicit F: Functor[F]): OptionT[F, B] = OptionT(value.fmap(_.toOption)) + + def isLeft(implicit F: Functor[F]): F[Boolean] = value.fmap(_.isLeft) + + def isRight(implicit F: Functor[F]): F[Boolean] = value.fmap(_.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.fmap(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.fmap(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.fmap(_.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))) + } +} \ No newline at end of file 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/Monad.scala b/app/util/Monad.scala new file mode 100644 index 000000000..7108645fe --- /dev/null +++ b/app/util/Monad.scala @@ -0,0 +1,14 @@ +package util + +import scala.language.higherKinds + +import play.api.libs.functional.{Applicative, Functor} + +trait Monad[F[_]] extends Functor[F] with Applicative[F] { + + def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] + + override def map[A, B](fa: F[A], f: A => B): F[B] = fmap(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/OptionT.scala b/app/util/OptionT.scala new file mode 100644 index 000000000..5a391b01e --- /dev/null +++ b/app/util/OptionT.scala @@ -0,0 +1,90 @@ +package util + +import scala.language.higherKinds + +import play.api.libs.functional.{Applicative, Functor} +import play.api.libs.functional.syntax._ +import syntax._ + +case class OptionT[F[_], A](value: F[Option[A]]) { + + def isEmpty(implicit F: Functor[F]): F[Boolean] = value.fmap(_.isEmpty) + + def isDefined(implicit F: Functor[F]): F[Boolean] = value.fmap(_.isDefined) + + def getOrElse[B >: A](default: => B)(implicit F: Functor[F]): F[B] = value.fmap(_.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.fmap(_.map(f))) + + def fold[B](ifEmpty: => B)(f: A => B)(implicit F: Functor[F]): F[B] = value.fmap(_.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.fmap(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.fmap(_.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.fmap(_.filterNot(f))) + + def contains[A1 >: A](elem: A1)(implicit F: Functor[F]): F[Boolean] = value.fmap(_.contains(elem)) + + def exists(f: A => Boolean)(implicit F: Functor[F]): F[Boolean] = value.fmap(_.exists(f)) + + def forall(f: A => Boolean)(implicit F: Functor[F]): F[Boolean] = value.fmap(_.forall(f)) + + def collect[B](f: PartialFunction[A, B])(implicit F: Functor[F]): OptionT[F, B] = + OptionT(value.fmap(_.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.fmap(Some(_))) +} \ No newline at end of file diff --git a/app/util/instances/AllInstances.scala b/app/util/instances/AllInstances.scala new file mode 100644 index 000000000..3b0140523 --- /dev/null +++ b/app/util/instances/AllInstances.scala @@ -0,0 +1,3 @@ +package util.instances + +trait AllInstances extends FutureInstances diff --git a/app/util/instances/FutureInstances.scala b/app/util/instances/FutureInstances.scala new file mode 100644 index 000000000..de50e8a01 --- /dev/null +++ b/app/util/instances/FutureInstances.scala @@ -0,0 +1,17 @@ +package util.instances + +import scala.concurrent.{ExecutionContext, Future} + +import util.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 fmap[A, B](m: Future[A], f: A => B): Future[B] = m.map(f) + override def apply[A, B](mf: Future[A => B], ma: Future[A]): Future[B] = mf.flatMap(f => ma.map(f)) + override def pure[A](a: A): Future[A] = Future.successful(a) + } + +} diff --git a/app/util/instances/package.scala b/app/util/instances/package.scala new file mode 100644 index 000000000..8e21f7189 --- /dev/null +++ b/app/util/instances/package.scala @@ -0,0 +1,7 @@ +package util + +package object instances { + + object all extends AllInstances + object future extends FutureInstances +} 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..ba8108a6a --- /dev/null +++ b/app/util/syntax/package.scala @@ -0,0 +1,14 @@ +package util + +import scala.language.higherKinds + +package object syntax extends ParallelSyntax { + + 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) + } +} From 0d30ad0bdb25378c9470bfaa8380860750c0c441 Mon Sep 17 00:00:00 2001 From: Unknown Date: Sat, 7 Apr 2018 00:19:32 +0200 Subject: [PATCH 02/18] Simplify Future.successful with transformers --- app/controllers/ApiController.scala | 87 ++++++++------------ app/controllers/Application.scala | 6 +- app/controllers/project/Channels.scala | 54 ++++++------ app/controllers/project/Pages.scala | 1 + app/controllers/project/Projects.scala | 14 ++-- app/controllers/project/Versions.scala | 52 ++++++------ app/form/project/TChannelData.scala | 23 ++---- app/models/project/Project.scala | 5 +- app/models/viewhelper/HeaderData.scala | 57 ++++++------- app/models/viewhelper/ProjectData.scala | 1 + app/ore/permission/PermissionPredicate.scala | 5 +- app/ore/project/factory/ProjectFactory.scala | 35 ++++---- app/views/users/admin/visibility.scala.html | 79 ++++++++---------- 13 files changed, 187 insertions(+), 232 deletions(-) diff --git a/app/controllers/ApiController.scala b/app/controllers/ApiController.scala index 3c49bcb60..222580854 100644 --- a/app/controllers/ApiController.scala +++ b/app/controllers/ApiController.scala @@ -9,6 +9,7 @@ import db.ModelService import db.impl.OrePostgresDriver.api._ import db.impl.ProjectApiKeyTable import util.instances.future._ +import _root_.util.EitherT import form.OreForms import javax.inject.Inject @@ -17,7 +18,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} @@ -181,60 +182,44 @@ final class ApiController @Inject()(api: OreRestfulApi, 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) - } 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)))) - } + + 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))) } - 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)) - } + catch { + case e: InvalidPluginFileException => + EitherT.leftT[Future, PendingVersion](BadRequest(error("upload", e.getMessage))) } } - - for { - user <- projectData.project.owner.user - owner <- user.toMaybeOrganization.semiFlatMap(_.owner.user).getOrElse(user) - result <- upload(owner) - } yield { - result - } } - } - 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) diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index 74ee20a84..6b0ae666c 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -518,10 +518,10 @@ final class Application @Inject()(data: DataHelper, } 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)) + val waitingProjects = projectChanges zip projectChangeRequests.flatten zip projectVisibilityChanges zip projectVisibilityChangers map { case (((a,b), c), d) => + (a,b,c,d.fold("Unknown")(_.name)) } Ok(views.users.admin.visibility(needsApproval, waitingProjects)) diff --git a/app/controllers/project/Channels.scala b/app/controllers/project/Channels.scala index 9205f7806..125d3410f 100755 --- a/app/controllers/project/Channels.scala +++ b/app/controllers/project/Channels.scala @@ -16,6 +16,9 @@ import views.html.projects.{channels => views} import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import util.EitherT +import util.syntax._ + /** * Controller for handling Channel related actions. */ @@ -106,32 +109,31 @@ 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(c => c.name.equals(channelName)).toRight(NotFound)) + .semiFlatMap { channel => + (channel.versions.nonEmpty, Future.sequence(channels.map(_.versions.nonEmpty)).map(l => l.count(_ == true))).parMapN { + (nonEmpty, channelCount) => (nonEmpty, channelCount, channel) } - - } - } - } + } + .filterOrElse( + { case (nonEmpty, channelCount, _) => nonEmpty && channelCount == 1}, + Redirect(self.showList(author, slug)).withError("error.channel.lastNonEmpty") + ) + .map(_._3) + .filterOrElse( + channel => { + val reviewedChannels = channels.filter(!_.isNonReviewed) + !channel.isNonReviewed && reviewedChannels.size <= 1 && reviewedChannels.contains(channel) + }, + Redirect(self.showList(author, slug)).withError("error.channel.lastReviewed") + ) + .map { channel => + this.projects.deleteChannel(channel) + Redirect(self.showList(author, slug)) + } + }.merge } } diff --git a/app/controllers/project/Pages.scala b/app/controllers/project/Pages.scala index 398435b42..7958a72bc 100755 --- a/app/controllers/project/Pages.scala +++ b/app/controllers/project/Pages.scala @@ -45,6 +45,7 @@ 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 diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index 0819e9359..c273f2be6 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -30,6 +30,7 @@ import scala.concurrent.{ExecutionContext, Future} import util.OptionT import util.instances.future._ +import util.syntax._ /** * Controller for handling Project related actions. @@ -116,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 zip pending.underlying.owner.user createOrga <- Future.sequence(orgas.map(orga => owner can CreateProject in orga)) } yield { val createdOrgas = orgas zip createOrga filter (_._2) map (_._1) @@ -152,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(_).value)) - 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)) } } diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index e3a0333c8..980896444 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -20,7 +20,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} @@ -219,28 +219,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 } /** @@ -424,11 +423,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, diff --git a/app/form/project/TChannelData.scala b/app/form/project/TChannelData.scala index dfc51c4d2..a48fcdb88 100644 --- a/app/form/project/TChannelData.scala +++ b/app/form/project/TChannelData.scala @@ -35,24 +35,11 @@ trait TChannelData { * @return Either the new channel or an error message */ def addTo(project: Project)(implicit ec: ExecutionContext): EitherT[Future, String, Channel] = { - EitherT( - 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(_)) - } - } - } - } - ) + EitherT.liftF(project.channels.all) + .filterOrElse(_.size >= config.projects.get[Int]("max-channels"), "A project may only have up to five channels.") + .filterOrElse(_.exists(_.name.equalsIgnoreCase(this.channelName)), "A channel with that name already exists.") + .filterOrElse(_.exists(_.color == this.color), "A channel with that color already exists.") + .semiFlatMap(_ => this.factory.createChannel(project, this.channelName, this.color, this.nonReviewed)) } /** diff --git a/app/models/project/Project.scala b/app/models/project/Project.scala index 893f92450..9542b95aa 100755 --- a/app/models/project/Project.scala +++ b/app/models/project/Project.scala @@ -558,10 +558,7 @@ case class Project(override val id: Option[Int] = None, def logger(implicit ec: ExecutionContext): Future[ProjectLog] = { val loggers = this.service.access[ProjectLog](classOf[ProjectLog]) - loggers.find(_.projectId === this.id.get).value.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/viewhelper/HeaderData.scala b/app/models/viewhelper/HeaderData.scala index 4e1a4c95b..207cdf4d1 100644 --- a/app/models/viewhelper/HeaderData.scala +++ b/app/models/viewhelper/HeaderData.scala @@ -13,9 +13,10 @@ import play.api.cache.AsyncCacheApi import play.api.mvc.Request import slick.jdbc.JdbcBackend import slick.lifted.TableQuery - import scala.concurrent.{ExecutionContext, Future} +import models.viewhelper.HeaderData.perms +import util.OptionT import util.syntax._ /** @@ -64,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) = { @@ -88,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 } } @@ -121,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) + } } } diff --git a/app/models/viewhelper/ProjectData.scala b/app/models/viewhelper/ProjectData.scala index 5859a0176..f74f1ff3d 100644 --- a/app/models/viewhelper/ProjectData.scala +++ b/app/models/viewhelper/ProjectData.scala @@ -76,6 +76,7 @@ object ProjectData { lastVisibilityChange, lastVisibilityChangeUser) + //TODO: Why Future here? Future.successful(data) } def of[A](project: Project)(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext): Future[ProjectData] = { diff --git a/app/ore/permission/PermissionPredicate.scala b/app/ore/permission/PermissionPredicate.scala index 1917afd8a..be40da387 100644 --- a/app/ore/permission/PermissionPredicate.scala +++ b/app/ore/permission/PermissionPredicate.scala @@ -53,8 +53,9 @@ case class PermissionPredicate(user: User, not: Boolean = false) { private def checkProjectPerm(project: Project): Future[Boolean] = { val orgTest = project.service .getModelBase(classOf[OrganizationBase]) - .get(project.ownerId).value - .flatMap(_.fold(Future.successful(false))(_.scope.test(user, p))) + .get(project.ownerId) + .fold(Future.successful(false))(_.scope.test(user, p)) + .flatten val projectTest = project.scope.test(user, p) diff --git a/app/ore/project/factory/ProjectFactory.scala b/app/ore/project/factory/ProjectFactory.scala index 9b1bff13c..d1aac0ee5 100755 --- a/app/ore/project/factory/ProjectFactory.scala +++ b/app/ore/project/factory/ProjectFactory.scala @@ -36,7 +36,7 @@ import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration.Duration import scala.util.Try -import util.OptionT +import util.{EitherT, OptionT} /** * Manages the project and version creation pipeline. @@ -110,26 +110,25 @@ trait ProjectFactory { def processSubsequentPluginUpload(uploadData: PluginUpload, owner: User, - project: Project)(implicit ec: ExecutionContext): 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 zip project.settings + 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) + } } - } + ) } } diff --git a/app/views/users/admin/visibility.scala.html b/app/views/users/admin/visibility.scala.html index 33200209d..dbbe5e6b9 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, 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,25 +99,23 @@

    Waiting Changes

    } @waitingProjects.map { case (project, lastChangeRequest, lastVisibilityChange, lastVisibilityChanger) => - @if(lastChangeRequest.isDefined) { -
  • -
  • +
    +
    + + @lastVisibilityChanger + requested changes on + + @project.ownerName/@project.slug + + +

    + Requests: + @lastChangeRequest.renderComment() +

    -
  • - } + + } From a065f1414d5b21815182eac0674bd0bc265d728a Mon Sep 17 00:00:00 2001 From: Unknown Date: Mon, 9 Apr 2018 03:22:46 +0200 Subject: [PATCH 03/18] More for comprehensions, less flatMap --- app/controllers/Application.scala | 12 ++-- app/controllers/Users.scala | 33 +++++------ app/controllers/project/Pages.scala | 22 ++++---- app/controllers/project/Projects.scala | 36 ++++++------ app/db/impl/access/OrganizationBase.scala | 27 ++++----- app/db/impl/access/ProjectBase.scala | 27 +++++---- app/db/impl/schema/ProjectSchema.scala | 9 ++- app/models/project/Project.scala | 24 ++++---- app/models/user/Organization.scala | 26 ++++----- app/models/viewhelper/OrganizationData.scala | 3 +- app/ore/project/factory/ProjectFactory.scala | 59 ++++++++------------ app/ore/user/MembershipDossier.scala | 23 +++----- 12 files changed, 135 insertions(+), 166 deletions(-) diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index 6b0ae666c..96383e8f3 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -342,13 +342,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 } diff --git a/app/controllers/Users.scala b/app/controllers/Users.scala index 6418914ff..b95c1f25e 100755 --- a/app/controllers/Users.scala +++ b/app/controllers/Users.scala @@ -25,6 +25,7 @@ import play.api.mvc._ import security.spauth.SingleSignOnConsumer import views.{html => views} import util.instances.future._ +import util.syntax._ import scala.concurrent.{ExecutionContext, Future} /** @@ -290,29 +291,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) + val iFilter: InviteFilter = inviteFilter + .flatMap(str => InviteFilters.values.find(_.name.equalsIgnoreCase(str))) + .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 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)) } } } diff --git a/app/controllers/project/Pages.scala b/app/controllers/project/Pages.scala index 7958a72bc..edd15f23f 100755 --- a/app/controllers/project/Pages.scala +++ b/app/controllers/project/Pages.scala @@ -101,19 +101,17 @@ 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))).subflatMap(_.id).getOrElse(-1).map((parts(1), _)) - } - p.flatMap { - case (name, parentId) => - data.project.pages.find(equalsIgnoreCase(_.slug, name)).getOrElseF(data.project.getOrCreatePage(name, parentId)) - } flatMap { p => - projects.queryProjectPages(data.project) map { pages => - 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)) + + 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 <- data.project.pages.find(equalsIgnoreCase(_.slug, name)).getOrElseF(data.project.getOrCreatePage(name, parentId)) + pages <- projects.queryProjectPages(data.project) + } 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)) } } diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index c273f2be6..d457eab5f 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -166,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 } } @@ -257,15 +256,16 @@ class Projects @Inject()(stats: StatTracker, Future.successful(BadRequest) else { // Do forum post and display errors to user if any - val poster = OptionT.fromOption[Future](formData.poster) - .flatMap(posterName => this.users.requestPermission(request.user, posterName, PostAsOrganization)) - .getOrElse(request.user) - 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 } } } diff --git a/app/db/impl/access/OrganizationBase.scala b/app/db/impl/access/OrganizationBase.scala index bdcceb1e3..0e62361f7 100644 --- a/app/db/impl/access/OrganizationBase.scala +++ b/app/db/impl/access/OrganizationBase.scala @@ -65,23 +65,18 @@ class OrganizationBase(override val service: ModelService, // 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 - .getOrElse(throw new IllegalStateException("User not created")) - .map { userOrg => - userOrg.pullForumData().flatMap(_.pullSpongeData()) - userOrg.setGlobalRoles(userOrg.globalRoles + RoleTypes.Organization) - userOrg - } - .flatMap { _ => - // Add the owner + 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...") @@ -96,10 +91,10 @@ class OrganizationBase(override val service: ModelService, } }) } - .map { _ => - Logger.info(" " + org) - org - } + } yield { + Logger.info(" " + org) + org + } } } diff --git a/app/db/impl/access/ProjectBase.scala b/app/db/impl/access/ProjectBase.scala index 33b4d1770..78c37bfd4 100644 --- a/app/db/impl/access/ProjectBase.scala +++ b/app/db/impl/access/ProjectBase.scala @@ -129,22 +129,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) + _ = 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) - } + // 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 } /** diff --git a/app/db/impl/schema/ProjectSchema.scala b/app/db/impl/schema/ProjectSchema.scala index 770a5f2aa..ce707bc08 100755 --- a/app/db/impl/schema/ProjectSchema.scala +++ b/app/db/impl/schema/ProjectSchema.scala @@ -25,11 +25,10 @@ class ProjectSchema(override val service: ModelService, implicit val users: User * @return Project authors */ def distinctAuthors(implicit ec: ExecutionContext): 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) + for { + userIds <- service.DB.db.run(this.baseQuery.map(_.userId).distinct.result) + inIds <- this.users.in(userIds.toSet) + } yield inIds.toSeq } /** diff --git a/app/models/project/Project.scala b/app/models/project/Project.scala index 9542b95aa..3564923ca 100755 --- a/app/models/project/Project.scala +++ b/app/models/project/Project.scala @@ -161,17 +161,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 } } 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/viewhelper/OrganizationData.scala b/app/models/viewhelper/OrganizationData.scala index 54e285a4b..3ae90082a 100644 --- a/app/models/viewhelper/OrganizationData.scala +++ b/app/models/viewhelper/OrganizationData.scala @@ -32,7 +32,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 diff --git a/app/ore/project/factory/ProjectFactory.scala b/app/ore/project/factory/ProjectFactory.scala index d1aac0ee5..a0a440ad3 100755 --- a/app/ore/project/factory/ProjectFactory.scala +++ b/app/ore/project/factory/ProjectFactory.scala @@ -312,15 +312,11 @@ trait ProjectFactory { 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 } /** @@ -334,36 +330,27 @@ trait ProjectFactory { val pendingVersion = pending.underlying - val channel = for { + 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 + _ = 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 { diff --git a/app/ore/user/MembershipDossier.scala b/app/ore/user/MembershipDossier.scala index 0cb9ecb08..df624e154 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) + _ <- this.roleAccess.add(role) + } yield () } /** @@ -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 () } /** From 0480616c5cc28aed42cb6a6f2f7c41abc7a48f8d Mon Sep 17 00:00:00 2001 From: Unknown Date: Mon, 9 Apr 2018 03:43:36 +0200 Subject: [PATCH 04/18] Merge and simplify some for comprehensions --- app/controllers/Application.scala | 17 +++--- app/db/impl/access/ProjectBase.scala | 62 +++++++------------- app/ore/project/factory/ProjectFactory.scala | 21 +++---- app/ore/rest/OreRestfulApi.scala | 5 +- 4 files changed, 39 insertions(+), 66 deletions(-) diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index 96383e8f3..f11827b80 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -382,15 +382,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)) } } diff --git a/app/db/impl/access/ProjectBase.scala b/app/db/impl/access/ProjectBase.scala index 78c37bfd4..b039a7eb5 100644 --- a/app/db/impl/access/ProjectBase.scala +++ b/app/db/impl/access/ProjectBase.scala @@ -152,30 +152,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.sequence(channels.map(_.versions.nonEmpty)).map(_.count(_ == true)) + _ = 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 +180,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 _ <- { diff --git a/app/ore/project/factory/ProjectFactory.scala b/app/ore/project/factory/ProjectFactory.scala index a0a440ad3..3386a9ee5 100755 --- a/app/ore/project/factory/ProjectFactory.scala +++ b/app/ore/project/factory/ProjectFactory.scala @@ -258,18 +258,13 @@ trait ProjectFactory { def createProject(pending: PendingProject)(implicit ec: ExecutionContext): Future[Project] = { val project = pending.underlying - val checks = for { + 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 + _ = 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) @@ -376,7 +371,7 @@ trait ProjectFactory { OptionT.fromOption[Future](dependenciesMatchingName.headOption) .filter(dep => dependencyVersionRegex.pattern.matcher(dep.version).matches()) .semiFlatMap { dep => - val tagToAdd = for { + for { tagsWithVersion <- service.access(classOf[ProjectTag]) .filter(t => t.name === tagName && t.data === dep.version) tag <- { @@ -397,9 +392,7 @@ trait ProjectFactory { Future.successful(tag) } } - } yield tag - - tagToAdd.map { tag => + } yield { newVersion.addTag(tag) tag } diff --git a/app/ore/rest/OreRestfulApi.scala b/app/ore/rest/OreRestfulApi.scala index a427f26ec..c950e79df 100755 --- a/app/ore/rest/OreRestfulApi.scala +++ b/app/ore/rest/OreRestfulApi.scala @@ -78,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]) = { From 0b27d0e33406992d87ffb69696560109edd67c99 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 18 Apr 2018 11:51:36 +0200 Subject: [PATCH 05/18] Fix reviews --- app/controllers/Reviews.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/Reviews.scala b/app/controllers/Reviews.scala index 372d47d92..cb76368dc 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -188,7 +188,7 @@ final class Reviews @Inject()(data: DataHelper, val closeOldReview = version.mostRecentUnfinishedReview.semiFlatMap { oldreview => for { (_, _) <- oldreview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim, System.currentTimeMillis(), "takeover")) zip - oldreview.setEnded(Timestamp.from(Instant.now())) + oldreview.setEnded(Some(Timestamp.from(Instant.now()))) } yield {} }.getOrElse(()) From b0f8f7f76c13d59fc45c155cb541a3c016d3b43c Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 18 Apr 2018 12:19:51 +0200 Subject: [PATCH 06/18] Parallelize for comprehensions --- app/controllers/Application.scala | 58 +++++++++++++++----------- app/controllers/Reviews.scala | 2 +- app/controllers/Users.scala | 12 +++--- app/controllers/project/Pages.scala | 7 +++- app/controllers/project/Projects.scala | 6 +-- app/controllers/project/Versions.scala | 8 ++-- 6 files changed, 52 insertions(+), 41 deletions(-) diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index f11827b80..52351fc03 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -217,8 +217,10 @@ 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)) + futUsers = Future.sequence(flags.map(_.user)) + futProjects = Future.sequence(flags.map(_.project)) + users <- futUsers + projects <- futProjects perms <- Future.sequence(projects.map { project => val perms = VisibilityTypes.values.map(_.permission).map { perm => request.user can perm in project map (value => (perm, value)) @@ -250,16 +252,15 @@ final class Application @Inject()(data: DataHelper, } 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)) } } @@ -399,13 +400,19 @@ final class Application @Inject()(data: DataHelper, def userAdmin(user: String) = UserAdminAction.async { implicit request => this.users.withName(user).semiFlatMap { u => for { - userData <- getUserData(request, user).value 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).value else Future.successful(None) - orgaData <- OrganizationData.of(orga).value - scopedOrgaData <- ScopedOrganizationData.of(Some(request.user), orga).value + (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)) @@ -484,16 +491,17 @@ 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) - lastChangeRequests <- Future.sequence(projectApprovals.map(_.lastChangeRequest.value)) - lastVisibilityChanges <- Future.sequence(projectApprovals.map(_.lastVisibilityChange.value)) - - projectChanges <- projectSchema.collect(ModelFilter[Project](_.visibility === VisibilityTypes.NeedsChanges).fn, ProjectSortingStrategies.Default, -1, 0) - projectVisibilityChanges <- Future.sequence(projectChanges.map(_.lastVisibilityChange.value)) + (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 - ) <- ( + (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)) diff --git a/app/controllers/Reviews.scala b/app/controllers/Reviews.scala index cb76368dc..9c311e61f 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -115,7 +115,7 @@ final class Reviews @Inject()(data: DataHelper, withVersionAsync(versionString) { version => version.mostRecentUnfinishedReview.semiFlatMap { review => for { - (_, _) <- review.setEnded(Timestamp.from(Instant.now())) zip + (_, _) <- review.setEnded(Some(Timestamp.from(Instant.now()))) zip // send notification that review happened sendReviewNotification(project, version, request.user) } yield { diff --git a/app/controllers/Users.scala b/app/controllers/Users.scala index b95c1f25e..63d2b507a 100755 --- a/app/controllers/Users.scala +++ b/app/controllers/Users.scala @@ -137,13 +137,15 @@ class Users @Inject()(fakeUser: FakeUser, 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).value starred <- user.starred() - starredRv <- Future.sequence(starred.map(_.recommendedVersion)) orga <- getOrga(request, username).value - orgaData <- OrganizationData.of(orga).value - scopedOrgaData <- ScopedOrganizationData.of(request.currentUser, orga).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) diff --git a/app/controllers/project/Pages.scala b/app/controllers/project/Pages.scala index edd15f23f..2715b7234 100755 --- a/app/controllers/project/Pages.scala +++ b/app/controllers/project/Pages.scala @@ -16,6 +16,7 @@ import security.spauth.SingleSignOnConsumer import util.StringUtils._ import views.html.projects.{pages => views} import util.instances.future._ +import util.syntax._ import scala.concurrent.{ExecutionContext, Future} /** @@ -106,8 +107,10 @@ class Pages @Inject()(forms: OreForms, (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 <- data.project.pages.find(equalsIgnoreCase(_.slug, name)).getOrElseF(data.project.getOrCreatePage(name, parentId)) - pages <- projects.queryProjectPages(data.project) + (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 } diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index d457eab5f..f2aa6ffd7 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -595,10 +595,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.value)) - 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)) diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index 980896444..fb0dd4170 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -533,10 +533,10 @@ 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(data.project, version, nonReviewed, dlType, token)) + .withCookies(warn.cookie) + } } } } From 2f34702e97d98cf5868a8ca28f7272a1acb6eb10 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 18 Apr 2018 12:32:46 +0200 Subject: [PATCH 07/18] Replace zip with parTupled and parMapN --- app/controllers/Reviews.scala | 28 +++++++++++--------- app/controllers/project/Projects.scala | 2 +- app/controllers/sugar/Actions.scala | 13 +++------ app/ore/project/factory/ProjectFactory.scala | 11 +++++--- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/app/controllers/Reviews.scala b/app/controllers/Reviews.scala index 9c311e61f..94b17e1f6 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -26,6 +26,7 @@ import slick.lifted.{Rep, TableQuery} import util.DataHelper import views.{html => views} import util.instances.future._ +import util.syntax._ import scala.concurrent.{ExecutionContext, Future} /** @@ -114,11 +115,11 @@ final class Reviews @Inject()(data: DataHelper, withProjectAsync(author, slug) { implicit project => withVersionAsync(versionString) { version => version.mostRecentUnfinishedReview.semiFlatMap { review => - for { - (_, _) <- review.setEnded(Some(Timestamp.from(Instant.now()))) zip - // send notification that review happened - sendReviewNotification(project, version, request.user) - } yield { + ( + review.setEnded(Some(Timestamp.from(Instant.now()))), + // send notification that review happened + sendReviewNotification(project, version, request.user) + ).parMapN { (_, _) => Redirect(routes.Reviews.showReviews(author, slug, versionString)) } }.getOrElse(NotFound) @@ -186,17 +187,18 @@ final class Reviews @Inject()(data: DataHelper, withVersionAsync(versionString) { version => // Close old review val closeOldReview = version.mostRecentUnfinishedReview.semiFlatMap { oldreview => - for { - (_, _) <- oldreview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim, System.currentTimeMillis(), "takeover")) zip - oldreview.setEnded(Some(Timestamp.from(Instant.now()))) - } yield {} + ( + oldreview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.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 { + ( + closeOldReview, + this.service.insert(Review(Some(1), Some(Timestamp.from(Instant.now())), version.id.get, request.user.id.get, None, "")) + ).parMapN { (_, _) => Redirect(routes.Reviews.showReviews(author, slug, versionString)) } } diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index f2aa6ffd7..779839739 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -117,7 +117,7 @@ class Projects @Inject()(stats: StatTracker, Future.successful(Redirect(self.showCreator())) case Some(pending) => for { - (orgas, owner) <- request.user.organizations.all zip 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) diff --git a/app/controllers/sugar/Actions.scala b/app/controllers/sugar/Actions.scala index b40fd87f4..fb4db472f 100644 --- a/app/controllers/sugar/Actions.scala +++ b/app/controllers/sugar/Actions.scala @@ -23,6 +23,7 @@ import scala.language.higherKinds import util.{FutureUtils, OptionT} import util.instances.future._ +import util.syntax._ /** * A set of actions used by Ore. @@ -243,11 +244,7 @@ trait Actions extends Calls with ActionHelpers { 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) : OptionT[Future, Project] = { @@ -338,10 +335,8 @@ 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) } } diff --git a/app/ore/project/factory/ProjectFactory.scala b/app/ore/project/factory/ProjectFactory.scala index 3386a9ee5..03de3a26a 100755 --- a/app/ore/project/factory/ProjectFactory.scala +++ b/app/ore/project/factory/ProjectFactory.scala @@ -31,6 +31,7 @@ import play.api.i18n.{Lang, MessagesApi} import security.pgp.PGPVerifier import util.StringUtils._ import util.instances.future._ +import util.syntax._ import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration.Duration @@ -117,7 +118,7 @@ trait ProjectFactory { else { EitherT( for { - (channels, settings) <- project.channels.all zip project.settings + (channels, settings) <- (project.channels.all, project.settings).parTupled version = this.startVersion(plugin, project, settings, channels.head.name) modelExists <- version.underlying.exists } yield { @@ -259,8 +260,10 @@ trait ProjectFactory { val project = pending.underlying for { - (exists, available) <- this.projects.exists(project) zip - this.projects.isNamespaceAvailable(project.ownerName, project.slug) + (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", "") @@ -327,7 +330,7 @@ trait ProjectFactory { for { // Create channel if not exists - (channel, exists) <- getOrCreateChannel(pending, project) zip pendingVersion.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 <- { From 6b3be6701b0895eea8c7168a6db14098af6cd7d4 Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 19 Apr 2018 09:44:39 +0200 Subject: [PATCH 08/18] Replace withX and withXAsync with methods that return EitherT --- app/controllers/OreBaseController.scala | 45 +++--- app/controllers/Reviews.scala | 181 ++++++++++++------------ app/controllers/project/Projects.scala | 12 +- app/controllers/project/Versions.scala | 147 ++++++++----------- 4 files changed, 185 insertions(+), 200 deletions(-) diff --git a/app/controllers/OreBaseController.scala b/app/controllers/OreBaseController.scala index 9bb3da9ff..c8ae0f8fc 100755 --- a/app/controllers/OreBaseController.scala +++ b/app/controllers/OreBaseController.scala @@ -17,6 +17,8 @@ import util.StringUtils._ import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import util.EitherT + /** * Represents a Secured base Controller for this application. */ @@ -45,38 +47,43 @@ abstract class OreBaseController(implicit val env: OreEnv, 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(fn).getOrElse(notFound) - - def withProjectAsync(author: String, slug: String)(fn: Project => Future[Result])(implicit request: OreRequest[_]): Future[Result] - = this.projects.withSlug(author, slug).semiFlatMap(fn).getOrElse(NotFound) + def withProject(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(fn).getOrElse(notFound) + def withVersion(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)).semiFlatMap(fn).getOrElse(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 withProjectVersion(author: String, slug: String, versionString: String)(implicit request: OreRequest[_]): EitherT[Future, Result, Version] + = for { + project <- withProject(author, slug) + version <- withVersion(project, versionString) + } yield version def OreAction = Action andThen oreAction diff --git a/app/controllers/Reviews.scala b/app/controllers/Reviews.scala index 94b17e1f6..540403228 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -23,12 +23,14 @@ import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer import slick.lifted.{Rep, TableQuery} -import util.DataHelper +import util.{DataHelper, EitherT} import views.{html => views} import util.instances.future._ import util.syntax._ import scala.concurrent.{ExecutionContext, Future} +import play.api.mvc.Result + /** * Controller for handling Review related actions. */ @@ -46,85 +48,86 @@ final class Reviews @Inject()(data: DataHelper, 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(_.name).value.map { u => - (r, u) - } - }) 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 <- withVersion(p, versionString) + reviews <- EitherT.right[Result](version.mostRecentReviews) + rv <- EitherT.right[Result]( + Future.traverse(reviews) { r => + r.userBase.get(r.userId).map(_.name).value.map { u => + (r, u) } } - } + ) + } yield { + val unfinished = reviews.filter(r => r.createdAt.isDefined && r.endedAt.isEmpty).sorted(Review.ordering2).headOption + 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)) - } - } + withProjectVersion(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 <- withProjectVersion(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 { review => - review.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim, System.currentTimeMillis(), "stop")) - review.setEnded(Timestamp.from(Instant.now())) - Redirect(routes.Reviews.showReviews(author, slug, versionString)) - }.getOrElse(NotFound) - } + val res = for { + version <- withProjectVersion(author, slug, versionString) + review <- version.mostRecentUnfinishedReview.toRight(notFound) + } yield { + 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)) } + + 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.semiFlatMap { review => - ( - review.setEnded(Some(Timestamp.from(Instant.now()))), - // send notification that review happened - sendReviewNotification(project, version, request.user) - ).parMapN { (_, _) => - Redirect(routes.Reviews.showReviews(author, slug, versionString)) - } - }.getOrElse(NotFound) + val res = for { + project <- withProject(author, slug) + version <- withVersion(project, versionString) + review <- version.mostRecentUnfinishedReview.toRight(notFound) + } yield { + ( + review.setEnded(Some(Timestamp.from(Instant.now()))), + // send notification that review happened + sendReviewNotification(project, version, request.user) + ).parMapN { (_, _) => + Redirect(routes.Reviews.showReviews(author, slug, versionString)) } } + + res.merge } } @@ -183,55 +186,59 @@ 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 => - // Close old review - val closeOldReview = version.mostRecentUnfinishedReview.semiFlatMap { oldreview => - ( - oldreview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.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 + val res = for { + version <- withProjectVersion(author, slug, versionString) + } yield { + // Close old review + val closeOldReview = version.mostRecentUnfinishedReview.semiFlatMap { oldreview => ( - closeOldReview, + oldreview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.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 { (_, _) => - Redirect(routes.Reviews.showReviews(author, slug, versionString)) - } + ).parMapN { (_, _, _) => () } + }.getOrElse(()) + + // Then make new one + ( + closeOldReview, + this.service.insert(Review(Some(1), Some(Timestamp.from(Instant.now())), version.id.get, request.user.id.get, None, "")) + ).parMapN { (_, _) => + Redirect(routes.Reviews.showReviews(author, slug, versionString)) } } + + res.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 { review => - review.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim)) - Ok("Review" + review) - }.getOrElse(NotFound) - } + val res = for { + version <- withProjectVersion(author, slug, versionString) + review <- version.reviewById(reviewId).toRight(notFound) + } yield { + review.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.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.semiFlatMap { recentReview => - users.current.semiFlatMap { currentUser => - if (recentReview.userId == currentUser.userId) { - recentReview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim)) - } else Future.successful(0) - }.getOrElse(1).map( _ => Ok("Review")) - }.getOrElse(Ok("Review")) + val ret = for { + version <- withProjectVersion(author, slug, versionString) + recentReview <- version.mostRecentUnfinishedReview.toRight(Ok("Review")) + currentUser <- users.current.toRight(Ok("Review")) + _ <- { + if (recentReview.userId == currentUser.userId) { + EitherT.right[Result](recentReview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim))) + } else EitherT.rightT[Future, Result](0) } - } + } yield Ok("Review") + + ret.merge } } } diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index 779839739..d92f2890f 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -613,10 +613,10 @@ class Projects @Inject()(stats: StatTracker, */ def delete(author: String, slug: String) = { (Authenticated andThen PermissionAction[AuthRequest](HardRemoveProject)).async { implicit request => - withProject(author, slug) { project => + withProject(author, slug).map { project => this.projects.delete(project) Redirect(ShowHome).withSuccess(this.messagesApi("project.deleted", project.name)) - } + }.merge } } @@ -644,9 +644,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 => + withProject(author, slug).map { project => Ok(views.admin.flags(request.data)) - } + }.merge } } @@ -668,10 +668,10 @@ class Projects @Inject()(stats: StatTracker, def addMessage(author: String, slug: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => - withProject(author, slug) { project => + withProject(author, slug).map { project => project.addNote(Note(this.forms.NoteDescription.bindFromRequest.get.trim, request.user.userId)) Ok("Review") - } + }.merge } } } \ No newline at end of file diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index fb0dd4170..749cf8117 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -72,19 +72,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 <- withVersion(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 } /** @@ -98,12 +93,10 @@ 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 => + withVersion(request.data.project, versionString).map { version => version.setDescription(this.forms.VersionDescription.bindFromRequest.get.trim) Redirect(self.show(author, slug, versionString)) - } + }.merge } } @@ -118,12 +111,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) + withVersion(request.data.project, versionString).map { version => + request.data.project.setRecommendedVersion(version) Redirect(self.show(author, slug, versionString)) - } + }.merge } } @@ -138,15 +129,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 => + withVersion(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 } } @@ -386,13 +375,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 + withVersion(p, versionString).map { version => this.projects.deleteVersion(version) Redirect(self.showList(author, slug, None, None)) - } + }.merge } } @@ -406,12 +394,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) - } + withVersion(project, versionString).semiFlatMap { version => + sendVersion(project, version, token) + }.merge } } @@ -484,13 +471,12 @@ 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 + + withVersion(project, target) + .filterOrElse(_.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 @@ -510,7 +496,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) ) } @@ -534,43 +520,36 @@ class Versions @Inject()(stats: StatTracker, .withHeaders("Content-Disposition" -> "inline; filename=\"README.txt\"")) } else { (warning, version.channel.map(_.isNonReviewed)).parMapN { (warn, nonReviewed) => - MultipleChoices(views.unsafeDownload(data.project, version, nonReviewed, dlType, token)) + 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).value.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) - } + withVersion(request.data.project, target) + .filterOrElse(_.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 } } @@ -638,11 +617,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)) + withVersion(project, versionString).semiFlatMap(version => sendJar(project, version, token)).merge } } @@ -711,21 +688,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).value.flatMap { _ => - sendJar(data.project, version, token, api = true) + withVersion(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 } } @@ -755,10 +728,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)) + withVersion(project, versionString).map(sendSignatureFile(_, project)).merge } } @@ -770,10 +742,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)) + withVersion(project, versionString).map(sendSignatureFile(_, project)).merge } /** From 8e8c3ddff041f45c25afe26adeefe482993cb18b Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 19 Apr 2018 11:09:43 +0200 Subject: [PATCH 09/18] Let's roll our own typeclasses instead of using the play ones --- app/controllers/ApiController.scala | 2 +- app/controllers/Application.scala | 3 +- app/controllers/OreBaseController.scala | 12 +++--- app/controllers/Reviews.scala | 23 ++++++----- app/controllers/project/Channels.scala | 2 +- app/controllers/project/Projects.scala | 8 ++-- app/controllers/project/Versions.scala | 27 ++++++------ app/controllers/sugar/Actions.scala | 3 +- app/db/ModelSchema.scala | 2 +- app/db/ModelService.scala | 3 +- app/db/access/ModelAccess.scala | 3 +- app/db/impl/access/OrganizationBase.scala | 4 +- app/db/impl/access/ProjectBase.scala | 4 +- app/db/impl/access/UserBase.scala | 2 +- app/db/impl/schema/PageSchema.scala | 2 +- app/db/impl/schema/ProjectSchema.scala | 2 +- app/db/impl/schema/StatSchema.scala | 2 +- app/db/impl/schema/UserSchema.scala | 2 +- app/form/project/TChannelData.scala | 2 +- app/models/admin/VisibilityChange.scala | 2 +- app/models/project/DownloadWarning.scala | 2 +- app/models/project/Page.scala | 3 +- app/models/project/Project.scala | 4 +- app/models/project/ProjectSettings.scala | 3 +- app/models/project/Version.scala | 4 +- app/models/statistic/StatEntry.scala | 3 +- app/models/user/User.scala | 2 +- app/models/viewhelper/HeaderData.scala | 2 +- app/models/viewhelper/OrganizationData.scala | 2 +- .../viewhelper/ScopedOrganizationData.scala | 2 +- app/ore/project/Dependency.scala | 2 +- app/ore/project/factory/ProjectFactory.scala | 2 +- app/ore/rest/OreRestfulApi.scala | 3 +- .../spauth/SingleSignOnConsumer.scala | 2 +- app/security/spauth/SpongeAuthApi.scala | 2 +- app/util/Monad.scala | 14 ------- app/util/functional/Applicative.scala | 22 ++++++++++ app/util/{ => functional}/EitherT.scala | 41 +++++++++---------- app/util/functional/Functor.scala | 17 ++++++++ app/util/functional/Monad.scala | 12 ++++++ app/util/{ => functional}/OptionT.scala | 33 +++++++-------- app/util/instances/FutureInstances.scala | 7 ++-- app/util/syntax/package.scala | 37 ++++++++++++++++- 43 files changed, 205 insertions(+), 126 deletions(-) delete mode 100644 app/util/Monad.scala create mode 100644 app/util/functional/Applicative.scala rename app/util/{ => functional}/EitherT.scala (86%) create mode 100644 app/util/functional/Functor.scala create mode 100644 app/util/functional/Monad.scala rename app/util/{ => functional}/OptionT.scala (80%) diff --git a/app/controllers/ApiController.scala b/app/controllers/ApiController.scala index 222580854..1b7afb227 100644 --- a/app/controllers/ApiController.scala +++ b/app/controllers/ApiController.scala @@ -9,7 +9,6 @@ import db.ModelService import db.impl.OrePostgresDriver.api._ import db.impl.ProjectApiKeyTable import util.instances.future._ -import _root_.util.EitherT import form.OreForms import javax.inject.Inject @@ -26,6 +25,7 @@ import ore.{OreConfig, OreEnv} import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import util.StatusZ +import util.functional.EitherT import play.api.libs.json._ import play.api.mvc._ import security.CryptoUtils diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index 52351fc03..532a79587 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -28,10 +28,11 @@ import play.api.Logger import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer -import util.{DataHelper, OptionT} +import util.DataHelper import views.{html => views} import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT import util.syntax._ import util.instances.future._ diff --git a/app/controllers/OreBaseController.scala b/app/controllers/OreBaseController.scala index c8ae0f8fc..144c889b6 100755 --- a/app/controllers/OreBaseController.scala +++ b/app/controllers/OreBaseController.scala @@ -17,7 +17,7 @@ import util.StringUtils._ import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} -import util.EitherT +import util.functional.EitherT /** * Represents a Secured base Controller for this application. @@ -54,7 +54,7 @@ abstract class OreBaseController(implicit val env: OreEnv, * @param request Incoming request * @return NotFound or project */ - def withProject(author: String, slug: String)(implicit request: OreRequest[_]): EitherT[Future, Result, Project] + def getProject(author: String, slug: String)(implicit request: OreRequest[_]): EitherT[Future, Result, Project] = this.projects.withSlug(author, slug).toRight(notFound) /** @@ -65,7 +65,7 @@ abstract class OreBaseController(implicit val env: OreEnv, * @param request Incoming request * @return NotFound or function result */ - def withVersion(project: Project, versionString: String) + def getVersion(project: Project, versionString: String) (implicit request: OreRequest[_]): EitherT[Future, Result, Version] = project.versions.find(equalsIgnoreCase[VersionTable](_.versionString, versionString)).toRight(notFound) @@ -79,10 +79,10 @@ abstract class OreBaseController(implicit val env: OreEnv, * @param request Incoming request * @return NotFound or project */ - def withProjectVersion(author: String, slug: String, versionString: String)(implicit request: OreRequest[_]): EitherT[Future, Result, Version] + def getProjectVersion(author: String, slug: String, versionString: String)(implicit request: OreRequest[_]): EitherT[Future, Result, Version] = for { - project <- withProject(author, slug) - version <- withVersion(project, versionString) + project <- getProject(author, slug) + version <- getVersion(project, versionString) } yield version def OreAction = Action andThen oreAction diff --git a/app/controllers/Reviews.scala b/app/controllers/Reviews.scala index 540403228..219c281ea 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -23,13 +23,14 @@ import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer import slick.lifted.{Rep, TableQuery} -import util.{DataHelper, EitherT} +import util.DataHelper import views.{html => views} import util.instances.future._ import util.syntax._ import scala.concurrent.{ExecutionContext, Future} import play.api.mvc.Result +import util.functional.EitherT /** * Controller for handling Review related actions. @@ -45,13 +46,13 @@ final class Reviews @Inject()(data: DataHelper, 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 p = request.data.project val res = for { - version <- withVersion(p, versionString) + version <- getVersion(p, versionString) reviews <- EitherT.right[Result](version.mostRecentReviews) rv <- EitherT.right[Result]( Future.traverse(reviews) { r => @@ -70,7 +71,7 @@ final class Reviews @Inject()(data: DataHelper, def createReview(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => - withProjectVersion(author, slug, versionString).map { version => + 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)) @@ -81,7 +82,7 @@ final class Reviews @Inject()(data: DataHelper, def reopenReview(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => val res = for { - version <- withProjectVersion(author, slug, versionString) + version <- getProjectVersion(author, slug, versionString) review <- EitherT.fromOptionF(version.mostRecentReviews.map(_.headOption), notFound) } yield { version.setReviewed(false) @@ -99,7 +100,7 @@ final class Reviews @Inject()(data: DataHelper, def stopReview(author: String, slug: String, versionString: String) = { Authenticated andThen PermissionAction[AuthRequest](ReviewProjects) async { implicit request => val res = for { - version <- withProjectVersion(author, slug, versionString) + version <- getProjectVersion(author, slug, versionString) review <- version.mostRecentUnfinishedReview.toRight(notFound) } yield { review.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim, System.currentTimeMillis(), "stop")) @@ -114,8 +115,8 @@ final class Reviews @Inject()(data: DataHelper, def approveReview(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => val res = for { - project <- withProject(author, slug) - version <- withVersion(project, versionString) + project <- getProject(author, slug) + version <- getVersion(project, versionString) review <- version.mostRecentUnfinishedReview.toRight(notFound) } yield { ( @@ -187,7 +188,7 @@ final class Reviews @Inject()(data: DataHelper, def takeoverReview(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => val res = for { - version <- withProjectVersion(author, slug, versionString) + version <- getProjectVersion(author, slug, versionString) } yield { // Close old review val closeOldReview = version.mostRecentUnfinishedReview.semiFlatMap { oldreview => @@ -214,7 +215,7 @@ final class Reviews @Inject()(data: DataHelper, def editReview(author: String, slug: String, versionString: String, reviewId: Int) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => val res = for { - version <- withProjectVersion(author, slug, versionString) + version <- getProjectVersion(author, slug, versionString) review <- version.reviewById(reviewId).toRight(notFound) } yield { review.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim)) @@ -228,7 +229,7 @@ final class Reviews @Inject()(data: DataHelper, def addMessage(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => val ret = for { - version <- withProjectVersion(author, slug, versionString) + version <- getProjectVersion(author, slug, versionString) recentReview <- version.mostRecentUnfinishedReview.toRight(Ok("Review")) currentUser <- users.current.toRight(Ok("Review")) _ <- { diff --git a/app/controllers/project/Channels.scala b/app/controllers/project/Channels.scala index 125d3410f..aa64d63da 100755 --- a/app/controllers/project/Channels.scala +++ b/app/controllers/project/Channels.scala @@ -16,7 +16,7 @@ import views.html.projects.{channels => views} import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} -import util.EitherT +import util.functional.EitherT import util.syntax._ /** diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index d92f2890f..cfbe8839d 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -28,7 +28,7 @@ import db.impl.OrePostgresDriver.api._ import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContext, Future} -import util.OptionT +import util.functional.OptionT import util.instances.future._ import util.syntax._ @@ -613,7 +613,7 @@ class Projects @Inject()(stats: StatTracker, */ def delete(author: String, slug: String) = { (Authenticated andThen PermissionAction[AuthRequest](HardRemoveProject)).async { implicit request => - withProject(author, slug).map { project => + getProject(author, slug).map { project => this.projects.delete(project) Redirect(ShowHome).withSuccess(this.messagesApi("project.deleted", project.name)) }.merge @@ -644,7 +644,7 @@ 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).map { project => + getProject(author, slug).map { project => Ok(views.admin.flags(request.data)) }.merge } @@ -668,7 +668,7 @@ class Projects @Inject()(stats: StatTracker, def addMessage(author: String, slug: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => - withProject(author, slug).map { project => + getProject(author, slug).map { project => project.addNote(Note(this.forms.NoteDescription.bindFromRequest.get.trim, request.user.userId)) Ok("Review") }.merge diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index 749cf8117..7c1c8785d 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -37,7 +37,7 @@ import util.syntax._ import views.html.projects.{versions => views} import scala.concurrent.{ExecutionContext, Future} -import util.{EitherT, OptionT} +import util.functional.{EitherT, OptionT} import util.instances.future._ /** @@ -74,7 +74,7 @@ class Versions @Inject()(stats: StatTracker, def show(author: String, slug: String, versionString: String) = ProjectAction(author, slug) async { request => implicit val r = request.request val res = for { - version <- withVersion(request.data.project, versionString) + 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 @@ -93,7 +93,7 @@ class Versions @Inject()(stats: StatTracker, def saveDescription(author: String, slug: String, versionString: String) = { VersionEditAction(author, slug).async { request => implicit val r = request.request - withVersion(request.data.project, versionString).map { version => + getVersion(request.data.project, versionString).map { version => version.setDescription(this.forms.VersionDescription.bindFromRequest.get.trim) Redirect(self.show(author, slug, versionString)) }.merge @@ -111,7 +111,7 @@ class Versions @Inject()(stats: StatTracker, def setRecommended(author: String, slug: String, versionString: String) = { VersionEditAction(author, slug).async { implicit request => implicit val r = request.request - withVersion(request.data.project, versionString).map { version => + getVersion(request.data.project, versionString).map { version => request.data.project.setRecommendedVersion(version) Redirect(self.show(author, slug, versionString)) }.merge @@ -130,7 +130,7 @@ class Versions @Inject()(stats: StatTracker, (AuthedProjectAction(author, slug, requireUnlock = true) andThen ProjectPermissionAction(ReviewProjects)).async { implicit request => implicit val r = request.request - withVersion(request.data.project, versionString).map { version => + getVersion(request.data.project, versionString).map { version => version.setReviewed(reviewed = true) version.setReviewer(request.user) version.setApprovedAt(this.service.theTime) @@ -377,7 +377,7 @@ class Versions @Inject()(stats: StatTracker, VersionEditAction(author, slug).async { implicit request => implicit val r = request.request implicit val p = request.data.project - withVersion(p, versionString).map { version => + getVersion(p, versionString).map { version => this.projects.deleteVersion(version) Redirect(self.showList(author, slug, None, None)) }.merge @@ -396,7 +396,7 @@ class Versions @Inject()(stats: StatTracker, ProjectAction(author, slug).async { implicit request => val project = request.data.project implicit val r = request.request - withVersion(project, versionString).semiFlatMap { version => + getVersion(project, versionString).semiFlatMap { version => sendVersion(project, version, token) }.merge } @@ -473,8 +473,7 @@ class Versions @Inject()(stats: StatTracker, val dlType = downloadType.flatMap(i => DownloadTypes.values.find(_.id == i)).getOrElse(DownloadTypes.UploadedFile) implicit val r = request.request val project = request.data.project - - withVersion(project, target) + getVersion(project, target) .filterOrElse(_.isReviewed, Redirect(ShowProject(author, slug))) .semiFlatMap { version => // generate a unique "warning" object to ensure the user has landed @@ -532,7 +531,7 @@ class Versions @Inject()(stats: StatTracker, def confirmDownload(author: String, slug: String, target: String, downloadType: Option[Int], token: String) = { ProjectAction(author, slug) async { request => implicit val r: OreRequest[_] = request.request - withVersion(request.data.project, target) + getVersion(request.data.project, target) .filterOrElse(_.isReviewed, Redirect(ShowProject(author, slug))) .flatMap(version => confirmDownload0(version.id.get, downloadType, token).toRight(Redirect(ShowProject(author, slug)))) .map { dl => @@ -619,7 +618,7 @@ class Versions @Inject()(stats: StatTracker, ProjectAction(author, slug).async { implicit request => val project = request.data.project implicit val r = request.request - withVersion(project, versionString).semiFlatMap(version => sendJar(project, version, token)).merge + getVersion(project, versionString).semiFlatMap(version => sendJar(project, version, token)).merge } } @@ -692,7 +691,7 @@ class Versions @Inject()(stats: StatTracker, ProjectAction(pluginId).async { implicit request => val project = request.data.project implicit val r = request.request - withVersion(project, versionString).semiFlatMap { version => + 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) @@ -730,7 +729,7 @@ class Versions @Inject()(stats: StatTracker, ProjectAction(author, slug).async { implicit request => val project = request.data.project implicit val r = request.request - withVersion(project, versionString).map(sendSignatureFile(_, project)).merge + getVersion(project, versionString).map(sendSignatureFile(_, project)).merge } } @@ -744,7 +743,7 @@ class Versions @Inject()(stats: StatTracker, def downloadSignatureById(pluginId: String, versionString: String) = ProjectAction(pluginId).async { implicit request => val project = request.data.project implicit val r = request.request - withVersion(project, versionString).map(sendSignatureFile(_, project)).merge + getVersion(project, versionString).map(sendSignatureFile(_, project)).merge } /** diff --git a/app/controllers/sugar/Actions.scala b/app/controllers/sugar/Actions.scala index fb4db472f..0bd19755a 100644 --- a/app/controllers/sugar/Actions.scala +++ b/app/controllers/sugar/Actions.scala @@ -21,7 +21,8 @@ import slick.jdbc.JdbcBackend import scala.concurrent.{ExecutionContext, Future} import scala.language.higherKinds -import util.{FutureUtils, OptionT} +import util.FutureUtils +import util.functional.OptionT import util.instances.future._ import util.syntax._ diff --git a/app/db/ModelSchema.scala b/app/db/ModelSchema.scala index ee8d4710b..583b08f11 100644 --- a/app/db/ModelSchema.scala +++ b/app/db/ModelSchema.scala @@ -6,7 +6,7 @@ import db.table.{AssociativeTable, ModelAssociation, ModelTable} import scala.concurrent.{ExecutionContext, Future, Promise} import scala.util.{Failure, Success} -import util.OptionT +import util.functional.OptionT import util.instances.future._ /** diff --git a/app/db/ModelService.scala b/app/db/ModelService.scala index 8a76d48a1..98ffadb9e 100644 --- a/app/db/ModelService.scala +++ b/app/db/ModelService.scala @@ -7,7 +7,6 @@ import db.ModelAction._ import db.ModelFilter.IdFilter import db.access.ModelAccess import db.table.{MappedType, ModelTable} -import util.OptionT import slick.ast.{AnonSymbol, Ref, SortBy} import slick.basic.DatabaseConfig import slick.jdbc.{JdbcProfile, JdbcType} @@ -17,6 +16,8 @@ import scala.concurrent.duration.Duration 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. */ diff --git a/app/db/access/ModelAccess.scala b/app/db/access/ModelAccess.scala index 81f277449..95110d058 100644 --- a/app/db/access/ModelAccess.scala +++ b/app/db/access/ModelAccess.scala @@ -4,10 +4,9 @@ import db.ModelFilter.IdFilter import db.impl.OrePostgresDriver.api._ import db.{Model, ModelFilter, ModelService} import slick.lifted.ColumnOrdered - import scala.concurrent.{ExecutionContext, Future} -import util.OptionT +import util.functional.OptionT /** * Provides simple, synchronous, access to a ModelTable. diff --git a/app/db/impl/access/OrganizationBase.scala b/app/db/impl/access/OrganizationBase.scala index 0e62361f7..ba680dd00 100644 --- a/app/db/impl/access/OrganizationBase.scala +++ b/app/db/impl/access/OrganizationBase.scala @@ -10,10 +10,12 @@ import ore.user.notification.NotificationTypes import play.api.cache.AsyncCacheApi import play.api.i18n.{Lang, MessagesApi} import security.spauth.SpongeAuthApi -import util.{EitherT, OptionT, StringUtils} +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, diff --git a/app/db/impl/access/ProjectBase.scala b/app/db/impl/access/ProjectBase.scala index b039a7eb5..cad52936d 100644 --- a/app/db/impl/access/ProjectBase.scala +++ b/app/db/impl/access/ProjectBase.scala @@ -15,11 +15,13 @@ import models.project.{Channel, Project, Version} import ore.project.io.ProjectFiles import ore.{OreConfig, OreEnv} import slick.lifted.TableQuery -import util.{FileUtils, OptionT} +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, diff --git a/app/db/impl/access/UserBase.scala b/app/db/impl/access/UserBase.scala index fd3da0eec..bdb482a86 100755 --- a/app/db/impl/access/UserBase.scala +++ b/app/db/impl/access/UserBase.scala @@ -17,7 +17,7 @@ import scala.concurrent.{ExecutionContext, Future} import ore.permission.role import ore.permission.role.RoleTypes import ore.permission.role.RoleTypes.RoleType -import util.OptionT +import util.functional.OptionT import util.instances.future._ /** diff --git a/app/db/impl/schema/PageSchema.scala b/app/db/impl/schema/PageSchema.scala index d4bba4427..edd49919f 100644 --- a/app/db/impl/schema/PageSchema.scala +++ b/app/db/impl/schema/PageSchema.scala @@ -6,7 +6,7 @@ import db.{ModelSchema, ModelService} import models.project.Page import scala.concurrent.{ExecutionContext, Future} -import util.OptionT +import util.functional.OptionT /** * Page related queries. diff --git a/app/db/impl/schema/ProjectSchema.scala b/app/db/impl/schema/ProjectSchema.scala index ce707bc08..474e07510 100755 --- a/app/db/impl/schema/ProjectSchema.scala +++ b/app/db/impl/schema/ProjectSchema.scala @@ -11,7 +11,7 @@ import ore.project.Categories.Category import ore.project.ProjectSortingStrategies.ProjectSortingStrategy import scala.concurrent.{ExecutionContext, Future} -import util.OptionT +import util.functional.OptionT /** * Project related queries diff --git a/app/db/impl/schema/StatSchema.scala b/app/db/impl/schema/StatSchema.scala index 7b2233aa6..1a15b31d4 100644 --- a/app/db/impl/schema/StatSchema.scala +++ b/app/db/impl/schema/StatSchema.scala @@ -5,7 +5,7 @@ import db.{ModelFilter, ModelSchema, ModelService} import models.statistic.StatEntry import scala.concurrent.{ExecutionContext, Future, Promise} -import util.OptionT +import util.functional.OptionT import util.instances.future._ /** diff --git a/app/db/impl/schema/UserSchema.scala b/app/db/impl/schema/UserSchema.scala index 4e56fa78b..fad42c36b 100644 --- a/app/db/impl/schema/UserSchema.scala +++ b/app/db/impl/schema/UserSchema.scala @@ -6,7 +6,7 @@ import db.{ModelSchema, ModelService} import models.user.User import scala.concurrent.{ExecutionContext, Future} -import util.OptionT +import util.functional.OptionT /** * User related queries. diff --git a/app/form/project/TChannelData.scala b/app/form/project/TChannelData.scala index a48fcdb88..68929c773 100644 --- a/app/form/project/TChannelData.scala +++ b/app/form/project/TChannelData.scala @@ -6,7 +6,7 @@ import ore.OreConfig import ore.project.factory.ProjectFactory import scala.concurrent.{ExecutionContext, Future} -import util.{EitherT, OptionT} +import util.functional.{EitherT, OptionT} /** * Represents submitted [[Channel]] data. diff --git a/app/models/admin/VisibilityChange.scala b/app/models/admin/VisibilityChange.scala index 655ea12a3..61ef64d37 100644 --- a/app/models/admin/VisibilityChange.scala +++ b/app/models/admin/VisibilityChange.scala @@ -11,7 +11,7 @@ import models.user.User import play.twirl.api.Html import scala.concurrent.{ExecutionContext, Future} -import util.OptionT +import util.functional.OptionT import util.instances.future._ case class VisibilityChange(override val id: Option[Int] = None, diff --git a/app/models/project/DownloadWarning.scala b/app/models/project/DownloadWarning.scala index d9efad47c..d06d85445 100644 --- a/app/models/project/DownloadWarning.scala +++ b/app/models/project/DownloadWarning.scala @@ -14,7 +14,7 @@ import models.project.DownloadWarning.COOKIE import play.api.mvc.Cookie import scala.concurrent.{ExecutionContext, Future} -import util.OptionT +import util.functional.OptionT import util.instances.future._ /** diff --git a/app/models/project/Page.scala b/app/models/project/Page.scala index 807ae5d68..33cdbb879 100644 --- a/app/models/project/Page.scala +++ b/app/models/project/Page.scala @@ -28,10 +28,11 @@ import ore.OreConfig import ore.permission.scope.ProjectScope import play.twirl.api.Html import util.StringUtils._ -import util.OptionT import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT + /** * Represents a documentation page within a project. * diff --git a/app/models/project/Project.scala b/app/models/project/Project.scala index 3564923ca..3c4be90c5 100755 --- a/app/models/project/Project.scala +++ b/app/models/project/Project.scala @@ -3,10 +3,10 @@ package models.project import java.sql.Timestamp import java.time.Instant -import _root_.util.{StringUtils, OptionT} +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 diff --git a/app/models/project/ProjectSettings.scala b/app/models/project/ProjectSettings.scala index 0c4b7de82..f321b0f24 100644 --- a/app/models/project/ProjectSettings.scala +++ b/app/models/project/ProjectSettings.scala @@ -8,7 +8,6 @@ import db.impl.OrePostgresDriver.api._ import db.impl._ import db.impl.model.OreModel import db.impl.table.ModelKeys._ -import util.OptionT import util.instances.future._ import form.project.ProjectSettingsForm import models.user.Notification @@ -24,6 +23,8 @@ import slick.lifted.TableQuery import util.StringUtils._ import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT + /** * Represents a [[Project]]'s settings. * diff --git a/app/models/project/Version.scala b/app/models/project/Version.scala index bde69a714..253c0f68b 100755 --- a/app/models/project/Version.scala +++ b/app/models/project/Version.scala @@ -19,10 +19,12 @@ import models.user.User import ore.permission.scope.ProjectScope import ore.project.Dependency import play.twirl.api.Html -import util.{FileUtils, OptionT} +import util.FileUtils import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT + /** * Represents a single version of a Project. * diff --git a/app/models/statistic/StatEntry.scala b/app/models/statistic/StatEntry.scala index 92b0598fe..3a983b0e1 100644 --- a/app/models/statistic/StatEntry.scala +++ b/app/models/statistic/StatEntry.scala @@ -9,11 +9,12 @@ import db.Model import db.impl.model.OreModel import db.impl.table.ModelKeys._ import db.impl.table.StatTable -import util.OptionT import util.instances.future._ import models.user.User import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT + /** * Represents a statistic entry in a StatTable. * diff --git a/app/models/user/User.scala b/app/models/user/User.scala index 931fd4500..3a29fadc4 100644 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -31,7 +31,7 @@ import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} import scala.util.control.Breaks._ -import util.OptionT +import util.functional.OptionT /** * Represents a Sponge user. diff --git a/app/models/viewhelper/HeaderData.scala b/app/models/viewhelper/HeaderData.scala index 207cdf4d1..c198ed746 100644 --- a/app/models/viewhelper/HeaderData.scala +++ b/app/models/viewhelper/HeaderData.scala @@ -16,7 +16,7 @@ import slick.lifted.TableQuery import scala.concurrent.{ExecutionContext, Future} import models.viewhelper.HeaderData.perms -import util.OptionT +import util.functional.OptionT import util.syntax._ /** diff --git a/app/models/viewhelper/OrganizationData.scala b/app/models/viewhelper/OrganizationData.scala index 3ae90082a..52938f143 100644 --- a/app/models/viewhelper/OrganizationData.scala +++ b/app/models/viewhelper/OrganizationData.scala @@ -9,7 +9,7 @@ import play.api.cache.AsyncCacheApi import slick.jdbc.JdbcBackend import scala.concurrent.{ExecutionContext, Future} -import util.OptionT +import util.functional.OptionT import util.instances.future._ case class OrganizationData(joinable: Organization, diff --git a/app/models/viewhelper/ScopedOrganizationData.scala b/app/models/viewhelper/ScopedOrganizationData.scala index 4b0a3c00f..083d50ba5 100644 --- a/app/models/viewhelper/ScopedOrganizationData.scala +++ b/app/models/viewhelper/ScopedOrganizationData.scala @@ -7,7 +7,7 @@ import play.api.cache.AsyncCacheApi import slick.jdbc.JdbcBackend import scala.concurrent.{ExecutionContext, Future} -import util.OptionT +import util.functional.OptionT import util.instances.future._ case class ScopedOrganizationData(permissions: Map[Permission, Boolean] = Map.empty) diff --git a/app/ore/project/Dependency.scala b/app/ore/project/Dependency.scala index 611f9fef1..50d98ccdd 100755 --- a/app/ore/project/Dependency.scala +++ b/app/ore/project/Dependency.scala @@ -4,7 +4,7 @@ import db.impl.access.ProjectBase import models.project.Project import scala.concurrent.{ExecutionContext, Future} -import util.OptionT +import util.functional.OptionT /** * Represents a dependency to another plugin. Either on or not on Ore. diff --git a/app/ore/project/factory/ProjectFactory.scala b/app/ore/project/factory/ProjectFactory.scala index 03de3a26a..990e01745 100755 --- a/app/ore/project/factory/ProjectFactory.scala +++ b/app/ore/project/factory/ProjectFactory.scala @@ -37,7 +37,7 @@ import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration.Duration import scala.util.Try -import util.{EitherT, OptionT} +import util.functional.{EitherT, OptionT} /** * Manages the project and version creation pipeline. diff --git a/app/ore/rest/OreRestfulApi.scala b/app/ore/rest/OreRestfulApi.scala index c950e79df..f272b8968 100755 --- a/app/ore/rest/OreRestfulApi.scala +++ b/app/ore/rest/OreRestfulApi.scala @@ -19,10 +19,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.OptionT import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT + /** * The Ore API */ diff --git a/app/security/spauth/SingleSignOnConsumer.scala b/app/security/spauth/SingleSignOnConsumer.scala index 629c64423..9fe38523a 100644 --- a/app/security/spauth/SingleSignOnConsumer.scala +++ b/app/security/spauth/SingleSignOnConsumer.scala @@ -17,7 +17,7 @@ import play.api.libs.ws.WSClient import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, Future} -import util.OptionT +import util.functional.OptionT import util.instances.future._ /** diff --git a/app/security/spauth/SpongeAuthApi.scala b/app/security/spauth/SpongeAuthApi.scala index 1fe579bda..b88236678 100644 --- a/app/security/spauth/SpongeAuthApi.scala +++ b/app/security/spauth/SpongeAuthApi.scala @@ -14,7 +14,7 @@ import play.api.libs.ws.{WSClient, WSResponse} import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ -import _root_.util.{EitherT, OptionT} +import _root_.util.functional.{EitherT, OptionT} import _root_.util.instances.future._ /** diff --git a/app/util/Monad.scala b/app/util/Monad.scala deleted file mode 100644 index 7108645fe..000000000 --- a/app/util/Monad.scala +++ /dev/null @@ -1,14 +0,0 @@ -package util - -import scala.language.higherKinds - -import play.api.libs.functional.{Applicative, Functor} - -trait Monad[F[_]] extends Functor[F] with Applicative[F] { - - def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] - - override def map[A, B](fa: F[A], f: A => B): F[B] = fmap(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/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/EitherT.scala b/app/util/functional/EitherT.scala similarity index 86% rename from app/util/EitherT.scala rename to app/util/functional/EitherT.scala index 88a0a5f82..7eb4521bf 100644 --- a/app/util/EitherT.scala +++ b/app/util/functional/EitherT.scala @@ -1,20 +1,17 @@ -package util +package util.functional import scala.language.higherKinds - -import play.api.libs.functional.{Applicative, Functor} -import play.api.libs.functional.syntax._ -import syntax._ +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.fmap(_.fold(fa, fb)) + 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.fmap(_.swap)) + 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.fmap(_.getOrElse(or)) + 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.fmap(_.fold(identity, ev.apply)) + 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 { @@ -32,14 +29,14 @@ case class EitherT[F[_], A, B](value: F[Either[A, B]]) { ) } - def contains[B1 >: B](elem: B1)(implicit F: Functor[F]): F[Boolean] = value.fmap(_.contains(elem)) + 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.fmap(_.forall(f)) + 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.fmap(_.exists(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.fmap(f)) + EitherT(value.map(f)) def flatMap[A1 >: A, D](f: B => EitherT[F, A1, D])(implicit F: Monad[F]): EitherT[F, A1, D] = EitherT( @@ -70,7 +67,7 @@ case class EitherT[F[_], A, B](value: F[Either[A, B]]) { def leftSemiFlatMap[D](f: A => F[D])(implicit F: Monad[F]): EitherT[F, D, B] = EitherT( value.flatMap { - case Left(a) => f(a).fmap(d => Left(d)) + 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 } ) @@ -81,25 +78,25 @@ case class EitherT[F[_], A, B](value: F[Either[A, B]]) { def bimap[C, D](fa: A => C, fb: B => D)(implicit F: Functor[F]): EitherT[F, C, D] = EitherT( - value.fmap { + 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.fmap(_.filterOrElse(p, zero))) + EitherT(value.map(_.filterOrElse(p, zero))) - def toOption(implicit F: Functor[F]): OptionT[F, B] = OptionT(value.fmap(_.toOption)) + def toOption(implicit F: Functor[F]): OptionT[F, B] = OptionT(value.map(_.toOption)) - def isLeft(implicit F: Functor[F]): F[Boolean] = value.fmap(_.isLeft) + def isLeft(implicit F: Functor[F]): F[Boolean] = value.map(_.isLeft) - def isRight(implicit F: Functor[F]): F[Boolean] = value.fmap(_.isRight) + 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.fmap(Left.apply)) + 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] @@ -111,7 +108,7 @@ object EitherT { 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.fmap(Right.apply)) + 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] @@ -141,7 +138,7 @@ object EitherT { } final def fromOptionF[F[_], E, A](fopt: F[Option[A]], ifNone: => E)(implicit F: Functor[F]): EitherT[F, E, A] = - EitherT(fopt.fmap(_.toRight(ifNone))) + EitherT(fopt.map(_.toRight(ifNone))) final def cond[F[_]]: CondPartiallyApplied[F] = new CondPartiallyApplied 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/OptionT.scala b/app/util/functional/OptionT.scala similarity index 80% rename from app/util/OptionT.scala rename to app/util/functional/OptionT.scala index 5a391b01e..45b200bd7 100644 --- a/app/util/OptionT.scala +++ b/app/util/functional/OptionT.scala @@ -1,25 +1,22 @@ -package util +package util.functional import scala.language.higherKinds - -import play.api.libs.functional.{Applicative, Functor} -import play.api.libs.functional.syntax._ -import syntax._ +import util.syntax._ case class OptionT[F[_], A](value: F[Option[A]]) { - def isEmpty(implicit F: Functor[F]): F[Boolean] = value.fmap(_.isEmpty) + def isEmpty(implicit F: Functor[F]): F[Boolean] = value.map(_.isEmpty) - def isDefined(implicit F: Functor[F]): F[Boolean] = value.fmap(_.isDefined) + def isDefined(implicit F: Functor[F]): F[Boolean] = value.map(_.isDefined) - def getOrElse[B >: A](default: => B)(implicit F: Functor[F]): F[B] = value.fmap(_.getOrElse(default)) + 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.fmap(_.map(f))) + 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.fmap(_.fold(ifEmpty)(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) @@ -30,24 +27,24 @@ case class OptionT[F[_], A](value: F[Option[A]]) { 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.fmap(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.fmap(_.filter(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.fmap(_.filterNot(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.fmap(_.contains(elem)) + 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.fmap(_.exists(f)) + 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.fmap(_.forall(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.fmap(_.collect(f))) + OptionT(value.map(_.collect(f))) def orElse(alternative: OptionT[F, A])(implicit F: Monad[F]): OptionT[F, A] = orElseF(alternative.value) @@ -86,5 +83,5 @@ object OptionT { OptionT(F.pure(value)) } - def liftF[F[_], A](fa: F[A])(implicit F: Functor[F]): OptionT[F, A] = OptionT(fa.fmap(Some(_))) + def liftF[F[_], A](fa: F[A])(implicit F: Functor[F]): OptionT[F, A] = OptionT(fa.map(Some(_))) } \ No newline at end of file diff --git a/app/util/instances/FutureInstances.scala b/app/util/instances/FutureInstances.scala index de50e8a01..43ac91bdb 100644 --- a/app/util/instances/FutureInstances.scala +++ b/app/util/instances/FutureInstances.scala @@ -2,16 +2,17 @@ package util.instances import scala.concurrent.{ExecutionContext, Future} -import util.Monad +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 fmap[A, B](m: Future[A], f: A => B): Future[B] = m.map(f) - override def apply[A, B](mf: Future[A => B], ma: Future[A]): Future[B] = mf.flatMap(f => ma.map(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/syntax/package.scala b/app/util/syntax/package.scala index ba8108a6a..c1b4ec0e0 100644 --- a/app/util/syntax/package.scala +++ b/app/util/syntax/package.scala @@ -2,13 +2,48 @@ 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 { + implicit class MonadFlattenOps[F[_], A](private val ffa: F[F[A]]) + extends AnyVal { def flatten(implicit F: Monad[F]): F[A] = F.flatten(ffa) } } From 08c7464a974e12219ad75967e1aff0034e646b27 Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 19 Apr 2018 11:21:00 +0200 Subject: [PATCH 10/18] Fix compile errors --- app/controllers/Reviews.scala | 62 +++++++++++--------- app/controllers/project/Projects.scala | 4 +- app/controllers/project/Versions.scala | 4 +- app/db/impl/access/OrganizationBase.scala | 2 +- app/form/project/TChannelData.scala | 1 + app/models/viewhelper/HeaderData.scala | 1 + app/ore/permission/PermissionPredicate.scala | 1 + app/ore/user/MembershipDossier.scala | 4 +- 8 files changed, 43 insertions(+), 36 deletions(-) diff --git a/app/controllers/Reviews.scala b/app/controllers/Reviews.scala index 219c281ea..e3b298bee 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -63,6 +63,7 @@ final class Reviews @Inject()(data: DataHelper, ) } 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)) } @@ -114,21 +115,22 @@ final class Reviews @Inject()(data: DataHelper, def approveReview(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => - val res = for { + val ret = for { project <- getProject(author, slug) version <- getVersion(project, versionString) review <- version.mostRecentUnfinishedReview.toRight(notFound) - } yield { - ( - review.setEnded(Some(Timestamp.from(Instant.now()))), - // send notification that review happened - sendReviewNotification(project, version, request.user) - ).parMapN { (_, _) => - Redirect(routes.Reviews.showReviews(author, slug, versionString)) - } - } + res <- EitherT.right[Result]( + ( + review.setEnded(Some(Timestamp.from(Instant.now()))), + // send notification that review happened + sendReviewNotification(project, version, request.user) + ).parMapN { (_, _) => + Redirect(routes.Reviews.showReviews(author, slug, versionString)) + } + ) + } yield res - res.merge + ret.merge } } @@ -187,28 +189,30 @@ final class Reviews @Inject()(data: DataHelper, def takeoverReview(author: String, slug: String, versionString: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => - val res = for { + val ret = for { version <- getProjectVersion(author, slug, versionString) - } yield { - // Close old review - val closeOldReview = version.mostRecentUnfinishedReview.semiFlatMap { oldreview => - ( - oldreview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim, System.currentTimeMillis(), "takeover")), - oldreview.setEnded(Some(Timestamp.from(Instant.now()))), + res <- { + // Close old review + val closeOldReview = version.mostRecentUnfinishedReview.semiFlatMap { oldreview => + ( + oldreview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.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 + val result = ( + closeOldReview, 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 - ( - closeOldReview, - this.service.insert(Review(Some(1), Some(Timestamp.from(Instant.now())), version.id.get, request.user.id.get, None, "")) - ).parMapN { (_, _) => - Redirect(routes.Reviews.showReviews(author, slug, versionString)) + ).parMapN { (_, _) => + Redirect(routes.Reviews.showReviews(author, slug, versionString)) + } + EitherT.right[Result](result) } - } + } yield res - res.merge + ret.merge } } diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index cfbe8839d..2fd5ef82a 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -658,11 +658,11 @@ class Projects @Inject()(stats: StatTracker, */ def showNotes(author: String, slug: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewFlags)).async { implicit request => - withProjectAsync(author, slug) { project => + 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 } } diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index 7c1c8785d..91f4ced4e 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -305,7 +305,7 @@ 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 => + getProject(author, slug).semiFlatMap { project => project.channels .find(equalsIgnoreCase(_.name, pendingVersion.channelName)) .toRight(versionData.addTo(project).value) @@ -325,7 +325,7 @@ class Versions @Inject()(stats: StatTracker, } .leftMap(error => Redirect(self.showCreatorWithMeta(author, slug, versionString)).withError(error)) .merge - } + }.merge case Some(pendingProject) => // Found a pending project, create it with first version pendingProject.complete.map { created => diff --git a/app/db/impl/access/OrganizationBase.scala b/app/db/impl/access/OrganizationBase.scala index ba680dd00..cc31eecca 100644 --- a/app/db/impl/access/OrganizationBase.scala +++ b/app/db/impl/access/OrganizationBase.scala @@ -71,7 +71,7 @@ class OrganizationBase(override val service: ModelService, userOrg <- org.toUser.getOrElse(throw new IllegalStateException("User not created")) _ <- userOrg.pullForumData() _ <- userOrg.pullSpongeData() - _ <- userOrg.setGlobalRoles(userOrg.globalRoles + RoleTypes.Organization) + _ = userOrg.setGlobalRoles(userOrg.globalRoles + RoleTypes.Organization) _ <- // Add the owner org.memberships.addRole(OrganizationRole( userId = ownerId, diff --git a/app/form/project/TChannelData.scala b/app/form/project/TChannelData.scala index 68929c773..bc0b6593f 100644 --- a/app/form/project/TChannelData.scala +++ b/app/form/project/TChannelData.scala @@ -7,6 +7,7 @@ import ore.project.factory.ProjectFactory import scala.concurrent.{ExecutionContext, Future} import util.functional.{EitherT, OptionT} +import util.instances.future._ /** * Represents submitted [[Channel]] data. diff --git a/app/models/viewhelper/HeaderData.scala b/app/models/viewhelper/HeaderData.scala index c198ed746..d031d2581 100644 --- a/app/models/viewhelper/HeaderData.scala +++ b/app/models/viewhelper/HeaderData.scala @@ -17,6 +17,7 @@ import scala.concurrent.{ExecutionContext, Future} import models.viewhelper.HeaderData.perms import util.functional.OptionT +import util.instances.future._ import util.syntax._ /** diff --git a/app/ore/permission/PermissionPredicate.scala b/app/ore/permission/PermissionPredicate.scala index be40da387..36aafe800 100644 --- a/app/ore/permission/PermissionPredicate.scala +++ b/app/ore/permission/PermissionPredicate.scala @@ -8,6 +8,7 @@ import ore.permission.scope.ScopeSubject import scala.concurrent.{ExecutionContext, Future} import util.FutureUtils +import util.instances.future._ /** * Permission wrapper used for chaining permission checks. diff --git a/app/ore/user/MembershipDossier.scala b/app/ore/user/MembershipDossier.scala index df624e154..4c9c67337 100644 --- a/app/ore/user/MembershipDossier.scala +++ b/app/ore/user/MembershipDossier.scala @@ -74,8 +74,8 @@ trait MembershipDossier { user <- role.user exists <- this.roles.exists(_.userId === user.id.get) _ <- if(!exists) addMember(user) else Future.successful(user) - _ <- this.roleAccess.add(role) - } yield () + ret <- this.roleAccess.add(role) + } yield ret } /** From bea748b17109e08f8c7c3b4ead7b574dd9a9e565 Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 20 Apr 2018 13:22:27 +0200 Subject: [PATCH 11/18] Replace bindFromRequest with transformers --- app/controllers/ApiController.scala | 166 +++++++++++------------- app/controllers/Application.scala | 99 +++++++------- app/controllers/OreBaseController.scala | 21 ++- app/controllers/Organizations.scala | 32 +++-- app/controllers/Reviews.scala | 12 +- app/controllers/Users.scala | 11 +- app/controllers/project/Channels.scala | 33 ++--- app/controllers/project/Projects.scala | 58 +++++---- app/controllers/project/Versions.scala | 11 +- app/util/functional/EitherT.scala | 6 + app/util/functional/OptionT.scala | 6 + app/util/functional/package.scala | 25 ++++ 12 files changed, 274 insertions(+), 206 deletions(-) create mode 100644 app/util/functional/package.scala diff --git a/app/controllers/ApiController.scala b/app/controllers/ApiController.scala index 1b7afb227..3e39ac11c 100644 --- a/app/controllers/ApiController.scala +++ b/app/controllers/ApiController.scala @@ -25,7 +25,9 @@ import ore.{OreConfig, OreEnv} import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import util.StatusZ -import util.functional.EitherT +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 @@ -85,46 +87,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. @@ -165,63 +158,60 @@ 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 + + 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 { + 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) - - 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))) - } + 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))) } } - .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 - } - ) + } + .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) } } diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index 532a79587..e60232b4a 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -423,64 +423,61 @@ final class Application @Inject()(data: DataHelper, def updateUser(userName: String) = UserAdminAction.async { implicit request => this.users.withName(userName).map { user => - this.forms.UserAdminUpdate.bindFromRequest.fold( - _ => OptionT.none[Future, Status], - { 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]) + 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 - } - case "deleteRole" => modelAccess.get(id).map { role => - if (role.roleType.isAssignable) { - role.remove() - Ok - } else BadRequest - } + } 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).map { role => + if (role.roleType.isAssignable) { + role.remove() + Ok + } else BadRequest } } + } - 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) - } - } - 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)))) + 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) } - case _ => OptionT.none[Future, Status] - } - }) + } + 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) } diff --git a/app/controllers/OreBaseController.scala b/app/controllers/OreBaseController.scala index 144c889b6..e88d80845 100755 --- a/app/controllers/OreBaseController.scala +++ b/app/controllers/OreBaseController.scala @@ -16,8 +16,11 @@ import security.spauth.SingleSignOnConsumer import util.StringUtils._ import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import scala.language.higherKinds -import util.functional.EitherT +import controllers.OreBaseController.{BindFormEitherTPartiallyApplied, BindFormOptionTPartiallyApplied} +import play.api.data.Form +import util.functional.{EitherT, Functor, OptionT} /** * Represents a Secured base Controller for this application. @@ -85,6 +88,10 @@ abstract class OreBaseController(implicit val env: OreEnv, version <- getVersion(project, versionString) } yield version + def bindFormEitherT[F[_]] = new BindFormEitherTPartiallyApplied[F] + + def bindFormOptionT[F[_]] = new BindFormOptionTPartiallyApplied[F] + def OreAction = Action andThen oreAction /** Ensures a request is authenticated */ @@ -170,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: Functor[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: Functor[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 0fd8ccfe7..dd32388a2 100755 --- a/app/controllers/Organizations.scala +++ b/app/controllers/Organizations.scala @@ -17,6 +17,8 @@ import views.{html => views} import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import util.functional.Id + /** * Controller for handling Organization based actions. */ @@ -67,15 +69,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()).bimap( - error => Redirect(failCall).withError(error), - organization => Redirect(routes.Users.showProjects(organization.name, None)) - ).merge - } - ) + 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 } } } @@ -127,10 +126,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 { user => + val res = for { + name <- bindFormOptionT[Future](this.forms.OrganizationMemberRemove) + user <- this.users.withName(name) + } yield { request.data.orga.memberships.removeMember(user) Redirect(ShowUser(organization)) - }.getOrElse(BadRequest) + } + + res.getOrElse(BadRequest) } /** @@ -140,8 +144,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 e3b298bee..a140e9f5d 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -103,8 +103,9 @@ final class Reviews @Inject()(data: DataHelper, val res = for { version <- getProjectVersion(author, slug, versionString) review <- version.mostRecentUnfinishedReview.toRight(notFound) + message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest) } yield { - review.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim, System.currentTimeMillis(), "stop")) + review.addMessage(Message(message.trim, System.currentTimeMillis(), "stop")) review.setEnded(Some(Timestamp.from(Instant.now()))) Redirect(routes.Reviews.showReviews(author, slug, versionString)) } @@ -191,11 +192,12 @@ final class Reviews @Inject()(data: DataHelper, (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => val ret = for { version <- getProjectVersion(author, slug, versionString) + message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest) res <- { // Close old review val closeOldReview = version.mostRecentUnfinishedReview.semiFlatMap { oldreview => ( - oldreview.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim, System.currentTimeMillis(), "takeover")), + 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 { (_, _, _) => () } @@ -221,8 +223,9 @@ final class Reviews @Inject()(data: DataHelper, val res = for { version <- getProjectVersion(author, slug, versionString) review <- version.reviewById(reviewId).toRight(notFound) + message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest) } yield { - review.addMessage(Message(this.forms.ReviewDescription.bindFromRequest.get.trim)) + review.addMessage(Message(message.trim)) Ok("Review" + review) } @@ -236,9 +239,10 @@ final class Reviews @Inject()(data: DataHelper, 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(this.forms.ReviewDescription.bindFromRequest.get.trim))) + EitherT.right[Result](recentReview.addMessage(Message(message.trim))) } else EitherT.rightT[Future, Result](0) } } yield Ok("Review") diff --git a/app/controllers/Users.scala b/app/controllers/Users.scala index 63d2b507a..736a46f58 100755 --- a/app/controllers/Users.scala +++ b/app/controllers/Users.scala @@ -183,16 +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 { 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)) } - }.getOrElse(NotFound) + } + + res.merge } /** diff --git a/app/controllers/project/Channels.scala b/app/controllers/project/Channels.scala index aa64d63da..c06c00c72 100755 --- a/app/controllers/project/Channels.scala +++ b/app/controllers/project/Channels.scala @@ -64,15 +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).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 } /** @@ -84,18 +81,12 @@ 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).cata( - Redirect(self.showList(author, slug)), - error => Redirect(self.showList(author, slug)).withError(error) - ) - } - ) + val res = for { + channelData <- bindFormEitherT[Future](this.forms.ChannelEdit)(hasErrors => Redirect(self.showList(author, slug)).withError(hasErrors.errors.head.message)) + _ <- channelData.saveTo(channelName).toLeft(()).leftMap(error => Redirect(self.showList(author, slug)).withError(error)) + } yield Redirect(self.showList(author, slug)) + + res.merge } /** diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index 2fd5ef82a..e9bc63be0 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -28,7 +28,8 @@ import db.impl.OrePostgresDriver.api._ import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContext, Future} -import util.functional.OptionT +import play.api.mvc.Result +import util.functional.{EitherT, Id, OptionT} import util.instances.future._ import util.syntax._ @@ -188,14 +189,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) + } yield { + pendingProject.roles = roles.build() + val pendingVersion = pendingProject.pendingVersion + Redirect(routes.Versions.showCreatorWithMeta(author, slug, pendingVersion.underlying.versionString)) } + + res.merge } /** @@ -487,10 +490,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 { user => + 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)) - }.getOrElse(BadRequest) + } + + res.getOrElse(BadRequest) } /** @@ -523,15 +531,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 } /** @@ -668,10 +677,15 @@ class Projects @Inject()(stats: StatTracker, def addMessage(author: String, slug: String) = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => - getProject(author, slug).map { 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) + } yield { + project.addNote(Note(description.trim, request.user.userId)) Ok("Review") - }.merge + } + + res.merge } } } \ No newline at end of file diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index 91f4ced4e..ee48bdbf8 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -93,10 +93,15 @@ class Versions @Inject()(stats: StatTracker, def saveDescription(author: String, slug: String, versionString: String) = { VersionEditAction(author, slug).async { request => implicit val r = request.request - getVersion(request.data.project, versionString).map { 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) + } yield { + version.setDescription(description.trim) Redirect(self.show(author, slug, versionString)) - }.merge + } + + res.merge } } diff --git a/app/util/functional/EitherT.scala b/app/util/functional/EitherT.scala index 7eb4521bf..69178aa2f 100644 --- a/app/util/functional/EitherT.scala +++ b/app/util/functional/EitherT.scala @@ -146,4 +146,10 @@ object EitherT { 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/OptionT.scala b/app/util/functional/OptionT.scala index 45b200bd7..3565bdf0e 100644 --- a/app/util/functional/OptionT.scala +++ b/app/util/functional/OptionT.scala @@ -84,4 +84,10 @@ object OptionT { } 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..13da46707 --- /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: Id[A])(f: A => Id[B]): Id[B] = f(fa) + override def pure[A](a: A): Id[A] = a + + override def ap[A, B](ff: Id[A => B])(fa: Id[A]): Id[B] = ff(fa) + override def flatten[A](ffa: Id[Id[A]]): Id[A] = ffa + override def map[A, B](fa: Id[A])(f: A => B): Id[B] = f(fa) + override def product[A, B](fa: Id[A], fb: Id[B]): (A, B) = (fa, fb) + override def map2[A, B, C](fa: Id[A], fb: Id[B])(f: (A, B) => C): Id[C] = f(fa, fb) + override def <*>[A, B](ff: Id[A => B])(fa: Id[A]): Id[B] = ff(fa) + override def *>[A, B](fa: Id[A])(fb: Id[B]): Id[B] = fb + override def <*[A, B](fa: Id[A])(fb: Id[B]): Id[A] = fa + override def as[A, B](fa: Id[A], b: B): Id[B] = b + override def fproduct[A, B](fa: Id[A])(f: A => B): (A, B) = (fa, f(fa)) + override def tupleLeft[A, B](fa: Id[A], b: B): (B, A) = (b, fa) + override def tupleRight[A, B](fa: Id[A], b: B): (A, B) = (fa, b) + } + +} From a07034c256b44e33cb9ebea52a2963d2b8293996 Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 20 Apr 2018 13:40:43 +0200 Subject: [PATCH 12/18] Implement a few common types for the typeclasses --- app/util/instances/AllInstances.scala | 6 +++++- app/util/instances/DBIOInstances.scala | 17 +++++++++++++++++ app/util/instances/EitherInstances.scala | 12 ++++++++++++ app/util/instances/OptionInstances.scala | 13 +++++++++++++ app/util/instances/package.scala | 3 +++ 5 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 app/util/instances/DBIOInstances.scala create mode 100644 app/util/instances/EitherInstances.scala create mode 100644 app/util/instances/OptionInstances.scala diff --git a/app/util/instances/AllInstances.scala b/app/util/instances/AllInstances.scala index 3b0140523..9830f1c99 100644 --- a/app/util/instances/AllInstances.scala +++ b/app/util/instances/AllInstances.scala @@ -1,3 +1,7 @@ package util.instances -trait AllInstances extends FutureInstances +trait AllInstances + extends FutureInstances + with OptionInstances + with EitherInstances + with DBIOInstances diff --git a/app/util/instances/DBIOInstances.scala b/app/util/instances/DBIOInstances.scala new file mode 100644 index 000000000..93c75c999 --- /dev/null +++ b/app/util/instances/DBIOInstances.scala @@ -0,0 +1,17 @@ +package util.instances + +import slick.dbio.DBIO +import util.functional.Monad + +trait DBIOInstances { + + implicit val dbioInstance: 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/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/package.scala b/app/util/instances/package.scala index 8e21f7189..d707c54d3 100644 --- a/app/util/instances/package.scala +++ b/app/util/instances/package.scala @@ -4,4 +4,7 @@ package object instances { object all extends AllInstances object future extends FutureInstances + object option extends OptionInstances + object either extends EitherInstances + object dbio extends DBIOInstances } From 58d921327702d6879be053e1f68c0aae548b61e0 Mon Sep 17 00:00:00 2001 From: Unknown Date: Sat, 21 Apr 2018 21:47:09 +0200 Subject: [PATCH 13/18] Forgot seq --- app/util/instances/AllInstances.scala | 1 + app/util/instances/SeqInstances.scala | 12 ++++++++++++ app/util/instances/package.scala | 1 + 3 files changed, 14 insertions(+) create mode 100644 app/util/instances/SeqInstances.scala diff --git a/app/util/instances/AllInstances.scala b/app/util/instances/AllInstances.scala index 9830f1c99..66167a19e 100644 --- a/app/util/instances/AllInstances.scala +++ b/app/util/instances/AllInstances.scala @@ -5,3 +5,4 @@ trait AllInstances with OptionInstances with EitherInstances with DBIOInstances + with SeqInstances \ No newline at end of file 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 index d707c54d3..48930710a 100644 --- a/app/util/instances/package.scala +++ b/app/util/instances/package.scala @@ -7,4 +7,5 @@ package object instances { object option extends OptionInstances object either extends EitherInstances object dbio extends DBIOInstances + object seq extends SeqInstances } From 52c238d7088b6e012cb4dfde6a42d78764bad4d1 Mon Sep 17 00:00:00 2001 From: Unknown Date: Sat, 21 Apr 2018 22:04:17 +0200 Subject: [PATCH 14/18] Compile fixes --- app/controllers/OreBaseController.scala | 6 +++--- app/controllers/Reviews.scala | 4 ++-- app/controllers/project/Channels.scala | 3 +++ app/controllers/project/Projects.scala | 4 ++-- app/controllers/project/Versions.scala | 2 +- app/util/functional/package.scala | 28 ++++++++++++------------- app/util/instances/DBIOInstances.scala | 4 +++- 7 files changed, 28 insertions(+), 23 deletions(-) diff --git a/app/controllers/OreBaseController.scala b/app/controllers/OreBaseController.scala index e88d80845..2e3697b0b 100755 --- a/app/controllers/OreBaseController.scala +++ b/app/controllers/OreBaseController.scala @@ -20,7 +20,7 @@ import scala.language.higherKinds import controllers.OreBaseController.{BindFormEitherTPartiallyApplied, BindFormOptionTPartiallyApplied} import play.api.data.Form -import util.functional.{EitherT, Functor, OptionT} +import util.functional.{EitherT, Functor, Monad, OptionT} /** * Represents a Secured base Controller for this application. @@ -180,12 +180,12 @@ abstract class OreBaseController(implicit val env: OreEnv, 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: Functor[F], request: Request[_]): EitherT[F, A, B] = + 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: Functor[F], request: Request[_]): OptionT[F, A] = + 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/Reviews.scala b/app/controllers/Reviews.scala index a140e9f5d..4be3dff38 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -103,7 +103,7 @@ final class Reviews @Inject()(data: DataHelper, val res = for { version <- getProjectVersion(author, slug, versionString) review <- version.mostRecentUnfinishedReview.toRight(notFound) - message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest) + message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest: Result) } yield { review.addMessage(Message(message.trim, System.currentTimeMillis(), "stop")) review.setEnded(Some(Timestamp.from(Instant.now()))) @@ -223,7 +223,7 @@ final class Reviews @Inject()(data: DataHelper, val res = for { version <- getProjectVersion(author, slug, versionString) review <- version.reviewById(reviewId).toRight(notFound) - message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest) + message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest: Result) } yield { review.addMessage(Message(message.trim)) Ok("Review" + review) diff --git a/app/controllers/project/Channels.scala b/app/controllers/project/Channels.scala index c06c00c72..65a2a4ed3 100755 --- a/app/controllers/project/Channels.scala +++ b/app/controllers/project/Channels.scala @@ -16,6 +16,7 @@ import views.html.projects.{channels => views} import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import models.project.Project import util.functional.EitherT import util.syntax._ @@ -81,6 +82,8 @@ 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 project: Project = request.data.project + val res = for { channelData <- bindFormEitherT[Future](this.forms.ChannelEdit)(hasErrors => Redirect(self.showList(author, slug)).withError(hasErrors.errors.head.message)) _ <- channelData.saveTo(channelName).toLeft(()).leftMap(error => Redirect(self.showList(author, slug)).withError(error)) diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index e9bc63be0..d2b610cac 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -191,7 +191,7 @@ class Projects @Inject()(stats: StatTracker, def showFirstVersionCreator(author: String, slug: String) = UserLock() { implicit request => val res = for { pendingProject <- EitherT.fromOption[Id](this.factory.getPendingProject(author, slug), Redirect(self.showCreator())) - roles <- bindFormEitherT[Id](this.forms.ProjectMemberRoles)(_ => BadRequest) + roles <- bindFormEitherT[Id](this.forms.ProjectMemberRoles)(_ => BadRequest: Result) } yield { pendingProject.roles = roles.build() val pendingVersion = pendingProject.pendingVersion @@ -679,7 +679,7 @@ class Projects @Inject()(stats: StatTracker, (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => val res = for { project <- getProject(author, slug) - description <- bindFormEitherT[Future](this.forms.NoteDescription)(_ => BadRequest) + description <- bindFormEitherT[Future](this.forms.NoteDescription)(_ => BadRequest: Result) } yield { project.addNote(Note(description.trim, request.user.userId)) Ok("Review") diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index ee48bdbf8..636ccd39d 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -95,7 +95,7 @@ class Versions @Inject()(stats: StatTracker, implicit val r = request.request val res = for { version <- getVersion(request.data.project, versionString) - description <- bindFormEitherT[Future](this.forms.VersionDescription)(_ => BadRequest) + description <- bindFormEitherT[Future](this.forms.VersionDescription)(_ => BadRequest: Result) } yield { version.setDescription(description.trim) Redirect(self.show(author, slug, versionString)) diff --git a/app/util/functional/package.scala b/app/util/functional/package.scala index 13da46707..8e96c620c 100644 --- a/app/util/functional/package.scala +++ b/app/util/functional/package.scala @@ -5,21 +5,21 @@ package object functional { type Id[A] = A implicit val idInstance: Monad[Id] = new Monad[Id] { - override def flatMap[A, B](fa: Id[A])(f: A => Id[B]): Id[B] = f(fa) - override def pure[A](a: A): Id[A] = a + 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: Id[A]): Id[B] = ff(fa) - override def flatten[A](ffa: Id[Id[A]]): Id[A] = ffa - override def map[A, B](fa: Id[A])(f: A => B): Id[B] = f(fa) - override def product[A, B](fa: Id[A], fb: Id[B]): (A, B) = (fa, fb) - override def map2[A, B, C](fa: Id[A], fb: Id[B])(f: (A, B) => C): Id[C] = f(fa, fb) - override def <*>[A, B](ff: Id[A => B])(fa: Id[A]): Id[B] = ff(fa) - override def *>[A, B](fa: Id[A])(fb: Id[B]): Id[B] = fb - override def <*[A, B](fa: Id[A])(fb: Id[B]): Id[A] = fa - override def as[A, B](fa: Id[A], b: B): Id[B] = b - override def fproduct[A, B](fa: Id[A])(f: A => B): (A, B) = (fa, f(fa)) - override def tupleLeft[A, B](fa: Id[A], b: B): (B, A) = (b, fa) - override def tupleRight[A, B](fa: Id[A], b: B): (A, B) = (fa, b) + 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/DBIOInstances.scala b/app/util/instances/DBIOInstances.scala index 93c75c999..232d1a412 100644 --- a/app/util/instances/DBIOInstances.scala +++ b/app/util/instances/DBIOInstances.scala @@ -1,11 +1,13 @@ package util.instances +import scala.concurrent.ExecutionContext + import slick.dbio.DBIO import util.functional.Monad trait DBIOInstances { - implicit val dbioInstance: Monad[DBIO] = new Monad[DBIO] { + 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 From 5634d2848503336fb1377e89b556d3337f735ed4 Mon Sep 17 00:00:00 2001 From: Unknown Date: Sun, 22 Apr 2018 02:36:47 +0200 Subject: [PATCH 15/18] Final changes before review --- app/controllers/ApiController.scala | 4 +- app/controllers/Application.scala | 22 ++++----- app/controllers/OreBaseController.scala | 2 +- app/controllers/Organizations.scala | 5 +- app/controllers/Reviews.scala | 29 ++++-------- app/controllers/Users.scala | 2 +- app/controllers/project/Channels.scala | 11 ++--- app/controllers/project/Pages.scala | 1 - app/controllers/project/Projects.scala | 1 - app/controllers/project/Versions.scala | 46 ++++++++----------- app/controllers/sugar/Actions.scala | 1 + app/db/ModelSchema.scala | 1 + app/db/ModelService.scala | 1 + app/db/access/ModelAccess.scala | 1 + app/db/impl/access/ProjectBase.scala | 1 - app/db/impl/access/UserBase.scala | 1 + app/discourse/OreDiscourseApi.scala | 3 +- app/discourse/RecoveryTask.scala | 4 +- app/discourse/SpongeForums.scala | 3 +- app/form/OreForms.scala | 5 +- app/form/project/TChannelData.scala | 4 +- app/mail/Mailer.scala | 3 +- app/models/admin/VisibilityChange.scala | 6 +-- app/models/project/DownloadWarning.scala | 7 ++- app/models/project/Page.scala | 5 +- app/models/project/Project.scala | 3 +- app/models/project/ProjectSettings.scala | 3 +- app/models/project/Version.scala | 5 +- app/models/statistic/StatEntry.scala | 5 +- app/models/user/User.scala | 6 +-- app/models/viewhelper/HeaderData.scala | 5 +- app/models/viewhelper/OrganizationData.scala | 1 + .../viewhelper/ScopedOrganizationData.scala | 1 + app/ore/StatTracker.scala | 2 +- app/ore/permission/PermissionPredicate.scala | 1 + app/ore/project/ProjectTask.scala | 5 +- app/ore/project/factory/ProjectFactory.scala | 7 +-- app/ore/rest/OreRestfulApi.scala | 1 - app/ore/user/UserSyncTask.scala | 2 +- app/ore/user/notification/InviteFilters.scala | 1 + .../notification/NotificationFilters.scala | 1 + .../spauth/SingleSignOnConsumer.scala | 8 ++-- app/security/spauth/SpongeAuthApi.scala | 9 ++-- app/views/users/admin/visibility.scala.html | 36 ++++++++------- 44 files changed, 118 insertions(+), 153 deletions(-) diff --git a/app/controllers/ApiController.scala b/app/controllers/ApiController.scala index 3e39ac11c..11edb7576 100644 --- a/app/controllers/ApiController.scala +++ b/app/controllers/ApiController.scala @@ -8,10 +8,8 @@ import controllers.sugar.Bakery import db.ModelService import db.impl.OrePostgresDriver.api._ import db.impl.ProjectApiKeyTable -import util.instances.future._ import form.OreForms import javax.inject.Inject - import models.api.ProjectApiKey import models.user.User import ore.permission.EditApiKeys @@ -27,12 +25,12 @@ 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, Future} /** diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index e60232b4a..6aecd9fc2 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -2,7 +2,6 @@ package controllers import java.sql.Timestamp import java.time.Instant - import javax.inject.Inject import controllers.sugar.Bakery @@ -29,12 +28,12 @@ import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer import util.DataHelper -import views.{html => views} -import scala.concurrent.{ExecutionContext, Future} - import util.functional.OptionT import util.syntax._ import util.instances.future._ +import views.{html => views} + +import scala.concurrent.{ExecutionContext, Future} /** * Main entry point for application. @@ -218,10 +217,7 @@ final class Application @Inject()(data: DataHelper, def showFlags() = FlagAction.async { implicit request => for { flags <- this.service.access[Flag](classOf[Flag]).filterNot(_.isResolved) - futUsers = Future.sequence(flags.map(_.user)) - futProjects = Future.sequence(flags.map(_.project)) - users <- futUsers - projects <- futProjects + (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)) @@ -444,11 +440,9 @@ final class Application @Inject()(data: DataHelper, role.setAccepted((json \ "accepted").as[Boolean]) Ok } - case "deleteRole" => modelAccess.get(id).map { role => - if (role.roleType.isAssignable) { - role.remove() - Ok - } else BadRequest + case "deleteRole" => modelAccess.get(id).filter(_.roleType.isAssignable).map { role => + role.remove() + Ok } } } @@ -525,7 +519,7 @@ final class Application @Inject()(data: DataHelper, 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.fold("Unknown")(_.name),e,f.fold("Unknown")(_.name)) } - val waitingProjects = projectChanges zip projectChangeRequests.flatten zip projectVisibilityChanges zip projectVisibilityChangers map { case (((a,b), c), d) => + val waitingProjects = projectChanges zip projectChangeRequests zip projectVisibilityChanges zip projectVisibilityChangers map { case (((a,b), c), d) => (a,b,c,d.fold("Unknown")(_.name)) } diff --git a/app/controllers/OreBaseController.scala b/app/controllers/OreBaseController.scala index 2e3697b0b..db13c8645 100755 --- a/app/controllers/OreBaseController.scala +++ b/app/controllers/OreBaseController.scala @@ -20,7 +20,7 @@ import scala.language.higherKinds import controllers.OreBaseController.{BindFormEitherTPartiallyApplied, BindFormOptionTPartiallyApplied} import play.api.data.Form -import util.functional.{EitherT, Functor, Monad, OptionT} +import util.functional.{EitherT, Monad, OptionT} /** * Represents a Secured base Controller for this application. diff --git a/app/controllers/Organizations.scala b/app/controllers/Organizations.scala index dd32388a2..012c51437 100755 --- a/app/controllers/Organizations.scala +++ b/app/controllers/Organizations.scala @@ -5,7 +5,6 @@ import db.ModelService import discourse.OreDiscourseApi import form.OreForms import javax.inject.Inject - import ore.permission.EditSettings import ore.rest.OreWrites import ore.user.MembershipDossier._ @@ -15,10 +14,10 @@ import play.api.i18n.MessagesApi import security.spauth.SingleSignOnConsumer import views.{html => views} import util.instances.future._ -import scala.concurrent.{ExecutionContext, Future} - import util.functional.Id +import scala.concurrent.{ExecutionContext, Future} + /** * Controller for handling Organization based actions. */ diff --git a/app/controllers/Reviews.scala b/app/controllers/Reviews.scala index 4be3dff38..d619e770e 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -10,7 +10,6 @@ import db.impl.OrePostgresDriver.api._ import db.impl._ import form.OreForms import javax.inject.Inject - import models.admin.{Message, Review} import models.project.{Project, Version} import models.user.{Notification, User} @@ -21,17 +20,17 @@ 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 scala.concurrent.{ExecutionContext, Future} - -import play.api.mvc.Result import util.functional.EitherT +import scala.concurrent.{ExecutionContext, Future} + /** * Controller for handling Review related actions. */ @@ -55,11 +54,7 @@ final class Reviews @Inject()(data: DataHelper, 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.map { u => - (r, u) - } - } + 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 @@ -120,16 +115,14 @@ final class Reviews @Inject()(data: DataHelper, project <- getProject(author, slug) version <- getVersion(project, versionString) review <- version.mostRecentUnfinishedReview.toRight(notFound) - res <- EitherT.right[Result]( + _ <- EitherT.right[Result]( ( review.setEnded(Some(Timestamp.from(Instant.now()))), // send notification that review happened sendReviewNotification(project, version, request.user) - ).parMapN { (_, _) => - Redirect(routes.Reviews.showReviews(author, slug, versionString)) - } + ).parTupled ) - } yield res + } yield Redirect(routes.Reviews.showReviews(author, slug, versionString)) ret.merge } @@ -193,7 +186,7 @@ final class Reviews @Inject()(data: DataHelper, val ret = for { version <- getProjectVersion(author, slug, versionString) message <- bindFormEitherT[Future](this.forms.ReviewDescription)(_ => BadRequest) - res <- { + _ <- { // Close old review val closeOldReview = version.mostRecentUnfinishedReview.semiFlatMap { oldreview => ( @@ -207,12 +200,10 @@ final class Reviews @Inject()(data: DataHelper, val result = ( closeOldReview, this.service.insert(Review(Some(1), Some(Timestamp.from(Instant.now())), version.id.get, request.user.id.get, None, "")) - ).parMapN { (_, _) => - Redirect(routes.Reviews.showReviews(author, slug, versionString)) - } + ).parTupled EitherT.right[Result](result) } - } yield res + } yield Redirect(routes.Reviews.showReviews(author, slug, versionString)) ret.merge } diff --git a/app/controllers/Users.scala b/app/controllers/Users.scala index 736a46f58..d3d5351d1 100755 --- a/app/controllers/Users.scala +++ b/app/controllers/Users.scala @@ -8,7 +8,6 @@ import db.impl.{ProjectTableMain, VersionTable} import discourse.OreDiscourseApi import form.OreForms import javax.inject.Inject - import mail.{EmailFactory, Mailer} import models.user.{SignOn, User} import models.viewhelper.{OrganizationData, ScopedOrganizationData} @@ -26,6 +25,7 @@ import security.spauth.SingleSignOnConsumer import views.{html => views} import util.instances.future._ import util.syntax._ + import scala.concurrent.{ExecutionContext, Future} /** diff --git a/app/controllers/project/Channels.scala b/app/controllers/project/Channels.scala index 65a2a4ed3..d126c11ee 100755 --- a/app/controllers/project/Channels.scala +++ b/app/controllers/project/Channels.scala @@ -5,7 +5,6 @@ import controllers.sugar.Bakery import db.ModelService import form.OreForms import javax.inject.Inject - import ore.permission.EditChannels import ore.project.factory.ProjectFactory import ore.{OreConfig, OreEnv} @@ -108,15 +107,15 @@ class Channels @Inject()(forms: OreForms, .flatMap { channels => EitherT.fromEither[Future](channels.find(c => c.name.equals(channelName)).toRight(NotFound)) .semiFlatMap { channel => - (channel.versions.nonEmpty, Future.sequence(channels.map(_.versions.nonEmpty)).map(l => l.count(_ == true))).parMapN { - (nonEmpty, channelCount) => (nonEmpty, channelCount, channel) - } + (channel.versions.nonEmpty, Future.sequence(channels.map(_.versions.nonEmpty)).map(l => l.count(_ == true))) + .parTupled + .tupleRight(channel) } .filterOrElse( - { case (nonEmpty, channelCount, _) => nonEmpty && channelCount == 1}, + { case ((nonEmpty, channelCount), _) => nonEmpty && channelCount == 1}, Redirect(self.showList(author, slug)).withError("error.channel.lastNonEmpty") ) - .map(_._3) + .map(_._2) .filterOrElse( channel => { val reviewedChannels = channels.filter(!_.isNonReviewed) diff --git a/app/controllers/project/Pages.scala b/app/controllers/project/Pages.scala index 2715b7234..011153445 100755 --- a/app/controllers/project/Pages.scala +++ b/app/controllers/project/Pages.scala @@ -6,7 +6,6 @@ import db.impl.OrePostgresDriver.api._ import db.{ModelFilter, ModelService} import form.OreForms import javax.inject.Inject - import models.project.{Page, Project} import ore.permission.EditPages import ore.{OreConfig, OreEnv, StatTracker} diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index d2b610cac..f5976dcf4 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -10,7 +10,6 @@ import db.ModelService import discourse.OreDiscourseApi import form.OreForms import javax.inject.Inject - import models.project.{Note, VisibilityTypes} import models.user.User import ore.permission._ diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index 636ccd39d..45fbf71ab 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -6,7 +6,6 @@ import java.sql.Timestamp import java.util.{Date, UUID} import com.github.tminglei.slickpg.InetString - import controllers.OreBaseController import controllers.sugar.Bakery import controllers.sugar.Requests.{OreRequest, ProjectRequest} @@ -15,7 +14,6 @@ import db.impl.OrePostgresDriver.api._ import discourse.OreDiscourseApi import form.OreForms import javax.inject.Inject - import models.project._ import models.viewhelper.{ProjectData, VersionData} import ore.permission.{EditVersions, ReviewProjects} @@ -244,34 +242,31 @@ 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 => - //TODO: Does order matter here. If not, the code could be rewritten a bit clearer val success = OptionT.fromOption[Future](this.factory.getPendingVersion(author, slug, versionString)) - .flatMap { pendingVersion => - // Get pending version - pendingOrReal(author, slug).semiFlatMap { - case pending: PendingProject => - ProjectData.of(request, pending).map(data => - Ok(views.create(data, data.settings.forumSync, Some(pendingVersion), None, showFileControls = false))) - case real: Project => - (real.channels.toSeq, ProjectData.of(real)).parMapN { case (channels, data) => - Ok(views - .create(data, data.settings.forumSync, Some(pendingVersion), Some(channels), showFileControls = true)) - } - } + // Get pending version + .flatMap(pendingVersion => pendingOrReal(author, slug).map(pendingVersion -> _)) + .semiFlatMap { + case (pendingVersion, Left(pending)) => + ProjectData.of(request, pending) + .map(data => (None, data, 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): OptionT[Future, 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) transform { - 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)) } /** @@ -310,11 +305,11 @@ class Versions @Inject()(stats: StatTracker, this.factory.getPendingProject(author, slug) match { case None => // No pending project, create version for existing project - getProject(author, slug).semiFlatMap { project => + getProject(author, slug).flatMap { project => project.channels .find(equalsIgnoreCase(_.name, pendingVersion.channelName)) - .toRight(versionData.addTo(project).value) - .leftFlatMap(EitherT.apply) + .toRight(versionData.addTo(project)) + .leftFlatMap(identity) .semiFlatMap { _ => // Update description versionData.content.foreach { content => @@ -329,7 +324,6 @@ class Versions @Inject()(stats: StatTracker, } } .leftMap(error => Redirect(self.showCreatorWithMeta(author, slug, versionString)).withError(error)) - .merge }.merge case Some(pendingProject) => // Found a pending project, create it with first version diff --git a/app/controllers/sugar/Actions.scala b/app/controllers/sugar/Actions.scala index 0bd19755a..bfddd58ac 100644 --- a/app/controllers/sugar/Actions.scala +++ b/app/controllers/sugar/Actions.scala @@ -18,6 +18,7 @@ import play.api.mvc.Results.{Redirect, Unauthorized} import play.api.mvc._ import security.spauth.SingleSignOnConsumer import slick.jdbc.JdbcBackend + import scala.concurrent.{ExecutionContext, Future} import scala.language.higherKinds diff --git a/app/db/ModelSchema.scala b/app/db/ModelSchema.scala index 583b08f11..e89367eae 100644 --- a/app/db/ModelSchema.scala +++ b/app/db/ModelSchema.scala @@ -3,6 +3,7 @@ package db import db.access.{ImmutableModelAccess, ModelAccess, ModelAssociationAccess} import db.impl.OrePostgresDriver.api._ import db.table.{AssociativeTable, ModelAssociation, ModelTable} + import scala.concurrent.{ExecutionContext, Future, Promise} import scala.util.{Failure, Success} diff --git a/app/db/ModelService.scala b/app/db/ModelService.scala index 98ffadb9e..b476ffa21 100644 --- a/app/db/ModelService.scala +++ b/app/db/ModelService.scala @@ -12,6 +12,7 @@ import slick.basic.DatabaseConfig import slick.jdbc.{JdbcProfile, JdbcType} import slick.lifted.{ColumnOrdered, WrappingQuery} import slick.util.ConstArray + import scala.concurrent.duration.Duration import scala.concurrent.{Await, ExecutionContext, Future, Promise} import scala.util.{Failure, Success, Try} diff --git a/app/db/access/ModelAccess.scala b/app/db/access/ModelAccess.scala index 95110d058..497ae68be 100644 --- a/app/db/access/ModelAccess.scala +++ b/app/db/access/ModelAccess.scala @@ -4,6 +4,7 @@ import db.ModelFilter.IdFilter import db.impl.OrePostgresDriver.api._ import db.{Model, ModelFilter, ModelService} import slick.lifted.ColumnOrdered + import scala.concurrent.{ExecutionContext, Future} import util.functional.OptionT diff --git a/app/db/impl/access/ProjectBase.scala b/app/db/impl/access/ProjectBase.scala index cad52936d..5a98f14cb 100644 --- a/app/db/impl/access/ProjectBase.scala +++ b/app/db/impl/access/ProjectBase.scala @@ -6,7 +6,6 @@ import java.sql.Timestamp import java.util.Date import com.google.common.base.Preconditions._ - import db.impl.OrePostgresDriver.api._ import db.impl.{PageTable, ProjectTableMain, VersionTable} import db.{ModelBase, ModelService} diff --git a/app/db/impl/access/UserBase.scala b/app/db/impl/access/UserBase.scala index bdb482a86..3adbc61f8 100755 --- a/app/db/impl/access/UserBase.scala +++ b/app/db/impl/access/UserBase.scala @@ -12,6 +12,7 @@ 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 diff --git a/app/discourse/OreDiscourseApi.scala b/app/discourse/OreDiscourseApi.scala index aa4f4a212..944e412f1 100644 --- a/app/discourse/OreDiscourseApi.scala +++ b/app/discourse/OreDiscourseApi.scala @@ -4,13 +4,12 @@ import java.nio.file.Path import akka.actor.Scheduler import com.google.common.base.Preconditions.{checkArgument, checkNotNull} - import db.impl.access.ProjectBase import models.project.{Project, Version} import models.user.User import org.spongepowered.play.discourse.DiscourseApi - import util.StringUtils._ + import scala.concurrent.duration.FiniteDuration import scala.concurrent.{ExecutionContext, Future, Promise} import scala.util.{Failure, Success} diff --git a/app/discourse/RecoveryTask.scala b/app/discourse/RecoveryTask.scala index 38f10fb56..bc2972291 100644 --- a/app/discourse/RecoveryTask.scala +++ b/app/discourse/RecoveryTask.scala @@ -1,10 +1,10 @@ package discourse -import scala.concurrent.ExecutionContext - import akka.actor.Scheduler import db.impl.OrePostgresDriver.api._ import db.impl.access.ProjectBase + +import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration /** diff --git a/app/discourse/SpongeForums.scala b/app/discourse/SpongeForums.scala index c38f9d3c4..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,7 +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 = this.actorSystem.dispatcher + 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 53ca84a40..56eec11bd 100755 --- a/app/form/OreForms.scala +++ b/app/form/OreForms.scala @@ -8,9 +8,6 @@ import db.impl.OrePostgresDriver.api._ import form.organization.{OrganizationAvatarUpdate, OrganizationMembersUpdate, OrganizationRoleSetBuilder} import form.project._ import javax.inject.Inject - -import scala.concurrent.ExecutionContext - import models.api.ProjectApiKey import models.project.{Channel, Page} import models.project.Page._ @@ -23,6 +20,8 @@ import play.api.data.Forms._ 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 /** diff --git a/app/form/project/TChannelData.scala b/app/form/project/TChannelData.scala index bc0b6593f..696a5fa93 100644 --- a/app/form/project/TChannelData.scala +++ b/app/form/project/TChannelData.scala @@ -4,11 +4,11 @@ import models.project.{Channel, Project} import ore.Colors.Color import ore.OreConfig import ore.project.factory.ProjectFactory -import scala.concurrent.{ExecutionContext, Future} - import util.functional.{EitherT, OptionT} import util.instances.future._ +import scala.concurrent.{ExecutionContext, Future} + /** * Represents submitted [[Channel]] data. */ diff --git a/app/mail/Mailer.scala b/app/mail/Mailer.scala index 20dc44391..ffa3327a7 100644 --- a/app/mail/Mailer.scala +++ b/app/mail/Mailer.scala @@ -9,10 +9,9 @@ import javax.inject.{Inject, Singleton} import javax.mail.Message.RecipientType import javax.mail.Session import javax.mail.internet.{InternetAddress, MimeMessage} +import play.api.Configuration import scala.concurrent.ExecutionContext - -import play.api.Configuration import scala.concurrent.duration._ /** diff --git a/app/models/admin/VisibilityChange.scala b/app/models/admin/VisibilityChange.scala index 61ef64d37..4dbea7d92 100644 --- a/app/models/admin/VisibilityChange.scala +++ b/app/models/admin/VisibilityChange.scala @@ -8,11 +8,11 @@ import db.impl.model.OreModel import db.impl.table.ModelKeys._ import models.project.Page import models.user.User -import play.twirl.api.Html -import scala.concurrent.{ExecutionContext, Future} - import util.functional.OptionT import util.instances.future._ +import play.twirl.api.Html + +import scala.concurrent.{ExecutionContext, Future} case class VisibilityChange(override val id: Option[Int] = None, override val createdAt: Option[Timestamp] = None, diff --git a/app/models/project/DownloadWarning.scala b/app/models/project/DownloadWarning.scala index d06d85445..1bad9976a 100644 --- a/app/models/project/DownloadWarning.scala +++ b/app/models/project/DownloadWarning.scala @@ -4,18 +4,17 @@ import java.sql.Timestamp import com.github.tminglei.slickpg.InetString import com.google.common.base.Preconditions._ - import controllers.sugar.Bakery import db.Expirable import db.impl.DownloadWarningsTable import db.impl.model.OreModel import db.impl.table.ModelKeys._ import models.project.DownloadWarning.COOKIE -import play.api.mvc.Cookie -import scala.concurrent.{ExecutionContext, Future} - import util.functional.OptionT import util.instances.future._ +import play.api.mvc.Cookie + +import scala.concurrent.{ExecutionContext, Future} /** * Represents an instance of a warning that a client has landed on. Warnings diff --git a/app/models/project/Page.scala b/app/models/project/Page.scala index 33cdbb879..72a60e1b2 100644 --- a/app/models/project/Page.scala +++ b/app/models/project/Page.scala @@ -16,7 +16,6 @@ import com.vladsch.flexmark.html.renderer._ import com.vladsch.flexmark.html.{HtmlRenderer, LinkResolver, LinkResolverFactory} import com.vladsch.flexmark.parser.Parser import com.vladsch.flexmark.util.options.MutableDataSet - import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ import db.impl.PageTable @@ -29,10 +28,10 @@ import ore.permission.scope.ProjectScope import play.twirl.api.Html import util.StringUtils._ import util.instances.future._ -import scala.concurrent.{ExecutionContext, Future} - import util.functional.OptionT +import scala.concurrent.{ExecutionContext, Future} + /** * Represents a documentation page within a project. * diff --git a/app/models/project/Project.scala b/app/models/project/Project.scala index 3c4be90c5..f384a73a6 100755 --- a/app/models/project/Project.scala +++ b/app/models/project/Project.scala @@ -8,7 +8,6 @@ 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._ import db.impl._ @@ -36,7 +35,9 @@ import play.api.libs.json._ import play.twirl.api.Html import slick.lifted import slick.lifted.{Rep, TableQuery} + import scala.concurrent.{ExecutionContext, Future} + /** * Represents an Ore package. * diff --git a/app/models/project/ProjectSettings.scala b/app/models/project/ProjectSettings.scala index f321b0f24..6abf584ab 100644 --- a/app/models/project/ProjectSettings.scala +++ b/app/models/project/ProjectSettings.scala @@ -21,9 +21,8 @@ import play.api.cache.AsyncCacheApi import play.api.i18n.{Lang, MessagesApi} import slick.lifted.TableQuery import util.StringUtils._ -import scala.concurrent.{ExecutionContext, Future} -import util.functional.OptionT +import scala.concurrent.{ExecutionContext, Future} /** * Represents a [[Project]]'s settings. diff --git a/app/models/project/Version.scala b/app/models/project/Version.scala index 253c0f68b..d0d13a65d 100755 --- a/app/models/project/Version.scala +++ b/app/models/project/Version.scala @@ -4,7 +4,6 @@ import java.sql.Timestamp import java.time.Instant import com.google.common.base.Preconditions.{checkArgument, checkNotNull} - import db.ModelService import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ @@ -21,10 +20,10 @@ import ore.project.Dependency import play.twirl.api.Html import util.FileUtils import util.instances.future._ -import scala.concurrent.{ExecutionContext, Future} - import util.functional.OptionT +import scala.concurrent.{ExecutionContext, Future} + /** * Represents a single version of a Project. * diff --git a/app/models/statistic/StatEntry.scala b/app/models/statistic/StatEntry.scala index 3a983b0e1..16b3afb1a 100644 --- a/app/models/statistic/StatEntry.scala +++ b/app/models/statistic/StatEntry.scala @@ -4,16 +4,15 @@ import java.sql.Timestamp import com.github.tminglei.slickpg.InetString import com.google.common.base.Preconditions._ - 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.{ExecutionContext, Future} -import util.functional.OptionT +import scala.concurrent.{ExecutionContext, Future} /** * Represents a statistic entry in a StatTable. diff --git a/app/models/user/User.scala b/app/models/user/User.scala index 3a29fadc4..ba4062fd3 100644 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -3,7 +3,6 @@ package models.user import java.sql.Timestamp import com.google.common.base.Preconditions._ - import db.{ModelFilter, Named} import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ @@ -21,18 +20,17 @@ import ore.permission.scope._ import ore.user.Prompts.Prompt import ore.user.UserOwned import org.spongepowered.play.discourse.model.DiscourseUser - import play.api.mvc.Request 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, Future} import scala.util.control.Breaks._ -import util.functional.OptionT - /** * Represents a Sponge user. * diff --git a/app/models/viewhelper/HeaderData.scala b/app/models/viewhelper/HeaderData.scala index d031d2581..414313871 100644 --- a/app/models/viewhelper/HeaderData.scala +++ b/app/models/viewhelper/HeaderData.scala @@ -13,13 +13,12 @@ import play.api.cache.AsyncCacheApi import play.api.mvc.Request import slick.jdbc.JdbcBackend import slick.lifted.TableQuery -import scala.concurrent.{ExecutionContext, Future} - -import models.viewhelper.HeaderData.perms import util.functional.OptionT import util.instances.future._ import util.syntax._ +import scala.concurrent.{ExecutionContext, Future} + /** * Holds global user specific data - When a User is not authenticated a dummy is used */ diff --git a/app/models/viewhelper/OrganizationData.scala b/app/models/viewhelper/OrganizationData.scala index 52938f143..abc7a10e3 100644 --- a/app/models/viewhelper/OrganizationData.scala +++ b/app/models/viewhelper/OrganizationData.scala @@ -7,6 +7,7 @@ import ore.organization.OrganizationMember import ore.permission._ import play.api.cache.AsyncCacheApi import slick.jdbc.JdbcBackend + import scala.concurrent.{ExecutionContext, Future} import util.functional.OptionT diff --git a/app/models/viewhelper/ScopedOrganizationData.scala b/app/models/viewhelper/ScopedOrganizationData.scala index 083d50ba5..03d919107 100644 --- a/app/models/viewhelper/ScopedOrganizationData.scala +++ b/app/models/viewhelper/ScopedOrganizationData.scala @@ -5,6 +5,7 @@ import models.user.{Organization, User} import ore.permission.{Permission, _} import play.api.cache.AsyncCacheApi import slick.jdbc.JdbcBackend + import scala.concurrent.{ExecutionContext, Future} import util.functional.OptionT diff --git a/app/ore/StatTracker.scala b/app/ore/StatTracker.scala index d07f523bd..77edf5aa2 100755 --- a/app/ore/StatTracker.scala +++ b/app/ore/StatTracker.scala @@ -8,12 +8,12 @@ import db.ModelService import db.impl.access.{ProjectBase, UserBase} import db.impl.schema.StatSchema import javax.inject.Inject - import models.project.Version import models.statistic.{ProjectView, VersionDownload} import ore.StatTracker.COOKIE_NAME import play.api.cache.AsyncCacheApi import play.api.mvc.{RequestHeader, Result} + import scala.concurrent.{ExecutionContext, Future} /** diff --git a/app/ore/permission/PermissionPredicate.scala b/app/ore/permission/PermissionPredicate.scala index 36aafe800..42e6c73ae 100644 --- a/app/ore/permission/PermissionPredicate.scala +++ b/app/ore/permission/PermissionPredicate.scala @@ -5,6 +5,7 @@ import models.project.Project import models.user.User import ore.permission.role.RoleTypes import ore.permission.scope.ScopeSubject + import scala.concurrent.{ExecutionContext, Future} import util.FutureUtils diff --git a/app/ore/project/ProjectTask.scala b/app/ore/project/ProjectTask.scala index 4d17f5dba..9c990c211 100644 --- a/app/ore/project/ProjectTask.scala +++ b/app/ore/project/ProjectTask.scala @@ -2,14 +2,11 @@ package ore.project import java.sql.Timestamp import java.time.Instant - import javax.inject.{Inject, Singleton} -import scala.concurrent.ExecutionContext - import akka.actor.ActorSystem import scala.concurrent.duration._ - +import scala.concurrent.ExecutionContext import db.impl.OrePostgresDriver.api._ import db.impl.schema.ProjectSchema import db.{ModelFilter, ModelService} diff --git a/app/ore/project/factory/ProjectFactory.scala b/app/ore/project/factory/ProjectFactory.scala index 990e01745..bf0fa1ecc 100755 --- a/app/ore/project/factory/ProjectFactory.scala +++ b/app/ore/project/factory/ProjectFactory.scala @@ -5,13 +5,11 @@ import java.nio.file.StandardCopyOption import akka.actor.ActorSystem import com.google.common.base.Preconditions._ - import db.ModelService import db.impl.OrePostgresDriver.api._ import db.impl.access.{ProjectBase, UserBase} import discourse.OreDiscourseApi import javax.inject.Inject - import models.project.TagColors.TagColor import models.project._ import models.user.role.ProjectRole @@ -25,20 +23,19 @@ import ore.project.factory.TagAlias.ProjectTag import ore.project.io.{InvalidPluginFileException, PluginFile, PluginUpload, ProjectFiles} import ore.user.notification.NotificationTypes import org.spongepowered.plugin.meta.PluginMetadata - 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, Future} import scala.concurrent.duration.Duration import scala.util.Try -import util.functional.{EitherT, OptionT} - /** * Manages the project and version creation pipeline. */ diff --git a/app/ore/rest/OreRestfulApi.scala b/app/ore/rest/OreRestfulApi.scala index f272b8968..c8e1bbc5f 100755 --- a/app/ore/rest/OreRestfulApi.scala +++ b/app/ore/rest/OreRestfulApi.scala @@ -8,7 +8,6 @@ import db.impl._ import db.impl.access.{ProjectBase, UserBase} import db.impl.schema.{ProjectSchema, ProjectTag} import javax.inject.Inject - import models.project._ import models.user.User import models.user.role.ProjectRole diff --git a/app/ore/user/UserSyncTask.scala b/app/ore/user/UserSyncTask.scala index 5b176334c..cbb24f3a7 100644 --- a/app/ore/user/UserSyncTask.scala +++ b/app/ore/user/UserSyncTask.scala @@ -4,8 +4,8 @@ import akka.actor.ActorSystem import db.ModelService import db.impl.access.UserBase import javax.inject.{Inject, Singleton} - import ore.OreConfig + import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ diff --git a/app/ore/user/notification/InviteFilters.scala b/app/ore/user/notification/InviteFilters.scala index d8032fd92..aa91afe91 100644 --- a/app/ore/user/notification/InviteFilters.scala +++ b/app/ore/user/notification/InviteFilters.scala @@ -2,6 +2,7 @@ package ore.user.notification import models.user.User import models.user.role.RoleModel + import scala.concurrent.{ExecutionContext, Future} import scala.language.implicitConversions diff --git a/app/ore/user/notification/NotificationFilters.scala b/app/ore/user/notification/NotificationFilters.scala index 36d636858..8c4d296ca 100644 --- a/app/ore/user/notification/NotificationFilters.scala +++ b/app/ore/user/notification/NotificationFilters.scala @@ -3,6 +3,7 @@ package ore.user.notification import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ import models.user.Notification + import scala.concurrent.{ExecutionContext, Future} import scala.language.implicitConversions diff --git a/app/security/spauth/SingleSignOnConsumer.scala b/app/security/spauth/SingleSignOnConsumer.scala index 9fe38523a..6fbbd16e1 100644 --- a/app/security/spauth/SingleSignOnConsumer.scala +++ b/app/security/spauth/SingleSignOnConsumer.scala @@ -8,18 +8,16 @@ import java.util.Base64 import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import javax.inject.Inject - import org.apache.commons.codec.binary.Hex - import play.api.Configuration import play.api.http.Status import play.api.libs.ws.WSClient -import scala.concurrent.duration._ -import scala.concurrent.{Await, ExecutionContext, Future} - import util.functional.OptionT import util.instances.future._ +import scala.concurrent.duration._ +import scala.concurrent.{Await, ExecutionContext, Future} + /** * Manages authentication to Sponge services. */ diff --git a/app/security/spauth/SpongeAuthApi.scala b/app/security/spauth/SpongeAuthApi.scala index b88236678..caf2e996f 100644 --- a/app/security/spauth/SpongeAuthApi.scala +++ b/app/security/spauth/SpongeAuthApi.scala @@ -4,19 +4,18 @@ 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, Future} import scala.concurrent.duration._ -import _root_.util.functional.{EitherT, OptionT} -import _root_.util.instances.future._ - /** * Interfaces with the SpongeAuth Web API */ diff --git a/app/views/users/admin/visibility.scala.html b/app/views/users/admin/visibility.scala.html index dbbe5e6b9..8d8125752 100644 --- a/app/views/users/admin/visibility.scala.html +++ b/app/views/users/admin/visibility.scala.html @@ -8,7 +8,7 @@ @* visChange4 is users.get(lastVisibilityChange.get.createdBy.getOrElse(1)).map(_.username) *@ @import ore.permission.Permission @(needsApproval: Seq[(Project, Map[Permission, Boolean], Option[VisibilityChange], String, Option[VisibilityChange], String)], - waitingProjects: Seq[(Project, VisibilityChange, Option[VisibilityChange], String)])(implicit messages: Messages, request: OreRequest[_], config: OreConfig) + waitingProjects: Seq[(Project, Option[VisibilityChange], Option[VisibilityChange], String)])(implicit messages: Messages, request: OreRequest[_], config: OreConfig) @projectRoutes = @{controllers.project.routes.Projects} @@ -99,23 +99,25 @@

    Waiting Changes

    } @waitingProjects.map { case (project, lastChangeRequest, lastVisibilityChange, lastVisibilityChanger) => -
  • -
    -
    - - @lastVisibilityChanger - requested changes on - - @project.ownerName/@project.slug - - -

    - Requests: - @lastChangeRequest.renderComment() -

    + @lastChangeRequest.map { lastRequest => +
  • +
    +
    + + @lastVisibilityChanger + requested changes on + + @project.ownerName/@project.slug + + +

    + Requests: + @lastRequest.renderComment() +

    +
    - -
  • + + } } From 476d264596251f2b9d599a46e17b6d41e2e1a04a Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 26 Apr 2018 21:15:21 +0200 Subject: [PATCH 16/18] Remove some TODOs --- app/controllers/ApiController.scala | 2 +- app/controllers/Organizations.scala | 29 ++++----- app/controllers/Users.scala | 10 +-- app/controllers/project/Versions.scala | 3 +- app/db/impl/access/UserBase.scala | 2 +- app/models/user/User.scala | 82 +++++++++++++------------ app/models/viewhelper/ProjectData.scala | 6 +- app/ore/rest/OreRestfulApi.scala | 5 +- app/ore/user/UserSyncTask.scala | 3 +- 9 files changed, 71 insertions(+), 71 deletions(-) diff --git a/app/controllers/ApiController.scala b/app/controllers/ApiController.scala index 11edb7576..0c0023c11 100644 --- a/app/controllers/ApiController.scala +++ b/app/controllers/ApiController.scala @@ -130,7 +130,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) } } diff --git a/app/controllers/Organizations.scala b/app/controllers/Organizations.scala index 012c51437..3213407f1 100755 --- a/app/controllers/Organizations.scala +++ b/app/controllers/Organizations.scala @@ -87,22 +87,19 @@ class Organizations @Inject()(forms: OreForms, */ def setInviteStatus(id: Int, status: String) = Authenticated.async { implicit request => val user = request.user - user.organizationRoles.get(id).semiFlatMap { role => - //TODO: Why access the organization when it's only used for one of the statuses? - 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) } diff --git a/app/controllers/Users.scala b/app/controllers/Users.scala index d3d5351d1..a744f3e3d 100755 --- a/app/controllers/Users.scala +++ b/app/controllers/Users.scala @@ -79,12 +79,12 @@ class Users @Inject()(fakeUser: FakeUser, // Redirected from SpongeSSO, decode SSO payload and convert to Ore user this.sso.authenticate(sso.get, sig.get)(isNonceValid).semiFlatMap { spongeUser => // Complete authentication + val fromSponge = User.fromSponge(spongeUser) for { - fromSponge <- User.fromSponge(spongeUser) - getOrCreate <- this.users.getOrCreate(fromSponge) - pulledForum <- getOrCreate.pullForumData() //TODO These two pull methods at the moment just return this at the end. - pulledSponge <- pulledForum.pullSpongeData() //Should their results be ignored and getOrCreate be used from there on? - result <- this.redirectBack(request.flash.get("url").getOrElse("/"), pulledSponge) + 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")) } diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index 45fbf71ab..4439f1083 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -249,8 +249,7 @@ class Versions @Inject()(stats: StatTracker, .flatMap(pendingVersion => pendingOrReal(author, slug).map(pendingVersion -> _)) .semiFlatMap { case (pendingVersion, Left(pending)) => - ProjectData.of(request, pending) - .map(data => (None, data, pendingVersion)) + 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)) diff --git a/app/db/impl/access/UserBase.scala b/app/db/impl/access/UserBase.scala index 3adbc61f8..f38f2898c 100755 --- a/app/db/impl/access/UserBase.scala +++ b/app/db/impl/access/UserBase.scala @@ -45,7 +45,7 @@ class UserBase(override val service: ModelService, */ def withName(username: String)(implicit ec: ExecutionContext): OptionT[Future, User] = { this.find(equalsIgnoreCase(_.name, username)).orElse { - this.auth.getUser(username).semiFlatMap(User.fromSponge).semiFlatMap(getOrCreate) + this.auth.getUser(username).map(User.fromSponge).semiFlatMap(getOrCreate) } } diff --git a/app/models/user/User.scala b/app/models/user/User.scala index ba4062fd3..2f429a182 100644 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -362,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) + } } /** @@ -404,9 +400,9 @@ case class User(override val id: Option[Int] = None, * * @return This user */ - def pullForumData()(implicit ec: ExecutionContext): Future[User] = { + def pullForumData()(implicit ec: ExecutionContext): Future[Unit] = { // Exceptions are ignored - OptionT(this.forums.fetchUser(this.name).recover{case _: Exception => None}).semiFlatMap(fill).getOrElse(this) + OptionT(this.forums.fetchUser(this.name).recover{case _: Exception => None}).cata((), fill) } /** @@ -414,8 +410,8 @@ case class User(override val id: Option[Int] = None, * * @return This user */ - def pullSpongeData()(implicit ec: ExecutionContext): Future[User] = { - this.auth.getUser(this.name).semiFlatMap(fill).getOrElse(throw new Exception("user doesn't exist on SpongeAuth?")) + def pullSpongeData()(implicit ec: ExecutionContext): Future[Unit] = { + this.auth.getUser(this.name).cata((), fill) } /** @@ -601,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)(implicit ec: ExecutionContext) = 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, ec: ExecutionContext) = 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/ProjectData.scala b/app/models/viewhelper/ProjectData.scala index f74f1ff3d..70885603e 100644 --- a/app/models/viewhelper/ProjectData.scala +++ b/app/models/viewhelper/ProjectData.scala @@ -49,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 @@ -76,9 +76,9 @@ object ProjectData { lastVisibilityChange, lastVisibilityChangeUser) - //TODO: Why Future here? - Future.successful(data) + data } + def of[A](project: Project)(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext): Future[ProjectData] = { implicit val userBase = project.userBase diff --git a/app/ore/rest/OreRestfulApi.scala b/app/ore/rest/OreRestfulApi.scala index c8e1bbc5f..c1de580c7 100755 --- a/app/ore/rest/OreRestfulApi.scala +++ b/app/ore/rest/OreRestfulApi.scala @@ -224,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) => @@ -243,7 +243,6 @@ trait OreRestfulApi { val limited = filtered.drop(offset.getOrElse(0)).take(lim) - //TODO: Why is this an Option again? for { data <- service.DB.db.run(limited.result) // Get Project Version Channel and AuthorName vTags <- service.DB.db.run(queryVersionTags(data.map(_._3)).result).map { p => p.groupBy(_._1) mapValues (_.map(_._2)) } @@ -251,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) } } diff --git a/app/ore/user/UserSyncTask.scala b/app/ore/user/UserSyncTask.scala index cbb24f3a7..d748b4e3f 100644 --- a/app/ore/user/UserSyncTask.scala +++ b/app/ore/user/UserSyncTask.scala @@ -36,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") } From 2e40d50212600d0894a5719caaa8f8d7b5bacbf0 Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 10 May 2018 01:15:07 +0200 Subject: [PATCH 17/18] Fix syncSso and user EitherT in it --- app/controllers/ApiController.scala | 69 ++++++++++++----------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/app/controllers/ApiController.scala b/app/controllers/ApiController.scala index 0c0023c11..bb7bda7bf 100644 --- a/app/controllers/ApiController.scala +++ b/app/controllers/ApiController.scala @@ -25,6 +25,7 @@ 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 @@ -280,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 } } From c2e186d0fb780538bec0238f254f7f8f8a2aefeb Mon Sep 17 00:00:00 2001 From: Unknown Date: Sat, 30 Jun 2018 23:34:06 +0200 Subject: [PATCH 18/18] Fix some errors, add capability to return multiple errors in results, and some more cleanup --- app/controllers/project/Channels.scala | 24 +++++----- app/controllers/project/Versions.scala | 28 ++++++------ app/controllers/sugar/ActionHelpers.scala | 16 +++++++ app/db/impl/access/ProjectBase.scala | 2 +- app/form/project/TChannelData.scala | 54 +++++++++++------------ app/models/project/Channel.scala | 5 +++ app/views/utils/alert.scala.html | 7 ++- conf/messages | 3 ++ 8 files changed, 79 insertions(+), 60 deletions(-) diff --git a/app/controllers/project/Channels.scala b/app/controllers/project/Channels.scala index d126c11ee..4aaf4465a 100755 --- a/app/controllers/project/Channels.scala +++ b/app/controllers/project/Channels.scala @@ -84,8 +84,8 @@ class Channels @Inject()(forms: OreForms, implicit val project: Project = request.data.project val res = for { - channelData <- bindFormEitherT[Future](this.forms.ChannelEdit)(hasErrors => Redirect(self.showList(author, slug)).withError(hasErrors.errors.head.message)) - _ <- channelData.saveTo(channelName).toLeft(()).leftMap(error => Redirect(self.showList(author, slug)).withError(error)) + 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 @@ -103,30 +103,26 @@ class Channels @Inject()(forms: OreForms, def delete(author: String, slug: String, channelName: String) = ChannelEditAction(author, slug).async { implicit request => implicit val data = request.data EitherT.right[Status](data.project.channels.all) - .filterOrElse(_.size == 1, Redirect(self.showList(author, slug)).withError("error.channel.last")) + .filterOrElse(_.size != 1, Redirect(self.showList(author, slug)).withError("error.channel.last")) .flatMap { channels => - EitherT.fromEither[Future](channels.find(c => c.name.equals(channelName)).toRight(NotFound)) + EitherT.fromEither[Future](channels.find(_.name == channelName).toRight(NotFound)) .semiFlatMap { channel => - (channel.versions.nonEmpty, Future.sequence(channels.map(_.versions.nonEmpty)).map(l => l.count(_ == true))) + (channel.versions.isEmpty, Future.traverse(channels.toSeq)(_.versions.nonEmpty).map(_.count(identity))) .parTupled .tupleRight(channel) } .filterOrElse( - { case ((nonEmpty, channelCount), _) => nonEmpty && channelCount == 1}, + { case ((emptyChannel, nonEmptyChannelCount), _) => + emptyChannel || nonEmptyChannelCount > 1}, Redirect(self.showList(author, slug)).withError("error.channel.lastNonEmpty") ) .map(_._2) .filterOrElse( - channel => { - val reviewedChannels = channels.filter(!_.isNonReviewed) - !channel.isNonReviewed && reviewedChannels.size <= 1 && reviewedChannels.contains(channel) - }, + channel => channel.isNonReviewed || channels.count(_.isReviewed) > 1, Redirect(self.showList(author, slug)).withError("error.channel.lastReviewed") ) - .map { channel => - this.projects.deleteChannel(channel) - Redirect(self.showList(author, slug)) - } + .semiFlatMap(channel => this.projects.deleteChannel(channel)) + .map(_ => Redirect(self.showList(author, slug))) }.merge } } diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index 4439f1083..756450f3f 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -420,26 +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 - } exists { - 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] = { @@ -472,7 +470,7 @@ class Versions @Inject()(stats: StatTracker, implicit val r = request.request val project = request.data.project getVersion(project, target) - .filterOrElse(_.isReviewed, Redirect(ShowProject(author, slug))) + .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 @@ -530,7 +528,7 @@ class Versions @Inject()(stats: StatTracker, ProjectAction(author, slug) async { request => implicit val r: OreRequest[_] = request.request getVersion(request.data.project, target) - .filterOrElse(_.isReviewed, Redirect(ShowProject(author, slug))) + .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 { 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/db/impl/access/ProjectBase.scala b/app/db/impl/access/ProjectBase.scala index 5a98f14cb..a1c1904dd 100644 --- a/app/db/impl/access/ProjectBase.scala +++ b/app/db/impl/access/ProjectBase.scala @@ -158,7 +158,7 @@ class ProjectBase(override val service: ModelService, _ = checkArgument(project.id.get == channel.projectId, "invalid project id", "") channels <- project.channels.all noVersion <- channel.versions.isEmpty - nonEmptyChannels <- Future.sequence(channels.map(_.versions.nonEmpty)).map(_.count(_ == true)) + 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) diff --git a/app/form/project/TChannelData.scala b/app/form/project/TChannelData.scala index 696a5fa93..3cd0ae163 100644 --- a/app/form/project/TChannelData.scala +++ b/app/form/project/TChannelData.scala @@ -6,6 +6,8 @@ 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} @@ -37,9 +39,9 @@ trait TChannelData { */ 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(_.exists(_.name.equalsIgnoreCase(this.channelName)), "A channel with that name already exists.") - .filterOrElse(_.exists(_.color == this.color), "A channel with that color already exists.") + .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)) } @@ -51,33 +53,27 @@ trait TChannelData { * @param project Project of channel * @return Error, if any */ - def saveTo(oldName: String)(implicit project: Project, ec: ExecutionContext): OptionT[Future, String] = { - OptionT( - project.channels.all.map { channels => - val channel = channels.find(_.name.equalsIgnoreCase(oldName)).get - val colorChan = channels.find(_.color.equals(this.color)) - val colorTaken = colorChan.exists(_ != channel) - if (colorTaken) { - Some("A channel with that color already exists.") - } else { - val nameChan = channels.find(_.name.equalsIgnoreCase(this.channelName)) - val nameTaken = nameChan.exists(_ != 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/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/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!