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

API counts for paginated routes #3899

Merged
merged 8 commits into from
Mar 28, 2019
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md).
- Added a shortcut (Q) and button in the actions dropdown to screenshot the tracing views. The screenshots will contain everything that is visible in the tracing views, so feel free to disable the crosshairs in the settings or toggle the tree visibility using the (1) and (2) shortcuts before triggering the screenshot. [#3834](https://github.com/scalableminds/webknossos/pull/3834)
- Neuroglancer precomputed datasets can now be added to webKnossos using the webknossos-connect (wk-connect) service. To setup a wk-connect datastore follow the instructions in the [Readme](https://github.com/scalableminds/webknossos-connect). Afterwards, datasets can be added through "Add Dataset" - "Add Dataset via wk-connect". [#3843](https://github.com/scalableminds/webknossos/pull/3843)
- The dataset settings within the tracing view allow to select between different loading strategies now ("best quality first" and "progressive quality"). Additionally, the rendering can use different magnifications as a fallback (instead of only one magnification). [#3801](https://github.com/scalableminds/webknossos/pull/3801)
- The mapping selection dropbown is now sorted alphabetically. [#3864](https://github.com/scalableminds/webknossos/pull/3864)
- The mapping selection dropdown is now sorted alphabetically. [#3864](https://github.com/scalableminds/webknossos/pull/3864)
- The HTML template now includes SEO tags for demo instances and hides internal instances from search engines.
- A maximize-button was added to the viewports in the annotation view. [#3876](https://github.com/scalableminds/webknossos/pull/3876)
- Paginated routes now send a `X-Total-Count` HTTP header which shows how many entries were found in total. [#3899](https://github.com/scalableminds/webknossos/pull/3899)

### Changed
- Improved the flight mode performance for tracings with very large trees (>80.000 nodes). [#3880](https://github.com/scalableminds/webknossos/pull/3880)
Expand Down
14 changes: 12 additions & 2 deletions app/controllers/ProjectController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import net.liftweb.common.Empty
import oxalis.security.WkEnv
import com.mohiva.play.silhouette.api.Silhouette
import com.mohiva.play.silhouette.api.actions.{SecuredRequest, UserAwareRequest}
import com.scalableminds.util.tools.DefaultConverters.BoolToOption
import play.api.i18n.{Messages, MessagesApi}
import play.api.libs.json.Json
import utils.ObjectId
Expand Down Expand Up @@ -120,16 +121,25 @@ class ProjectController @Inject()(projectService: ProjectService,
Ok(js)
}

def tasksForProject(projectName: String, limit: Option[Int] = None, pageNumber: Option[Int] = None) =
def tasksForProject(projectName: String,
limit: Option[Int] = None,
pageNumber: Option[Int] = None,
includeTotalCount: Option[Boolean]) =
sil.SecuredAction.async { implicit request =>
for {
project <- projectDAO.findOneByName(projectName) ?~> Messages("project.notFound", projectName) ~> NOT_FOUND
_ <- Fox.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed" ~> FORBIDDEN
tasks <- taskDAO.findAllByProject(project._id, limit.getOrElse(Int.MaxValue), pageNumber.getOrElse(0))(
GlobalAccessContext)
taskCount <- Fox.runOptional(includeTotalCount.flatMap(BoolToOption.convert))(_ =>
taskDAO.countAllByProject(project._id)(GlobalAccessContext))
js <- Fox.serialCombined(tasks)(task => taskService.publicWrites(task))
} yield {
Ok(Json.toJson(js))
val result = Ok(Json.toJson(js))
taskCount match {
case Some(count) => result.withHeaders("X-Total-Count" -> count.toString)
case None => result
}
}
}

Expand Down
71 changes: 54 additions & 17 deletions app/controllers/UserController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,32 +52,49 @@ class UserController @Inject()(userService: UserService,
}
}

def annotations(isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int] = None) =
def annotations(isFinished: Option[Boolean],
limit: Option[Int],
pageNumber: Option[Int] = None,
includeTotalCount: Option[Boolean] = None) =
sil.SecuredAction.async { implicit request =>
for {
annotations <- annotationDAO.findAllFor(request.identity._id,
isFinished,
AnnotationType.Explorational,
limit.getOrElse(defaultAnnotationLimit),
pageNumber.getOrElse(0))
annotationCount <- Fox.runOptional(includeTotalCount.flatMap(BoolToOption.convert))(_ =>
annotationDAO.countAllFor(request.identity._id, isFinished, AnnotationType.Explorational))
jsonList <- Fox.serialCombined(annotations)(a => annotationService.compactWrites(a))
} yield {
Ok(Json.toJson(jsonList))
val result = Ok(Json.toJson(jsonList))
annotationCount match {
case Some(count) => result.withHeaders("X-Total-Count" -> count.toString)
case None => result
}
}
}

def tasks(isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int] = None) = sil.SecuredAction.async {
implicit request =>
for {
annotations <- annotationDAO.findAllFor(request.identity._id,
isFinished,
AnnotationType.Task,
limit.getOrElse(defaultAnnotationLimit),
pageNumber.getOrElse(0))
jsonList <- Fox.serialCombined(annotations)(a => annotationService.publicWrites(a, Some(request.identity)))
} yield {
Ok(Json.toJson(jsonList))
def tasks(isFinished: Option[Boolean],
limit: Option[Int],
pageNumber: Option[Int] = None,
includeTotalCount: Option[Boolean] = None) = sil.SecuredAction.async { implicit request =>
for {
annotations <- annotationDAO.findAllFor(request.identity._id,
isFinished,
AnnotationType.Task,
limit.getOrElse(defaultAnnotationLimit),
pageNumber.getOrElse(0))
annotationCount <- Fox.runOptional(includeTotalCount.flatMap(BoolToOption.convert))(_ =>
annotationDAO.countAllFor(request.identity._id, isFinished, AnnotationType.Task))
jsonList <- Fox.serialCombined(annotations)(a => annotationService.publicWrites(a, Some(request.identity)))
} yield {
val result = Ok(Json.toJson(jsonList))
annotationCount match {
case Some(count) => result.withHeaders("X-Total-Count" -> count.toString)
case None => result
}
}
}

def userLoggedTime(userId: String) = sil.SecuredAction.async { implicit request =>
Expand Down Expand Up @@ -132,7 +149,11 @@ class UserController @Inject()(userService: UserService,
.map(loggedTime => Ok(Json.toJson(loggedTime)))
}

def userAnnotations(userId: String, isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int] = None) =
def userAnnotations(userId: String,
isFinished: Option[Boolean],
limit: Option[Int],
pageNumber: Option[Int] = None,
includeTotalCount: Option[Boolean] = None) =
sil.SecuredAction.async { implicit request =>
for {
userIdValidated <- ObjectId.parse(userId) ?~> "user.id.invalid"
Expand All @@ -143,13 +164,23 @@ class UserController @Inject()(userService: UserService,
AnnotationType.Explorational,
limit.getOrElse(defaultAnnotationLimit),
pageNumber.getOrElse(0))
annotationCount <- Fox.runOptional(includeTotalCount.flatMap(BoolToOption.convert))(_ =>
annotationDAO.countAllFor(userIdValidated, isFinished, AnnotationType.Explorational))
jsonList <- Fox.serialCombined(annotations)(a => annotationService.compactWrites(a))
} yield {
Ok(Json.toJson(jsonList))
val result = Ok(Json.toJson(jsonList))
annotationCount match {
case Some(count) => result.withHeaders("X-Total-Count" -> count.toString)
case None => result
}
}
}

def userTasks(userId: String, isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int] = None) =
def userTasks(userId: String,
isFinished: Option[Boolean],
limit: Option[Int],
pageNumber: Option[Int] = None,
includeTotalCount: Option[Boolean] = None) =
sil.SecuredAction.async { implicit request =>
for {
userIdValidated <- ObjectId.parse(userId) ?~> "user.id.invalid"
Expand All @@ -160,9 +191,15 @@ class UserController @Inject()(userService: UserService,
AnnotationType.Task,
limit.getOrElse(defaultAnnotationLimit),
pageNumber.getOrElse(0))
annotationCount <- Fox.runOptional(includeTotalCount.flatMap(BoolToOption.convert))(_ =>
annotationDAO.countAllFor(userIdValidated, isFinished, AnnotationType.Task))
jsonList <- Fox.serialCombined(annotations)(a => annotationService.publicWrites(a, Some(request.identity)))
} yield {
Ok(Json.toJson(jsonList))
val result = Ok(Json.toJson(jsonList))
annotationCount match {
case Some(count) => result.withHeaders("X-Total-Count" -> count.toString)
case None => result
}
}
}

Expand Down
26 changes: 21 additions & 5 deletions app/models/annotation/Annotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,19 @@ class AnnotationDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContex
parsed <- parse(r) ?~> ("SQLDAO Error: Could not parse database row for object " + id + " in " + collectionName)
} yield parsed

private def getStateQuery(isFinished: Option[Boolean]) =
isFinished match {
case Some(true) => s"state = '${AnnotationState.Finished.toString}'"
case Some(false) => s"state = '${AnnotationState.Active.toString}'"
case None => s"state != '${AnnotationState.Cancelled.toString}'"
}

def findAllFor(userId: ObjectId,
isFinished: Option[Boolean],
annotationType: AnnotationType,
limit: Int,
pageNumber: Int = 0)(implicit ctx: DBAccessContext): Fox[List[Annotation]] = {
val stateQuery = isFinished match {
case Some(true) => s"state = '${AnnotationState.Finished.toString}'"
case Some(false) => s"state = '${AnnotationState.Active.toString}'"
case None => s"state != '${AnnotationState.Cancelled.toString}'"
}
val stateQuery = getStateQuery(isFinished)
for {
accessQuery <- readAccessQuery
r <- run(sql"""select #${columns} from #${existingCollectionName}
Expand All @@ -128,6 +131,19 @@ class AnnotationDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContex
} yield parsed
}

def countAllFor(userId: ObjectId, isFinished: Option[Boolean], annotationType: AnnotationType)(
implicit ctx: DBAccessContext) = {
val stateQuery = getStateQuery(isFinished)
for {
accessQuery <- readAccessQuery
r <- run(
sql"""select count(*) from #${existingCollectionName}
where _user = ${userId.id} and typ = '#${annotationType.toString}' and #${stateQuery} and #${accessQuery}"""
.as[Int])
parsed <- r.headOption
} yield parsed
}

// hint: does not use access query (because they dont support prefixes yet). use only after separate access check
def findAllFinishedForProject(projectId: ObjectId)(implicit ctx: DBAccessContext): Fox[List[Annotation]] =
for {
Expand Down
9 changes: 9 additions & 0 deletions app/models/task/Task.scala
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ class TaskDAO @Inject()(sqlClient: SQLClient, projectDAO: ProjectDAO)(implicit e
parsed <- Fox.combined(r.toList.map(parse))
} yield parsed

def countAllByProject(projectId: ObjectId)(implicit ctx: DBAccessContext) =
for {
accessQuery <- readAccessQuery
r <- run(
sql"""select count(*) from #${existingCollectionName} where _project = ${projectId.id} and #${accessQuery}"""
.as[Int])
parsed <- r.headOption
} yield parsed

private def findNextTaskQ(userId: ObjectId, teamIds: List[ObjectId]) =
s"""
select ${columnsWithPrefix("webknossos.tasks_.")}
Expand Down
10 changes: 5 additions & 5 deletions conf/webknossos.latest.routes
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@ GET /user/tasks/peek

GET /users controllers.UserController.list
GET /user controllers.UserController.current
GET /user/tasks controllers.UserController.tasks(isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int])
GET /user/annotations controllers.UserController.annotations(isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int])
GET /user/tasks controllers.UserController.tasks(isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int], includeTotalCount: Option[Boolean])
GET /user/annotations controllers.UserController.annotations(isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int], includeTotalCount: Option[Boolean])
GET /user/loggedTime controllers.UserController.loggedTime
GET /users/:id controllers.UserController.user(id: String)
PATCH /users/:id controllers.UserController.update(id: String)
PUT /users/:id/taskTypeId controllers.UserController.updateLastTaskTypeId(id: String)
GET /users/:id/tasks controllers.UserController.userTasks(id: String, isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int])
GET /users/:id/tasks controllers.UserController.userTasks(id: String, isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int], includeTotalCount: Option[Boolean])
GET /users/:id/loggedTime controllers.UserController.userLoggedTime(id: String)
POST /users/loggedTime controllers.UserController.usersLoggedTime
GET /users/:id/annotations controllers.UserController.userAnnotations(id: String, isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int])
GET /users/:id/annotations controllers.UserController.userAnnotations(id: String, isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int], includeTotalCount: Option[Boolean])

# Team
GET /teams controllers.TeamController.list
Expand Down Expand Up @@ -148,7 +148,7 @@ POST /projects
GET /projects/:name controllers.ProjectController.read(name: String)
DELETE /projects/:name controllers.ProjectController.delete(name: String)
PUT /projects/:name controllers.ProjectController.update(name: String)
GET /projects/:name/tasks controllers.ProjectController.tasksForProject(name: String, limit: Option[Int], pageNumber: Option[Int])
GET /projects/:name/tasks controllers.ProjectController.tasksForProject(name: String, limit: Option[Int], pageNumber: Option[Int], includeTotalCount: Option[Boolean])
PATCH /projects/:name/incrementEachTasksInstances controllers.ProjectController.incrementEachTasksInstances(name: String, delta: Option[Long])
PATCH /projects/:name/pause controllers.ProjectController.pause(name: String)
PATCH /projects/:name/resume controllers.ProjectController.resume(name: String)
Expand Down
17 changes: 16 additions & 1 deletion docs/rest_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,12 @@ List your own task annotations
- return only the first x results (defaults to 1000)
- Optional GET parameter `pageNumber=[INT]`
- return the results starting at offset `limit` * `pageNumber` (defaults to 0)
- Optional GET parameter `includeTotalCount=[BOOLEAN]`
- if true returns the total count of entries (defaults to false)

#### Returns
- 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


---
Expand All @@ -78,9 +81,12 @@ List your own explorative annotations
- return only the first x results (defaults to 1000)
- Optional GET parameter `pageNumber=[INT]`
- return the results starting at offset `limit` * `pageNumber` (defaults to 0)
- Optional GET parameter `includeTotalCount=[BOOLEAN]`
- if true returns the total count of entries (defaults to false)

#### Returns
- 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



Expand Down Expand Up @@ -109,10 +115,13 @@ List the task annotations of a user
- return only the first x results (defaults to 1000)
- Optional GET parameter `pageNumber=[INT]`
- return the results starting at offset `limit` * `pageNumber` (defaults to 0)
- Optional GET parameter `includeTotalCount=[BOOLEAN]`
- if true returns the total count of entries (defaults to false)

#### Returns
- 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

---
### `GET /users/:id/annotations`
Expand All @@ -128,9 +137,12 @@ List the explorative annotations of a uaser
- return only the first x results (defaults to 1000)
- Optional GET parameter `pageNumber=[INT]`
- return the results starting at offset `limit` * `pageNumber` (defaults to 0)
- Optional GET parameter `includeTotalCount=[BOOLEAN]`
- if true returns the total count of entries (defaults to false)

#### Returns
- 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



Expand Down Expand Up @@ -528,9 +540,12 @@ List all tasks of a project
- return only the first x results (defaults to infinity)
- Optional GET parameter `pageNumber=[INT]`
- return the results starting at offset `limit` * `pageNumber` (defaults to 0)
- Optional GET parameter `includeTotalCount=[BOOLEAN]`
- if true returns the total count of entries (defaults to false)

#### Returns
- JSON list of objects containing task information
- total count of tasks in the HTTP header `X-Total-Count` if parameter is set

#### Note
- For smoother backwards compatibility, the limit defaults to infinity. However, to ease server load and improve response time, we suggest using a limit of 1000
Expand Down
Loading