Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable publications to link to annotations #6315

Merged
merged 36 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1d59b43
add schema change, publicationDAO
leowe Jul 5, 2022
181c160
add one more step
leowe Jul 5, 2022
439ffb1
add backend routes for publication
leowe Jul 7, 2022
febba67
Merge branch 'master' into publications-annotations
leowe Jul 8, 2022
2d55c19
fix csv file
leowe Jul 8, 2022
2c9ddcb
add more
leowe Jul 8, 2022
ec45e94
update db queries
leowe Jul 8, 2022
10a1597
reformat
leowe Jul 13, 2022
bb1c72e
Merge branch 'master' into publications-annotations
leowe Jul 13, 2022
29195bd
remove unnecessary parts
leowe Jul 13, 2022
904eed1
remove unnecessary parts
leowe Jul 13, 2022
d6629d7
bump schema.sql version to 84
leowe Jul 13, 2022
920df47
remove unneeded import
leowe Jul 13, 2022
8f53ab2
add Changelog entry
leowe Jul 14, 2022
e7f0255
incroporate feedback
leowe Jul 14, 2022
cacf91c
update evolution
leowe Jul 14, 2022
69e194c
use comma instead of semicolon
leowe Jul 14, 2022
f0c89e2
merge master
fm3 Aug 3, 2022
2fbfe5d
remove unused variables
fm3 Aug 3, 2022
14d426a
update schema version
fm3 Aug 3, 2022
f118ebf
adds frontend for new publication API
normanrz Aug 3, 2022
aef0dab
lint
normanrz Aug 3, 2022
9631e00
fixes
normanrz Aug 4, 2022
17136e5
Merge branch 'master' into publications-annotations
normanrz Aug 4, 2022
3490e2e
rename /publication/:id -> /publications/:id
normanrz Aug 4, 2022
61d357b
Merge branch 'publications-annotations' of github.com:scalableminds/w…
normanrz Aug 4, 2022
22bfe14
remove publication from datasets
normanrz Aug 4, 2022
1e4d3dd
e2e
normanrz Aug 4, 2022
365d81b
rm CompactPublicationService
normanrz Aug 4, 2022
73d62c9
pr feedback
normanrz Aug 4, 2022
b631e75
pr feedback
normanrz Aug 4, 2022
b8f6e50
snapshots
fm3 Aug 4, 2022
f5b62aa
Merge branch 'publications-annotations' of github.com:scalableminds/w…
fm3 Aug 4, 2022
b1112f7
remove unused stuff
fm3 Aug 4, 2022
a3b6109
Merge branch 'master' into publications-annotations
fm3 Aug 4, 2022
56d48a2
Merge branch 'master' into publications-annotations
fm3 Aug 4, 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
2 changes: 2 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- WebKnossos now remembers the tool that was active in between disabling and enabling the segmentation layer. [#6362](https://github.com/scalableminds/webknossos/pull/6362)
- Segmentation layers which were not previously editable now show an (un)lock icon button which shortcuts to the Add Volume Layer modal with the layer being preselected. [#6330](https://github.com/scalableminds/webknossos/pull/6330)
- The NML file in volume annotation download now includes segment metadata like names and anchor positions. [#6347](https://github.com/scalableminds/webknossos/pull/6347)
- Added new backend API route for requesting all publications. Those publications can now have also attached annotations. [#6315](https://github.com/scalableminds/webknossos/pull/6315)


### 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 @@ -12,3 +12,4 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md).

### Postgres Evolutions:
- [084-annotation-contributors.sql](conf/evolutions/084-annotation-contributors.sql)
- [085-add-annotations-publicationforeignkey](conf/evolutions/085-add-annotations-publicationforeignkey.sql)
11 changes: 6 additions & 5 deletions app/controllers/AnnotationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,26 @@ import com.scalableminds.util.tools.{Fox, FoxImplicits}
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 models.annotation.AnnotationLayerType.AnnotationLayerType
import models.annotation.AnnotationState.Cancelled
import models.annotation._
import models.binary.{DataSetDAO, DataSetService}
import models.organization.OrganizationDAO
import models.project.ProjectDAO
import models.task.TaskDAO
import models.team.{TeamDAO, TeamService}
import models.user.time._
import models.user.{User, UserDAO, UserService}
import oxalis.mail.{MailchimpClient, MailchimpTag}
import oxalis.security.{URLSharing, WkEnv}
import play.api.i18n.{Messages, MessagesProvider}
import play.api.libs.json.{JsArray, _}
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}

import scala.concurrent.ExecutionContext
import scala.concurrent.duration._

Expand Down
8 changes: 4 additions & 4 deletions app/controllers/DataSetController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@ class DataSetController @Inject()(userService: UserService,
dataSetService.publicWrites(
d,
requestingUser,
organization,
dataStore,
Some(organization),
Some(dataStore),
skipResolutions = true,
requestingUserTeamManagerMemberships) ?~> Messages("dataset.list.writesFailed", d.name)
}
Expand Down Expand Up @@ -233,7 +233,7 @@ class DataSetController @Inject()(userService: UserService,
dataSetLastUsedTimesDAO.updateForDataSetAndUser(dataSet._id, user._id))
// Access checked above via dataset. In case of shared dataset/annotation, show datastore even if not otherwise accessible
dataStore <- dataSetService.dataStoreFor(dataSet)(GlobalAccessContext)
js <- dataSetService.publicWrites(dataSet, request.identity, organization, dataStore)
js <- dataSetService.publicWrites(dataSet, request.identity, Some(organization), Some(dataStore))
_ = request.identity.map { user =>
analyticsService.track(OpenDatasetEvent(user, dataSet))
if (dataSet.isPublic) {
Expand Down Expand Up @@ -307,7 +307,7 @@ Expects:
_ = analyticsService.track(ChangeDatasetSettingsEvent(request.identity, updated))
organization <- organizationDAO.findOne(updated._organization)(GlobalAccessContext)
dataStore <- dataSetService.dataStoreFor(updated)
js <- dataSetService.publicWrites(updated, Some(request.identity), organization, dataStore)
js <- dataSetService.publicWrites(updated, Some(request.identity), Some(organization), Some(dataStore))
} yield Ok(Json.toJson(js))
}
}
Expand Down
44 changes: 44 additions & 0 deletions app/controllers/PublicationController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package controllers

import com.mohiva.play.silhouette.api.Silhouette
import com.scalableminds.util.tools.{Fox, FoxImplicits}
import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits
import io.swagger.annotations._
import javax.inject.Inject
import models.binary.{PublicationDAO, PublicationService}
import oxalis.security.WkEnv
import play.api.libs.json._
import play.api.mvc.{Action, AnyContent}
import utils.ObjectId

import scala.concurrent.ExecutionContext

@Api
class PublicationController @Inject()(publicationService: PublicationService,
publicationDAO: PublicationDAO,
sil: Silhouette[WkEnv])(implicit ec: ExecutionContext)
extends Controller
with ProtoGeometryImplicits
with FoxImplicits {

@ApiOperation(value = "Information about a publication", nickname = "publicationInfo")
@ApiResponses(
Array(new ApiResponse(code = 200, message = "JSON object containing information about this publication."),
new ApiResponse(code = 400, message = badRequestLabel)))
def read(@ApiParam(value = "The id of the publication") publicationId: String): Action[AnyContent] =
sil.UserAwareAction.async { implicit request =>
for {
publication <- publicationDAO.findOne(ObjectId(publicationId)) ?~> "publication.notFound" ~> NOT_FOUND
js <- publicationService.publicWrites(publication)
} yield Ok(js)
}

def listPublications: Action[AnyContent] = sil.UserAwareAction.async { implicit request =>
{
for {
publications <- publicationDAO.findAll ?~> "publication.notFound" ~> NOT_FOUND
jsResult <- Fox.serialCombined(publications)(publicationService.publicWrites)
} yield Ok(Json.toJson(jsResult))
}
}
}
9 changes: 9 additions & 0 deletions app/models/annotation/Annotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,15 @@ class AnnotationDAO @Inject()(sqlClient: SQLClient, annotationLayerDAO: Annotati
parsed <- parseAll(r)
} yield parsed

def findAllByPublication(publicationId: ObjectId)(implicit ctx: DBAccessContext): Fox[List[Annotation]] =
for {
accessQuery <- readAccessQuery
r <- run(
sql"select #$columns from #$existingCollectionName where _publication = $publicationId and #$accessQuery"
.as[AnnotationsRow]).map(_.toList)
parsed <- parseAll(r)
} yield parsed

def findOneByTracingId(tracingId: String)(implicit ctx: DBAccessContext): Fox[Annotation] =
for {
annotationId <- annotationLayerDAO.findAnnotationIdByTracingId(tracingId)
Expand Down
23 changes: 21 additions & 2 deletions app/models/annotation/AnnotationService.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package models.annotation

import java.io.{BufferedOutputStream, File, FileOutputStream}

import akka.actor.ActorSystem
import akka.stream.Materializer
import com.scalableminds.util.accesscontext.{AuthorizedAccessContext, DBAccessContext, GlobalAccessContext}
Expand Down Expand Up @@ -31,6 +30,7 @@ import com.scalableminds.webknossos.tracingstore.tracings.volume.{
}
import com.typesafe.scalalogging.LazyLogging
import controllers.AnnotationLayerParameters

import javax.inject.Inject
import models.annotation.AnnotationState._
import models.annotation.AnnotationType.AnnotationType
Expand Down Expand Up @@ -98,7 +98,8 @@ class AnnotationService @Inject()(
temporaryFileCreator: TemporaryFileCreator,
meshDAO: MeshDAO,
meshService: MeshService,
sharedAnnotationsDAO: SharedAnnotationsDAO)(implicit ec: ExecutionContext, val materializer: Materializer)
sharedAnnotationsDAO: SharedAnnotationsDAO
)(implicit ec: ExecutionContext, val materializer: Materializer)
extends BoxImplicits
with FoxImplicits
with ProtoGeometryImplicits
Expand Down Expand Up @@ -823,6 +824,24 @@ class AnnotationService @Inject()(
}
}

def writesWithDataset(annotation: Annotation): Fox[JsObject] = {
implicit val ctx: DBAccessContext = GlobalAccessContext
for {
dataSet <- dataSetDAO.findOne(annotation._dataSet) ?~> "dataSet.notFoundForAnnotation"
tracingStore <- tracingStoreDAO.findFirst
tracingStoreJs <- tracingStoreService.publicWrites(tracingStore)
dataSetJs <- dataSetService.publicWrites(dataSet, None, None, None)
} yield
Json.obj(
"id" -> annotation._id.id,
"name" -> annotation.name,
"description" -> annotation.description,
"typ" -> annotation.typ,
"tracingStore" -> tracingStoreJs,
"dataSet" -> dataSetJs
)
}

private def userJsonForAnnotation(userId: ObjectId, userOpt: Option[User] = None): Fox[Option[JsObject]] =
if (userId == ObjectId.dummyId) {
Fox.successful(None)
Expand Down
9 changes: 9 additions & 0 deletions app/models/binary/DataSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ class DataSetDAO @Inject()(sqlClient: SQLClient,
parsed <- parseAll(r)
} yield parsed

def findAllByPublication(publicationId: ObjectId)(implicit ctx: DBAccessContext): Fox[List[DataSet]] =
for {
accessQuery <- readAccessQuery
r <- run(
sql"select #$columns from #$existingCollectionName where _publication = $publicationId and #$accessQuery"
.as[DatasetsRow]).map(_.toList)
parsed <- parseAll(r)
} yield parsed

/* Disambiguation method for legacy URLs and NMLs: if the user has access to multiple datasets of the same name, use the oldest.
* This is reasonable, because the legacy URL/NML was likely created before this ambiguity became possible */
def getOrganizationForDataSet(dataSetName: String)(implicit ctx: DBAccessContext): Fox[ObjectId] =
Expand Down
14 changes: 8 additions & 6 deletions app/models/binary/DataSetService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO,
teamDAO: TeamDAO,
workerDAO: WorkerDAO,
publicationDAO: PublicationDAO,
publicationService: PublicationService,
dataStoreService: DataStoreService,
teamService: TeamService,
userService: UserService,
Expand Down Expand Up @@ -343,21 +342,25 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO,

def publicWrites(dataSet: DataSet,
requestingUserOpt: Option[User],
organization: Organization,
dataStore: DataStore,
organization: Option[Organization],
dataStore: Option[DataStore],
skipResolutions: Boolean = false,
requestingUserTeamManagerMemberships: Option[List[TeamMembership]] = None)(
implicit ctx: DBAccessContext): Fox[JsObject] =
for {
organization <- Fox.fillOption(organization) {
organizationDAO.findOne(dataSet._organization) ?~> "organization.notFound"
}
dataStore <- Fox.fillOption(dataStore) {
dataStoreFor(dataSet)
}
teams <- allowedTeamsFor(dataSet._id, requestingUserOpt) ?~> "dataset.list.fetchAllowedTeamsFailed"
teamsJs <- Fox.serialCombined(teams)(t => teamService.publicWrites(t, Some(organization))) ?~> "dataset.list.teamWritesFailed"
logoUrl <- logoUrlFor(dataSet, Some(organization)) ?~> "dataset.list.fetchLogoUrlFailed"
isEditable <- isEditableBy(dataSet, requestingUserOpt, requestingUserTeamManagerMemberships) ?~> "dataset.list.isEditableCheckFailed"
lastUsedByUser <- lastUsedTimeFor(dataSet._id, requestingUserOpt) ?~> "dataset.list.fetchLastUsedTimeFailed"
dataStoreJs <- dataStoreService.publicWrites(dataStore) ?~> "dataset.list.dataStoreWritesFailed"
dataSource <- dataSourceFor(dataSet, Some(organization), skipResolutions) ?~> "dataset.list.fetchDataSourceFailed"
publicationOpt <- Fox.runOptional(dataSet._publication)(publicationDAO.findOne(_)) ?~> "dataset.list.fetchPublicationFailed"
publicationJson <- Fox.runOptional(publicationOpt)(publicationService.publicWrites) ?~> "dataset.list.publicationWritesFailed"
worker <- workerDAO.findOneByDataStore(dataStore.name).futureBox
jobsEnabled = conf.Features.jobsEnabled && worker.nonEmpty
} yield {
Expand All @@ -377,7 +380,6 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO,
"logoUrl" -> logoUrl,
"sortingKey" -> dataSet.sortingKey,
"details" -> dataSet.details,
"publication" -> publicationJson,
"isUnreported" -> Json.toJson(isUnreported(dataSet)),
"isForeign" -> dataStore.isForeign,
"jobsEnabled" -> jobsEnabled,
Expand Down
51 changes: 41 additions & 10 deletions app/models/binary/Publication.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package models.binary

import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext}
import com.scalableminds.util.tools.Fox
import com.scalableminds.webknossos.schema.Tables._
import javax.inject.Inject
import models.annotation.{AnnotationDAO, AnnotationService}
import play.api.http.Status.NOT_FOUND
import play.api.libs.json.Format.GenericFormat
import play.api.libs.json.{JsObject, Json}
import slick.jdbc.PostgresProfile.api._
import slick.lifted.Rep
Expand All @@ -18,17 +22,32 @@ case class Publication(_id: ObjectId,
created: Long = System.currentTimeMillis(),
isDeleted: Boolean = false)

class PublicationService @Inject()()(implicit ec: ExecutionContext) {
def publicWrites(p: Publication): Fox[JsObject] =
Fox.successful(
class PublicationService @Inject()(dataSetService: DataSetService,
dataSetDAO: DataSetDAO,
annotationService: AnnotationService,
annotationDAO: AnnotationDAO)(implicit ec: ExecutionContext) {

def publicWrites(publication: Publication): Fox[JsObject] = {
implicit val ctx: DBAccessContext = GlobalAccessContext
for {
dataSets <- dataSetDAO.findAllByPublication(publication._id) ?~> "not found" ~> NOT_FOUND
annotations <- annotationDAO.findAllByPublication(publication._id) ?~> "not found" ~> NOT_FOUND
dataSetsJson <- Fox.serialCombined(dataSets)(d => dataSetService.publicWrites(d, None, None, None))
annotationsJson <- Fox.serialCombined(annotations) { annotation =>
annotationService.writesWithDataset(annotation)
}
} yield
Json.obj(
"id" -> p._id.id,
"publicationDate" -> p.publicationDate,
"imageUrl" -> p.imageUrl,
"title" -> p.title,
"description" -> p.description,
"created" -> p.created
))
"id" -> publication._id.id,
"publicationDate" -> publication.publicationDate,
"imageUrl" -> publication.imageUrl,
"title" -> publication.title,
"description" -> publication.description,
"created" -> publication.created,
"datasets" -> dataSetsJson,
"annotations" -> annotationsJson
)
}
}

class PublicationDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext)
Expand All @@ -52,6 +71,18 @@ class PublicationDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionConte
)
)

