Skip to content

Commit 2dd6ffb

Browse files
fm3philippotto
andauthored
Per-User State in Annotations: Camera, Visibility Toggles (#8542)
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. <details> <summary> User-Specific state for annotations and tracings </summary> - Annotation - Camera (editPos/rot/zoomLevel) - Skeleton - activeNodeId - Tree visibilities - TreeGroup expanded states - boundingBox visibilities - Volume - activeSegmentId - SegmentGroup expanded states - boundingBox visibilities - segment visibilities </details> <details> <summary> Update actions that now mutate (only) the user state </summary> - UpdateCameraAnnotationAction - UpdateSegmentGroupsExpandedStateVolumeAction - UpdateSegmentVisibilityVolumeAction - UpdateTreeGroupVisibilitySkeletonAction - UpdateTreeGroupsExpandedStateSkeletonAction - UpdateTreeVisibilitySkeletonAction - UpdateUserBoundingBoxVisibilitySkeletonAction - UpdateUserBoundingBoxVisibilityVolumeAction - UpdateActiveNodeSkeletonAction - UpdateActiveSegmentIdVolumeAction </details> https://annotationuserstate.webknossos.xyz/ ### Steps to test: **Note: since new update actions are introduced here, annotations that are edited on this branch cannot be loaded after switching back to master. Refresh your db to be sure everything is clean** - Edit annotation with multiple users, user-state should be saved and used after reload - Annotation owner’s state should be used as a fallback (per element) - Layer Deletion/Addition (bbox visibilities should be moved to user state of new precedence layer) - NML download should “render” the visibilities etc so that it matches what the requesting user also sees in the browser - Merging annotations should preserve user states (despite id remapping of segments/trees etc) - Duplicating annotations should preserve the user state for the requesting user (who will be owner of the new annotation). ### TODOs: <details> <summary>Backend</summary> - [x] User state in annotation, skeleton, volume proto - [x] Define new update actions - [x] Annotation - [x] Skeleton - [x] Volume - [x] Implement applying new update actions - [x] When layer precedence changes, move relevant user states (bbox visibilities) - [x] Better Naming (now we have class UpdateUserStateSkeletonAction and trait UserStateVolumeUpdateAction…) - [x] Render user state for NML writing - [x] Camera - [x] Skeleton specifics - [x] Volume specifics - [x] Update with the new `UpdateSegmentGroupVisibilityVolumeAction` and `UpdateSegmentVisibilityVolumeAction` - [x] discuss what NML parser should do (create a user state? or use old properties?) - [x] discuss what duplicate should do (the owner changes, should user state be mutated?) - [x] check other tracingstore routes for potential interactions with user state - [x] Merge User State when merging Annotations (beware of remapped ids) - [x] merge skeleton user states - [x] merge volume user states - [x] adapt user states to new owner if needed - [x] add unit tests for render logic - [x] add unit tests for merge logic - [x] Refresh snapshots (blocked by #8541) - [x] Cleanup - [x] Make sure that everything still behaves as expected after #8492 is merged </details> <details> <summary>Frontend</summary> - [x] Read relevant properties from user state, with fallback hierarchy - [x] Create new update actions - [x] Don’t update everything immediately after load - [x] fix tests </details> ### Issues: - fixes #8531 ------ - [x] Updated [changelog](../blob/master/CHANGELOG.unreleased.md#unreleased) - [x] Removed dev-only changes like prints and application.conf edits - [x] Considered [common edge cases](../blob/master/.github/common_edge_cases.md) - [x] Needs datastore update after deployment --------- Co-authored-by: Philipp Otto <philipp.4096@gmail.com>
1 parent 42a8011 commit 2dd6ffb

File tree

82 files changed

+2619
-939
lines changed

Some content is hidden

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

82 files changed

+2619
-939
lines changed

CHANGELOG.unreleased.md

Lines changed: 1 addition & 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

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

app/models/annotation/AnnotationService.scala

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,8 @@ class AnnotationService @Inject()(
363363
duplicatedAnnotationProto <- tracingStoreClient.duplicateAnnotation(
364364
annotationBaseId,
365365
initializingAnnotationId,
366+
annotationBase._user,
367+
user._id,
366368
version = None,
367369
isFromTask = false, // isFromTask is when duplicate is called on a task annotation, not when a task is assigned
368370
datasetBoundingBox = None
@@ -531,8 +533,11 @@ class AnnotationService @Inject()(
531533
volumeTracingIdOpt,
532534
skeletonTracingOpt,
533535
volumeTracingOpt)
536+
// user state is not used in compound download, so the annotationProto can be a dummy one and requestingUser can be None.
537+
annotationProto = AnnotationProto("", 0L, Seq.empty, 0L)
534538
nml = nmlWriter.toNmlStream(
535539
name,
540+
annotationProto,
536541
fetchedAnnotationLayersForAnnotation,
537542
Some(annotation),
538543
voxelSizeOpt,
@@ -541,10 +546,11 @@ class AnnotationService @Inject()(
541546
conf.Http.uri,
542547
datasetName,
543548
datasetId,
544-
Some(user),
549+
user,
545550
taskOpt,
546551
skipVolumeData,
547-
volumeDataZipFormat
552+
volumeDataZipFormat,
553+
requestingUser = None
548554
)
549555
} yield (nml, volumeDataOpt)
550556
}

0 commit comments

Comments
 (0)