Skip to content

Commit d443e02

Browse files
Merge branch 'users-can-change-email' of github.com:scalableminds/webknossos into users-can-change-email
2 parents 9ec548d + 56098bc commit d443e02

File tree

114 files changed

+3324
-1547
lines changed

Some content is hidden

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

114 files changed

+3324
-1547
lines changed

CHANGELOG.released.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
77
and this project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MICRO`.
88
For upgrade instructions, please check the [migration guide](MIGRATIONS.released.md).
99

10+
## [25.06.2](https://github.com/scalableminds/webknossos/releases/tag/25.06.2) - 2025-06-17
11+
[Commits](https://github.com/scalableminds/webknossos/compare/25.06.1...25.06.2)
12+
13+
### Fixed
14+
- Fixed a regression that led to incorrect behavior when trying to jump to the last branchpoint even though no branchpoint existed. [#8695](https://github.com/scalableminds/webknossos/pull/8695)
15+
1016
## [25.06.1](https://github.com/scalableminds/webknossos/releases/tag/25.06.1) - 2025-06-03
1117
[Commits](https://github.com/scalableminds/webknossos/compare/25.06.0...25.06.1)
1218

CHANGELOG.unreleased.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,23 @@ and this project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MIC
88
For upgrade instructions, please check the [migration guide](MIGRATIONS.released.md).
99

1010
## Unreleased
11-
[Commits](https://github.com/scalableminds/webknossos/compare/25.06.1...HEAD)
11+
[Commits](https://github.com/scalableminds/webknossos/compare/25.06.2...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

1718
### Changed
19+
- The datasource-properties.json as exposed in zarr streaming routes and volume annotation download now includes an explicit “path” attribute for every mag. The supplied path is relative to the location of the datasource-properties.json itself, e.g. `./color/2-2-1`. [#8518](https://github.com/scalableminds/webknossos/pull/8518)
1820

1921
### Fixed
2022
- Improved efficiency of saving bounding box related changes. [#8492](https://github.com/scalableminds/webknossos/pull/8492)
2123
- 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)
24+
- Fixed the contrast of the WelcomeToast buttons. Updated `antd` to version `5.22`.[#8688](https://github.com/scalableminds/webknossos/pull/8688)
25+
- Fixed a race condition when starting proofreading with a split action. [#8676](https://github.com/scalableminds/webknossos/pull/8676)
26+
- Fixed that activating a mapping got stuck when a dataset was opened in "view" mode. [#8687](https://github.com/scalableminds/webknossos/pull/8687)
27+
- Fixed a regression that led to incorrect behavior when trying to jump to the last branchpoint even though no branchpoint existed. [#8695](https://github.com/scalableminds/webknossos/pull/8695)
2228

2329
### Removed
2430

MIGRATIONS.released.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ See `MIGRATIONS.unreleased.md` for the changes which are not yet part of an offi
66
This project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MICRO`.
77
User-facing changes are documented in the [changelog](CHANGELOG.released.md).
88

9+
## [25.06.2](https://github.com/scalableminds/webknossos/releases/tag/25.06.2) - 2025-06-17
10+
[Commits](https://github.com/scalableminds/webknossos/compare/25.06.1...25.06.2)
11+
12+
### Postgres Evolutions:
13+
None.
14+
915
## [25.06.1](https://github.com/scalableminds/webknossos/releases/tag/25.06.1) - 2025-06-03
1016
[Commits](https://github.com/scalableminds/webknossos/compare/25.06.0...25.06.1)
1117

MIGRATIONS.unreleased.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ This project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MICRO`.
66
User-facing changes are documented in the [changelog](CHANGELOG.released.md).
77

88
## Unreleased
9-
[Commits](https://github.com/scalableminds/webknossos/compare/25.06.1...HEAD)
9+
[Commits](https://github.com/scalableminds/webknossos/compare/25.06.2...HEAD)
10+
11+
### Postgres Evolutions:
12+
- The default thread pool size was increased from 5 to 10 times the number of available CPUs (capped at 1000). Note that wk may need slightly more memory because of this. [#8686](https://github.com/scalableminds/webknossos/pull/8686)
1013

1114
### Postgres Evolutions:
1215
- [134-dataset-layer-attachments.sql](conf/evolutions/134-dataset-layer-attachments.sql)

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
}

0 commit comments

Comments
 (0)