Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Project and TaskType names unique by Organization #5334

Merged
merged 16 commits into from
Apr 8, 2021
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Added a link to dataset view mode from annotation mode info tab. [#5262](https://github.com/scalableminds/webknossos/pull/5262)
- Added the possibility to export also volume annotations as tiff (if long-runnings jobs are enabled). [#5246](https://github.com/scalableminds/webknossos/pull/5246)
- WKW Dataset uploads with missing mag or layer dir no longer fail, instead the paths are automatically added (defaults to color/1). [#5285](https://github.com/scalableminds/webknossos/pull/5285)
- The names of Task Types and Projects no longer need to be globally unique, instead only within their respective organization. [#5334](https://github.com/scalableminds/webknossos/pull/5334)

### Changed
- Measured distances will be shown in voxel space, too. [#5240](https://github.com/scalableminds/webknossos/pull/5240)
- In the new REST API version 4, projects are no longer referenced by name, but instead by id. [#5334](https://github.com/scalableminds/webknossos/pull/5334)

### Fixed
- Fixed a regression in the task search which could lead to a frontend crash. [#5267](https://github.com/scalableminds/webknossos/pull/5267)
Expand Down
1 change: 1 addition & 0 deletions MIGRATIONS.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md).
### Postgres Evolutions:
- [066-publications-foreign-key.sql](conf/evolutions/066-publications-foreign-key.sql)
- [067-drop-analytics.sql](conf/evolutions/067-drop-analytics.sql)
- [068-tasktype-project-unique-per-orga.sql](conf/evolutions/068-tasktype-project-unique-per-orga.sql)
4 changes: 2 additions & 2 deletions app/controllers/InitialDataController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ Samplecountry
"sampleTaskType",
"Check those cells out!"
)
for { _ <- taskTypeDAO.insertOne(taskType) } yield ()
for { _ <- taskTypeDAO.insertOne(taskType, defaultOrganization._id) } yield ()
} else Fox.successful(())
}.toFox

Expand All @@ -207,7 +207,7 @@ Samplecountry
paused = false,
Some(5400000),
isBlacklistedFromReport = false)
for { _ <- projectDAO.insertOne(project) } yield ()
for { _ <- projectDAO.insertOne(project, defaultOrganization._id) } yield ()
}
} else Fox.successful(())
}.toFox
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/JobsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ class JobDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext)
if (celeryJobIds.isEmpty) Fox.successful(List())
else {
for {
rList <- run(
r <- run(
sql"select #$columns from #$existingCollectionName where celeryJobId in #${writeStructTupleWithQuotes(celeryJobIds)}"
.as[JobsRow])
parsed <- Fox.combined(rList.toList.map(parse))
parsed <- parseAll(r)
} yield parsed
}

Expand Down
85 changes: 83 additions & 2 deletions app/controllers/LegacyApiController.scala
Original file line number Diff line number Diff line change
@@ -1,22 +1,103 @@
package controllers

import com.mohiva.play.silhouette.api.Silhouette
import com.scalableminds.util.tools.Fox
import javax.inject.Inject
import models.project.ProjectDAO
import models.task.{TaskDAO, TaskService}
import oxalis.security.WkEnv
import play.api.http.HttpEntity
import play.api.libs.json.{JsArray, JsObject, JsValue, Json}
import play.api.mvc.{Action, AnyContent, PlayBodyParsers, Result}
import utils.WkConf
import utils.ObjectId

import scala.concurrent.ExecutionContext

class LegacyApiController @Inject()(annotationController: AnnotationController,
taskController: TaskController,
userController: UserController,
conf: WkConf,
projectController: ProjectController,
projectDAO: ProjectDAO,
taskDAO: TaskDAO,
taskService: TaskService,
sil: Silhouette[WkEnv])(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers)
extends Controller {

/* to provide v3, find projects by name */

def projectRead(name: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
project <- projectDAO.findOneByNameAndOrganization(name, request.identity._organization)
result <- projectController.read(project._id.toString)(request)
} yield result
}

def projectDelete(name: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
project <- projectDAO.findOneByNameAndOrganization(name, request.identity._organization)
result <- projectController.delete(project._id.toString)(request)
} yield result
}

