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 New Task Terminology in Code #7220

Merged
merged 18 commits into from
Aug 14, 2023
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,3 +29,4 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
### Removed

### Breaking Changes
- The task and project api have changed. Make sure to update to the latest webknossos python library version when interacting with task and projects via the python library. Compare [webknossos-libs#930](https://github.com/scalableminds/webknossos-libs/pull/930). [#7220](https://github.com/scalableminds/webknossos/pull/7220)
3 changes: 3 additions & 0 deletions MIGRATIONS.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ To set all email addresses as unverified, execute this query:
UPDATE webknossos.multiUsers SET isEmailVerified = false;
```

- When interacting with webknossos via the python library, make sure you update to the latest version, as the task and project api have changed. Compare [webknossos-libs#930](https://github.com/scalableminds/webknossos-libs/pull/930). [#7220](https://github.com/scalableminds/webknossos/pull/7220)

### Postgres Evolutions:
- [105-verify-email.sql](conf/evolutions/105-verify-email.sql)
- [106-folder-no-slashes.sql](conf/evolutions/106-folder-no-slashes.sql)
- [107-task-terminology.sql](conf/evolutions/107-task-terminology.sql)
2 changes: 1 addition & 1 deletion app/RequestHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class RequestHandler @Inject()(webCommands: WebCommands,
with LazyLogging {

override def routeRequest(request: RequestHeader): Option[Handler] =
if (request.uri.matches("^(/api/|/data/|/tracings/|/swagger/|/\\.well-known/).*$")) {
if (request.uri.matches("^(/api/|/data/|/tracings/|/swagger|/\\.well-known/).*$")) {
super.routeRequest(request)
} else if (request.uri.matches("^(/assets/).*$")) {
val path = request.path.replaceFirst("^(/assets/)", "")
Expand Down
16 changes: 8 additions & 8 deletions app/controllers/ProjectController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ class ProjectController @Inject()(projectService: ProjectService,
def listWithStatus: Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
projects <- projectDAO.findAll ?~> "project.list.failed"
allCounts <- taskDAO.countOpenInstancesAndTimeByProject
allCounts <- taskDAO.countPendingInstancesAndTimeByProject
js <- Fox.serialCombined(projects) { project =>
for {
openInstancesAndTime <- Fox.successful(allCounts.getOrElse(project._id, (0L, 0L)))
r <- projectService.publicWritesWithStatus(project, openInstancesAndTime._1, openInstancesAndTime._2)
pendingInstancesAndTime <- Fox.successful(allCounts.getOrElse(project._id, (0L, 0L)))
r <- projectService.publicWritesWithStatus(project, pendingInstancesAndTime._1, pendingInstancesAndTime._2)
} yield r
}
} yield Ok(Json.toJson(js))
Expand Down Expand Up @@ -165,11 +165,11 @@ Expects:
taskTypeIdValidated <- ObjectId.fromString(taskTypeId)
_ <- taskTypeDAO.findOne(taskTypeIdValidated) ?~> "taskType.notFound" ~> NOT_FOUND
projects <- projectDAO.findAllWithTaskType(taskTypeId) ?~> "project.list.failed"
allCounts <- taskDAO.countOpenInstancesAndTimeByProject
allCounts <- taskDAO.countPendingInstancesAndTimeByProject
js <- Fox.serialCombined(projects) { project =>
for {
openInstancesAndTime <- Fox.successful(allCounts.getOrElse(project._id, (0L, 0L)))
r <- projectService.publicWritesWithStatus(project, openInstancesAndTime._1, openInstancesAndTime._2)
pendingInstancesAndTime <- Fox.successful(allCounts.getOrElse(project._id, (0L, 0L)))
r <- projectService.publicWritesWithStatus(project, pendingInstancesAndTime._1, pendingInstancesAndTime._2)
} yield r
}
} yield {
Expand Down Expand Up @@ -217,8 +217,8 @@ Expects:
projectIdValidated <- ObjectId.fromString(id)
project <- projectDAO.findOne(projectIdValidated) ?~> "project.notFound" ~> NOT_FOUND
_ <- taskDAO.incrementTotalInstancesOfAllWithProject(project._id, delta.getOrElse(1L))
openInstancesAndTime <- taskDAO.countOpenInstancesAndTimeForProject(project._id)
js <- projectService.publicWritesWithStatus(project, openInstancesAndTime._1, openInstancesAndTime._2)
pendingInstancesAndTime <- taskDAO.countPendingInstancesAndTimeForProject(project._id)
js <- projectService.publicWritesWithStatus(project, pendingInstancesAndTime._1, pendingInstancesAndTime._2)
} yield Ok(js)
}

Expand Down
38 changes: 23 additions & 15 deletions app/controllers/ReportController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,20 @@ import utils.sql.{SimpleSQLDAO, SqlClient}
import javax.inject.Inject
import scala.concurrent.ExecutionContext

case class OpenTasksEntry(id: String, user: String, totalAssignments: Int, assignmentsByProjects: Map[String, Int])
object OpenTasksEntry { implicit val jsonFormat: OFormat[OpenTasksEntry] = Json.format[OpenTasksEntry] }
case class AvailableTaskCountsEntry(id: String,
user: String,
totalAvailableTasks: Int,
availableTasksByProjects: Map[String, Int])
object AvailableTaskCountsEntry {
implicit val jsonFormat: OFormat[AvailableTaskCountsEntry] = Json.format[AvailableTaskCountsEntry]
}

case class ProjectProgressEntry(projectName: String,
paused: Boolean,
priority: Long,
totalTasks: Int,
totalInstances: Int,
openInstances: Int,
pendingInstances: Int,
finishedInstances: Int,
activeInstances: Int,
billedMilliseconds: Long)
Expand Down Expand Up @@ -70,7 +75,7 @@ class ReportDAO @Inject()(sqlClient: SqlClient, annotationDAO: AnnotationDAO)(im
p.priority priority,
count(t._id) totalTasks,
sum(t.totalInstances) totalInstances,
sum(t.openInstances) openInstances,
sum(t.pendingInstances) pendingInstances,
sum(t.tracingTime) tracingTime
from
filteredProjects p
Expand All @@ -87,17 +92,17 @@ class ReportDAO @Inject()(sqlClient: SqlClient, annotationDAO: AnnotationDAO)(im
)


select s1.projectName, s1.paused, s1.priority, s1.totalTasks, s1.totalInstances, s1.openInstances, (s1.totalInstances - s1.openInstances - s2.activeInstances) finishedInstances, s2.activeInstances, s1.tracingTime
select s1.projectName, s1.paused, s1.priority, s1.totalTasks, s1.totalInstances, s1.pendingInstances, (s1.totalInstances - s1.pendingInstances - s2.activeInstances) finishedInstances, s2.activeInstances, s1.tracingTime
from s1
join s2 on s1._id = s2._id
join projectModifiedTimes pmt on s1._id = pmt._id
where (not (s1.paused and s1.totalInstances = s1.openInstances)) and ((s1.openInstances > 0 and not s1.paused) or s2.activeInstances > 0 or pmt.modified > NOW() - INTERVAL '30 days')
where (not (s1.paused and s1.totalInstances = s1.pendingInstances)) and ((s1.pendingInstances > 0 and not s1.paused) or s2.activeInstances > 0 or pmt.modified > NOW() - INTERVAL '30 days')
""".as[(String, Boolean, Long, Int, Int, Int, Int, Int, Long)])
} yield {
r.toList.map(row => ProjectProgressEntry(row._1, row._2, row._3, row._4, row._5, row._6, row._7, row._8, row._9))
}

def getAssignmentsByProjectsFor(userId: ObjectId): Fox[Map[String, Int]] =
def getAvailableTaskCountsByProjectsFor(userId: ObjectId): Fox[Map[String, Int]] =
for {
r <- run(q"""
select p._id, p.name, t.neededExperience_domain, t.neededExperience_value, count(t._id)
Expand All @@ -110,14 +115,14 @@ class ReportDAO @Inject()(sqlClient: SqlClient, annotationDAO: AnnotationDAO)(im
as ue on t.neededExperience_domain = ue.domain and t.neededExperience_value <= ue.value
join webknossos.projects_ p on t._project = p._id
left join (select _task from webknossos.annotations_ where _user = $userId and typ = ${AnnotationType.Task}) as userAnnotations ON t._id = userAnnotations._task
where t.openInstances > 0
where t.pendingInstances > 0
and userAnnotations._task is null
and not p.paused
group by p._id, p.name, t.neededExperience_domain, t.neededExperience_value
""".as[(String, String, String, Int, Int)])
} yield {
val formattedList = r.toList.map(row => (row._2 + "/" + row._3 + ": " + row._4, row._5))
formattedList.toMap.filter(_ match { case (_: String, openTaskCount: Int) => openTaskCount > 0 })
formattedList.toMap.filter(_ match { case (_: String, availableTasksCount: Int) => availableTasksCount > 0 })
}

}
Expand All @@ -130,31 +135,34 @@ class ReportController @Inject()(reportDAO: ReportDAO,
extends Controller
with FoxImplicits {

def projectProgressOverview(teamId: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
def projectProgressReport(teamId: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
teamIdValidated <- ObjectId.fromString(teamId)
_ <- teamDAO.findOne(teamIdValidated) ?~> "team.notFound" ~> NOT_FOUND
entries <- reportDAO.projectProgress(teamIdValidated)
} yield Ok(Json.toJson(entries))
}

def openTasksOverview(teamId: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
def availableTasksReport(teamId: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
teamIdValidated <- ObjectId.fromString(teamId)
team <- teamDAO.findOne(teamIdValidated) ?~> "team.notFound" ~> NOT_FOUND
users <- userDAO.findAllByTeams(List(team._id))
nonUnlistedUsers = users.filter(!_.isUnlisted)
nonAdminUsers <- Fox.filterNot(nonUnlistedUsers)(u => userService.isTeamManagerOrAdminOf(u, teamIdValidated))
entries: List[OpenTasksEntry] <- getAllAvailableTaskCountsAndProjects(nonAdminUsers)
entries: List[AvailableTaskCountsEntry] <- getAvailableTaskCountsAndProjects(nonAdminUsers)
} yield Ok(Json.toJson(entries))
}

private def getAllAvailableTaskCountsAndProjects(users: Seq[User]): Fox[List[OpenTasksEntry]] = {
private def getAvailableTaskCountsAndProjects(users: Seq[User]): Fox[List[AvailableTaskCountsEntry]] = {
val foxes = users.map { user =>
for {
assignmentCountsByProject <- reportDAO.getAssignmentsByProjectsFor(user._id)
pendingTaskCountsByProjects <- reportDAO.getAvailableTaskCountsByProjectsFor(user._id)
} yield {
OpenTasksEntry(user._id.toString, user.name, assignmentCountsByProject.values.sum, assignmentCountsByProject)
AvailableTaskCountsEntry(user._id.toString,
user.name,
pendingTaskCountsByProjects.values.sum,
pendingTaskCountsByProjects)
}
}
Fox.combined(foxes.toList)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/StatisticsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class StatisticsController @Inject()(timeSpanService: TimeSpanService,
numberOfUsers <- userDAO.countAllForOrganization(organizationId)
numberOfDatasets <- dataSetDAO.countAllForOrganization(organizationId)
numberOfAnnotations <- annotationDAO.countAllForOrganization(organizationId)
numberOfAssignments <- taskDAO.countAllOpenInstancesForOrganization(organizationId)
numberOfAssignments <- taskDAO.countAllPendingInstancesForOrganization(organizationId)
} yield {
Ok(
Json.obj(
Expand Down
5 changes: 3 additions & 2 deletions app/controllers/TaskController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Expects:
- As Form data:
- taskTypeId (string) id of the task type to be used for the new tasks
- neededExperience (Experience) experience domain and level that selects which users can get the new tasks
- openInstances (int) if greater than one, multiple instances of the task will be given to users to annotate
- pendingInstances (int) if greater than one, multiple instances of the task will be given to users to annotate
philippotto marked this conversation as resolved.
Show resolved Hide resolved
- projectName (string) name of the project the task should be part of
- scriptId (string, optional) id of a user script that should be loaded for the annotators of the new tasks
- boundingBox (BoundingBox, optional) limit the bounding box where the annotators should be active
Expand Down Expand Up @@ -141,7 +141,8 @@ Expects:
project <- projectDAO.findOne(task._project)
_ <- Fox
.assertTrue(userService.isTeamManagerOrAdminOf(request.identity, project._team)) ?~> "notAllowed" ~> FORBIDDEN
_ <- taskDAO.updateTotalInstances(task._id, task.totalInstances + params.openInstances - task.openInstances)
_ <- taskDAO.updateTotalInstances(task._id,
task.totalInstances + params.pendingInstances - task.pendingInstances)
updatedTask <- taskDAO.findOne(taskIdValidated)
json <- taskService.publicWrites(updatedTask)
} yield JsonOk(json, Messages("task.editSuccess"))
Expand Down
4 changes: 2 additions & 2 deletions app/models/project/Project.scala
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,12 @@ class ProjectService @Inject()(projectDAO: ProjectDAO, teamDAO: TeamDAO, userSer
)
}

def publicWritesWithStatus(project: Project, openTaskInstances: Long, tracingTime: Long)(
def publicWritesWithStatus(project: Project, pendingInstances: Long, tracingTime: Long)(
implicit ctx: DBAccessContext): Fox[JsObject] =
for {
projectJson <- publicWrites(project)
} yield {
projectJson ++ Json.obj("numberOfOpenAssignments" -> JsNumber(openTaskInstances), "tracingTime" -> tracingTime)
projectJson ++ Json.obj("pendingInstances" -> JsNumber(pendingInstances), "tracingTime" -> tracingTime)
}

}
11 changes: 0 additions & 11 deletions app/models/task/CompletionStatus.scala

This file was deleted.

26 changes: 13 additions & 13 deletions app/models/task/Task.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ case class Task(
_taskType: ObjectId,
neededExperience: Experience,
totalInstances: Long,
openInstances: Long,
pendingInstances: Long,
tracingTime: Option[Long],
boundingBox: Option[BoundingBox],
editPosition: Vec3Int,
Expand Down Expand Up @@ -54,7 +54,7 @@ class TaskDAO @Inject()(sqlClient: SqlClient, projectDAO: ProjectDAO)(implicit e
ObjectId(r._Tasktype),
Experience(r.neededexperienceDomain, r.neededexperienceValue),
r.totalinstances,
r.openinstances,
r.pendinginstances,
r.tracingtime,
r.boundingbox.map(b => parseArrayLiteral(b).map(_.toInt)).flatMap(BoundingBox.fromSQL),
editPosition,
Expand Down Expand Up @@ -123,7 +123,7 @@ class TaskDAO @Inject()(sqlClient: SqlClient, projectDAO: ProjectDAO)(implicit e
left join (
select _task from webknossos.annotations_ where _user = $userId and typ = ${AnnotationType.Task} and not ($isTeamManagerOrAdmin and state = ${AnnotationState.Cancelled})
) as userAnnotations ON webknossos.tasks_._id = userAnnotations._task
where webknossos.tasks_.openInstances > 0
where webknossos.tasks_.pendingInstances > 0
and webknossos.projects_._team in ${SqlToken.tupleFromList(teamIds)}
and userAnnotations._task is null
and not webknossos.projects_.paused
Expand All @@ -133,7 +133,7 @@ class TaskDAO @Inject()(sqlClient: SqlClient, projectDAO: ProjectDAO)(implicit e

private def findNextTaskByIdQ(taskId: ObjectId) =
q"""select $columns from $existingCollectionName
where _id = $taskId and openInstances > 0
where _id = $taskId and pendingInstances > 0
limit 1
"""

Expand All @@ -156,7 +156,7 @@ class TaskDAO @Inject()(sqlClient: SqlClient, projectDAO: ProjectDAO)(implicit e
_ <- run(
insertAnnotationQ.withTransactionIsolation(Serializable),
retryCount = 50,
retryIfErrorContains = List(transactionSerializationError, "Negative openInstances for Task")
retryIfErrorContains = List(transactionSerializationError, "Negative pendingInstances for Task")
)
r <- run(findTaskOfInsertedAnnotationQ(annotationId))
parsed <- parseFirst(r, "task assignment query")
Expand Down Expand Up @@ -187,7 +187,7 @@ class TaskDAO @Inject()(sqlClient: SqlClient, projectDAO: ProjectDAO)(implicit e
_ <- run(
insertAnnotationQ.withTransactionIsolation(Serializable),
retryCount = 50,
retryIfErrorContains = List(transactionSerializationError, "Negative openInstances for Task")
retryIfErrorContains = List(transactionSerializationError, "Negative pendingInstances for Task")
)
r <- run(findTaskOfInsertedAnnotationQ(annotationId))
parsed <- parseFirst(r, "task assignment query")
Expand Down Expand Up @@ -247,26 +247,26 @@ class TaskDAO @Inject()(sqlClient: SqlClient, projectDAO: ProjectDAO)(implicit e
} yield parsed
}

def countAllOpenInstancesForOrganization(organizationId: ObjectId): Fox[Long] =
def countAllPendingInstancesForOrganization(organizationId: ObjectId): Fox[Long] =
for {
result <- run(q"""SELECT SUM(t.openInstances)
result <- run(q"""SELECT SUM(t.pendingInstances)
FROM webknossos.tasks_ t JOIN webknossos.projects_ p ON t._project = p._id
WHERE $organizationId in (select _organization from webknossos.users_ where _id = p._owner)""".as[Long])
firstResult <- result.headOption
} yield firstResult

def countOpenInstancesAndTimeForProject(projectId: ObjectId): Fox[(Long, Long)] =
def countPendingInstancesAndTimeForProject(projectId: ObjectId): Fox[(Long, Long)] =
for {
result <- run(q"""select sum(openInstances), sum(tracingtime)
result <- run(q"""select sum(pendingInstances), sum(tracingtime)
from webknossos.tasks_
where _project = $projectId
group by _project""".as[(Long, Option[Long])])
firstResult <- result.headOption
} yield (firstResult._1, firstResult._2.getOrElse(0L))

def countOpenInstancesAndTimeByProject: Fox[Map[ObjectId, (Long, Long)]] =
def countPendingInstancesAndTimeByProject: Fox[Map[ObjectId, (Long, Long)]] =
for {
rowsRaw <- run(q"""select _project, sum(openInstances), sum(tracingtime)
rowsRaw <- run(q"""select _project, sum(pendingInstances), sum(tracingtime)
from webknossos.tasks_
group by _project""".as[(String, Long, Option[Long])])
} yield rowsRaw.toList.map(r => (ObjectId(r._1), (r._2, r._3.getOrElse(0L)))).toMap
Expand All @@ -281,7 +281,7 @@ class TaskDAO @Inject()(sqlClient: SqlClient, projectDAO: ProjectDAO)(implicit e
for {
_ <- run(q"""insert into webknossos.tasks(_id, _project, _script, _taskType,
neededExperience_domain, neededExperience_value,
totalInstances, openInstances, tracingTime, boundingBox,
totalInstances, pendingInstances, tracingTime, boundingBox,
editPosition, editRotation, creationInfo,
created, isDeleted)
values(${t._id}, ${t._project}, ${t._script}, ${t._taskType},
Expand Down
4 changes: 2 additions & 2 deletions app/models/task/TaskCreationParameters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import play.api.libs.json.{Format, Json}

case class TaskParameters(taskTypeId: String,
neededExperience: Experience,
openInstances: Int,
pendingInstances: Int,
projectName: String,
scriptId: Option[String],
boundingBox: Option[BoundingBox],
Expand All @@ -23,7 +23,7 @@ object TaskParameters {

case class NmlTaskParameters(taskTypeId: String,
neededExperience: Experience,
openInstances: Int,
pendingInstances: Int,
projectName: String,
scriptId: Option[String],
boundingBox: Option[BoundingBox])
Expand Down
Loading