From 7a0e07b7e4d3c23a1fefca2216654c1903bf3e39 Mon Sep 17 00:00:00 2001 From: Florian M Date: Thu, 8 Apr 2021 11:29:31 +0200 Subject: [PATCH] Revert "Make Project and TaskType names unique by Organization (#5334)" This reverts commit d06b56f6d590d5ced63fb1f93ad87deed4d78e7c. --- CHANGELOG.unreleased.md | 2 - MIGRATIONS.unreleased.md | 1 - app/controllers/InitialDataController.scala | 4 +- app/controllers/JobsController.scala | 4 +- app/controllers/LegacyApiController.scala | 85 +----------- app/controllers/ProjectController.scala | 123 +++++++++--------- app/controllers/TaskController.scala | 6 +- app/controllers/TaskTypeController.scala | 2 +- app/models/annotation/Annotation.scala | 20 +-- .../annotation/AnnotationIdentifier.scala | 2 +- app/models/annotation/TracingStore.scala | 5 +- app/models/binary/DataSet.scala | 28 ++-- app/models/binary/DataSetService.scala | 2 +- app/models/binary/DataStore.scala | 14 +- app/models/mesh/Mesh.scala | 6 +- app/models/organization/Organization.scala | 9 +- app/models/project/Project.scala | 45 +++---- app/models/task/Task.scala | 24 ++-- app/models/task/TaskCreationService.scala | 18 +-- app/models/task/TaskType.scala | 21 +-- app/models/team/Team.scala | 14 +- app/models/user/MultiUser.scala | 9 +- app/models/user/User.scala | 7 +- app/utils/SQLHelpers.scala | 12 -- .../069-tasktype-project-unique-per-orga.sql | 27 ---- .../069-tasktype-project-unique-per-orga.sql | 19 --- conf/messages | 4 +- conf/webknossos.latest.routes | 18 +-- conf/webknossos.versioned.routes | 11 -- docs/rest_api.md | 54 +++----- frontend/javascripts/admin/admin_rest_api.js | 27 ++-- .../admin/project/project_create_view.js | 16 +-- .../admin/project/project_list_view.js | 16 +-- .../admin/task/task_search_form.js | 12 +- frontend/javascripts/router.js | 10 +- .../backend-snapshot-tests/projects.e2e.js | 38 +++--- .../teamstructure.e2e.js | 6 +- .../backend-snapshot-tests/projects.e2e.js.md | 12 +- .../projects.e2e.js.snap | Bin 2673 -> 2653 bytes test/db/projects.csv | 10 +- test/db/taskTypes.csv | 6 +- tools/postgres/schema.sql | 12 +- .../com/scalableminds/util/tools/Fox.scala | 4 +- 43 files changed, 310 insertions(+), 455 deletions(-) delete mode 100644 conf/evolutions/069-tasktype-project-unique-per-orga.sql delete mode 100644 conf/evolutions/reversions/069-tasktype-project-unique-per-orga.sql diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index bfaf3a1c179..bfa57c4e5de 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -11,13 +11,11 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released [Commits](https://github.com/scalableminds/webknossos/compare/21.04.0...HEAD) ### Added -- 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) - Upgraded UI library antd to version 4, creating a slightly more modern look and behavior of many UI elements. [#5350](https://github.com/scalableminds/webknossos/pull/5350) ### Changed - webKnossos is now part of the [image.sc support community](https://forum.image.sc/tag/webknossos). [#5332](https://github.com/scalableminds/webknossos/pull/5332) - Meshes that are imported by the user in the meshes tab are now rendered the same way as generated isosurface meshes. [#5326](https://github.com/scalableminds/webknossos/pull/5326) -- 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 bug where some values in the project list were displayed incorrectly after pausing/unpausing the project. [#5339](https://github.com/scalableminds/webknossos/pull/5339) diff --git a/MIGRATIONS.unreleased.md b/MIGRATIONS.unreleased.md index 165fdb878a9..41f52d1716c 100644 --- a/MIGRATIONS.unreleased.md +++ b/MIGRATIONS.unreleased.md @@ -10,4 +10,3 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md). ### Postgres Evolutions: - [068-pricing-plan.sql](conf/evolutions/068-pricing-plan.sql) -- [069-tasktype-project-unique-per-orga.sql](conf/evolutions/069-tasktype-project-unique-per-orga.sql) diff --git a/app/controllers/InitialDataController.scala b/app/controllers/InitialDataController.scala index ead5b807dc9..229d0e73555 100644 --- a/app/controllers/InitialDataController.scala +++ b/app/controllers/InitialDataController.scala @@ -192,7 +192,7 @@ Samplecountry "sampleTaskType", "Check those cells out!" ) - for { _ <- taskTypeDAO.insertOne(taskType, defaultOrganization._id) } yield () + for { _ <- taskTypeDAO.insertOne(taskType) } yield () } else Fox.successful(()) }.toFox @@ -208,7 +208,7 @@ Samplecountry paused = false, Some(5400000), isBlacklistedFromReport = false) - for { _ <- projectDAO.insertOne(project, defaultOrganization._id) } yield () + for { _ <- projectDAO.insertOne(project) } yield () } } else Fox.successful(()) }.toFox diff --git a/app/controllers/JobsController.scala b/app/controllers/JobsController.scala index 063c6b6e007..c4c3feaf36c 100644 --- a/app/controllers/JobsController.scala +++ b/app/controllers/JobsController.scala @@ -66,10 +66,10 @@ class JobDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) if (celeryJobIds.isEmpty) Fox.successful(List()) else { for { - r <- run( + rList <- run( sql"select #$columns from #$existingCollectionName where celeryJobId in #${writeStructTupleWithQuotes(celeryJobIds)}" .as[JobsRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(rList.toList.map(parse)) } yield parsed } diff --git a/app/controllers/LegacyApiController.scala b/app/controllers/LegacyApiController.scala index d47678de36c..8d2ac1127d9 100644 --- a/app/controllers/LegacyApiController.scala +++ b/app/controllers/LegacyApiController.scala @@ -1,103 +1,22 @@ 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.ObjectId +import utils.WkConf import scala.concurrent.ExecutionContext class LegacyApiController @Inject()(annotationController: AnnotationController, taskController: TaskController, userController: UserController, - projectController: ProjectController, - projectDAO: ProjectDAO, - taskDAO: TaskDAO, - taskService: TaskService, + conf: WkConf, 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] = diff --git a/app/controllers/ProjectController.scala b/app/controllers/ProjectController.scala index bdcaf875c83..7e48d856b3b 100644 --- a/app/controllers/ProjectController.scala +++ b/app/controllers/ProjectController.scala @@ -4,18 +4,19 @@ 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 play.api.mvc.{Action, AnyContent} import utils.ObjectId +import javax.inject.Inject +import play.api.mvc.{Action, AnyContent} -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext, Future} class ProjectController @Inject()(projectService: ProjectService, projectDAO: ProjectDAO, @@ -49,67 +50,70 @@ class ProjectController @Inject()(projectService: ProjectService, } yield Ok(Json.toJson(js)) } - def read(id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => + def read(projectName: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { - projectIdValidated <- ObjectId.parse(id) - project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND + project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> NOT_FOUND js <- projectService.publicWrites(project) - } yield Ok(js) + } yield { + Ok(js) + } } - def delete(id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => + def delete(projectName: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { - projectIdValidated <- ObjectId.parse(id) - project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND + project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> 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 => - 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) + 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"))) + } } } - def update(id: String): Action[JsValue] = sil.SecuredAction.async(parse.json) { implicit request => + def update(projectName: String): Action[JsValue] = sil.SecuredAction.async(parse.json) { implicit request => withJsonBodyUsing(Project.projectPublicReads) { updateRequest => for { - projectIdValidated <- ObjectId.parse(id) - project <- projectDAO.findOne(projectIdValidated)(GlobalAccessContext) ?~> "project.notFound" ~> NOT_FOUND + project <- projectDAO.findOneByName(projectName)(GlobalAccessContext) ?~> Messages("project.notFound", + projectName) ~> NOT_FOUND _ <- Fox .assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed" ~> FORBIDDEN - _ <- projectDAO - .updateOne(updateRequest.copy(_id = project._id, paused = project.paused)) ?~> "project.update.failed" - updated <- projectDAO.findOne(projectIdValidated) + _ <- projectDAO.updateOne(updateRequest.copy(_id = project._id, paused = project.paused)) ?~> Messages( + "project.update.failed", + projectName) + updated <- projectDAO.findOneByName(projectName) js <- projectService.publicWrites(updated) } yield Ok(js) } } - def pause(id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => - updatePauseStatus(id, isPaused = true) + def pause(projectName: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => + updatePauseStatus(projectName, isPaused = true) } - def resume(id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => - updatePauseStatus(id, isPaused = false) + def resume(projectName: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => + updatePauseStatus(projectName, isPaused = false) } - private def updatePauseStatus(id: String, isPaused: Boolean)(implicit request: SecuredRequest[WkEnv, _]) = + private def updatePauseStatus(projectName: String, isPaused: Boolean)(implicit request: SecuredRequest[WkEnv, _]) = for { - projectIdValidated <- ObjectId.parse(id) - project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND + project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> NOT_FOUND _ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed" ~> FORBIDDEN - _ <- projectDAO.updatePaused(project._id, isPaused) ?~> "project.update.failed" - updatedProject <- projectDAO.findOne(projectIdValidated) + _ <- projectDAO.updatePaused(project._id, isPaused) ?~> Messages("project.update.failed", projectName) + updatedProject <- projectDAO.findOne(project._id) ?~> Messages("project.notFound", projectName) js <- projectService.publicWrites(updatedProject) } yield Ok(js) @@ -130,14 +134,13 @@ class ProjectController @Inject()(projectService: ProjectService, } } - def tasksForProject(id: String, + def tasksForProject(projectName: String, limit: Option[Int] = None, pageNumber: Option[Int] = None, includeTotalCount: Option[Boolean]): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { - projectIdValidated <- ObjectId.parse(id) - project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND + project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> 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))(_ => @@ -152,45 +155,43 @@ class ProjectController @Inject()(projectService: ProjectService, } } - def incrementEachTasksInstances(id: String, delta: Option[Long]): Action[AnyContent] = + def incrementEachTasksInstances(projectName: String, delta: Option[Long]): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { _ <- bool2Fox(delta.getOrElse(1L) >= 0) ?~> "project.increaseTaskInstances.negative" - projectIdValidated <- ObjectId.parse(id) - project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND + project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> 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(id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => + def usersWithActiveTasks(projectName: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { - projectIdValidated <- ObjectId.parse(id) - project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND - usersWithActiveTasks <- projectDAO.findUsersWithActiveTasks(project._id) + _ <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> NOT_FOUND + usersWithActiveTasks <- projectDAO.findUsersWithActiveTasks(projectName) } yield { Ok(Json.toJson(usersWithActiveTasks.map(tuple => Json.obj("email" -> tuple._1, "firstName" -> tuple._2, "lastName" -> tuple._3, "activeTasks" -> tuple._4)))) } } - 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 + 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 } } diff --git a/app/controllers/TaskController.scala b/app/controllers/TaskController.scala index 3a5bbf48eef..3dd86bbe831 100755 --- a/app/controllers/TaskController.scala +++ b/app/controllers/TaskController.scala @@ -75,7 +75,7 @@ class TaskController @Inject()(taskCreationService: TaskCreationService, taskTypeIdValidated <- ObjectId.parse(params.taskTypeId) ?~> "taskType.id.invalid" taskType <- taskTypeDAO.findOne(taskTypeIdValidated) ?~> "taskType.notFound" ~> NOT_FOUND project <- projectDAO - .findOneByNameAndOrganization(params.projectName, request.identity._organization) ?~> "project.notFound" ~> NOT_FOUND + .findOneByName(params.projectName) ?~> Messages("project.notFound", params.projectName) ~> NOT_FOUND _ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) extractedFiles = nmlService.extractFromFiles(inputFiles.map(f => (f.ref.path.toFile, f.filename)), useZipName = false, @@ -132,12 +132,12 @@ class TaskController @Inject()(taskCreationService: TaskCreationService, def listTasks: Action[JsValue] = sil.SecuredAction.async(parse.json) { implicit request => for { userIdOpt <- Fox.runOptional((request.body \ "user").asOpt[String])(ObjectId.parse) - projectIdOpt <- Fox.runOptional((request.body \ "project").asOpt[String])(ObjectId.parse) + projectNameOpt = (request.body \ "project").asOpt[String] 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(projectIdOpt, + tasks <- taskDAO.findAllByProjectAndTaskTypeAndIdsAndUser(projectNameOpt, taskTypeIdOpt, taskIdsOpt, userIdOpt, diff --git a/app/controllers/TaskTypeController.scala b/app/controllers/TaskTypeController.scala index 2bfe6e45b14..001d5316928 100755 --- a/app/controllers/TaskTypeController.scala +++ b/app/controllers/TaskTypeController.scala @@ -37,7 +37,7 @@ class TaskTypeController @Inject()(taskTypeDAO: TaskTypeDAO, withJsonBodyUsing(taskTypePublicReads) { taskType => for { _ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, taskType._team)) ?~> "notAllowed" ~> FORBIDDEN - _ <- taskTypeDAO.insertOne(taskType, request.identity._organization) + _ <- taskTypeDAO.insertOne(taskType) js <- taskTypeService.publicWrites(taskType) } yield Ok(js) } diff --git a/app/models/annotation/Annotation.scala b/app/models/annotation/Annotation.scala index 7a0c1732430..55a321f1eb0 100755 --- a/app/models/annotation/Annotation.scala +++ b/app/models/annotation/Annotation.scala @@ -110,9 +110,10 @@ class AnnotationDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContex override def findOne(id: ObjectId)(implicit ctx: DBAccessContext): Fox[Annotation] = for { accessQuery <- readAccessQuery - r <- run( + rList <- run( sql"select #$columns from #$existingCollectionName where _id = ${id.id} and #$accessQuery".as[AnnotationsRow]) - parsed <- parseFirst(r, id) + r <- rList.headOption.toFox ?~> ("Could not find object " + id + " in " + collectionName) + parsed <- parse(r) ?~> ("SQLDAO Error: Could not parse database row for object " + id + " in " + collectionName) } yield parsed private def getStateQuery(isFinished: Option[Boolean]) = @@ -133,7 +134,7 @@ class AnnotationDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContex r <- run(sql"""select #$columns from #$existingCollectionName where _user = ${userId.id} and typ = '#${annotationType.toString}' and #$stateQuery and #$accessQuery order by _id desc limit $limit offset ${pageNumber * limit}""".as[AnnotationsRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed } @@ -164,7 +165,7 @@ class AnnotationDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContex join webknossos.tasks_ t on a._task = t._id where t._project = ${projectId.id} and a.typ = '#${AnnotationType.Task.toString}' and a.state = '#${AnnotationState.Finished.toString}'""" .as[AnnotationsRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed // Does not use access query (because they dont support prefixes). Use only after separate access check! @@ -188,17 +189,20 @@ class AnnotationDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContex sql"""select #$columns from #$existingCollectionName where _task = ${taskId.id} and typ = '#${typ.toString}' and state != '#${AnnotationState.Cancelled.toString}' and #$accessQuery""" .as[AnnotationsRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed def findOneByTracingId(tracingId: String)(implicit ctx: DBAccessContext): Fox[Annotation] = for { accessQuery <- readAccessQuery - r <- run( + rList <- run( sql"select #$columns from #$existingCollectionName where (skeletonTracingId = $tracingId or volumeTracingId = $tracingId) and #$accessQuery" .as[AnnotationsRow]) - parsed <- parseFirst(r, s"tracingId=$tracingId") - } yield parsed + r <- rList.headOption.toFox + parsed <- parse(r) + } yield { + parsed + } // count operations diff --git a/app/models/annotation/AnnotationIdentifier.scala b/app/models/annotation/AnnotationIdentifier.scala index f72b45984c4..2616f4e0c57 100644 --- a/app/models/annotation/AnnotationIdentifier.scala +++ b/app/models/annotation/AnnotationIdentifier.scala @@ -17,7 +17,7 @@ object AnnotationIdentifier extends FoxImplicits { def parse(typ: String, id: String)(implicit ec: ExecutionContext): Fox[AnnotationIdentifier] = for { - identifier <- ObjectId.parse(id) + identifier <- ObjectId.parse(id) ?~> ("Invalid ObjectId: " + id) typ <- AnnotationType.fromString(typ) ?~> ("Invalid AnnotationType: " + typ) } yield AnnotationIdentifier(typ, identifier) diff --git a/app/models/annotation/TracingStore.scala b/app/models/annotation/TracingStore.scala index abaa9035ab0..1c5a2e7ab1c 100644 --- a/app/models/annotation/TracingStore.scala +++ b/app/models/annotation/TracingStore.scala @@ -97,9 +97,10 @@ class TracingStoreDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionCont def findOneByUrl(url: String)(implicit ctx: DBAccessContext): Fox[TracingStore] = for { accessQuery <- readAccessQuery - r <- run( + rList <- run( sql"select #$columns from webknossos.tracingstores_ where url = $url and #$accessQuery".as[TracingstoresRow]) - parsed <- parseFirst(r, url) + r <- rList.headOption.toFox + parsed <- parse(r) } yield parsed def findFirst(implicit ctx: DBAccessContext): Fox[TracingStore] = diff --git a/app/models/binary/DataSet.scala b/app/models/binary/DataSet.scala index 9815e0fc5f7..20072e8ecfb 100755 --- a/app/models/binary/DataSet.scala +++ b/app/models/binary/DataSet.scala @@ -123,16 +123,17 @@ class DataSetDAO @Inject()(sqlClient: SQLClient, override def findOne(id: ObjectId)(implicit ctx: DBAccessContext): Fox[DataSet] = for { accessQuery <- readAccessQuery - r <- run( + rList <- run( sql"select #$columns from #$existingCollectionName where _id = ${id.id} and #$accessQuery".as[DatasetsRow]) - parsed <- parseFirst(r, id) + r <- rList.headOption.toFox ?~> ("Could not find object " + id + " in " + collectionName) + parsed <- parse(r) ?~> ("SQLDAO Error: Could not parse database row for object " + id + " in " + collectionName) } yield parsed override def findAll(implicit ctx: DBAccessContext): Fox[List[DataSet]] = for { accessQuery <- readAccessQuery r <- run(sql"select #$columns from #$existingCollectionName where #$accessQuery".as[DatasetsRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed def isEmpty: Fox[Boolean] = @@ -158,19 +159,20 @@ class DataSetDAO @Inject()(sqlClient: SQLClient, implicit ctx: DBAccessContext): Fox[DataSet] = for { accessQuery <- readAccessQuery - r <- run( + rList <- run( sql"select #$columns from #$existingCollectionName where name = $name and _organization = $organizationId and #$accessQuery" .as[DatasetsRow]) - parsed <- parseFirst(r, s"$organizationId/$name") + r <- rList.headOption.toFox + parsed <- parse(r) } yield parsed def findAllByNamesAndOrganization(names: List[String], organizationId: ObjectId)( implicit ctx: DBAccessContext): Fox[List[DataSet]] = for { accessQuery <- readAccessQuery - r <- run(sql"select #$columns from #$existingCollectionName where name in #${writeStructTupleWithQuotes( + rows <- run(sql"select #$columns from #$existingCollectionName where name in #${writeStructTupleWithQuotes( names.map(sanitize))} and _organization = $organizationId and #$accessQuery".as[DatasetsRow]).map(_.toList) - parsed <- parseAll(r) + parsed <- Fox.combined(rows.map(parse)) } yield parsed /* Disambiguation method for legacy URLs and NMLs: if the user has access to multiple datasets of the same name, use the oldest. @@ -334,7 +336,9 @@ class DataSetResolutionsDAO @Inject()(sqlClient: SQLClient)(implicit ec: Executi DatasetResolutions.filter(r => r._Dataset === dataSetId.id && r.datalayername === dataLayerName).result) .map(_.toList) rowsParsed <- Fox.combined(rows.map(parseRow)) ?~> "could not parse resolution row" - } yield rowsParsed + } yield { + rowsParsed + } def updateResolutions(_dataSet: ObjectId, dataLayersOpt: Option[List[DataLayer]]): Fox[Unit] = { val clearQuery = sqlu"delete from webknossos.dataSet_resolutions where _dataSet = ${_dataSet.id}" @@ -416,7 +420,9 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: for { rows <- run(DatasetLayers.filter(_._Dataset === dataSetId.id).result).map(_.toList) rowsParsed <- Fox.combined(rows.map(parseRow(_, dataSetId, skipResolutions))) - } yield rowsParsed + } yield { + rowsParsed + } def findOneByNameForDataSet(dataLayerName: String, dataSetId: ObjectId): Fox[DataLayer] = for { @@ -424,7 +430,9 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: .map(_.toList) firstRow <- rows.headOption.toFox ?~> ("Could not find data layer " + dataLayerName) parsed <- parseRow(firstRow, dataSetId) - } yield parsed + } yield { + parsed + } private def insertLayerQuery(_dataSet: ObjectId, layer: DataLayer): SqlAction[Int, NoStream, Effect] = layer match { diff --git a/app/models/binary/DataSetService.scala b/app/models/binary/DataSetService.scala index 73a95a04d2b..96c65f21098 100644 --- a/app/models/binary/DataSetService.scala +++ b/app/models/binary/DataSetService.scala @@ -48,7 +48,7 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO, def isProperDataSetName(name: String): Boolean = name.matches("[A-Za-z0-9_\\-]*") - def assertNewDataSetName(name: String, organizationId: ObjectId): Fox[Unit] = + def assertNewDataSetName(name: String, organizationId: ObjectId): Fox[Boolean] = dataSetDAO.findOneByNameAndOrganization(name, organizationId)(GlobalAccessContext).reverse def reserveDataSetName(dataSetName: String, organizationName: String, dataStore: DataStore): Fox[ObjectId] = { diff --git a/app/models/binary/DataStore.scala b/app/models/binary/DataStore.scala index c028cb7ba54..caf67b13138 100644 --- a/app/models/binary/DataStore.scala +++ b/app/models/binary/DataStore.scala @@ -114,22 +114,26 @@ class DataStoreDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext def findOneByName(name: String)(implicit ctx: DBAccessContext): Fox[DataStore] = for { accessQuery <- readAccessQuery - r <- run(sql"select #$columns from webknossos.datastores_ where name = $name and #$accessQuery".as[DatastoresRow]) - parsed <- parseFirst(r, name) + rList <- run( + sql"select #$columns from webknossos.datastores_ where name = $name and #$accessQuery".as[DatastoresRow]) + r <- rList.headOption.toFox + parsed <- parse(r) } yield parsed def findOneByUrl(url: String)(implicit ctx: DBAccessContext): Fox[DataStore] = for { accessQuery <- readAccessQuery - r <- run(sql"select #$columns from webknossos.datastores_ where url = $url and #$accessQuery".as[DatastoresRow]) - parsed <- parseFirst(r, url) + rList <- run( + sql"select #$columns from webknossos.datastores_ where url = $url and #$accessQuery".as[DatastoresRow]) + r <- rList.headOption.toFox + parsed <- parse(r) } yield parsed override def findAll(implicit ctx: DBAccessContext): Fox[List[DataStore]] = for { accessQuery <- readAccessQuery r <- run(sql"select #$columns from webknossos.datastores_ where #$accessQuery order by name".as[DatastoresRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed def updateUrlByName(name: String, url: String): Fox[Unit] = { diff --git a/app/models/mesh/Mesh.scala b/app/models/mesh/Mesh.scala index be30dd48504..cefbf543df0 100644 --- a/app/models/mesh/Mesh.scala +++ b/app/models/mesh/Mesh.scala @@ -89,8 +89,8 @@ class MeshDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) accessQuery <- readAccessQuery rList <- run( sql"select #$infoColumns from #$existingCollectionName where _id = ${id.id} and #$accessQuery".as[InfoTuple]) - r <- rList.headOption.toFox - parsed <- parseInfo(r) + r <- rList.headOption.toFox ?~> ("Could not find object " + id + " in " + collectionName) + parsed <- parseInfo(r) ?~> ("SQLDAO Error: Could not parse database row for object " + id + " in " + collectionName) } yield parsed def findAllWithAnnotation(_annotation: ObjectId)(implicit ctx: DBAccessContext): Fox[List[MeshInfo]] = @@ -123,7 +123,7 @@ class MeshDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) for { accessQuery <- readAccessQuery rList <- run(sql"select data from webknossos.meshes where _id = $id and #$accessQuery".as[Option[String]]) - r <- rList.headOption.flatten.toFox + r <- rList.headOption.flatten.toFox ?~> ("Could not find object " + id + " in " + collectionName) binary = BaseEncoding.base64().decode(r) } yield binary diff --git a/app/models/organization/Organization.scala b/app/models/organization/Organization.scala index ee15e3ae7ff..b9bb0cb995b 100755 --- a/app/models/organization/Organization.scala +++ b/app/models/organization/Organization.scala @@ -65,16 +65,17 @@ class OrganizationDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionCont override def findAll(implicit ctx: DBAccessContext): Fox[List[Organization]] = for { accessQuery <- readAccessQuery - r <- run(sql"select #$columns from #$existingCollectionName where #$accessQuery".as[OrganizationsRow]) - parsed <- parseAll(r) + rList <- run(sql"select #$columns from #$existingCollectionName where #$accessQuery".as[OrganizationsRow]) + parsed <- Fox.serialCombined(rList.toList)(r => parse(r)) } yield parsed def findOneByName(name: String)(implicit ctx: DBAccessContext): Fox[Organization] = for { accessQuery <- readAccessQuery - r <- run( + rList <- run( sql"select #$columns from #$existingCollectionName where name = $name and #$accessQuery".as[OrganizationsRow]) - parsed <- parseFirst(r, name) + r <- rList.headOption.toFox + parsed <- parse(r) } yield parsed def insertOne(o: Organization): Fox[Unit] = diff --git a/app/models/project/Project.scala b/app/models/project/Project.scala index 9490202ba3a..5aef99cda55 100755 --- a/app/models/project/Project.scala +++ b/app/models/project/Project.scala @@ -76,11 +76,8 @@ class ProjectDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) )) override def readAccessQ(requestingUserId: ObjectId) = - s"""( - (_team in (select _team from webknossos.user_team_roles where _user = '${requestingUserId.id}')) - or _owner = '${requestingUserId.id}' - or _organization = (select _organization from webknossos.users_ where _id = '${requestingUserId.id}' and isAdmin) - )""" + s"""((_team in (select _team from webknossos.user_team_roles where _user = '${requestingUserId.id}')) or _owner = '${requestingUserId.id}' + or (select _organization from webknossos.users_ where _id = '${requestingUserId.id}' and isAdmin) = (select _organization from webknossos.users_ where _id = _owner))""" override def deleteAccessQ(requestingUserId: ObjectId) = s"_owner = '${requestingUserId.id}'" // read operations @@ -88,15 +85,17 @@ class ProjectDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) override def findOne(id: ObjectId)(implicit ctx: DBAccessContext): Fox[Project] = for { accessQuery <- readAccessQuery - r <- run(sql"select #$columns from #$existingCollectionName where _id = $id and #$accessQuery".as[ProjectsRow]) - parsed <- parseFirst(r, id) + rList <- run( + sql"select #$columns from #$existingCollectionName where _id = $id and #$accessQuery".as[ProjectsRow]) + r <- rList.headOption.toFox ?~> ("Could not find object " + id + " in " + collectionName) + parsed <- parse(r) ?~> ("SQLDAO Error: Could not parse database row for object " + id + " in " + collectionName) } yield parsed override def findAll(implicit ctx: DBAccessContext): Fox[List[Project]] = for { accessQuery <- readAccessQuery r <- run(sql"select #$columns from #$existingCollectionName where #$accessQuery order by created".as[ProjectsRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed // Does not use access query (because they dont support prefixes). Use only after separate access check! @@ -110,20 +109,20 @@ class ProjectDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) where tt._id = $taskTypeId """.as[ProjectsRow] ) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed - def findOneByNameAndOrganization(name: String, organizationId: ObjectId)( - implicit ctx: DBAccessContext): Fox[Project] = + def findOneByName(name: String)(implicit ctx: DBAccessContext): Fox[Project] = for { accessQuery <- readAccessQuery - r <- run( - sql"select #$columns from #$existingCollectionName where name = '#${sanitize(name)}' and _organization = $organizationId and #$accessQuery" + rList <- run( + sql"select #$columns from #$existingCollectionName where name = '#${sanitize(name)}' and #$accessQuery" .as[ProjectsRow]) - parsed <- parseFirst(r, s"$organizationId/$name") + r <- rList.headOption.toFox + parsed <- parse(r) } yield parsed - def findUsersWithActiveTasks(projectId: ObjectId): Fox[List[(String, String, String, Int)]] = + def findUsersWithActiveTasks(name: String): Fox[List[(String, String, String, Int)]] = for { rSeq <- run(sql"""select m.email, u.firstName, u.lastName, count(a._id) from @@ -132,7 +131,7 @@ class ProjectDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) join webknossos.projects_ p on t._project = p._id join webknossos.users_ u on a._user = u._id join webknossos.multiusers_ m on u._multiUser = m._id - where p._id = $projectId + where p.name = $name and a.state = '#${AnnotationState.Active.toString}' and a.typ = '#${AnnotationType.Task}' group by m.email, u.firstName, u.lastName @@ -141,18 +140,16 @@ class ProjectDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) // write operations - def insertOne(p: Project, organizationId: ObjectId): Fox[Unit] = + def insertOne(p: Project): Fox[Unit] = for { - _ <- run(sqlu"""insert into webknossos.projects( - _id, _organization, _team, _owner, name, priority, - paused, expectedTime, isblacklistedfromreport, created, isDeleted) - values(${p._id}, $organizationId, ${p._team}, ${p._owner}, ${p.name}, ${p.priority}, - ${p.paused}, ${p.expectedTime}, ${p.isBlacklistedFromReport}, - ${new java.sql.Timestamp(p.created)}, ${p.isDeleted})""") + _ <- run( + sqlu"""insert into webknossos.projects(_id, _team, _owner, name, priority, paused, expectedTime, isblacklistedfromreport, created, isDeleted) + values(${p._id}, ${p._team}, ${p._owner}, ${p.name}, ${p.priority}, ${p.paused}, ${p.expectedTime}, ${p.isBlacklistedFromReport}, ${new java.sql.Timestamp( + p.created)}, ${p.isDeleted})""") } yield () def updateOne(p: Project)(implicit ctx: DBAccessContext): Fox[Unit] = - for { // note that p.created is immutable, hence skipped here + for { //note that p.created is skipped _ <- assertUpdateAccess(p._id) _ <- run(sqlu"""update webknossos.projects set diff --git a/app/models/task/Task.scala b/app/models/task/Task.scala index 1d8ad0bf9fa..e360d6265ad 100755 --- a/app/models/task/Task.scala +++ b/app/models/task/Task.scala @@ -76,8 +76,10 @@ class TaskDAO @Inject()(sqlClient: SQLClient, projectDAO: ProjectDAO)(implicit e override def findOne(id: ObjectId)(implicit ctx: DBAccessContext): Fox[Task] = for { accessQuery <- readAccessQuery - r <- run(sql"select #$columns from #$existingCollectionName where _id = ${id.id} and #$accessQuery".as[TasksRow]) - parsed <- parseFirst(r, id) + rList <- run( + sql"select #$columns from #$existingCollectionName where _id = ${id.id} and #$accessQuery".as[TasksRow]) + r <- rList.headOption.toFox ?~> ("Could not find object " + id + " in " + collectionName) + parsed <- parse(r) ?~> ("SQLDAO Error: Could not parse database row for object " + id + " in " + collectionName) } yield parsed override def findAll(implicit ctx: DBAccessContext): Fox[List[Task]] = @@ -162,19 +164,21 @@ class TaskDAO @Inject()(sqlClient: SQLClient, projectDAO: ProjectDAO)(implicit e retryCount = 50, retryIfErrorContains = List(transactionSerializationError, "Negative openInstances for Task") ) - r <- run(findTaskOfInsertedAnnotationQ) - parsed <- parseFirst(r, "task assignment query") + rList <- run(findTaskOfInsertedAnnotationQ) + r <- rList.headOption.toFox + parsed <- parse(r) } yield (parsed, annotationId) } def peekNextAssignment(userId: ObjectId, teamIds: List[ObjectId], isTeamManagerOrAdmin: Boolean = false): Fox[Task] = for { - r <- run(sql"#${findNextTaskQ(userId, teamIds, isTeamManagerOrAdmin)}".as[TasksRow]) - parsed <- parseFirst(r, "task peek query") + rList <- run(sql"#${findNextTaskQ(userId, teamIds, isTeamManagerOrAdmin)}".as[TasksRow]) + r <- rList.headOption.toFox + parsed <- parse(r) } yield parsed def findAllByProjectAndTaskTypeAndIdsAndUser( - projectIdOpt: Option[ObjectId], + projectNameOpt: Option[String], taskTypeIdOpt: Option[ObjectId], taskIdsOpt: Option[List[ObjectId]], userIdOpt: Option[ObjectId], @@ -188,7 +192,10 @@ class TaskDAO @Inject()(sqlClient: SQLClient, projectDAO: ProjectDAO)(implicit e case Some(true) => "ORDER BY random()" case _ => "" } - val projectFilter = projectIdOpt.map(pId => s"(t._project = '$pId')").getOrElse("true") + val projectFilterFox = projectNameOpt match { + case Some(pName) => for { project <- projectDAO.findOneByName(pName) } yield s"(t._project = '${project._id}')" + case _ => Fox.successful("true") + } val taskTypeFilter = taskTypeIdOpt.map(ttId => s"(t._taskType = '$ttId')").getOrElse("true") val taskIdsFilter = taskIdsOpt .map(tIds => if (tIds.isEmpty) "false" else s"(t._id in ${writeStructTupleWithQuotes(tIds.map(_.toString))})") @@ -202,6 +209,7 @@ class TaskDAO @Inject()(sqlClient: SQLClient, projectDAO: ProjectDAO)(implicit e .getOrElse("true") for { + projectFilter <- projectFilterFox accessQuery <- accessQueryFromAccessQ(listAccessQ) q = sql"""select #${columnsWithPrefix("t.")} from webknossos.tasks_ t diff --git a/app/models/task/TaskCreationService.scala b/app/models/task/TaskCreationService.scala index ea1f594808d..f9afc52cfa5 100644 --- a/app/models/task/TaskCreationService.scala +++ b/app/models/task/TaskCreationService.scala @@ -420,7 +420,7 @@ class TaskCreationService @Inject()(taskTypeService: TaskTypeService, dataSet._id, description = tuple._1.map(_._1.description).openOr(None) )) - warnings <- warnIfTeamHasNoAccess(fullTasks.map(_._1), dataSet, requestingUser) + warnings <- warnIfTeamHasNoAccess(fullTasks.map(_._1), dataSet) zippedTasksAndAnnotations = taskObjects zip createAnnotationBaseResults taskJsons = zippedTasksAndAnnotations.map(tuple => taskToJsonWithOtherFox(tuple._1, tuple._2)) result <- TaskCreationResult.fromTaskJsFoxes(taskJsons, warnings) @@ -480,12 +480,11 @@ class TaskCreationService @Inject()(taskTypeService: TaskTypeService, case _ => Fox.successful(None) } - private def warnIfTeamHasNoAccess(requestedTasks: List[TaskParameters], dataSet: DataSet, requestingUser: User)( + private def warnIfTeamHasNoAccess(requestedTasks: List[TaskParameters], dataSet: DataSet)( implicit ctx: DBAccessContext): Fox[List[String]] = { val projectNames = requestedTasks.map(_.projectName).distinct for { - projects: List[Project] <- Fox.serialCombined(projectNames)( - projectDAO.findOneByNameAndOrganization(_, requestingUser._organization)) + projects: List[Project] <- Fox.serialCombined(projectNames)(projectDAO.findOneByName(_)) dataSetTeams <- teamDAO.findAllForDataSet(dataSet._id) noAccessTeamIds = projects.map(_._team).diff(dataSetTeams.map(_._id)) noAccessTeamIdsTransitive <- Fox.serialCombined(noAccessTeamIds)(id => @@ -514,17 +513,18 @@ class TaskCreationService @Inject()(taskTypeService: TaskTypeService, case _ => Fox.successful(()) } - private def createTaskWithoutAnnotationBase(paramBox: Box[TaskParameters], - skeletonTracingIdBox: Box[Option[String]], - volumeTracingIdBox: Box[Option[String]], - requestingUser: User)(implicit ctx: DBAccessContext): Fox[Task] = + private def createTaskWithoutAnnotationBase( + paramBox: Box[TaskParameters], + skeletonTracingIdBox: Box[Option[String]], + volumeTracingIdBox: Box[Option[String]], + requestingUser: User)(implicit ctx: DBAccessContext, mp: MessagesProvider): Fox[Task] = for { params <- paramBox.toFox skeletonIdOpt <- skeletonTracingIdBox.toFox volumeIdOpt <- volumeTracingIdBox.toFox _ <- bool2Fox(skeletonIdOpt.isDefined || volumeIdOpt.isDefined) ?~> "task.create.needsEitherSkeletonOrVolume" taskTypeIdValidated <- ObjectId.parse(params.taskTypeId) - project <- projectDAO.findOneByNameAndOrganization(params.projectName, requestingUser._organization) ?~> "project.notFound" + project <- projectDAO.findOneByName(params.projectName) ?~> Messages("project.notFound", params.projectName) _ <- validateScript(params.scriptId) ?~> "script.invalid" _ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(requestingUser, project._team)) task = Task( diff --git a/app/models/task/TaskType.scala b/app/models/task/TaskType.scala index 6c57730b09d..09a699041fa 100755 --- a/app/models/task/TaskType.scala +++ b/app/models/task/TaskType.scala @@ -99,35 +99,38 @@ class TaskTypeDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) override def readAccessQ(requestingUserId: ObjectId) = s"""(_team in (select _team from webknossos.user_team_roles where _user = '${requestingUserId.id}') - or _organization = (select _organization from webknossos.users_ where _id = '${requestingUserId.id}' and isAdmin))""" + or (select _organization from webknossos.teams where webknossos.teams._id = _team) + in (select _organization from webknossos.users_ where _id = '${requestingUserId.id}' and isAdmin))""" override def updateAccessQ(requestingUserId: ObjectId) = s"""(_team in (select _team from webknossos.user_team_roles where isTeamManager and _user = '${requestingUserId.id}') - or _organization = (select _organization from webknossos.users_ where _id = '${requestingUserId.id}' and isAdmin))""" + or (select _organization from webknossos.teams where webknossos.teams._id = _team) + in (select _organization from webknossos.users_ where _id = '${requestingUserId.id}' and isAdmin))""" override def findOne(id: ObjectId)(implicit ctx: DBAccessContext): Fox[TaskType] = for { accessQuery <- readAccessQuery - r <- run( + rList <- run( sql"select #$columns from #$existingCollectionName where _id = ${id.id} and #$accessQuery".as[TasktypesRow]) - parsed <- parseFirst(r, id.toString) + r <- rList.headOption.toFox ?~> ("Could not find object " + id + " in " + collectionName) + parsed <- parse(r) ?~> ("SQLDAO Error: Could not parse database row for object " + id + " in " + collectionName) } yield parsed override def findAll(implicit ctx: DBAccessContext): Fox[List[TaskType]] = for { accessQuery <- readAccessQuery r <- run(sql"select #$columns from #$existingCollectionName where #$accessQuery".as[TasktypesRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) ?~> ("SQLDAO Error: Could not parse one of the database rows in " + collectionName) } yield parsed - def insertOne(t: TaskType, organizationId: ObjectId): Fox[Unit] = + def insertOne(t: TaskType): Fox[Unit] = for { _ <- run(sqlu"""insert into webknossos.taskTypes( - _id, _organization, _team, summary, description, settings_allowedModes, settings_preferredMode, + _id, _team, summary, description, settings_allowedModes, settings_preferredMode, settings_branchPointsAllowed, settings_somaClickingAllowed, settings_mergerMode, settings_resolutionRestrictions_min, settings_resolutionRestrictions_max, recommendedConfiguration, tracingType, created, isDeleted) - values(${t._id.id}, $organizationId, ${t._team.id}, ${t.summary}, ${t.description}, + values(${t._id.id}, ${t._team.id}, ${t.summary}, ${t.description}, '#${sanitize(writeArrayTuple(t.settings.allowedModes.map(_.toString)))}', #${optionLiteral(t.settings.preferredMode.map(sanitize))}, ${t.settings.branchPointsAllowed}, @@ -142,7 +145,7 @@ class TaskTypeDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) } yield () def updateOne(t: TaskType)(implicit ctx: DBAccessContext): Fox[Unit] = - for { // note that t.created is immutable, hence skipped here + for { //note that t.created is skipped _ <- assertUpdateAccess(t._id) allowedModesLiteral = sanitize(writeArrayTuple(t.settings.allowedModes.map(_.toString))) resolutionMinLiteral = optionLiteral(t.settings.resolutionRestrictions.min.map(_.toString)) diff --git a/app/models/team/Team.scala b/app/models/team/Team.scala index ca859080771..5600eae9734 100755 --- a/app/models/team/Team.scala +++ b/app/models/team/Team.scala @@ -89,15 +89,17 @@ class TeamDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) override def findOne(id: ObjectId)(implicit ctx: DBAccessContext): Fox[Team] = for { accessQuery <- readAccessQuery - r <- run(sql"select #$columns from #$existingCollectionName where _id = ${id.id} and #$accessQuery".as[TeamsRow]) - parsed <- parseFirst(r, id) + rList <- run( + sql"select #$columns from #$existingCollectionName where _id = ${id.id} and #$accessQuery".as[TeamsRow]) + r <- rList.headOption.toFox ?~> ("Could not find object " + id + " in " + collectionName) + parsed <- parse(r) ?~> ("SQLDAO Error: Could not parse database row for object " + id + " in " + collectionName) } yield parsed override def findAll(implicit ctx: DBAccessContext): Fox[List[Team]] = for { accessQuery <- readAccessQuery r <- run(sql"select #$columns from #$existingCollectionName where #$accessQuery".as[TeamsRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed def findAllEditable(implicit ctx: DBAccessContext): Fox[List[Team]] = @@ -108,7 +110,7 @@ class TeamDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) where (_id in (select _team from webknossos.user_team_roles where _user = ${requestingUserId.id} and isTeamManager) or _organization in (select _organization from webknossos.users_ where _id = ${requestingUserId.id} and isAdmin)) and #$accessQuery""".as[TeamsRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed def findAllByOrganization(organizationId: ObjectId)(implicit ctx: DBAccessContext): Fox[List[Team]] = @@ -117,7 +119,7 @@ class TeamDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) r <- run( sql"select #$columns from #$existingCollectionName where _organization = ${organizationId.id} and #$accessQuery" .as[TeamsRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed def findAllIdsByOrganization(organizationId: ObjectId)(implicit ctx: DBAccessContext): Fox[List[ObjectId]] = @@ -135,7 +137,7 @@ class TeamDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) r <- run(sql"""select #${columnsWithPrefix("t.")} from #$existingCollectionName t join webknossos.dataSet_allowedTeams at on t._id = at._team where at._dataSet = $dataSetId and #$accessQuery""".as[TeamsRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed def insertOne(t: Team): Fox[Unit] = diff --git a/app/models/user/MultiUser.scala b/app/models/user/MultiUser.scala index 80c0b0d0788..42dc6358af2 100644 --- a/app/models/user/MultiUser.scala +++ b/app/models/user/MultiUser.scala @@ -106,10 +106,13 @@ class MultiUserDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext def findOneByEmail(email: String)(implicit ctx: DBAccessContext): Fox[MultiUser] = for { accessQuery <- readAccessQuery - r <- run( + rList <- run( sql"select #$columns from #$existingCollectionName where email = $email and #$accessQuery".as[MultiusersRow]) - parsed <- parseFirst(r, email) - } yield parsed + r <- rList.headOption.toFox + parsed <- parse(r) + } yield { + parsed + } def emailNotPresentYet(email: String)(implicit ctx: DBAccessContext): Fox[Boolean] = for { diff --git a/app/models/user/User.scala b/app/models/user/User.scala index 8b6f4c9ba57..7cd4d639196 100755 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -96,8 +96,9 @@ class UserDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) override def findOne(id: ObjectId)(implicit ctx: DBAccessContext): Fox[User] = for { accessQuery <- readAccessQuery - r <- run(sql"select #$columns from #$existingCollectionName where _id = $id and #$accessQuery".as[UsersRow]) - parsed <- parseFirst(r, id) + rList <- run(sql"select #$columns from #$existingCollectionName where _id = $id and #$accessQuery".as[UsersRow]) + r <- rList.headOption.toFox ?~> ("Could not find object " + id + " in " + collectionName) + parsed <- parse(r) ?~> ("SQLDAO Error: Could not parse database row for object " + id + " in " + collectionName) } yield parsed override def findAll(implicit ctx: DBAccessContext): Fox[List[User]] = @@ -105,7 +106,7 @@ class UserDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) accessQuery <- readAccessQuery r <- run( sql"select #$columns from #$existingCollectionName where isUnlisted = false and #$accessQuery".as[UsersRow]) - parsed <- parseAll(r) + parsed <- Fox.combined(r.toList.map(parse)) } yield parsed def findAllByTeams(teams: List[ObjectId], includeDeactivated: Boolean = true)( diff --git a/app/utils/SQLHelpers.scala b/app/utils/SQLHelpers.scala index 4f2eb9471aa..56f394cc149 100644 --- a/app/utils/SQLHelpers.scala +++ b/app/utils/SQLHelpers.scala @@ -222,18 +222,6 @@ abstract class SQLDAO[C, R, X <: AbstractTable[R]] @Inject()(sqlClient: SQLClien def parse(row: X#TableElementType): Fox[C] - def parseFirst(rowSeq: Seq[X#TableElementType], queryLabel: ObjectId): Fox[C] = - parseFirst(rowSeq, queryLabel.toString) - - def parseFirst(rowSeq: Seq[X#TableElementType], queryLabel: String): Fox[C] = - for { - firstRow <- rowSeq.headOption.toFox // No error chain here, as this should stay Fox.Empty - parsed <- parse(firstRow) ?~> s"Parsing failed for row in ${collectionName} queried by $queryLabel" - } yield parsed - - def parseAll(rowSeq: Seq[X#TableElementType]): Fox[List[C]] = - Fox.combined(rowSeq.toList.map(parse)) ?~> s"Parsing failed for a row in $collectionName during list query" - @silent // suppress warning about unused implicit ctx, as it is used in subclasses def findOne(id: ObjectId)(implicit ctx: DBAccessContext): Fox[C] = run(collection.filter(r => isDeletedColumn(r) === false && idColumn(r) === id.id).result.headOption).map { diff --git a/conf/evolutions/069-tasktype-project-unique-per-orga.sql b/conf/evolutions/069-tasktype-project-unique-per-orga.sql deleted file mode 100644 index 64b1202e3af..00000000000 --- a/conf/evolutions/069-tasktype-project-unique-per-orga.sql +++ /dev/null @@ -1,27 +0,0 @@ --- https://github.com/scalableminds/webknossos/pull/5334 - -START TRANSACTION; - -DROP VIEW webknossos.taskTypes_; -DROP VIEW webknossos.projects_; - -ALTER TABLE webknossos.taskTypes ADD COLUMN _organization CHAR(24); -ALTER TABLE webknossos.projects ADD COLUMN _organization CHAR(24); - -ALTER TABLE webknossos.taskTypes DROP CONSTRAINT tasktypes_summary_key; - -UPDATE webknossos.projects set _organization = (select _organization from webknossos.users where _id = _owner); -UPDATE webknossos.tasktypes set _organization = (select _organization from webknossos.teams where _id = _team); - -ALTER TABLE webknossos.projects ALTER COLUMN _organization SET NOT NULL; -ALTER TABLE webknossos.tasktypes ALTER COLUMN _organization SET NOT NULL; - -ALTER TABLE webknossos.taskTypes ADD CONSTRAINT tasktypes_summary__organization_key UNIQUE(summary, _organization); -ALTER TABLE webknossos.projects ADD CONSTRAINT projects_name__organization_key UNIQUE(name, _organization); - -CREATE VIEW webknossos.projects_ AS SELECT * FROM webknossos.projects WHERE NOT isDeleted; -CREATE VIEW webknossos.taskTypes_ AS SELECT * FROM webknossos.taskTypes WHERE NOT isDeleted; - -UPDATE webknossos.releaseInformation SET schemaVersion = 69; - -COMMIT TRANSACTION; diff --git a/conf/evolutions/reversions/069-tasktype-project-unique-per-orga.sql b/conf/evolutions/reversions/069-tasktype-project-unique-per-orga.sql deleted file mode 100644 index 0eed2f96da2..00000000000 --- a/conf/evolutions/reversions/069-tasktype-project-unique-per-orga.sql +++ /dev/null @@ -1,19 +0,0 @@ -START TRANSACTION; - -DROP VIEW webknossos.taskTypes_; -DROP VIEW webknossos.projects_; - -ALTER TABLE webknossos.taskTypes DROP CONSTRAINT tasktypes_summary__organization_key; -ALTER TABLE webknossos.projects DROP CONSTRAINT projects_name__organization_key; - -ALTER TABLE webknossos.projects DROP COLUMN _organization; -ALTER TABLE webknossos.tasktypes DROP COLUMN _organization; - -ALTER TABLE webknossos.taskTypes ADD CONSTRAINT tasktypes_summary_key UNIQUE(summary); - -CREATE VIEW webknossos.projects_ AS SELECT * FROM webknossos.projects WHERE NOT isDeleted; -CREATE VIEW webknossos.taskTypes_ AS SELECT * FROM webknossos.taskTypes WHERE NOT isDeleted; - -UPDATE webknossos.releaseInformation SET schemaVersion = 68; - -COMMIT TRANSACTION; diff --git a/conf/messages b/conf/messages index 0ff6f598c97..5465c6fa256 100644 --- a/conf/messages +++ b/conf/messages @@ -194,8 +194,8 @@ project.remove.confirm=Do you really want to delete this project? All annotation project.remove.notAllowed=You are not authorized to remove this project. Talk to the owner. project.remove.success=Project was removed project.remove.failure=Project couldn’t get removed -project.notFound=Project couldn’t be found -project.update.failed=Project update failed +project.notFound=Project {0} couldn’t be found +project.update.failed=Project {0} update failed project.noAnnotations=We couldn’t find annotations for this project project.increaseTaskInstances.negative=Cannot increment task counts by negative number project.list.failed=Failed to retrieve list of projects. diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index af0ad05fb58..621b24ddda8 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -163,15 +163,15 @@ DELETE /scripts/:id c GET /projects controllers.ProjectController.list GET /projects/assignments controllers.ProjectController.listWithStatus POST /projects controllers.ProjectController.create -GET /projects/:id controllers.ProjectController.read(id: String) -DELETE /projects/:id controllers.ProjectController.delete(id: String) -PUT /projects/:id controllers.ProjectController.update(id: String) -GET /projects/:id/tasks controllers.ProjectController.tasksForProject(id: String, limit: Option[Int], pageNumber: Option[Int], includeTotalCount: Option[Boolean]) -PATCH /projects/:id/incrementEachTasksInstances controllers.ProjectController.incrementEachTasksInstances(id: String, delta: Option[Long]) -PATCH /projects/:id/pause controllers.ProjectController.pause(id: String) -PATCH /projects/:id/resume controllers.ProjectController.resume(id: String) -GET /projects/:id/usersWithActiveTasks controllers.ProjectController.usersWithActiveTasks(id:String) -POST /projects/:id/transferActiveTasks controllers.ProjectController.transferActiveTasks(id:String) +GET /projects/:name controllers.ProjectController.read(name: String) +DELETE /projects/:name controllers.ProjectController.delete(name: String) +PUT /projects/:name controllers.ProjectController.update(name: String) +GET /projects/:name/tasks controllers.ProjectController.tasksForProject(name: String, limit: Option[Int], pageNumber: Option[Int], includeTotalCount: Option[Boolean]) +PATCH /projects/:name/incrementEachTasksInstances controllers.ProjectController.incrementEachTasksInstances(name: String, delta: Option[Long]) +PATCH /projects/:name/pause controllers.ProjectController.pause(name: String) +PATCH /projects/:name/resume controllers.ProjectController.resume(name: String) +GET /projects/:name/usersWithActiveTasks controllers.ProjectController.usersWithActiveTasks(name:String) +POST /projects/:name/transferActiveTasks controllers.ProjectController.transferActiveTasks(name:String) # Statistics GET /statistics/webknossos controllers.StatisticsController.webKnossos(interval: String, start: Option[Long], end: Option[Long]) diff --git a/conf/webknossos.versioned.routes b/conf/webknossos.versioned.routes index fa1d6685128..cd78c06a390 100644 --- a/conf/webknossos.versioned.routes +++ b/conf/webknossos.versioned.routes @@ -2,17 +2,6 @@ # example: assume, the features route has changed, introducing v2. The older v1 needs to be provided in the legacyApiController --> /v4/ webknossos.latest.Routes - -POST /v3/tasks/list controllers.LegacyApiController.taskListTasks -GET /v3/projects/:name controllers.LegacyApiController.projectRead(name: String) -DELETE /v3/projects/:name controllers.LegacyApiController.projectDelete(name: String) -PUT /v3/projects/:name controllers.LegacyApiController.projectUpdate(name: String) -GET /v3/projects/:name/tasks controllers.LegacyApiController.projectTasksForProject(name: String, limit: Option[Int], pageNumber: Option[Int], includeTotalCount: Option[Boolean]) -PATCH /v3/projects/:name/incrementEachTasksInstances controllers.LegacyApiController.projectIncrementEachTasksInstances(name: String, delta: Option[Long]) -PATCH /v3/projects/:name/pause controllers.LegacyApiController.projectPause(name: String) -PATCH /v3/projects/:name/resume controllers.LegacyApiController.projectResume(name: String) - -> /v3/ webknossos.latest.Routes PATCH /v2/annotations/:typ/:id/finish controllers.LegacyApiController.annotationFinishV2(typ: String, id: String) diff --git a/docs/rest_api.md b/docs/rest_api.md index e90f5ad5d7c..4d7d3978e6d 100644 --- a/docs/rest_api.md +++ b/docs/rest_api.md @@ -14,9 +14,7 @@ The API is subject to frequent changes. However, older versions will be supporte New versions will be documented here, detailing the changes. Note, however, that some changes are not considered to be breaking the API and will not lead to new versions. Such changes include new optional parameters as well as new fields in the responses. The same goes for error message wording. -### Current api version is `v4` - -* New in v4: /projects routes no longer expect `name` but now `id`. The same goes for `POST /tasks/list` when filtering by project. +### Current api version is `v3` * New in v3: the info and finish requests of annotations now expect an additional `timestamp` GET parameter that should be set to the time the request is sent (e.g. Date.now()). @@ -518,7 +516,7 @@ List tasks matching search criteria #### Expects - JSON object with four optional fields: - `"user"` `[STRING]` show only tasks on which the user with this id has worked - - `"project"` `[STRING]` show only tasks of the project with this id + - `"project"` `[STRING]` show only tasks of the project with this name - `"ids"` `[JSON LIST OF STRINGS]` show only tasks with these ids - `"tasktype"` `[STRING]` show only tasks matching the task type with this id - `"random"` `[BOOLEAN]` if true, return randomized subset of the results, rather than the first 1000 matches in the database @@ -527,9 +525,6 @@ List tasks matching search criteria - JSON list of objects containing task information - Note that a maximum of 1000 results is returned -#### Changes Introduced in `v4` - - The `"project"` field in the JSON object is no longer its name but instead its id. - --- ### `GET /tasks/:id` @@ -626,33 +621,27 @@ JSON object containing project information about the newly created project, incl --- -### `GET /projects/:id` +### `GET /projects/:name` #### Expects - - In the url: `:id` id of a project + - In the url: `:name` name of a project #### Returns - JSON object containing project information about the selected project -#### Changes Introduced in `v4` - - The request no longer expects `name` in the url, but instead `id` - --- -### `DELETE /projects/:id` +### `DELETE /projects/:name` Delete a project and all its tasks and annotations #### Expects - - In the url: `:id` id of a project - -#### Changes Introduced in `v4` - - The request no longer expects `name` in the url, but instead `id` + - In the url: `:name` name of a project --- -### `PUT /projects/:id` +### `PUT /projects/:name` Update a project @@ -662,18 +651,15 @@ Update a project #### Returns - JSON object containing project information about the updated project -#### Changes Introduced in `v4` - - The request no longer expects `name` in the url, but instead `id` - --- -### `GET /projects/:id/tasks` +### `GET /projects/:name/tasks` List all tasks of a project #### Expects - - In the url: `:id` id of a project + - In the url: `:name` name of a project - Optional GET parameter `limit=[INT]` - return only the first x results (defaults to infinity) - Optional GET parameter `pageNumber=[INT]` @@ -688,52 +674,42 @@ List all tasks of a project #### Note - For smoother backwards compatibility, the limit defaults to infinity. However, to ease server load and improve response time, we suggest using a limit of 1000 -#### Changes Introduced in `v4` - - The request no longer expects `name` in the url, but instead `id` - --- -### `PATCH /projects/:id/incrementEachTasksInstances` +### `PATCH /projects/:name/incrementEachTasksInstances` Increment the open instances for each task of a project. #### Expects - - In the url: `id` `[STRING]` id of a project + - In the url: `name` `[STRING]` name of a project - Optional GET parameter `delta=[INT]` number of additional instances for each task (defaults to 1) #### Returns - JSON object containing project information about the updated project, with additional field `numberOfOpenAssignments` -#### Changes Introduced in `v4` - - The request no longer expects `name` in the url, but instead `id` - --- -### `PATCH /projects/:id/pause` +### `PATCH /projects/:name/pause` Pause a project (no tasks will be assigned until resumed) #### Expects - - In the url: `:id` `[STRING]` id of a project + - In the url: `:name` `[STRING]` name of a project #### Returns - JSON object containing project information about the updated project -#### Changes Introduced in `v4` - - The request no longer expects `name` in the url, but instead `id` --- -### `PATCH /projects/:id/resume` +### `PATCH /projects/:name/resume` Resume a paused project #### Expects - - In the url: `:id` `[STRING]` id of a project + - In the url: `:name` `[STRING]` name of a project #### Returns - JSON object containing project information about the updated project -#### Changes Introduced in `v4` - - The request no longer expects `name` in the url, but instead `id` diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index d96796cf511..5e38f5effd5 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -363,24 +363,24 @@ export async function getProjectsForTaskType( return responses.map(transformProject); } -export async function getProject(projectId: string): Promise { - const project = await Request.receiveJSON(`/api/projects/${projectId}`); +export async function getProject(projectName: string): Promise { + const project = await Request.receiveJSON(`/api/projects/${projectName}`); return transformProject(project); } export async function increaseProjectTaskInstances( - projectId: string, + projectName: string, delta?: number = 1, ): Promise { const project = await Request.receiveJSON( - `/api/projects/${projectId}/incrementEachTasksInstances?delta=${delta}`, + `/api/projects/${projectName}/incrementEachTasksInstances?delta=${delta}`, { method: "PATCH" }, ); return transformProject(project); } -export function deleteProject(projectId: string): Promise { - return Request.receiveJSON(`/api/projects/${projectId}`, { +export function deleteProject(projectName: string): Promise { + return Request.receiveJSON(`/api/projects/${projectName}`, { method: "DELETE", }); } @@ -395,26 +395,29 @@ export function createProject(project: APIProjectCreator): Promise { }); } -export function updateProject(projectId: string, project: APIProjectUpdater): Promise { +export function updateProject( + projectName: string, + project: APIProjectUpdater, +): Promise { const transformedProject = Object.assign({}, project, { expectedTime: Utils.minutesToMilliseconds(project.expectedTime), }); - return Request.sendJSONReceiveJSON(`/api/projects/${projectId}`, { + return Request.sendJSONReceiveJSON(`/api/projects/${projectName}`, { method: "PUT", data: transformedProject, }); } -export async function pauseProject(projectId: string): Promise { - const project = await Request.receiveJSON(`/api/projects/${projectId}/pause`, { +export async function pauseProject(projectName: string): Promise { + const project = await Request.receiveJSON(`/api/projects/${projectName}/pause`, { method: "PATCH", }); return transformProject(project); } -export async function resumeProject(projectId: string): Promise { - const project = await Request.receiveJSON(`/api/projects/${projectId}/resume`, { +export async function resumeProject(projectName: string): Promise { + const project = await Request.receiveJSON(`/api/projects/${projectName}/resume`, { method: "PATCH", }); return transformProject(project); diff --git a/frontend/javascripts/admin/project/project_create_view.js b/frontend/javascripts/admin/project/project_create_view.js index 8619b3e4117..bb7094048fa 100644 --- a/frontend/javascripts/admin/project/project_create_view.js +++ b/frontend/javascripts/admin/project/project_create_view.js @@ -19,7 +19,7 @@ import { FormItemWithInfo } from "../../dashboard/dataset/helper_components"; const FormItem = Form.Item; type OwnProps = {| - projectId?: ?string, + projectName?: ?string, |}; type StateProps = {||}; type Props = {| ...OwnProps, ...StateProps |}; @@ -27,7 +27,7 @@ type PropsWithRouter = {| ...Props, |}; -function ProjectCreateView({ projectId }: PropsWithRouter) { +function ProjectCreateView({ projectName }: PropsWithRouter) { const [teams, setTeams] = useState>([]); const [users, setUsers] = useState>([]); const [isFetchingData, setIsFetchingData] = useState(false); @@ -49,7 +49,7 @@ function ProjectCreateView({ projectId }: PropsWithRouter) { } async function applyDefaults() { - const project = projectId ? await getProject(projectId) : null; + const project = projectName ? await getProject(projectName) : null; const defaultValues = { priority: 100, expectedTime: 90, @@ -63,18 +63,16 @@ function ProjectCreateView({ projectId }: PropsWithRouter) { } const handleSubmit = async formValues => { - if (projectId) { - await updateProject(projectId, formValues); + if (projectName) { + await updateProject(projectName, formValues); } else { await createProject(formValues); } history.push("/projects"); }; - const isEditMode = projectId != null; - const projectName = form.getFieldValue("name"); - const title = - isEditMode && projectId ? `Update Project ${projectName || projectId}` : "Create Project"; + const isEditMode = projectName != null; + const title = isEditMode && projectName ? `Update Project ${projectName}` : "Create Project"; const fullWidth = { width: "100%" }; return ( diff --git a/frontend/javascripts/admin/project/project_list_view.js b/frontend/javascripts/admin/project/project_list_view.js index a8e8e2872cf..a3cacb0ecbe 100644 --- a/frontend/javascripts/admin/project/project_list_view.js +++ b/frontend/javascripts/admin/project/project_list_view.js @@ -140,7 +140,7 @@ class ProjectListView extends React.PureComponent { }); try { - await deleteProject(project.id); + await deleteProject(project.name); this.setState(prevState => ({ projects: prevState.projects.filter(p => p.id !== project.id), })); @@ -164,7 +164,7 @@ class ProjectListView extends React.PureComponent { project: APIProjectWithAssignments, APICall: string => Promise, ) => { - const updatedProject = await APICall(project.id); + const updatedProject = await APICall(project.name); this.setState(prevState => ({ projects: prevState.projects.map(p => p.id === project.id ? this.mergeProjectWithUpdated(p, updatedProject) : p, @@ -180,7 +180,7 @@ class ProjectListView extends React.PureComponent { this.setState({ isLoading: true, }); - const updatedProject = await increaseProjectTaskInstances(project.id); + const updatedProject = await increaseProjectTaskInstances(project.name); this.setState(prevState => ({ projects: prevState.projects.map(p => (p.id === project.id ? updatedProject : p)), })); @@ -200,9 +200,9 @@ class ProjectListView extends React.PureComponent { }); }; - maybeShowNoFallbackDataInfo = async (projectId: string) => { + maybeShowNoFallbackDataInfo = async (projectName: string) => { const tasks = await getTasks({ - project: projectId, + project: projectName, }); if (tasks.some(task => task.type.tracingType !== "skeleton")) { Toast.info(messages["project.no_fallback_data_included"], { @@ -346,7 +346,7 @@ class ProjectListView extends React.PureComponent { View
- + Edit @@ -374,7 +374,7 @@ class ProjectListView extends React.PureComponent {
)} - + Tasks @@ -390,7 +390,7 @@ class ProjectListView extends React.PureComponent { { - this.maybeShowNoFallbackDataInfo(project.id); + this.maybeShowNoFallbackDataInfo(project.name); await downloadNml(project.id, "CompoundProject"); }} title="Download all Finished Annotations" diff --git a/frontend/javascripts/admin/task/task_search_form.js b/frontend/javascripts/admin/task/task_search_form.js index 3c786cf6f69..e232f0f5923 100644 --- a/frontend/javascripts/admin/task/task_search_form.js +++ b/frontend/javascripts/admin/task/task_search_form.js @@ -26,7 +26,7 @@ export type QueryObject = { export type TaskFormFieldValues = { taskId?: string, taskTypeId?: string, - projectId?: string, + projectName?: string, userId?: string, random?: boolean, }; @@ -52,7 +52,7 @@ const persistence: Persistence = new Persistence( fieldValues: PropTypes.shape({ taskId: PropTypes.string, taskTypeId: PropTypes.string, - projectId: PropTypes.string, + projectName: PropTypes.string, userId: PropTypes.string, }), }, @@ -132,8 +132,8 @@ class TaskSearchForm extends React.Component { queryObject.user = formValues.userId; } - if (formValues.projectId) { - queryObject.project = formValues.projectId; + if (formValues.projectName) { + queryObject.project = formValues.projectName; } if (isRandom) { @@ -217,7 +217,7 @@ class TaskSearchForm extends React.Component { - +