def projectUpdate(name: String): Action[JsValue] = sil.SecuredAction.async(parse.json) { implicit request =>
for {
project <- projectDAO.findOneByNameAndOrganization(name, request.identity._organization)
result <- projectController.update(project._id.toString)(request)
} yield result
}

def projectTasksForProject(name: String,
limit: Option[Int] = None,
pageNumber: Option[Int] = None,
includeTotalCount: Option[Boolean]): Action[AnyContent] =
sil.SecuredAction.async { implicit request =>
for {
project <- projectDAO.findOneByNameAndOrganization(name, request.identity._organization)
result <- projectController.tasksForProject(project._id.toString, limit, pageNumber, includeTotalCount)(request)
} yield result
}

def projectIncrementEachTasksInstances(name: String, delta: Option[Long]): Action[AnyContent] =
sil.SecuredAction.async { implicit request =>
for {
project <- projectDAO.findOneByNameAndOrganization(name, request.identity._organization)
result <- projectController.incrementEachTasksInstances(project._id.toString, delta)(request)
} yield result
}

def projectPause(name: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
project <- projectDAO.findOneByNameAndOrganization(name, request.identity._organization)
result <- projectController.pause(project._id.toString)(request)
} yield result
}

def projectResume(name: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
project <- projectDAO.findOneByNameAndOrganization(name, request.identity._organization)
result <- projectController.resume(project._id.toString)(request)
} yield result
}

def taskListTasks: Action[JsValue] = sil.SecuredAction.async(parse.json) { implicit request =>
for {
userIdOpt <- Fox.runOptional((request.body \ "user").asOpt[String])(ObjectId.parse)
projectNameOpt = (request.body \ "project").asOpt[String]
projectOpt <- Fox.runOptional(projectNameOpt)(projectName =>
projectDAO.findOneByNameAndOrganization(projectName, request.identity._organization))
taskIdsOpt <- Fox.runOptional((request.body \ "ids").asOpt[List[String]])(ids =>
Fox.serialCombined(ids)(ObjectId.parse))
taskTypeIdOpt <- Fox.runOptional((request.body \ "taskType").asOpt[String])(ObjectId.parse)
randomizeOpt = (request.body \ "random").asOpt[Boolean]
tasks <- taskDAO.findAllByProjectAndTaskTypeAndIdsAndUser(projectOpt.map(_._id),
taskTypeIdOpt,
taskIdsOpt,
userIdOpt,
randomizeOpt)
jsResult <- Fox.serialCombined(tasks)(taskService.publicWrites(_))
} yield Ok(Json.toJson(jsResult))
}

/* to provide v2, insert automatic timestamp in finish and info request */

def annotationFinishV2(typ: String, id: String): Action[AnyContent] =
Expand Down
123 changes: 61 additions & 62 deletions app/controllers/ProjectController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ import com.mohiva.play.silhouette.api.actions.SecuredRequest
import com.scalableminds.util.accesscontext.GlobalAccessContext
import com.scalableminds.util.tools.DefaultConverters.BoolToOption
import com.scalableminds.util.tools.{Fox, FoxImplicits}
import javax.inject.Inject
import models.annotation.{AnnotationDAO, AnnotationService, AnnotationType}
import models.project._
import models.task._
import models.user.UserService
import net.liftweb.common.Empty
import oxalis.security.WkEnv
import play.api.i18n.Messages
import play.api.libs.json.{JsValue, Json}
import utils.ObjectId
import javax.inject.Inject
import play.api.mvc.{Action, AnyContent}
import utils.ObjectId

import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.ExecutionContext

class ProjectController @Inject()(projectService: ProjectService,
projectDAO: ProjectDAO,
Expand Down Expand Up @@ -50,70 +49,67 @@ class ProjectController @Inject()(projectService: ProjectService,
} yield Ok(Json.toJson(js))
}

