Skip to content

Commit 76616d0

Browse files
committed
Merge branch 'master' of github.com:scalableminds/webknossos into account-settings-page
2 parents 6a4b559 + 2dd6ffb commit 76616d0

File tree

90 files changed

+2978
-1194
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+2978
-1194
lines changed

CHANGELOG.unreleased.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1111
[Commits](https://github.com/scalableminds/webknossos/compare/25.06.1...HEAD)
1212

1313
### Added
14+
- In shared annotations with multiple authors, some changes are now stored per user. This means that other users won’t see all those changes if their own diverge. This includes the current position and zoom, visibilities of trees, bounding boxes, and segments (as specified with the checkboxes in the lists), as well as which groups are expanded in the lists. The annotation owner’s user state is used as a fallback for users who haven’t explicitly changed these values themselves. [#8542](https://github.com/scalableminds/webknossos/pull/8542)
1415
- Added the ability to duplicate trees in skeleton annotations. Users can create a copy of any tree (including all nodes, edges, and properties) via the context menu in the skeleton tab. [#8662](https://github.com/scalableminds/webknossos/pull/8662)
1516
- Meshes are now reloaded using their previous opacity value. [#8622](https://github.com/scalableminds/webknossos/pull/8622)
1617

@@ -19,6 +20,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1920
### Fixed
2021
- Improved efficiency of saving bounding box related changes. [#8492](https://github.com/scalableminds/webknossos/pull/8492)
2122
- When deleting a dataset, its caches are cleared, so that if a new dataset by the same name is uploaded afterwards, only new data is loaded. [#8638](https://github.com/scalableminds/webknossos/pull/8638)
23+
- Fixed the contrast of the WelcomeToast buttons. Updated `antd` to version `5.22`.[#8688](https://github.com/scalableminds/webknossos/pull/8688)
2224
- Fixed a race condition when starting proofreading with a split action. [#8676](https://github.com/scalableminds/webknossos/pull/8676)
2325
- Fixed that activating a mapping got stuck when a dataset was opened in "view" mode. [#8687](https://github.com/scalableminds/webknossos/pull/8687)
2426

app/controllers/AnnotationController.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@ class AnnotationController @Inject()(
420420
newAnnotationProto <- tracingStoreClient.duplicateAnnotation(
421421
annotation._id,
422422
newAnnotationId,
423+
annotation._user,
424+
user._id,
423425
version = None,
424426
isFromTask = annotation._task.isDefined,
425427
datasetBoundingBox = dataSource.map(_.boundingBox)

app/controllers/AnnotationIOController.scala

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ class AnnotationIOController @Inject()(
413413

414414
private def downloadExplorational(annotationId: ObjectId,
415415
typ: String,
416-
issuingUser: Option[User],
416+
requestingUser: Option[User],
417417
version: Option[Long],
418418
skipVolumeData: Boolean,
419419
volumeDataZipFormat: VolumeDataZipFormat)(implicit ctx: DBAccessContext) = {
@@ -425,10 +425,12 @@ class AnnotationIOController @Inject()(
425425
tracingStoreClient <- tracingStoreService.clientFor(dataset)
426426
fetchedAnnotationLayers <- Fox.serialCombined(annotation.skeletonAnnotationLayers)(
427427
tracingStoreClient.getSkeletonTracing(annotation._id, _, version))
428-
user <- userService.findOneCached(annotation._user)(GlobalAccessContext)
428+
annotationProto <- tracingStoreClient.getAnnotationProto(annotation._id, version)
429+
annotationOwner <- userService.findOneCached(annotation._user)(GlobalAccessContext)
429430
taskOpt <- Fox.runOptional(annotation._task)(taskDAO.findOne)
430431
nmlStream = nmlWriter.toNmlStream(
431432
"temp",
433+
annotationProto,
432434
fetchedAnnotationLayers,
433435
Some(annotation),
434436
dataset.voxelSize,
@@ -437,10 +439,11 @@ class AnnotationIOController @Inject()(
437439
conf.Http.uri,
438440
dataset.name,
439441
dataset._id,
440-
Some(user),
442+
annotationOwner,
441443
taskOpt,
442444
skipVolumeData,
443-
volumeDataZipFormat
445+
volumeDataZipFormat,
446+
requestingUser
444447
)
445448
nmlTemporaryFile = tempFileService.create()
446449
temporaryFileStream = new BufferedOutputStream(new FileOutputStream(new File(nmlTemporaryFile.toString)))
@@ -467,10 +470,12 @@ class AnnotationIOController @Inject()(
467470
skeletonAnnotationLayer =>
468471
tracingStoreClient.getSkeletonTracing(annotation._id, skeletonAnnotationLayer, version)
469472
} ?~> "annotation.download.fetchSkeletonLayer.failed"
470-
user <- userService.findOneCached(annotation._user)(GlobalAccessContext) ?~> "annotation.download.findUser.failed"
473+
annotationOwner <- userService.findOneCached(annotation._user)(GlobalAccessContext) ?~> "annotation.download.findUser.failed"
471474
taskOpt <- Fox.runOptional(annotation._task)(taskDAO.findOne(_)(GlobalAccessContext)) ?~> "task.notFound"
475+
annotationProto <- tracingStoreClient.getAnnotationProto(annotation._id, version)
472476
nmlStream = nmlWriter.toNmlStream(
473477
name,
478+
annotationProto,
474479
fetchedSkeletonLayers ::: fetchedVolumeLayers,
475480
Some(annotation),
476481
dataset.voxelSize,
@@ -479,10 +484,11 @@ class AnnotationIOController @Inject()(
479484
conf.Http.uri,
480485
dataset.name,
481486
dataset._id,
482-
Some(user),
487+
annotationOwner,
483488
taskOpt,
484489
skipVolumeData,
485-
volumeDataZipFormat
490+
volumeDataZipFormat,
491+
requestingUser
486492
)
487493
temporaryFile = tempFileService.create()
488494
zipper = ZipIO.startZip(new BufferedOutputStream(new FileOutputStream(new File(temporaryFile.toString))))
@@ -520,13 +526,13 @@ class AnnotationIOController @Inject()(
520526
zipMimeType
521527

522528
for {
523-
annotation <- provider.provideAnnotation(typ, annotationId, issuingUser) ~> NOT_FOUND
529+
annotation <- provider.provideAnnotation(typ, annotationId, requestingUser) ~> NOT_FOUND
524530
restrictions <- provider.restrictionsFor(typ, annotationId) ?~> "annotation.restrictions.unavailable"
525531
name <- provider.nameFor(annotation) ?~> "annotation.name.impossible"
526532
fileExtension = exportExtensionForAnnotation(annotation)
527533
fileName = name + fileExtension
528534
mimeType = exportMimeTypeForAnnotation(annotation)
529-
_ <- restrictions.allowDownload(issuingUser) ?~> "annotation.download.notAllowed" ~> FORBIDDEN
535+
_ <- restrictions.allowDownload(requestingUser) ?~> "annotation.download.notAllowed" ~> FORBIDDEN
530536
dataset <- datasetDAO.findOne(annotation._dataset)(GlobalAccessContext) ?~> "dataset.notFoundForAnnotation" ~> NOT_FOUND
531537
organization <- organizationDAO.findOne(dataset._organization)(GlobalAccessContext) ?~> "organization.notFound" ~> NOT_FOUND
532538
temporaryFile <- annotationToTemporaryFile(dataset, annotation, name, organization._id) ?~> "annotation.writeTemporaryFile.failed"
@@ -538,13 +544,13 @@ class AnnotationIOController @Inject()(
538544
}
539545
}
540546

541-
private def downloadProject(projectId: ObjectId, userOpt: Option[User], skipVolumeData: Boolean)(
547+
private def downloadProject(projectId: ObjectId, requestingUserOpt: Option[User], skipVolumeData: Boolean)(
542548
implicit ctx: DBAccessContext,
543549
m: MessagesProvider) =
544550
for {
545-
user <- userOpt.toFox ?~> Messages("notAllowed") ~> FORBIDDEN
551+
requestingUser <- requestingUserOpt.toFox ?~> Messages("notAllowed") ~> FORBIDDEN
546552
project <- projectDAO.findOne(projectId) ?~> Messages("project.notFound", projectId) ~> NOT_FOUND
547-
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(user, project._team)) ?~> "notAllowed" ~> FORBIDDEN
553+
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(requestingUser, project._team)) ?~> "notAllowed" ~> FORBIDDEN
548554
annotations <- annotationDAO.findAllFinishedForProject(projectId)
549555
zipTempFilePath <- annotationService.zipAnnotations(annotations,
550556
project.name,
@@ -556,7 +562,7 @@ class AnnotationIOController @Inject()(
556562
fileName = _ => Some(TextUtils.normalize(project.name + "_nmls.zip")))
557563
}
558564

559-
private def downloadTask(taskId: ObjectId, userOpt: Option[User], skipVolumeData: Boolean)(
565+
private def downloadTask(taskId: ObjectId, requestingUserOpt: Option[User], skipVolumeData: Boolean)(
560566
implicit ctx: DBAccessContext,
561567
m: MessagesProvider) = {
562568
def createTaskZip(task: Task): Fox[Path] = annotationService.annotationsFor(task._id).flatMap { annotations =>
@@ -566,10 +572,10 @@ class AnnotationIOController @Inject()(
566572
}
567573

568574
for {
569-
user <- userOpt.toFox ?~> Messages("notAllowed") ~> FORBIDDEN
575+
requestingUser <- requestingUserOpt.toFox ?~> Messages("notAllowed") ~> FORBIDDEN
570576
task <- taskDAO.findOne(taskId) ?~> Messages("task.notFound") ~> NOT_FOUND
571577
project <- projectDAO.findOne(task._project) ?~> Messages("project.notFound") ~> NOT_FOUND
572-
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(user, project._team)) ?~> Messages("notAllowed") ~> FORBIDDEN
578+
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(requestingUser, project._team)) ?~> Messages("notAllowed") ~> FORBIDDEN
573579
zipTempFilePath <- createTaskZip(task)
574580
} yield {
575581
Ok.sendPath(zipTempFilePath,
@@ -578,7 +584,7 @@ class AnnotationIOController @Inject()(
578584
}
579585
}
580586

581-
private def downloadTaskType(taskTypeId: ObjectId, userOpt: Option[User], skipVolumeData: Boolean)(
587+
private def downloadTaskType(taskTypeId: ObjectId, requestingUserOpt: Option[User], skipVolumeData: Boolean)(
582588
implicit ctx: DBAccessContext,
583589
m: MessagesProvider) = {
584590
def createTaskTypeZip(taskType: TaskType) =
@@ -593,9 +599,9 @@ class AnnotationIOController @Inject()(
593599
} yield zip
594600

595601
for {
596-
user <- userOpt.toFox ?~> Messages("notAllowed") ~> FORBIDDEN
602+
requestingUser <- requestingUserOpt.toFox ?~> Messages("notAllowed") ~> FORBIDDEN
597603
taskType <- taskTypeDAO.findOne(taskTypeId) ?~> "taskType.notFound" ~> NOT_FOUND
598-
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(user, taskType._team)) ?~> "notAllowed" ~> FORBIDDEN
604+
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(requestingUser, taskType._team)) ?~> "notAllowed" ~> FORBIDDEN
599605
zipTempFilePath <- createTaskTypeZip(taskType)
600606
} yield {
601607
Ok.sendPath(zipTempFilePath,

app/controllers/TaskController.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ class TaskController @Inject()(taskCreationService: TaskCreationService,
6767
taskParametersFull <- taskCreationService.createTracingsFromBaseAnnotations(taskParametersWithIds,
6868
taskType,
6969
dataset,
70-
dataSource)
70+
dataSource,
71+
request.identity._id)
7172
skeletonBaseOpts: List[Option[SkeletonTracing]] = taskCreationService.createTaskSkeletonTracingBases(
7273
taskParametersFull)
7374
volumeBaseOpts: List[Option[(VolumeTracing, Option[File])]] <- taskCreationService.createTaskVolumeTracingBases(

app/models/annotation/AnnotationLayerPrecedence.scala

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ package models.annotation
22

33
import com.scalableminds.util.objectid.ObjectId
44
import com.scalableminds.util.tools.{Fox, FoxImplicits}
5-
import com.scalableminds.webknossos.datastore.SkeletonTracing.SkeletonTracing
6-
import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing
5+
import com.scalableminds.webknossos.datastore.IdWithBool.Id32WithBool
6+
import com.scalableminds.webknossos.datastore.SkeletonTracing.{SkeletonTracing, SkeletonUserStateProto}
7+
import com.scalableminds.webknossos.datastore.VolumeTracing.{VolumeTracing, VolumeUserStateProto}
78
import com.scalableminds.webknossos.datastore.geometry.{
89
AdditionalCoordinateProto,
910
NamedBoundingBoxProto,
1011
Vec3DoubleProto,
1112
Vec3IntProto
1213
}
14+
import com.scalableminds.webknossos.datastore.helpers.SkeletonTracingDefaults
1315
import com.scalableminds.webknossos.datastore.models.annotation.{
1416
AnnotationLayer,
1517
AnnotationLayerType,
@@ -27,6 +29,7 @@ case class RedundantTracingProperties(
2729
zoomLevel: Double,
2830
userBoundingBoxes: Seq[NamedBoundingBoxProto],
2931
editPositionAdditionalCoordinates: Seq[AdditionalCoordinateProto],
32+
userStateBoundingBoxVisibilities: Map[String, Seq[Id32WithBool]] // UserId → Seq(bboxId, bboxIsVisible)
3033
)
3134

3235
trait AnnotationLayerPrecedence extends FoxImplicits {
@@ -54,7 +57,8 @@ trait AnnotationLayerPrecedence extends FoxImplicits {
5457
editRotation = p.editRotation,
5558
zoomLevel = p.zoomLevel,
5659
userBoundingBoxes = p.userBoundingBoxes,
57-
editPositionAdditionalCoordinates = p.editPositionAdditionalCoordinates
60+
editPositionAdditionalCoordinates = p.editPositionAdditionalCoordinates,
61+
userStates = adaptSkeletonUserStates(skeletonTracing.userStates, p)
5862
)
5963
}.getOrElse(skeletonTracing)
6064

@@ -66,10 +70,61 @@ trait AnnotationLayerPrecedence extends FoxImplicits {
6670
editRotation = p.editRotation,
6771
zoomLevel = p.zoomLevel,
6872
userBoundingBoxes = p.userBoundingBoxes,
69-
editPositionAdditionalCoordinates = p.editPositionAdditionalCoordinates
73+
editPositionAdditionalCoordinates = p.editPositionAdditionalCoordinates,
74+
userStates = adaptVolumeUserStates(volumeTracing.userStates, p)
7075
)
7176
}.getOrElse(volumeTracing)
7277

78+
private def adaptSkeletonUserStates(
79+
userStates: Seq[SkeletonUserStateProto],
80+
oldPrecedenceLayerProperties: RedundantTracingProperties): Seq[SkeletonUserStateProto] = {
81+
val adaptedExistingUserStates = userStates.map { userState =>
82+
val userId = userState.userId
83+
oldPrecedenceLayerProperties.userStateBoundingBoxVisibilities.get(userId) match {
84+
case None => userState
85+
case Some(precedenceBboxVisibilities) =>
86+
userState.copy(boundingBoxVisibilities = precedenceBboxVisibilities)
87+
}
88+
}
89+
// We also have to create new user states for the users the old precedence layer has, but the new precedence layer is missing.
90+
val newUserPrecedenceProperties = oldPrecedenceLayerProperties.userStateBoundingBoxVisibilities.filter(tuple =>
91+
!userStates.exists(_.userId == tuple._1))
92+
val newUserStates = newUserPrecedenceProperties.map {
93+
case (userId: String, boundingBoxVisibilities: Seq[Id32WithBool]) =>
94+
SkeletonTracingDefaults
95+
.emptyUserState(userId)
96+
.copy(
97+
boundingBoxVisibilities = boundingBoxVisibilities
98+
)
99+
}
100+
adaptedExistingUserStates ++ newUserStates
101+
}
102+
103+
private def adaptVolumeUserStates(
104+
userStates: Seq[VolumeUserStateProto],
105+
oldPrecedenceLayerProperties: RedundantTracingProperties): Seq[VolumeUserStateProto] = {
106+
val adaptedExistingUserStates = userStates.map { userState =>
107+
val userId = userState.userId
108+
oldPrecedenceLayerProperties.userStateBoundingBoxVisibilities.get(userId) match {
109+
case None => userState
110+
case Some(precedenceBboxVisibilities) =>
111+
userState.copy(boundingBoxVisibilities = precedenceBboxVisibilities)
112+
}
113+
}
114+
// We also have to create new user states for the users the old precedence layer has, but the new precedence layer is missing.
115+
val newUserPrecedenceProperties = oldPrecedenceLayerProperties.userStateBoundingBoxVisibilities.filter(tuple =>
116+
!userStates.exists(_.userId == tuple._1))
117+
val newUserStates = newUserPrecedenceProperties.map {
118+
case (userId: String, boundingBoxVisibilities: Seq[Id32WithBool]) =>
119+
VolumeTracingDefaults
120+
.emptyUserState(userId)
121+
.copy(
122+
boundingBoxVisibilities = boundingBoxVisibilities
123+
)
124+
}
125+
adaptedExistingUserStates ++ newUserStates
126+
}
127+
73128
protected def getOldPrecedenceLayerProperties(existingAnnotationId: Option[ObjectId],
74129
existingAnnotationLayers: List[AnnotationLayer],
75130
previousVersion: Option[Long],
@@ -138,7 +193,8 @@ trait AnnotationLayerPrecedence extends FoxImplicits {
138193
s.zoomLevel,
139194
s.userBoundingBoxes ++ s.userBoundingBox.map(
140195
com.scalableminds.webknossos.datastore.geometry.NamedBoundingBoxProto(0, None, None, None, _)),
141-
s.editPositionAdditionalCoordinates
196+
s.editPositionAdditionalCoordinates,
197+
s.userStates.map(userState => (userState.userId, userState.boundingBoxVisibilities)).toMap
142198
)
143199
case Right(v) =>
144200
RedundantTracingProperties(
@@ -147,7 +203,8 @@ trait AnnotationLayerPrecedence extends FoxImplicits {
147203
v.zoomLevel,
148204
v.userBoundingBoxes ++ v.userBoundingBox.map(
149205
com.scalableminds.webknossos.datastore.geometry.NamedBoundingBoxProto(0, None, None, None, _)),
150-
v.editPositionAdditionalCoordinates
206+
v.editPositionAdditionalCoordinates,
207+
v.userStates.map(userState => (userState.userId, userState.boundingBoxVisibilities)).toMap
151208
)
152209
}
153210
}

app/models/annotation/AnnotationMerger.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ class AnnotationMerger @Inject()(datasetDAO: DatasetDAO, tracingStoreService: Tr
4646
Fox.empty
4747
else {
4848
for {
49-
mergedAnnotationLayers <- mergeAnnotationsInTracingstore(annotations, datasetId, newId, toTemporaryStore) ?~> "Failed to merge annotations in tracingstore."
49+
mergedAnnotationLayers <- mergeAnnotationsInTracingstore(
50+
annotations,
51+
datasetId,
52+
newId,
53+
userId,
54+
toTemporaryStore) ?~> "Failed to merge annotations in tracingstore."
5055
} yield {
5156
Annotation(
5257
newId,
@@ -64,13 +69,16 @@ class AnnotationMerger @Inject()(datasetDAO: DatasetDAO, tracingStoreService: Tr
6469
annotations: List[Annotation],
6570
datasetId: ObjectId,
6671
newAnnotationId: ObjectId,
72+
requestingUserId: ObjectId,
6773
toTemporaryStore: Boolean)(implicit ctx: DBAccessContext): Fox[List[AnnotationLayer]] =
6874
for {
6975
dataset <- datasetDAO.findOne(datasetId)
7076
tracingStoreClient: WKRemoteTracingStoreClient <- tracingStoreService.clientFor(dataset)
7177
mergedAnnotationProto <- tracingStoreClient.mergeAnnotationsByIds(annotations.map(_.id),
78+
annotations.map(_._user),
7279
newAnnotationId,
73-
toTemporaryStore)
80+
toTemporaryStore,
81+
requestingUserId)
7482
layers = mergedAnnotationProto.annotationLayers.map(AnnotationLayer.fromProto)
7583
} yield layers.toList
7684

0 commit comments

Comments
 (0)