override def findOne(id: ObjectId)(implicit ctx: DBAccessContext): Fox[Publication] =
for {
r <- run(sql"select #$columns from #$existingCollectionName where _id = ${id.id}".as[PublicationsRow])
parsed <- parseFirst(r, id)
} yield parsed

override def findAll(implicit ctx: DBAccessContext): Fox[List[Publication]] =
for {
r <- run(sql"select #$columns from #$existingCollectionName".as[PublicationsRow])
parsed <- parseAll(r)
} yield parsed

def insertOne(p: Publication): Fox[Unit] =
for {
_ <- run(
Expand Down
12 changes: 12 additions & 0 deletions conf/evolutions/085-annotation-publication.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
START TRANSACTION;

DROP VIEW webknossos.annotations_;
ALTER TABLE webknossos.annotations
ADD COLUMN _publication CHAR(24),
ADD CONSTRAINT publication_ref FOREIGN KEY(_publication) REFERENCES webknossos.publications(_id) DEFERRABLE;

CREATE VIEW webknossos.annotations_ AS SELECT * FROM webknossos.annotations WHERE NOT isDeleted;

UPDATE webknossos.releaseInformation SET schemaVersion = 85;

COMMIT TRANSACTION;
11 changes: 11 additions & 0 deletions conf/evolutions/reversions/085-annotation-publication.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
START TRANSACTION;

DROP VIEW webknossos.annotations_;
ALTER TABLE webknossos.annotations
DROP COLUMN _publication CHAR(24);

CREATE VIEW webknossos.annotations_ AS SELECT * FROM webknossos.annotations WHERE NOT isDeleted;

UPDATE webknossos.releaseInformation SET schemaVersion = 84;

COMMIT TRANSACTION;
2 changes: 2 additions & 0 deletions conf/messages
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ job.meshFile.notAllowed.organization = Calculating mesh files is only allowed fo
job.globalizeFloodfill.notAllowed.organization = Globalizing floodfills is only allowed for datasets of your own organization.
job.applyMergerMode.notAllowed.organization = Applying merger mode tracings is only allowed for datasets of your own organization.

publication.notFound = Publication could not be found.

agglomerateSkeleton.failed=Could not generate agglomerate skeleton.

unexpectedReturn=Unexpected return of parameters
4 changes: 4 additions & 0 deletions conf/webknossos.latest.routes
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,7 @@ POST /jobs/run/materializeVolumeAnnotation/:organizationName/:dataSetNa
GET /jobs/:id controllers.JobsController.get(id: String)
PATCH /jobs/:id/cancel controllers.JobsController.cancel(id: String)
POST /jobs/:id/status controllers.WKRemoteWorkerController.updateJobStatus(key: String, id: String)

# Publications
GET /publications controllers.PublicationController.listPublications()
GET /publications/:id controllers.PublicationController.read(id: String)
13 changes: 13 additions & 0 deletions frontend/javascripts/admin/admin_rest_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
APIProjectProgressReport,
APIProjectUpdater,
APIProjectWithAssignments,
APIPublication,
APIResolutionRestrictions,
APIScript,
APIScriptCreator,
Expand Down Expand Up @@ -1672,6 +1673,18 @@ export async function getMeanAndStdDevFromDataset(
);
}

// #### Publications
export async function getPublications(): Promise<Array<APIPublication>> {
const publications = await Request.receiveJSON("/api/publications");
assertResponseLimit(publications);
return publications;
}

export async function getPublication(id: string): Promise<APIPublication> {
const publication = await Request.receiveJSON(`/api/publications/${id}`);
return publication;
}

// #### Datastores
export async function getDatastores(): Promise<Array<APIDataStore>> {
const datastores = await Request.receiveJSON("/api/datastores");
Expand Down
Loading