def read(projectName: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
def read(id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> NOT_FOUND
projectIdValidated <- ObjectId.parse(id)
project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND
js <- projectService.publicWrites(project)
} yield {
Ok(js)
}
} yield Ok(js)
}

def delete(projectName: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
def delete(id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> NOT_FOUND
projectIdValidated <- ObjectId.parse(id)
project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND
_ <- bool2Fox(project.isDeletableBy(request.identity)) ?~> "project.remove.notAllowed" ~> FORBIDDEN
_ <- projectService.deleteOne(project._id) ?~> "project.remove.failure"
} yield {
JsonOk(Messages("project.remove.success"))
}
} yield JsonOk(Messages("project.remove.success"))
}

def create: Action[JsValue] = sil.SecuredAction.async(parse.json) { implicit request =>
withJsonBodyUsing(Project.projectPublicReads) { project =>
projectDAO.findOneByName(project.name)(GlobalAccessContext).futureBox.flatMap {
case Empty =>
for {
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed" ~> FORBIDDEN
_ <- projectDAO.insertOne(project) ?~> "project.creation.failed"
js <- projectService.publicWrites(project)
} yield Ok(js)
case _ =>
Future.successful(JsonBadRequest(Messages("project.name.alreadyTaken")))
}
for {
_ <- projectDAO
.findOneByNameAndOrganization(project.name, request.identity._organization)(GlobalAccessContext)
.reverse ?~> "project.name.alreadyTaken"
_ <- Fox
.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed" ~> FORBIDDEN
_ <- projectDAO.insertOne(project, request.identity._organization) ?~> "project.creation.failed"
js <- projectService.publicWrites(project)
} yield Ok(js)
}
}

