Skip to content

Commit

Permalink
Merge branch 'master' of github.com:scalableminds/webknossos into sel…
Browse files Browse the repository at this point in the history
…ect_spinner

* 'master' of github.com:scalableminds/webknossos:
  added more usage shortcuts to status bar (#6549)
  Look up annotation dataset by tracing id instead of (broken) organizationName field (#6548)
  Add links to resource creation pages (#6513)
  Improve Zarr Import UI (and general editing of datasets) (#6485)
  Remove unused dataSetName and organizationName property of protobuf objects (#6559)
  WIP: Added manual task assignment (#6551)
  Fix zarr private links for volume tracings and some minor improvements to private links UI (#6541)
  Fix stack overflow in compactMovedNodesAndEdges (#6557)
  • Loading branch information
hotzenklotz committed Oct 18, 2022
2 parents 06ec13b + 8758d3e commit 1cbb641
Show file tree
Hide file tree
Showing 54 changed files with 1,460 additions and 778 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
[Commits](https://github.com/scalableminds/webknossos/compare/22.10.0...HEAD)

### Added
- The task creation page now links to creation pages for task types, projects etc., for a smoother task administration experience. [#6513](https://github.com/scalableminds/webknossos/pull/6513)
- Support for a new mesh file format which allows up to billions of meshes. [#6491](https://github.com/scalableminds/webknossos/pull/6491)
- Remote n5 datasets can now also be explored and added. [#6520](https://github.com/scalableminds/webknossos/pull/6520)
- Improved performance for applying agglomerate mappings on segmentation data. [#6532](https://github.com/scalableminds/webknossos/pull/6532)
- Added backspace as an additional keyboard shortcut for deleting the active node. [#6554](https://github.com/scalableminds/webknossos/pull/6554)
- Tasks can now be assigned to individual users directly. [#6551](https://github.com/scalableminds/webknossos/pull/6551)

### Changed
- Creating tasks in bulk now also supports referencing task types by their summary instead of id. [#6486](https://github.com/scalableminds/webknossos/pull/6486)

### Fixed
- Fixed a bug where some file requests replied with error 400 instead of 404, confusing some zarr clients. [#6515](https://github.com/scalableminds/webknossos/pull/6515)
- Fixed URL for private Zarr streaming links to volume annotations. [#6515](https://github.com/scalableminds/webknossos/pull/6541)
- Fixed a bug where the `transform` of a new mesh file wasn't taken into account for the rendering of meshes. [#6552](https://github.com/scalableminds/webknossos/pull/6552)
- Fixed a rare crash when splitting/merging a large skeleton. [#6557](https://github.com/scalableminds/webknossos/pull/6557)
- Fixed a bug where some features were unavailable for annotations for datasets of foreign organizations. [#6548](https://github.com/scalableminds/webknossos/pull/6548)

### Removed

Expand Down
2 changes: 2 additions & 0 deletions app/controllers/AnnotationIOController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ Expects:
dataSet.scale,
None,
organizationName,
dataSet.name,
Some(user),
taskOpt)
nmlTemporaryFile = temporaryFileCreator.create()
Expand Down Expand Up @@ -397,6 +398,7 @@ Expects:
dataset.scale,
None,
organizationName,
dataset.name,
Some(user),
taskOpt,
skipVolumeData)
Expand Down
31 changes: 27 additions & 4 deletions app/controllers/TaskController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,7 @@ Expects:
taskIdValidated <- ObjectId.fromString(taskId) ?~> "task.id.invalid"
task <- taskDAO.findOne(taskIdValidated) ?~> "task.notFound" ~> NOT_FOUND
project <- projectDAO.findOne(task._project)
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> Messages(
"notAllowed")
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed"
_ <- taskDAO.removeOneAndItsAnnotations(task._id) ?~> "task.remove.failed"
} yield JsonOk(Messages("task.removed"))
}
Expand Down Expand Up @@ -196,15 +195,39 @@ Expects:
for {
teams <- taskService.getAllowedTeamsForNextTask(user)
isTeamManagerOrAdmin <- userService.isTeamManagerOrAdminOfOrg(user, user._organization)
(task, initializingAnnotationId) <- taskDAO
(taskId, initializingAnnotationId) <- taskDAO
.assignNext(user._id, teams, isTeamManagerOrAdmin) ?~> "task.unavailable"
insertedAnnotationBox <- annotationService.createAnnotationFor(user, task, initializingAnnotationId).futureBox
insertedAnnotationBox <- annotationService.createAnnotationFor(user, taskId, initializingAnnotationId).futureBox
_ <- annotationService.abortInitializedAnnotationOnFailure(initializingAnnotationId, insertedAnnotationBox)
annotation <- insertedAnnotationBox.toFox
annotationJSON <- annotationService.publicWrites(annotation, Some(user))
} yield JsonOk(annotationJSON, Messages("task.assigned"))
}
}
@ApiOperation(hidden = true, value = "")
def assignOne(id: String, userId: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
log() {
for {
taskIdValidated <- ObjectId.fromString(id)
userIdValidated <- ObjectId.fromString(userId)
assignee <- userService.findOneById(userIdValidated, useCache = true)
teams <- userService.teamIdsFor(userIdValidated)
task <- taskDAO.findOne(taskIdValidated)
project <- projectDAO.findOne(task._project)
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed"
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, assignee)) ?~> "notAllowed"
(_, initializingAnnotationId) <- taskDAO
.assignOneTo(taskIdValidated, userIdValidated, teams) ?~> "task.unavailable"
insertedAnnotationBox <- annotationService
.createAnnotationFor(assignee, taskIdValidated, initializingAnnotationId)
.futureBox
_ <- annotationService.abortInitializedAnnotationOnFailure(initializingAnnotationId, insertedAnnotationBox)
_ <- insertedAnnotationBox.toFox
taskUpdated <- taskDAO.findOne(taskIdValidated)
taskJson <- taskService.publicWrites(taskUpdated)(GlobalAccessContext)
} yield Ok(taskJson)
}
}

@ApiOperation(hidden = true, value = "")
def peekNext: Action[AnyContent] = sil.SecuredAction.async { implicit request =>
Expand Down
15 changes: 1 addition & 14 deletions app/controllers/UserTokenController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ class UserTokenController @Inject()(dataSetDAO: DataSetDAO,
annotationPrivateLinkDAO: AnnotationPrivateLinkDAO,
userService: UserService,
organizationDAO: OrganizationDAO,
annotationStore: AnnotationStore,
annotationInformationProvider: AnnotationInformationProvider,
dataStoreService: DataStoreService,
tracingStoreService: TracingStoreService,
Expand Down Expand Up @@ -169,18 +168,6 @@ class UserTokenController @Inject()(dataSetDAO: DataSetDAO,
// Access is explicitly checked by userBox, not by DBAccessContext, as there is no token sharing for annotations
// Optionally, a accessToken can be provided which explicitly looks up the read right the private link table

def findAnnotationForTracing(tracingId: String)(implicit ctx: DBAccessContext): Fox[Annotation] = {
val annotationFox = annotationDAO.findOneByTracingId(tracingId)
for {
annotationBox <- annotationFox.futureBox
} yield {
annotationBox match {
case Full(_) => annotationBox
case _ => annotationStore.findCachedByTracingId(tracingId)
}
}
}

def checkRestrictions(restrictions: AnnotationRestrictions) =
mode match {
case AccessMode.read => restrictions.allowAccess(userBox)
Expand All @@ -192,7 +179,7 @@ class UserTokenController @Inject()(dataSetDAO: DataSetDAO,
Fox.successful(UserAccessAnswer(granted = true))
else {
for {
annotation <- findAnnotationForTracing(tracingId)(GlobalAccessContext) ?~> "annotation.notFound"
annotation <- annotationInformationProvider.annotationForTracing(tracingId)(GlobalAccessContext) ?~> "annotation.notFound"
annotationAccessByToken <- token
.map(annotationPrivateLinkDAO.findOneByAccessToken)
.getOrElse(Fox.empty)
Expand Down
30 changes: 19 additions & 11 deletions app/controllers/WKRemoteTracingStoreController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package controllers

import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext}
import com.scalableminds.util.tools.{Fox, FoxImplicits}
import com.scalableminds.webknossos.datastore.models.datasource.DataSourceId
import com.scalableminds.webknossos.tracingstore.TracingUpdatesReport

import javax.inject.Inject
import models.analytics.{AnalyticsService, UpdateAnnotationEvent, UpdateAnnotationViewOnlyEvent}
import models.annotation.AnnotationState._
import models.annotation.{Annotation, AnnotationDAO, TracingStoreService}
import models.annotation.{Annotation, AnnotationDAO, AnnotationInformationProvider, TracingStoreService}
import models.binary.{DataSetDAO, DataSetService}
import models.organization.OrganizationDAO
import models.user.UserDAO
Expand All @@ -25,6 +27,7 @@ class WKRemoteTracingStoreController @Inject()(
dataSetService: DataSetService,
organizationDAO: OrganizationDAO,
userDAO: UserDAO,
annotationInformationProvider: AnnotationInformationProvider,
analyticsService: AnalyticsService,
dataSetDAO: DataSetDAO,
annotationDAO: AnnotationDAO)(implicit ec: ExecutionContext, playBodyParsers: PlayBodyParsers)
Expand Down Expand Up @@ -67,25 +70,30 @@ class WKRemoteTracingStoreController @Inject()(
if (annotation.state == Finished) Fox.failure("annotation already finshed")
else Fox.successful(())

def dataSource(name: String, key: String, organizationName: Option[String], dataSetName: String): Action[AnyContent] =
def dataSourceForTracing(name: String, key: String, tracingId: String): Action[AnyContent] =
Action.async { implicit request =>
tracingStoreService.validateAccess(name, key) { _ =>
implicit val ctx: DBAccessContext = GlobalAccessContext
for {
organizationIdOpt <- Fox.runOptional(organizationName) {
organizationDAO.findOneByName(_).map(_._id)
} ?~> Messages("organization.notFound", organizationName.getOrElse("")) ~> NOT_FOUND
organizationId <- Fox.fillOption(organizationIdOpt) {
dataSetDAO.getOrganizationForDataSet(dataSetName)
} ?~> Messages("dataSet.noAccess", dataSetName) ~> FORBIDDEN
dataSet <- dataSetDAO.findOneByNameAndOrganization(dataSetName, organizationId) ?~> Messages(
"dataSet.noAccess",
dataSetName) ~> FORBIDDEN
annotation <- annotationInformationProvider.annotationForTracing(tracingId) ?~> s"No annotation for tracing $tracingId"
dataSet <- dataSetDAO.findOne(annotation._dataSet)
dataSource <- dataSetService.dataSourceFor(dataSet)
} yield Ok(Json.toJson(dataSource))
}
}

def dataSourceIdForTracing(name: String, key: String, tracingId: String): Action[AnyContent] =
Action.async { implicit request =>
tracingStoreService.validateAccess(name, key) { _ =>
implicit val ctx: DBAccessContext = GlobalAccessContext
for {
annotation <- annotationInformationProvider.annotationForTracing(tracingId) ?~> s"No annotation for tracing $tracingId"
dataSet <- dataSetDAO.findOne(annotation._dataSet)
organization <- organizationDAO.findOne(dataSet._organization)
} yield Ok(Json.toJson(DataSourceId(dataSet.name, organization.name)))
}
}

def dataStoreURIForDataSet(name: String,
key: String,
organizationName: Option[String],
Expand Down
13 changes: 13 additions & 0 deletions app/models/annotation/AnnotationInformationProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package models.annotation

import com.scalableminds.util.accesscontext.DBAccessContext
import com.scalableminds.util.tools.{Fox, FoxImplicits}

import javax.inject.Inject
import models.annotation.AnnotationType.AnnotationType
import models.annotation.handler.AnnotationInformationHandlerSelector
import models.user.User
import net.liftweb.common.Full
import utils.ObjectId

import scala.concurrent.ExecutionContext
Expand Down Expand Up @@ -62,4 +64,15 @@ class AnnotationInformationProvider @Inject()(
private def handlerForTyp(typ: AnnotationType) =
annotationInformationHandlerSelector.informationHandlers(typ)

def annotationForTracing(tracingId: String)(implicit ctx: DBAccessContext): Fox[Annotation] = {
val annotationFox = annotationDAO.findOneByTracingId(tracingId)
for {
annotationBox <- annotationFox.futureBox
} yield {
annotationBox match {
case Full(_) => annotationBox
case _ => annotationStore.findCachedByTracingId(tracingId)
}
}
}
}
31 changes: 18 additions & 13 deletions app/models/annotation/AnnotationService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ case class DownloadAnnotation(skeletonTracingIdOpt: Option[String],
annotation: Annotation,
user: User,
taskOpt: Option[Task],
organizationName: String)
organizationName: String,
datasetName: String)

// Used to pass duplicate properties when creating a new tracing to avoid masking them.
// Uses the proto-generated geometry classes, hence the full qualifiers.
Expand Down Expand Up @@ -133,7 +134,7 @@ class AnnotationService @Inject()(

private def createVolumeTracing(
dataSource: DataSource,
organizationName: String,
datasetOrganizationName: String,
fallbackLayer: Option[SegmentationLayer],
boundingBox: Option[BoundingBox] = None,
startPosition: Option[Vec3Int] = None,
Expand All @@ -158,7 +159,7 @@ class AnnotationService @Inject()(
combineLargestSegmentIdsByPrecedence(fromNml = None, fromFallbackLayer = fallbackLayer.map(_.largestSegmentId)),
0,
VolumeTracingDefaults.zoomLevel,
organizationName = Some(organizationName),
organizationName = Some(datasetOrganizationName),
resolutions = resolutionsRestricted.map(vec3IntToProto)
)
}
Expand Down Expand Up @@ -195,7 +196,7 @@ class AnnotationService @Inject()(
private def createTracingsForExplorational(dataSet: DataSet,
dataSource: DataSource,
allAnnotationLayerParameters: List[AnnotationLayerParameters],
organizationName: String,
datasetOrganizationName: String,
existingAnnotationLayers: List[AnnotationLayer] = List())(
implicit ctx: DBAccessContext): Fox[List[AnnotationLayer]] = {

Expand Down Expand Up @@ -224,7 +225,7 @@ class AnnotationService @Inject()(
val skeleton = SkeletonTracingDefaults.createInstance.copy(
dataSetName = dataSet.name,
editPosition = dataSource.center,
organizationName = Some(organizationName),
organizationName = Some(datasetOrganizationName),
)
val skeletonAdapted = oldPrecedenceLayerProperties.map { p =>
skeleton.copy(
Expand All @@ -240,7 +241,7 @@ class AnnotationService @Inject()(
fallbackLayer <- Fox.runOptional(annotationLayerParameters.fallbackLayerName)(getFallbackLayer)
volumeTracing <- createVolumeTracing(
dataSource,
organizationName,
datasetOrganizationName,
fallbackLayer,
resolutionRestrictions =
annotationLayerParameters.resolutionRestrictions.getOrElse(ResolutionRestrictions.empty)
Expand Down Expand Up @@ -330,12 +331,12 @@ class AnnotationService @Inject()(
for {
dataSet <- dataSetDAO.findOne(_dataSet) ?~> "dataSet.noAccessById"
dataSource <- dataSetService.dataSourceFor(dataSet)
organization <- organizationDAO.findOne(user._organization)
datasetOrganization <- organizationDAO.findOne(dataSet._organization)
usableDataSource <- dataSource.toUsable ?~> Messages("dataSet.notImported", dataSource.id.name)
annotationLayers <- createTracingsForExplorational(dataSet,
usableDataSource,
annotationLayerParameters,
organization.name)
datasetOrganization.name)
teamId <- selectSuitableTeam(user, dataSet) ?~> "annotation.create.forbidden"
annotation = Annotation(ObjectId.generate, _dataSet, None, teamId, user._id, annotationLayers)
_ <- annotationDAO.insertOne(annotation)
Expand Down Expand Up @@ -433,14 +434,14 @@ class AnnotationService @Inject()(
tracingStoreClient.duplicateVolumeTracing(volumeId))
} yield (newSkeletonId, newVolumeId)

def createAnnotationFor(user: User, task: Task, initializingAnnotationId: ObjectId)(
def createAnnotationFor(user: User, taskId: ObjectId, initializingAnnotationId: ObjectId)(
implicit m: MessagesProvider,
ctx: DBAccessContext): Fox[Annotation] = {
def useAsTemplateAndInsert(annotation: Annotation) =
for {
dataSetName <- dataSetDAO.getNameById(annotation._dataSet)(GlobalAccessContext) ?~> "dataSet.notFoundForAnnotation"
dataSet <- dataSetDAO.findOne(annotation._dataSet) ?~> Messages("dataSet.noAccess", dataSetName)
(newSkeletonId, newVolumeId) <- tracingsFromBase(annotation, dataSet) ?~> s"Failed to use annotation base as template for task ${task._id} with annotation base ${annotation._id}"
(newSkeletonId, newVolumeId) <- tracingsFromBase(annotation, dataSet) ?~> s"Failed to use annotation base as template for task $taskId with annotation base ${annotation._id}"
annotationLayers <- AnnotationLayer.layersFromIds(newSkeletonId, newVolumeId)
newAnnotation = annotation.copy(
_id = initializingAnnotationId,
Expand All @@ -455,7 +456,7 @@ class AnnotationService @Inject()(
} yield newAnnotation

for {
annotationBase <- baseForTask(task._id) ?~> "Failed to retrieve annotation base."
annotationBase <- baseForTask(taskId) ?~> "Failed to retrieve annotation base."
result <- useAsTemplateAndInsert(annotationBase).toFox
} yield result
}
Expand Down Expand Up @@ -599,7 +600,8 @@ class AnnotationService @Inject()(
annotation,
user,
taskOpt,
organizationName) =>
organizationName,
datasetName) =>
for {
fetchedAnnotationLayersForAnnotation <- FetchedAnnotationLayer.layersFromTracings(skeletonTracingIdOpt,
volumeTracingIdOpt,
Expand All @@ -610,6 +612,7 @@ class AnnotationService @Inject()(
scaleOpt,
Some(name + "_data.zip"),
organizationName,
datasetName,
Some(user),
taskOpt)
} yield (nml, name, volumeDataOpt)
Expand All @@ -625,6 +628,7 @@ class AnnotationService @Inject()(
user <- userService.findOneById(annotation._user, useCache = true) ?~> "user.notFound"
taskOpt <- Fox.runOptional(annotation._task)(taskDAO.findOne) ?~> "task.notFound"
name <- savedTracingInformationHandler.nameForAnnotation(annotation)
dataset <- dataSetDAO.findOne(annotation._dataSet)
organizationName <- organizationDAO.findOrganizationNameForAnnotation(annotation._id)
skeletonTracingIdOpt <- annotation.skeletonTracingId
volumeTracingIdOpt <- annotation.volumeTracingId
Expand All @@ -639,7 +643,8 @@ class AnnotationService @Inject()(
annotation,
user,
taskOpt,
organizationName)
organizationName,
dataset.name)

def getSkeletonTracings(dataSetId: ObjectId, tracingIds: List[Option[String]]): Fox[List[Option[SkeletonTracing]]] =
for {
Expand Down
Loading

0 comments on commit 1cbb641

Please sign in to comment.