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 (second try) #5381

Merged
merged 6 commits into from
Apr 15, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ 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)
- Added a screenshot of the 3D view when using the screenshot functionality in the tracing view. [#5324](https://github.com/scalableminds/webknossos/pull/5324)
- Tiff export jobs of volume annotations now show the link back to the annotation in the jobs list. [#5378](https://github.com/scalableminds/webknossos/pull/5378)

### 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)
Expand Down
1 change: 1 addition & 0 deletions MIGRATIONS.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ 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)
4 changes: 2 additions & 2 deletions app/controllers/InitialDataController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,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 @@ -208,7 +208,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(name = project.name, _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