def update(projectName: String): Action[JsValue] = sil.SecuredAction.async(parse.json) { implicit request =>
def update(id: String): Action[JsValue] = sil.SecuredAction.async(parse.json) { implicit request =>
withJsonBodyUsing(Project.projectPublicReads) { updateRequest =>
for {
project <- projectDAO.findOneByName(projectName)(GlobalAccessContext) ?~> Messages("project.notFound",
projectName) ~> NOT_FOUND
projectIdValidated <- ObjectId.parse(id)
project <- projectDAO.findOne(projectIdValidated)(GlobalAccessContext) ?~> "project.notFound" ~> NOT_FOUND
_ <- Fox
.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed" ~> FORBIDDEN
_ <- projectDAO.updateOne(updateRequest.copy(_id = project._id, paused = project.paused)) ?~> Messages(
"project.update.failed",
projectName)
updated <- projectDAO.findOneByName(projectName)
_ <- projectDAO
.updateOne(updateRequest.copy(_id = project._id, paused = project.paused)) ?~> "project.update.failed"
updated <- projectDAO.findOne(projectIdValidated)
js <- projectService.publicWrites(updated)
} yield Ok(js)
}
}

def pause(projectName: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
updatePauseStatus(projectName, isPaused = true)
def pause(id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
updatePauseStatus(id, isPaused = true)
}

def resume(projectName: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
updatePauseStatus(projectName, isPaused = false)
def resume(id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
updatePauseStatus(id, isPaused = false)
}

private def updatePauseStatus(projectName: String, isPaused: Boolean)(implicit request: SecuredRequest[WkEnv, _]) =
private def updatePauseStatus(id: String, isPaused: Boolean)(implicit request: SecuredRequest[WkEnv, _]) =
for {
project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> NOT_FOUND
projectIdValidated <- ObjectId.parse(id)
project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed" ~> FORBIDDEN
_ <- projectDAO.updatePaused(project._id, isPaused) ?~> Messages("project.update.failed", projectName)
updatedProject <- projectDAO.findOne(project._id) ?~> Messages("project.notFound", projectName)
_ <- projectDAO.updatePaused(project._id, isPaused) ?~> "project.update.failed"
updatedProject <- projectDAO.findOne(projectIdValidated)
js <- projectService.publicWrites(updatedProject)
} yield Ok(js)

Expand All @@ -134,13 +130,14 @@ class ProjectController @Inject()(projectService: ProjectService,
}
}

def tasksForProject(projectName: String,
def tasksForProject(id: String,
limit: Option[Int] = None,
pageNumber: Option[Int] = None,
includeTotalCount: Option[Boolean]): Action[AnyContent] =
sil.SecuredAction.async { implicit request =>
for {
project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> NOT_FOUND
projectIdValidated <- ObjectId.parse(id)
project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed" ~> FORBIDDEN
tasks <- taskDAO.findAllByProject(project._id, limit.getOrElse(Int.MaxValue), pageNumber.getOrElse(0))
taskCount <- Fox.runOptional(includeTotalCount.flatMap(BoolToOption.convert))(_ =>
Expand All @@ -155,43 +152,45 @@ class ProjectController @Inject()(projectService: ProjectService,
}
}

def incrementEachTasksInstances(projectName: String, delta: Option[Long]): Action[AnyContent] =
def incrementEachTasksInstances(id: String, delta: Option[Long]): Action[AnyContent] =
sil.SecuredAction.async { implicit request =>
for {
_ <- bool2Fox(delta.getOrElse(1L) >= 0) ?~> "project.increaseTaskInstances.negative"
project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> NOT_FOUND
projectIdValidated <- ObjectId.parse(id)
project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND
_ <- taskDAO.incrementTotalInstancesOfAllWithProject(project._id, delta.getOrElse(1L))
openInstancesAndTime <- taskDAO.countOpenInstancesAndTimeForProject(project._id)
js <- projectService.publicWritesWithStatus(project, openInstancesAndTime._1, openInstancesAndTime._2)
} yield Ok(js)
}

def usersWithActiveTasks(projectName: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
def usersWithActiveTasks(id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
_ <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> NOT_FOUND
usersWithActiveTasks <- projectDAO.findUsersWithActiveTasks(projectName)
projectIdValidated <- ObjectId.parse(id)
project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND
usersWithActiveTasks <- projectDAO.findUsersWithActiveTasks(project._id)
} yield {
Ok(Json.toJson(usersWithActiveTasks.map(tuple =>
Json.obj("email" -> tuple._1, "firstName" -> tuple._2, "lastName" -> tuple._3, "activeTasks" -> tuple._4))))
}
}

def transferActiveTasks(projectName: String): Action[JsValue] = sil.SecuredAction.async(parse.json) {
implicit request =>
for {
project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> NOT_FOUND
_ <- Fox
.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed" ~> FORBIDDEN
newUserId <- (request.body \ "userId").asOpt[String].toFox ?~> "user.id.notFound" ~> NOT_FOUND
newUserIdValidated <- ObjectId.parse(newUserId)
activeAnnotations <- annotationDAO.findAllActiveForProject(project._id)
updated <- Fox.serialCombined(activeAnnotations) { id =>
annotationService.transferAnnotationToUser(AnnotationType.Task.toString,
id.toString,
newUserIdValidated,
request.identity)
}
} yield Ok
def transferActiveTasks(id: String): Action[JsValue] = sil.SecuredAction.async(parse.json) { implicit request =>
for {
projectIdValidated <- ObjectId.parse(id)
project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND
_ <- Fox
.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed" ~> FORBIDDEN
newUserId <- (request.body \ "userId").asOpt[String].toFox ?~> "user.id.notFound" ~> NOT_FOUND
newUserIdValidated <- ObjectId.parse(newUserId)
activeAnnotations <- annotationDAO.findAllActiveForProject(project._id)
_ <- Fox.serialCombined(activeAnnotations) { id =>
annotationService.transferAnnotationToUser(AnnotationType.Task.toString,
id.toString,
newUserIdValidated,
request.identity)
}
} yield Ok

}
}
Loading