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

Use Client Timestamp for info/finish timetracking #4580

Merged
merged 7 commits into from
Apr 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -29,6 +29,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Unconnected trees no longer cause an error during NML import in the tracing view. Instead, unconnected trees are split into their components. The split components are wrapped in a tree group with the original tree's name. [#4541](https://github.com/scalableminds/webknossos/pull/4541)
- Made the NML importer in the tracing view less strict. Incorrect timestamps, missing tree names or missing node radii no longer cause an error. [#4541](https://github.com/scalableminds/webknossos/pull/4541)
- Disabled the backend-side apply agglomerate feature for downsampled magnifications (still allowed for 1 to 8) to save server capacity. [#4578](https://github.com/scalableminds/webknossos/pull/4578)
- REST API endpoints finish and info now expect additional GET parameter `timestamp=[INT]` timestamp in milliseconds (time the request is sent). [#4580](https://github.com/scalableminds/webknossos/pull/4580)

### Fixed
- Users only get tasks of datasets that they can access. [#4488](https://github.com/scalableminds/webknossos/pull/4488)
Expand Down
16 changes: 8 additions & 8 deletions app/controllers/AnnotationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class AnnotationController @Inject()(
implicit val timeout = Timeout(5 seconds)
val taskReopenAllowed = (conf.Features.taskReopenAllowed + (10 seconds)).toMillis

def info(typ: String, id: String) = sil.UserAwareAction.async { implicit request =>
def info(typ: String, id: String, timestamp: Long) = sil.UserAwareAction.async { implicit request =>
log {
val notFoundMessage =
if (request.identity.isEmpty) "annotation.notFound.considerLoggingIn" else "annotation.notFound"
Expand All @@ -63,7 +63,7 @@ class AnnotationController @Inject()(
.publicWrites(annotation, request.identity, Some(restrictions)) ?~> "annotation.write.failed"
_ <- Fox.runOptional(request.identity) { user =>
if (typedTyp == AnnotationType.Task || typedTyp == AnnotationType.Explorational) {
timeSpanService.logUserInteraction(user, annotation) // log time when a user starts working
timeSpanService.logUserInteraction(timestamp, user, annotation) // log time when a user starts working
} else Fox.successful(())
}
} yield {
Expand Down Expand Up @@ -174,22 +174,22 @@ class AnnotationController @Inject()(
}
}

private def finishAnnotation(typ: String, id: String, issuingUser: User)(
private def finishAnnotation(typ: String, id: String, issuingUser: User, timestamp: Long)(
implicit ctx: DBAccessContext): Fox[(Annotation, String)] =
for {
annotation <- provider.provideAnnotation(typ, id, issuingUser) ~> NOT_FOUND
restrictions <- provider.restrictionsFor(typ, id) ?~> "restrictions.notFound" ~> NOT_FOUND
message <- annotationService.finish(annotation, issuingUser, restrictions) ?~> "annotation.finish.failed"
updated <- provider.provideAnnotation(typ, id, issuingUser)
_ <- timeSpanService.logUserInteraction(issuingUser, annotation) // log time on tracing end
_ <- timeSpanService.logUserInteraction(timestamp, issuingUser, annotation) // log time on tracing end
} yield {
(updated, message)
}

def finish(typ: String, id: String) = sil.SecuredAction.async { implicit request =>
def finish(typ: String, id: String, timestamp: Long) = sil.SecuredAction.async { implicit request =>
log {
for {
(updated, message) <- finishAnnotation(typ, id, request.identity) ?~> "annotation.finish.failed"
(updated, message) <- finishAnnotation(typ, id, request.identity, timestamp) ?~> "annotation.finish.failed"
restrictions <- provider.restrictionsFor(typ, id)
json <- annotationService.publicWrites(updated, Some(request.identity), Some(restrictions))
} yield {
Expand All @@ -198,11 +198,11 @@ class AnnotationController @Inject()(
}
}

def finishAll(typ: String) = sil.SecuredAction.async(parse.json) { implicit request =>
def finishAll(typ: String, timestamp: Long) = sil.SecuredAction.async(parse.json) { implicit request =>
log {
withJsonAs[JsArray](request.body \ "annotations") { annotationIds =>
val results = Fox.serialSequence(annotationIds.value.toList) { jsValue =>
jsValue.asOpt[String].toFox.flatMap(id => finishAnnotation(typ, id, request.identity))
jsValue.asOpt[String].toFox.flatMap(id => finishAnnotation(typ, id, request.identity, timestamp))
}

results.map { results =>
Expand Down
14 changes: 12 additions & 2 deletions app/controllers/LegacyApiController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ class LegacyApiController @Inject()(annotationController: AnnotationController,
sil: Silhouette[WkEnv])(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers)
extends Controller {

/* to provide v2, insert automatic timestamp in finish and info request */

def annotationFinishV2(typ: String, id: String) =
annotationController.finish(typ, id, System.currentTimeMillis)

def annotationInfoV2(typ: String, id: String) =
annotationController.info(typ, id, System.currentTimeMillis)

/* to provide v1, replace new field “visibility” in annotation json by old boolean field “isPublic” */

def annotationDuplicate(typ: String, id: String) = sil.SecuredAction.async { implicit request =>
for {
result <- annotationController.duplicate(typ, id)(request)
Expand All @@ -25,7 +35,7 @@ class LegacyApiController @Inject()(annotationController: AnnotationController,

def annotationFinish(typ: String, id: String) = sil.SecuredAction.async { implicit request =>
for {
result <- annotationController.finish(typ, id)(request)
result <- annotationController.finish(typ, id, System.currentTimeMillis)(request)
} yield replaceVisibilityInResultJson(result)
}

Expand All @@ -49,7 +59,7 @@ class LegacyApiController @Inject()(annotationController: AnnotationController,

def annotationInfo(typ: String, id: String) = sil.SecuredAction.async { implicit request =>
for {
result <- annotationController.info(typ, id)(request)
result <- annotationController.info(typ, id, System.currentTimeMillis)(request)
} yield replaceVisibilityInResultJson(result)
}

Expand Down
5 changes: 2 additions & 3 deletions app/models/user/time/TimeSpanService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@ class TimeSpanService @Inject()(annotationDAO: AnnotationDAO,
private val MaxTracingPause =
conf.WebKnossos.User.Time.tracingPauseInSeconds.toMillis

def logUserInteraction(user: User, annotation: Annotation)(implicit ctx: DBAccessContext): Fox[Unit] = {
val timestamp = System.currentTimeMillis
def logUserInteraction(timestamp: Long, user: User, annotation: Annotation)(
implicit ctx: DBAccessContext): Fox[Unit] =
logUserInteraction(Seq(timestamp), user, annotation)
}

def logUserInteraction(timestamps: Seq[Long], user: User, annotation: Annotation)(
implicit ctx: DBAccessContext): Fox[Unit] =
Expand Down
6 changes: 3 additions & 3 deletions conf/webknossos.latest.routes
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,13 @@ POST /annotations/upload
POST /annotations/:typ/:id/duplicate controllers.AnnotationController.duplicate(typ: String, id: String)
PATCH /annotations/:typ/:id/edit controllers.AnnotationController.editAnnotation(typ: String, id: String)

PATCH /annotations/:typ/:id/finish controllers.AnnotationController.finish(typ: String, id: String)
PATCH /annotations/:typ/finish controllers.AnnotationController.finishAll(typ: String)
PATCH /annotations/:typ/:id/finish controllers.AnnotationController.finish(typ: String, id: String, timestamp: Long)
PATCH /annotations/:typ/finish controllers.AnnotationController.finishAll(typ: String, timestamp: Long)
PATCH /annotations/:typ/:id/reopen controllers.AnnotationController.reopen(typ: String, id: String)
PUT /annotations/:typ/:id/reset controllers.AnnotationController.reset(typ: String, id: String)
PATCH /annotations/:typ/:id/transfer controllers.AnnotationController.transfer(typ: String, id: String)

GET /annotations/:typ/:id/info controllers.AnnotationController.info(typ: String, id: String)
GET /annotations/:typ/:id/info controllers.AnnotationController.info(typ: String, id: String, timestamp: Long)
PATCH /annotations/:typ/:id/makeHybrid controllers.AnnotationController.makeHybrid(typ: String, id: String)
DELETE /annotations/:typ/:id controllers.AnnotationController.cancel(typ: String, id: String)
POST /annotations/:typ/:id/merge/:mergedTyp/:mergedId controllers.AnnotationController.merge(typ: String, id: String, mergedTyp: String, mergedId: String)
Expand Down
8 changes: 6 additions & 2 deletions conf/webknossos.versioned.routes
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# API versioning is handled here. Higher-Priority routes first

# example: assume, the features route has changed, introducing v2. The older v1 needs to be provided in the legacyApiController
-> /v2/ webknossos.latest.Routes

-> /v3/ webknossos.latest.Routes

PATCH /v2/annotations/:typ/:id/finish controllers.LegacyApiController.annotationFinishV2(typ: String, id: String)
GET /v2/annotations/:typ/:id/info controllers.LegacyApiController.annotationInfoV2(typ: String, id: String)

-> /v2/ webknossos.latest.Routes

# GET /v1/features controllers.LegacyApiController.v1features
#
PATCH /v1/annotations/:typ/:id/edit controllers.LegacyApiController.editAnnotation(typ: String, id: String)
# input needs to change
Expand Down
90 changes: 50 additions & 40 deletions docs/rest_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ The API is subject to frequent changes. However, older versions will be supporte

New versions will be documented here, detailing the changes. Note, however, that some changes are not considered to be breaking the API and will not lead to new versions. Such changes include new optional parameters as well as new fields in the responses. The same goes for error message wording.

### Current api version is `v2`
### Current api version is `v3`

In comparison to `v1` the annotation `isPublic` flag was replaced by a `visibility` field. This field can have the following values: `Public, Internal, Private`.
* New in v3: the info and finish requests of annotations now expect an additional `timestamp` GET parameter that should be set to the time the request is sent (e.g. Date.now()).

* New in v2: in comparison to `v1` the annotation `isPublic` flag was replaced by a `visibility` field. This field can have the following values: `Public, Internal, Private`.


## Routes
Expand Down Expand Up @@ -72,8 +74,8 @@ List your own task annotations
- JSON list of objects containing annotation information about your own task annotations, also including task and task type information
- total count of task annotations in the HTTP header `X-Total-Count` if parameter is set

#### `v1` differences
- The annotation objects int the returned JSON contain the `isPublic` flag instead of the `visibility` field
#### Changes Introduced in `v2`
- The annotation objects in the returned JSON contain the `visibility` field instead of the old `isPublic` field

---
### `GET /user/annotations`
Expand All @@ -95,8 +97,8 @@ List your own explorative annotations
- JSON list of objects containing annotation information about your own explorative annotations
- total count of explorative annotations in the HTTP header `X-Total-Count` if parameter is set

#### `v1` differences
- The annotation objects int the returned JSON contain the `isPublic` flag instead of the `visibility` field
#### Changes Introduced in `v2`
- The annotation objects in the returned JSON contain the `visibility` field instead of the old `isPublic` field



Expand Down Expand Up @@ -131,10 +133,10 @@ List the task annotations of a user
- JSON list of objects containing annotation information about the task annotations of the user, also including task and task type information
- total count of task annotations in the HTTP header `X-Total-Count` if parameter is set
- total count of task annotations in the HTTP header `X-Total-Count` if parameter is set
#### `v1` differences
- The annotation objects int the returned JSON contain the `isPublic` flag instead of the `visibility` field


#### Changes Introduced in `v2`
- The annotation objects in the returned JSON contain the `visibility` field instead of the old `isPublic` field

---
### `GET /users/:id/annotations`
Expand All @@ -157,8 +159,8 @@ List the explorative annotations of a user
- JSON list of objects containing annotation information about the explorative annotations of the user
- total count of explorative annotations in the HTTP header `X-Total-Count` if parameter is set

#### `v1` differences
- The annotation objects int the returned JSON contain the `isPublic` flag instead of the `visibility` field
#### Changes Introduced in `v2`
- The annotation objects in the returned JSON contain the `visibility` field instead of the old `isPublic` field


---
Expand Down Expand Up @@ -225,25 +227,25 @@ Create a new datastore
- `"isScratch"` `[BOOLEAN]` (optional, default: `false`) whether or not the datastore is hosted on a scratch/experimental environment
- `"isForeign"` `[BOOLEAN]` (optional, default: `false`) whether or not the datastore belongs to this wk instance or belongs to a foreign wk instance
- `"isConnector"` `[BOOLEAN]` (optional, default: `false`) whether or not the datastore is a wk-connect instance


#### Returns
- JSON object containing information about the newly created datastore

#### Note
- This route is only accessible for administrators.

---
### `DELETE /datastores/:name`

Deletes a datastore from the wk database

#### Expects
- In the url: `:name` - the name of the datastore which should be deleted

#### Note
- This route is only accessible for administrators.

---
### `PUT /datastores/:name`

Expand All @@ -258,14 +260,14 @@ Update an existing datastore
- `"isScratch"` `[BOOLEAN]` (optional, default: `false`) whether or not the datastore is hosted on a scratch/experimental environment
- `"isForeign"` `[BOOLEAN]` (optional, default: `false`) whether or not the datastore belongs to this wk instance or belongs to a foreign wk instance
- `"isConnector"` `[BOOLEAN]` (optional, default: `false`) whether or not the datastore is a wk-connect instance


#### Returns
- JSON object containing information about the updated datastore

#### Note
- This route is only accessible for administrators.

---
### `PUT /tracingstores/:name`

Expand All @@ -277,10 +279,10 @@ Update an existing tracingstore
- `"name"` `[STRING]` name of the tracingstore
- `"url"` `[STRING]` url from the tracingstore, used for communication with wk
- `"publicUrl"` `[STRING]` publicly accessible url from the tracingstore, used for user facing links

#### Returns
- JSON object containing information about the updated tracingstore

#### Note
- This route is only accessible for administrators.

Expand All @@ -294,6 +296,7 @@ Update an existing tracingstore
- for `CompoundTask` `:id` is a task id
- for `CompoundProject` `:id` is a project id
- for `CompoundTaskType` `:id` is a task type id
- GET parameter `timestamp=[INT]` timestamp in milliseconds (time the request is sent)

#### Returns
- JSON object containing annotation information about the selected annotation
Expand All @@ -302,8 +305,11 @@ Update an existing tracingstore

The compound annotations are created as merged from the finished annotations associated with the Task/Project/TaskType. This merging is performed before this info request is answered and can be slow for large numbers of annotations. The merged annotations are then stored in a cache for a few minutes, but not on disk. If requested again within this time, the request will be answered more quickly, but newly finished annotations will not be included yet. This cache is shared between this info request and the download request.

#### `v1` differences
- The returned JSON contains the `isPublic` flag instead of the `visibility` field
#### Changes Introduced in `v3`
- Expects additional GET parameter `timestamp=[INT]` timestamp in milliseconds (time the request is sent)

#### Changes Introduced in `v2`
- The returned JSON contains the `visibility` field instead of the `isPublic` flag

---
### `GET /annotations/:typ/:id/download`
Expand Down Expand Up @@ -346,9 +352,9 @@ Duplicate an annotation (“copy to my account”)

#### Returns
- JSON object containing annotation information about the newly created (duplicated) annotation, including the assigned id
#### `v1` differences
- The returned JSON contains the `isPublic` flag instead of the `visibility` field

#### Changes Introduced in `v2`
- The returned JSON contains the `visibility` field instead of the `isPublic` flag


---
Expand All @@ -368,21 +374,25 @@ Edit metadata of an annotation
#### Returns
- JSON object containing annotation information about the edited annotation

#### `v1` differences
- The request and returned JSON contain the `isPublic` `[BOOLEAN]` flag instead of the `visibility` field
#### Changes Introduced in `v2`
- The request and returned JSON contain the `isPublic` `[BOOLEAN]` flag instead of the `visibility` field

---
### `PATCH /annotations/:typ/:id/finish`

#### Expects
- In the url: `:typ` – one of `Task`, `Explorational`
- In the url: `:id` an annotation id
- GET parameter `timestamp=[INT]` timestamp in milliseconds (time the request is sent)

#### Returns
- JSON object containing annotation information about the finished annotation

#### `v1` differences
- The returned JSON contains the `isPublic` flag instead of the `visibility` field

#### Changes Introduced in `v3`
- Expects additional GET parameter `timestamp=[INT]` timestamp in milliseconds (time the request is sent)

#### Changes Introduced in `v2`
- The returned JSON contains the `visibility` field instead of the `isPublic` flag


---
Expand All @@ -395,8 +405,8 @@ Edit metadata of an annotation
#### Returns
- JSON object containing annotation information about the reopened annotation

#### `v1` differences
- The returned JSON contains the `isPublic` flag instead of the `visibility` field
#### Changes Introduced in `v2`
- The returned JSON contains the `visibility` field instead of the `isPublic` flag

---
### `PUT /annotations/Task/:id/reset`
Expand All @@ -409,8 +419,8 @@ Reset a task annotation to its base state
#### Returns
- JSON object containing annotation information about the reset task annotation

#### `v1` differences
- The returned JSON contains the `isPublic` flag instead of the `visibility` field
#### Changes Introduced in `v2`
- The returned JSON contains the `visibility` field instead of the `isPublic` flag

---
### `POST /annotations/:typ/:id/merge/:mergedTyp/:mergedId`
Expand All @@ -424,8 +434,8 @@ Merge two annotations, creating a new explorative.
#### Returns
- JSON object containing annotation information about the merged annotation

#### `v1` differences
- The returned JSON contains the `isPublic` flag instead of the `visibility` field
#### Changes Introduced in `v2`
- The returned JSON contains the `visibility` field instead of the `isPublic` flag

---
### `POST /tasks`
Expand Down Expand Up @@ -561,8 +571,8 @@ List annotations of a task
- JSON list of objects containing annotation information on the annotations of the task
- Cancelled annotations are not returned

#### `v1` differences
- The annotation objects in the returned JSON contain the `isPublic` flag instead of the `visibility` field
#### Changes Introduced in `v2`
- The annotation objects in the returned JSON contain the `visibility` flag instead of the `isPublic` field

---
### `GET /projects`
Expand Down
Loading