Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
68ab52b
change schema
leowe Jul 14, 2022
554259c
add first version
leowe Jul 20, 2022
5130ed3
reformat
leowe Jul 20, 2022
e067bc5
adding debugging
leowe Jul 21, 2022
10f35e2
remove printlns
leowe Jul 21, 2022
7c3493e
add evolution
leowe Jul 21, 2022
a4b80ad
fix evolution
leowe Jul 21, 2022
1c77305
add tokens to dsremotetracingstore requests
leowe Jul 21, 2022
e5ab0f5
reformat
Aug 2, 2022
2b74b92
add CRUD for privatelinks
Aug 2, 2022
c17a875
add variable changes
Aug 4, 2022
fda0f57
newer evolution version
Aug 4, 2022
bf06570
Merge branch 'master' into zarr-private-links
Aug 4, 2022
6a30899
use fromString
Aug 4, 2022
de3ee17
remove s from uninterpolated string
Aug 4, 2022
30707bf
Merge branch 'master' into zarr-private-links
leowe Aug 4, 2022
cc218e3
change table name to fit standard
Aug 4, 2022
784f332
implement rudimentary list route for annotation private links
philippotto Aug 10, 2022
2a37459
Update CHANGELOG.unreleased.md
leowe Aug 10, 2022
7d3a8fd
implement rudimentary create/list/delete frontend for private links i…
philippotto Aug 10, 2022
f75ca1f
Merge branch 'zarr-private-links' of github.com:scalableminds/webknos…
philippotto Aug 10, 2022
7629128
add pr feedback
Aug 10, 2022
61d2b49
add update access query
Aug 10, 2022
3c1ff8d
reformat
Aug 11, 2022
e9d06b6
convert id to string
Aug 11, 2022
1f9a369
Merge branch 'master' into zarr-private-links
Aug 11, 2022
d3026bf
re-add NOT FOUND messages
Aug 11, 2022
9993006
fix changelog
Aug 11, 2022
c70eb13
using NOT_FOUND correctly
Aug 11, 2022
1a87e17
fix delete route usage and adapt to new routes
philippotto Aug 11, 2022
6cf327a
remove _ in public writes and use JsonOk
Aug 11, 2022
9115ff6
Merge branch 'zarr-private-links' of github.com:scalableminds/webknos…
philippotto Aug 11, 2022
c1d132f
add readaccessquery
Aug 11, 2022
cfb08da
add expiry date UI, use antd List, refactor query hooks etc
philippotto Aug 11, 2022
8b1f1ce
Merge branch 'zarr-private-links' of github.com:scalableminds/webknos…
philippotto Aug 11, 2022
80629da
update update route
Aug 11, 2022
ea78bc0
complete expiration date editing; improve error messages; use loading…
philippotto Aug 12, 2022
e98bf11
clean up UI
philippotto Aug 12, 2022
5174c0e
dont hardcode annotation id; extract into own modal
philippotto Aug 12, 2022
e2add06
highlight expired links
philippotto Aug 12, 2022
cf0f04c
fix linting
philippotto Aug 12, 2022
38f3f92
Update frontend/javascripts/oxalis/view/action-bar/private_links_view…
philippotto Aug 15, 2022
df88da1
format
philippotto Aug 15, 2022
ab23355
use correct url schema
philippotto Aug 15, 2022
76ab744
rename endpoints to links
philippotto Aug 15, 2022
2d3713b
copy layer-specific urls via dropdown
philippotto Aug 15, 2022
dcf91a0
use correct names for volume tracings when copying zarr links
philippotto Aug 15, 2022
11b67ac
Update frontend/javascripts/oxalis/view/action-bar/tracing_actions_vi…
philippotto Aug 15, 2022
035d9ce
Merge branch 'master' into zarr-private-links
Aug 17, 2022
a0e7ced
incorporate pr feedback
Aug 18, 2022
0cde222
re-add messagesprovider
Aug 18, 2022
b996307
reformat
Aug 18, 2022
a221a0f
default 30 day expiration period; show loading spinner when links are…
philippotto Aug 19, 2022
e33939f
Merge branch 'zarr-private-links' of github.com:scalableminds/webknos…
philippotto Aug 19, 2022
937fedb
add /data to zarr links
philippotto Aug 19, 2022
51228a9
update changelog
philippotto Aug 19, 2022
823b21f
incorporate pr feedback
philippotto Aug 19, 2022
b04764c
center the 'Reloading webknossos...' message horizontally and vertically
philippotto Aug 19, 2022
e3a6fc7
add target=_blank for zarr doc link
philippotto Aug 19, 2022
58ea063
also add rel='noreferrer'
philippotto Aug 19, 2022
5ec3eb7
also show zarr link in dataset share modal
philippotto Aug 22, 2022
1b6885a
clarify that skeletons are not shared via zarr
philippotto Aug 22, 2022
2d86333
use momentjs for expiration calculation
philippotto Aug 22, 2022
f0edfc9
Merge branch 'master' of github.com:scalableminds/webknossos into zar…
philippotto Aug 22, 2022
dbbdc55
add missing evolution (got lost during merge)
philippotto Aug 22, 2022
54e260e
fix schema version in migration
philippotto Aug 22, 2022
7fbd48e
fix linting
philippotto Aug 22, 2022
2e64f07
add reversion
Aug 22, 2022
75458f3
rename reversion
Aug 22, 2022
d3ceec8
add swagger annotations
Aug 22, 2022
3cb74f6
change NOT_FOUND in swagger annotation to status codes
Aug 23, 2022
3d4c7cc
Merge branch 'master' into zarr-private-links
Aug 23, 2022
e00b064
finish merge
Aug 23, 2022
4425899
pretty
fm3 Aug 23, 2022
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
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Added a "duplicate" button for annotations. [#6386](https://github.com/scalableminds/webknossos/pull/6386)
- Added new filter options for the dataset list api at `api/datasets`: `organizationName: String`, `onlyMyOrganization: Boolean`, `uploaderId: String` [#6377](https://github.com/scalableminds/webknossos/pull/6377)
- The Add Dataset view now hosts an Add Zarr Dataset tab to explore, configure and import remote Zarr datasets. [#6335](https://github.com/scalableminds/webknossos/pull/6335)
- Added a UI to manage Zarr links for an annotation so that the annotation can be streamed to 3rd party tools. This change also includes new backend API routes for using the (private) zarr links to annotations as well as creating them. [#6367](https://github.com/scalableminds/webknossos/pull/6367)

### Changed
- webKnossos uses WebGL 2 instead of WebGL 1 now. In case your browser/hardware does not support this, webKnossos will alert you and you need to upgrade your system. [#6350](https://github.com/scalableminds/webknossos/pull/6350)
Expand Down
1 change: 1 addition & 0 deletions MIGRATIONS.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md).
- [084-annotation-contributors.sql](conf/evolutions/084-annotation-contributors.sql)
- [085-add-annotations-publicationforeignkey](conf/evolutions/085-add-annotations-publicationforeignkey.sql)
- [086-drop-foreign-datastores.sql](conf/evolutions/086-drop-foreign-datastores.sql.sql)
- [087-zarr-private-links](conf/evolutions/087-zarr-private-links.sql)
9 changes: 7 additions & 2 deletions app/controllers/AnnotationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import com.mohiva.play.silhouette.api.Silhouette
import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext}
import com.scalableminds.util.geometry.BoundingBox
import com.scalableminds.util.tools.{Fox, FoxImplicits}
import com.scalableminds.webknossos.datastore.models.annotation.{AnnotationLayer, AnnotationLayerType}
import com.scalableminds.webknossos.tracingstore.tracings.volume.ResolutionRestrictions
import com.scalableminds.webknossos.tracingstore.tracings.{TracingIds, TracingType}
import io.swagger.annotations._

import javax.inject.Inject
import models.analytics.{AnalyticsService, CreateAnnotationEvent, OpenAnnotationEvent}
import com.scalableminds.webknossos.datastore.models.annotation.AnnotationLayerType.AnnotationLayerType
import models.annotation.AnnotationState.Cancelled
import models.annotation._
import models.binary.{DataSetDAO, DataSetService}
Expand All @@ -18,13 +23,12 @@ import models.user.time._
import models.user.{User, UserDAO, UserService}
import oxalis.security.{URLSharing, WkEnv}
import play.api.i18n.{Messages, MessagesProvider}
import play.api.libs.json.{JsArray, _}
import play.api.libs.json._
import play.api.mvc.{Action, AnyContent, PlayBodyParsers}
import utils.{ObjectId, WkConf}

import javax.inject.Inject
import models.analytics.{AnalyticsService, CreateAnnotationEvent, OpenAnnotationEvent}
import models.annotation.AnnotationLayerType.AnnotationLayerType
import models.organization.OrganizationDAO
import oxalis.mail.{MailchimpClient, MailchimpTag}

Expand Down Expand Up @@ -53,6 +57,7 @@ class AnnotationController @Inject()(
teamService: TeamService,
projectDAO: ProjectDAO,
teamDAO: TeamDAO,
annotationPrivateLinkDAO: AnnotationPrivateLinkDAO,
timeSpanService: TimeSpanService,
annotationMerger: AnnotationMerger,
tracingStoreService: TracingStoreService,
Expand Down
7 changes: 6 additions & 1 deletion app/controllers/AnnotationIOController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package controllers

import java.io.{BufferedOutputStream, File, FileOutputStream}
import java.util.zip.Deflater

import akka.actor.ActorSystem
import akka.stream.Materializer
import com.mohiva.play.silhouette.api.Silhouette
Expand All @@ -12,6 +11,11 @@ import com.scalableminds.util.tools.{Fox, FoxImplicits, TextUtils}
import com.scalableminds.webknossos.datastore.SkeletonTracing.{SkeletonTracing, SkeletonTracingOpt, SkeletonTracings}
import com.scalableminds.webknossos.datastore.VolumeTracing.{VolumeTracing, VolumeTracingOpt, VolumeTracings}
import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits
import com.scalableminds.webknossos.datastore.models.annotation.{
AnnotationLayer,
AnnotationLayerType,
FetchedAnnotationLayer
}
import com.scalableminds.webknossos.datastore.models.datasource.{
AbstractSegmentationLayer,
DataLayerLike,
Expand All @@ -22,6 +26,7 @@ import com.scalableminds.webknossos.tracingstore.tracings.TracingType
import com.scalableminds.webknossos.tracingstore.tracings.volume.VolumeTracingDefaults
import com.typesafe.scalalogging.LazyLogging
import io.swagger.annotations._

import javax.inject.Inject
import models.analytics.{AnalyticsService, DownloadAnnotationEvent, UploadAnnotationEvent}
import models.annotation.AnnotationState._
Expand Down
174 changes: 174 additions & 0 deletions app/controllers/AnnotationPrivateLinkController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package controllers

import com.mohiva.play.silhouette.api.Silhouette
import com.scalableminds.util.accesscontext.GlobalAccessContext
import com.scalableminds.util.tools.Fox
import com.scalableminds.util.tools.FoxImplicits
import io.swagger.annotations._
import play.api.libs.json._

import javax.inject.Inject
import models.annotation._
import oxalis.security.WkEnv
import play.api.mvc.{Action, AnyContent, PlayBodyParsers}
import utils.ObjectId

import scala.concurrent.ExecutionContext
@Api
class AnnotationPrivateLinkController @Inject()(
annotationDAO: AnnotationDAO,
annotationService: AnnotationService,
annotationPrivateLinkDAO: AnnotationPrivateLinkDAO,
annotationPrivateLinkService: AnnotationPrivateLinkService,
sil: Silhouette[WkEnv])(implicit ec: ExecutionContext, val bodyParsers: PlayBodyParsers)
extends Controller
with FoxImplicits {

@ApiOperation(hidden = true, value = "")
def lookupPrivateLink(accessToken: String): Action[AnyContent] = Action.async { implicit request =>
for {
annotationPrivateLink <- annotationPrivateLinkDAO.findOneByAccessToken(accessToken)
_ <- bool2Fox(annotationPrivateLink.expirationDateTime.forall(_ > System.currentTimeMillis())) ?~> "Token expired" ~> 404
annotation: Annotation <- annotationDAO.findOne(annotationPrivateLink._annotation)(GlobalAccessContext)
writtenAnnotation <- annotationService.writesAsAnnotationSource(annotation)
} yield Ok(writtenAnnotation)
}

@ApiOperation(value = "List all existing private zarr links for a user", nickname = "listPrivateLinks")
@ApiResponses(
Array(new ApiResponse(code = 200, message = "JSON object containing string that private link was deleted."),
new ApiResponse(code = 400, message = badRequestLabel)))
def list: Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
links <- annotationPrivateLinkDAO.findAll
linksJsonList <- Fox.serialCombined(links)(annotationPrivateLinkService.publicWrites)
} yield Ok(Json.toJson(linksJsonList))
}

@ApiOperation(value = "List all existing private zarr links for a user for a given annotation",
nickname = "listPrivateLinksByAnnotation")
@ApiResponses(
Array(new ApiResponse(code = 200, message = "JSON object containing string that private link was deleted."),
new ApiResponse(code = 400, message = badRequestLabel)))
def listByAnnotation(@ApiParam(value = "The id of the annotation") annotationId: String): Action[AnyContent] =
sil.SecuredAction.async { implicit request =>
for {
annotationIdValidated <- ObjectId.fromString(annotationId)
links <- annotationPrivateLinkDAO.findAllByAnnotation(annotationIdValidated)
linksJsonList <- Fox.serialCombined(links)(annotationPrivateLinkService.publicWrites)
} yield Ok(Json.toJson(linksJsonList))
}

@ApiOperation(value = "Get the private zarr link for a user for a given id if it exists", nickname = "getPrivateLink")
@ApiResponses(
Array(
new ApiResponse(code = 200, message = "JSON object containing string that private link was deleted."),
new ApiResponse(code = 400, message = badRequestLabel),
new ApiResponse(code = 404, message = badRequestLabel)
))
def get(@ApiParam(value = "The id of the private link") id: String): Action[AnyContent] = sil.SecuredAction.async {
implicit request =>
for {
idValidated <- ObjectId.fromString(id)

annotationPrivateLink <- annotationPrivateLinkDAO.findOne(idValidated)
_ <- bool2Fox(annotationPrivateLink.expirationDateTime.forall(_ > System.currentTimeMillis())) ?~> "Token expired" ~> NOT_FOUND
_ <- annotationDAO.findOne(annotationPrivateLink._annotation) ?~> "annotation.notFound" ~> NOT_FOUND

annotationPrivateLinkJs <- annotationPrivateLinkService.publicWrites(annotationPrivateLink)
} yield Ok(annotationPrivateLinkJs)
}

@ApiOperation(
value = """Creates a given private link for an annotation for zarr streaming
Expects:
- As JSON object body with keys:
- annotation (string): annotation id to create private link for
- expirationDateTime (Optional[bool]): optional UNIX timestamp, expiration date and time for the link
""",
nickname = "createPrivateLink"
)
@ApiImplicitParams(
Array(
new ApiImplicitParam(name = "annotationPrivateLinkParams",
required = true,
dataTypeClass = classOf[AnnotationPrivateLinkParams],
paramType = "body")))
@ApiResponses(
Array(
new ApiResponse(code = 200, message = "JSON object containing string that private link was deleted."),
new ApiResponse(code = 400, message = badRequestLabel),
new ApiResponse(code = 404, message = badRequestLabel),
new ApiResponse(code = 403, message = badRequestLabel)
))
def create: Action[AnnotationPrivateLinkParams] = sil.SecuredAction.async(validateJson[AnnotationPrivateLinkParams]) {
implicit request =>
val params = request.body
val _id = ObjectId.generate
val accessToken = annotationPrivateLinkService.generateToken
for {
annotationId <- ObjectId.fromString(params.annotation)
_ <- annotationDAO.assertUpdateAccess(annotationId) ?~> "notAllowed" ~> FORBIDDEN
_ <- annotationPrivateLinkDAO.insertOne(
AnnotationPrivateLink(_id, annotationId, accessToken, params.expirationDateTime)) ?~> "create.failed"
inserted <- annotationPrivateLinkDAO.findOne(_id)
js <- annotationPrivateLinkService.publicWrites(inserted)
} yield Ok(js)
}

@ApiOperation(
value = """Updates a given private link for an annotation for zarr streaming
Expects:
- As JSON object body with keys:
- annotation (string): annotation id to create private link for
- expirationDateTime (Optional[bool]): optional UNIX timestamp, expiration date and time for the link
""",
nickname = "updatePrivateLink"
)
@ApiImplicitParams(
Array(
new ApiImplicitParam(name = "annotationPrivateLinkParams",
required = true,
dataTypeClass = classOf[AnnotationPrivateLinkParams],
paramType = "body")))
@ApiResponses(
Array(
new ApiResponse(code = 200, message = "JSON object containing string that private link was deleted."),
new ApiResponse(code = 400, message = badRequestLabel),
new ApiResponse(code = 404, message = badRequestLabel),
new ApiResponse(code = 403, message = badRequestLabel)
))
def update(@ApiParam(value = "The id of the private link") id: String): Action[AnnotationPrivateLinkParams] =
sil.SecuredAction.async(validateJson[AnnotationPrivateLinkParams]) { implicit request =>
val params = request.body
for {
annotationId <- ObjectId.fromString(params.annotation)
idValidated <- ObjectId.fromString(id)
aPLInfo <- annotationPrivateLinkDAO.findOne(idValidated) ?~> "annotation private link not found" ~> NOT_FOUND
_ <- annotationDAO.assertUpdateAccess(aPLInfo._annotation) ?~> "notAllowed" ~> FORBIDDEN
_ <- annotationDAO.assertUpdateAccess(annotationId) ?~> "notAllowed" ~> FORBIDDEN
_ <- annotationPrivateLinkDAO.updateOne(idValidated, annotationId, params.expirationDateTime) ?~> "update.failed"
updated <- annotationPrivateLinkDAO.findOne(idValidated) ?~> "not Found"
js <- annotationPrivateLinkService.publicWrites(updated) ?~> "write failed"
} yield Ok(js)
}

@ApiOperation(value = "Deletes a given private link for an annotation for zarr streaming",
nickname = "deletePrivateLink")
@ApiResponses(
Array(
new ApiResponse(code = 200, message = "JSON object containing string that private link was deleted."),
new ApiResponse(code = 400, message = badRequestLabel),
new ApiResponse(code = 404, message = badRequestLabel),
new ApiResponse(code = 403, message = badRequestLabel)
))
def delete(@ApiParam(value = "The id of the private link") id: String): Action[AnyContent] = sil.SecuredAction.async {
implicit request =>
for {
idValidated <- ObjectId.fromString(id)
aPLInfo <- annotationPrivateLinkDAO.findOne(idValidated) ?~> "notFound" ~> NOT_FOUND
_ <- annotationDAO.assertUpdateAccess(aPLInfo._annotation) ?~> "notAllowed" ~> FORBIDDEN
_ <- annotationPrivateLinkDAO.deleteOne(idValidated) ?~> "delete failed"
} yield JsonOk("privateLink deleted")
}
}
2 changes: 1 addition & 1 deletion app/controllers/LegacyApiController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package controllers
import com.mohiva.play.silhouette.api.Silhouette
import com.mohiva.play.silhouette.api.actions.{SecuredRequest, UserAwareRequest}
import com.scalableminds.util.tools.Fox
import com.scalableminds.webknossos.datastore.models.annotation.{AnnotationLayer, AnnotationLayerType}
import com.scalableminds.webknossos.tracingstore.tracings.volume.ResolutionRestrictions
import javax.inject.Inject
import models.annotation.{AnnotationLayer, AnnotationLayerType}
import models.project.ProjectDAO
import models.task.{TaskDAO, TaskService}
import models.user.User
Expand Down
20 changes: 16 additions & 4 deletions app/controllers/UserTokenController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ object RpcTokenHolder {
* The token is refreshed on every wK restart.
* Keep it secret!
*/
lazy val webKnossosToken: String = CompactRandomIDGenerator.generateBlocking()
lazy val webKnossosToken: String = RandomIDGenerator.generateBlocking()
}

@Api
class UserTokenController @Inject()(dataSetDAO: DataSetDAO,
dataSetService: DataSetService,
annotationDAO: AnnotationDAO,
annotationPrivateLinkDAO: AnnotationPrivateLinkDAO,
userService: UserService,
organizationDAO: OrganizationDAO,
annotationStore: AnnotationStore,
Expand Down Expand Up @@ -101,7 +102,7 @@ class UserTokenController @Inject()(dataSetDAO: DataSetDAO,
case AccessResourceType.datasource =>
handleDataSourceAccess(accessRequest.resourceId, accessRequest.mode, userBox)(sharingTokenAccessCtx)
case AccessResourceType.tracing =>
handleTracingAccess(accessRequest.resourceId.name, accessRequest.mode, userBox)
handleTracingAccess(accessRequest.resourceId.name, accessRequest.mode, userBox, token)
case AccessResourceType.jobExport =>
handleJobExportAccess(accessRequest.resourceId.name, accessRequest.mode, userBox)
case _ =>
Expand Down Expand Up @@ -163,8 +164,12 @@ class UserTokenController @Inject()(dataSetDAO: DataSetDAO,
}
}

private def handleTracingAccess(tracingId: String, mode: AccessMode, userBox: Box[User]): Fox[UserAccessAnswer] = {
private def handleTracingAccess(tracingId: String,
mode: AccessMode,
userBox: Box[User],
token: Option[String]): Fox[UserAccessAnswer] = {
// 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)
Expand All @@ -190,9 +195,16 @@ class UserTokenController @Inject()(dataSetDAO: DataSetDAO,
else {
for {
annotation <- findAnnotationForTracing(tracingId)(GlobalAccessContext) ?~> "annotation.notFound"
annotationAccessByToken <- token
.map(annotationPrivateLinkDAO.findOneByAccessToken)
.getOrElse(Fox.empty)
.futureBox

allowedByToken = annotationAccessByToken.exists(annotation._id == _._annotation)
restrictions <- annotationInformationProvider.restrictionsFor(
AnnotationIdentifier(annotation.typ, annotation._id))(GlobalAccessContext) ?~> "restrictions.notFound"
allowed <- checkRestrictions(restrictions) ?~> "restrictions.failedToCheck"
allowedByUser <- checkRestrictions(restrictions) ?~> "restrictions.failedToCheck"
allowed = allowedByToken || allowedByUser
} yield {
if (allowed) UserAccessAnswer(granted = true)
else UserAccessAnswer(granted = false, Some(s"No ${mode.toString} access to tracing"))
Expand Down
2 changes: 2 additions & 0 deletions app/models/annotation/Annotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package models.annotation

import com.scalableminds.util.accesscontext.DBAccessContext
import com.scalableminds.util.tools.{Fox, FoxImplicits, JsonHelper}
import com.scalableminds.webknossos.datastore.models.annotation.{AnnotationLayer, AnnotationLayerType}
import com.scalableminds.webknossos.schema.Tables._
import com.scalableminds.webknossos.tracingstore.tracings.TracingType

import javax.inject.Inject
import models.annotation.AnnotationState._
import models.annotation.AnnotationType.AnnotationType
Expand Down
2 changes: 2 additions & 0 deletions app/models/annotation/AnnotationMerger.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package models.annotation

import com.scalableminds.util.accesscontext.DBAccessContext
import com.scalableminds.util.tools.{Fox, FoxImplicits}
import com.scalableminds.webknossos.datastore.models.annotation.{AnnotationLayer, AnnotationLayerType}
import com.typesafe.scalalogging.LazyLogging

import javax.inject.Inject
import models.annotation.AnnotationType.AnnotationType
import models.binary.DataSetDAO
Expand Down
Loading